* Ruler tab stops: corner selector, click to set, drag to move, drag off to clear
The ruler's markers were read-only (double-click opened the Tabs dialog,
click sets (Left, Center, Right, Decimal, Bar; right-click cycles back),
clicking the ruler sets one on the half-division grid (Alt places
freely), dragging moves it with a dashed guide down the page and a
position tooltip, and dragging it off the ruler clears it. Markers draw
per kind, mirrored for right-to-left paragraphs. Each gesture runs
para.tabs once on release, so it is one undo step and inherited-stop
clearing keeps working. Double-click still opens the dialog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Progress docs: ruler tab stops (ui-parity Rulers row and revision history, ROADMAP progress log)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tabs dialog: OK applies what the fields show; double-click a stop to edit its position
Selecting a stop, changing its alignment or leader and pressing OK now
applies the change without Set, and a new position typed while a stop
is selected (the dialog preselects the first) is set on OK, so adding a
stop never requires leaving the dialog for the ruler. Double-clicking a
stop in the list edits its position in place: Enter or clicking away
commits (the interface unit by default, any unit spelled out, either
decimal separator), Escape cancels, and the commit moves the stop in
one clear + set, keeping its alignment and leader. Clear still wins
over a re-typed cleared position, and junk input changes nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tabs dialog: New button, delete from the list, and the list fills its frame
New stages a fresh stop - deselects, proposes the next free multiple of
the default interval after the last stop (skipping cleared positions),
resets alignment and leader, and focuses the position field - and Set
or OK adds it. Every row gets a small x that clears its stop, and the
Delete key clears the selected one when nothing is being typed; the
selection and an in-progress inline edit follow when a row above them
goes. The list is a fixed 150 pt wide and the rows and scrollbar fill
it, so labels stay left-aligned, the x sits at the frame's edge clear
of the scrollbar, and the scrollbar docks at the edge instead of
floating mid-list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tabs dialog: the stop list is an inline-edit table
Each row edits its stop in place: a unit-aware position field (rows
resort into position order once the edit ends, never mid-typing; two
stops landing on one position merge, the edited row winning), an
alignment dropdown and a leader dropdown (the closed box shows just the
fill), and the x that clears the row. The double-click editor and
move_stop go: the table subsumes them. The selected row keeps an accent
outline, its edits sync the fields below, and the frame widens to fit
the three columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tabs dialog: the table is the whole editor
New adds a row straight into the table at the next free multiple of
the default interval, selected and ready to edit in place, and the
position field, the Alignment and Leader radio groups and the Set and
Clear buttons go: every one of their jobs is a table cell or the row's
x. What stays is New, Clear All, the default interval, the cleared
list and OK/Cancel, with OK applying the staged diff as one para.tabs
call. The staged-field methods (set, clear, on_ok) go with them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review fixes: safe ruler gestures, one tab tolerance, dialog cleanups
Ruler gestures are primary-button only, a click on an indent marker
places nothing (the markers absorb their clicks), Escape cancels a drag
like a table-border drag (#217), and a drag is stamped with its
paragraph: the caret leaving it mid-drag, or a release the ruler never
saw, abandons the gesture instead of applying it elsewhere or late. The
dragged marker no longer double-draws on its first frame. One
TAB_POS_EPS in the doc layer replaces four hardcoded 0.5s across the
engine, the dialog and the ruler. The dialog's normalize is a no-op
unless a position edit happened, its combos share the ruler's KINDS and
one LEADERS table, dead code and a stale doc comment go, and the
ui-parity revision row moves to the table's top. Regression tests cover
each gesture rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Selecting one cell (Table Layout > Select > Select Cell, or a click along
its left edge) and dragging its left or right border moved the border in
the whole column: one selected cell wasn't told apart from text selected
inside it. `ResizeTarget::limit_to_selection` now takes the selected cells
from `Session::cell_range` (#637) when they're in the dragged table, so one
selected cell's border moves in its row only, as in Word, like the border
of cells selected across (#720). Text selected within a cell still moves
the whole column.
Tests: engine `a_selected_cells_border_moves_only_in_its_row`; kittest
`a_selected_cells_border_drags_in_its_row_only` (a click along the cell's
edge, then a real drag). Progress docs: ui-parity, ROADMAP.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M8: keep long Web and Draft pages sharp with raster tiles
* M8: document Web Layout blur fix with before-and-after screenshots
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Clicks on ribbon chrome that isn't a command button (Styles gallery tiles, tab
names, empty ribbon space, View check boxes, status-bar items) left the page
without keyboard focus. The click was caught at the end of the frame, after the
page had been drawn: want_focus was only picked up next frame and the page's
focused flag, which gates the keys, not recomputed until then either, so the
keystroke right after the click went nowhere.
Claim the keyboard in the same frame a ribbon command button does, using the
page's widget id from the frame just drawn.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The active ribbon tab was saved to ui.json and restored verbatim at startup,
so relaunching resumed whichever tab was last used (Review, Help, Draw).
Word always opens on Home. apply_prefs now forces DEFAULT_TAB on load.
Going File and back to the editor still returns to the tab that was showing,
contextual tabs included; tab is still written to ui.json, so the UiState JSON
contract (MCP/control) is unchanged. resolve_tab uses the same DEFAULT_TAB so
the startup default and the contextual-tab fallback can't drift.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Save got focus only when nothing else had it, but the page keeps focus while typing and claims Tab and the arrow keys, so the prompt opened without a focused button and the keys moved the caret behind it. Save now takes the focus whenever none of the prompt's buttons has it.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On macOS five of Word for Mac's default shortcuts were missing or unreachable:
- Cmd+Shift+S is Save As (it opened Apply Styles, which keeps Ctrl+Shift+S
on Windows and Linux).
- Cmd+Option+E inserts an endnote. macOS takes Cmd+Option+D to show and
hide the Dock, so the old key never reached the app.
- Cmd+Option+A adds a comment (Cmd+Option+M still works).
- Control+H opens Find and Replace (Cmd+H still works).
- Cmd+Option+R collapses or expands the ribbon (ui.collapseRibbon).
Keys on Windows, Linux and the web are unchanged. Tests pin the bindings
on each platform.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Crop handles: Picture Format › Crop crop mode on the canvas, picture.crop takes the new size
ui.crop toggles crop mode for the selected (unturned) picture: black crop
handles trim a side or two, keeping the scale and the visible part in place;
the cropped-away part shows dimmed; Esc, Enter, a click outside or Crop again
keep the crop. A drop is one picture.crop with width/height, one undo step;
a floating picture moves by what a left/top crop trims.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crop handles: flipped pictures trim the side under the handle, stale drags drop unapplied
Review round 1: the crop maths, the dimmed preview and the floating
position shift map the on-screen side to the source side (flip H swaps
left/right, flip V top/bottom). A drag remembers its picture and is
dropped unapplied if that picture is no longer the one in crop mode.
Sizes stay within 1-4000 pt at the same scale. Pointer-path tests drive
a real handle drag and a click outside. The dim is darker, and the
canvas keeps following a crop drag's pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs after rebasing on main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crop handles: ui-parity row merged with main; #1242's Cell Size note moved to the Tables row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Insert › Link › Place in This Document: link.targets, links to bookmarks, headings and the top (#1234)
* Insert Hyperlink: Link to › Place in This Document (#1234)
The Insert Hyperlink dialog gets Word's "Link to:" choice. Place in This Document
lists the top of the document, its headings (indented by level) and its
bookmarks, and links to the chosen one; the text to display follows the choice
until it is typed over. It uses insert.link with `top`, `heading` or `bookmark`
and link.targets for the list.
* Docs: update the progress docs for this change
* Docs: put the new sentence in the WordCraft / evidence column
* i18n: Insert Hyperlink link-target strings in every catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A picture with Wrap Text > Behind Text could not be dragged where it overlapped text, in the body or in a header/footer. Pressing it did nothing; once it was selected some other way (Selection Pane, arrow keys) the arrow keys moved it, but the mouse never started a drag. Reported from the field (#82).
Root cause: the hit test deliberately lets body text win over an object behind it (crates/layout/src/hit.rs: 'objects behind the text only where there's no text'), so objects::pointer never began a frame drag.
Fix: grab_at/pointer now treat a Behind Text picture, shape or group as a drag candidate even where text covers it, in body and header/footer stories. The press does not take the gesture at once: it becomes an object drag only after the pointer passes the drag threshold (pending_behind), so a plain click still places the text caret in the overlapping text. Text boxes are unchanged (their inside is their text; only their border grabs them).
Verified with three egui_kittest UI tests driving real pointer events, plus a headless ui_shot run against the real app:
- floating_picture_can_be_dragged_in_front_of_or_behind_text
- floating_header_picture_can_be_dragged_in_front_of_or_behind_text
- behind_picture_dragged_over_text_without_being_selected_first
Full wordcraft-ui-egui suite: 365 passed.
Docs: ROADMAP progress log and revision, docs/ui-parity.md, docs/gaps.md (#3).
Co-authored-by: Muthiullah Hib <thye911@mail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Insert › Bookmark: Delete and Go To, laid out as Word's (#1236)
The Bookmark dialog had only Add: a bookmark could not be deleted, and a
click in its list jumped to the bookmark at once. It is now laid out as
Word's (observed in Word 2019 on Windows): the name box over the list, Add,
Delete and Go To beside them, Sort by Name or Location and Hidden bookmarks
below. A click in the list picks a name; Add (Enter) adds and closes; Delete
and Go To leave the dialog open, and after a delete Cancel reads Close. The
dialog opens on the bookmark around the caret, and Add is off for a name
the command would refuse.
insert.deleteBookmark {name} removes a bookmark's start and end wherever
they are, also across paragraphs, as one undo step, keeping the selection
on its text; from the command search without a name it opens the dialog.
Adding a bookmark under a name in use goes through the same removal: it
looked for the old end only in the start's paragraph, so a bookmark across
paragraphs left a stray end behind. edit.goto with a bookmark selects its
text, as Word's Go To does, instead of placing the caret at its start.
Closes#1236
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert › Bookmark: Ctrl+click on a link to a bookmark keeps a caret (#1236)
Word's Go To selects a bookmark's text, but Ctrl+click on a link to a
bookmark only puts the caret at the bookmark's start (checked in Word 2019
on Windows). Following a link runs edit.goto, which now selects, so it
collapses the selection to its start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve the doc conflict markers left by the previous merge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Table Layout: the Cell Size boxes apply to the whole selection (#1242)
table.rowHeight and table.columnWidth read the caret's cell only. With the whole
table selected the caret is in the last cell, so only the last row's height and
the last column's width changed.
Both now use the cells the selection covers (selected_cells): every selected
row gets the height and every grid column the selected cells touch gets the
width, as one edit. Without a selection that spans cells they still change the
caret's row and column.
* Docs: update the progress docs for this change
* Docs: put the new sentence in the WordCraft / evidence column
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Windows Magnifier keeps the text cursor in view through UI Automation: the
caret's own box has no width, so it takes the box of the character after
the caret, and when that has none either, of the two characters before it.
The canvas described a line's end mark (the paragraph mark, a line break)
with no width, so on the empty line Enter or Shift+Enter makes Magnifier
looked at the end of the line above and stayed there until two more
letters were typed.
A line's end mark now takes the room a selection shows a paragraph mark
with: PARA_MARK_WIDTH (5 pt, now shared with selection_rects) at the line's
visual end (Line::mark_box). Checked live with Windows Magnifier, its view
read with MagGetFullscreenTransform: after Enter and Shift+Enter it moves
to the new line at once.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Right-click on a table of contents entry offers Update Field (part of #1197)
* Test: the TOC context menu check allows more than one Paste item
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Table Design and Table Layout were in the keytip tab list inside a table but had no letter, so no badge showed and no key reached them. They now take Word's two-letter badges, JT and JL: J waits for the second letter and narrows the tab badges to the J ones, then the tab's control badges show as on any other tab.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10: retain local crash recovery on current main
* i18n: Backstage Recover in every catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #778: .xls and legacy-encoding recipient lists; filter and sort recipients
- Recipient lists: legacy Excel 97-2003 .xls (BIFF8 reader on the cfb crate already in the
tree); Excel 5.0/95 and password-protected .xls refused with a clear message; a CSV named
.xls reads as text, an HTML one gets an error. .xls added to the picker filters (desktop,
web, built-in browser) and drop handling.
- Text lists (and plain-text import) detect legacy encodings when not UTF-8/UTF-16: GBK/GB18030,
Big5, Shift_JIS, EUC-KR, Windows code pages (encoding_rs + chardetng, MIT/Apache); an
`encoding` param overrides.
- mailings.filterRecipients / mailings.sortRecipients: field, comparison (equal, notEqual,
less, greater, lessOrEqual, greaterOrEqual, blank, notBlank, contains, notContains), value,
And/Or joins; sort keys. Preview, Finish & Merge and labels use only the matching records
in sort order. Edit Recipient List edits every record and keeps the filter.
- Recipient List dialog: Filter and Sort section and a merge-order column; catalog rows;
strings in every catalog; docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Hungarian and French strings for this change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #885: total order for recipient sort, Bulgarian strings
- compare_cells now ranks numbers (compared numerically) before text (compared
case-insensitively), so it is a total order; a column mixing 9, 10 and 1a
could make slice::sort_by panic. Regression test sorts mixed columns.
- Bulgarian catalog: the 24 Filter and Sort strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vi: mail merge Filter and Sort strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)
* Comments: a click inside a selected balloon moves the caret in its text (#1013)
- The canvas under the markup area also "contains" the pointer, so a press on the balloon counted as a click on the page and left it
- Presses over a balloon card now belong to the card, in both the deselect check and the document click in mouse()
- Test: press, drag and release inside the selected balloon; a click on the page still leaves it
* Comments: an option to show only the selected text's comment (#1055)
- Track Changes Options › Comments: "In the margin" (default, as in Word) or "Only for the selected text"
- The second mode takes comments out of the margin column; the comment under the caret, or the clicked one, floats beside the page, over its edge in a narrow window
- The floating card has its own foreground layer, so presses on it never reach the canvas
- review.trackingOptions takes commentDisplay: margin|selected
- comment_range_at_caret: the comment whose range holds the caret
- Strings in all catalogs; tests for the option, the helper and the card's clicks
* vi: comment display options
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Clippy 1.96: simplify boolean guards, drop a needless drop()
Rust 1.96's clippy flags nonminimal_bool in find_pattern, table_mouse and
table_resize, and drop_non_drop in a ribbon test; `cargo xtask ci` failed at
clippy on main. Same logic, rewritten as clippy suggests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: mini toolbar steps aside for the ribbon
The mini toolbar covered the ribbon's drop-downs (the font size list opened
under it). A press outside the document and the bar (the ribbon, a pane, the
rulers, the status bar) now puts the bar away until the selection changes or
the next press in the document; presses on popups (the bar's own lists, an
open ribbon list over the page) neither hide nor restore it. The bar also
moves from the Tooltip layer to Middle, so every popup and menu draws over
it, as its comment always said.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: mini toolbar scrolls under the ribbon, not over it
With text selected, scrolling the selection up toward the ribbon carried the
mini toolbar along on top of the rulers and ribbon. The bar is now clipped to
the document area: it slides under the rulers and ribbon (or the status bar)
with the text and goes once none of it shows. Its area is constrained to the
document for hit testing too, so the hidden part no longer swallows clicks
and drags meant for the ruler or the ribbon underneath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(ui): implement Ctrl+drag to copy objects on canvas
* feat(ui): implement Ctrl+drag to copy objects on canvas and update translations for "Copy to"
* vi: Copy to
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M10: enable document renaming and account settings
* no-mistakes(review): Allow case-only document renames without overwriting files
* no-mistakes(review): Keep unsaved rename as session file name, not Title
* no-mistakes(review): Keep AutoSave and Recent across renames; Compare clears name
* no-mistakes(document): Document file.rename write-root rule; dedupe rename gap note
* vi: Rename Document strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #916: Cross-reference opens a dialog
The ribbon's Cross-reference button ran insert.crossReference with no
target, which only returns the target list, so nothing happened. It now
opens a Cross-reference dialog: reference type (heading, bookmark,
equation, figure, table), what to insert (text, number, page,
above/below) and the item, ending in insert.crossReference. Scripts and
agents still get the target list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Cross-reference dialog strings in the audited catalogs (ru, hu, tr, bg)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Cross-reference dialog strings in pl, it and vi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: justinjohn0306 <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tables: AutoFit in a nested table fits within its cell (#971)
A double-click on a nested table's column border let the table widen up to
the page's right margin, far past the cell around it. It now widens only up
to the right edge of that cell's text (a right-to-left table's start margin
on the right); the columns right of the border still keep their widths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tables: a selection through a table shades its rows' whole cells (#971)
A selection from text into or across a table takes whole rows (#722), but
only the letters in the cells were highlighted. Every row it takes from start
to end now shades all its cells (an empty one too), as selected cells do, and
the letters in them aren't shaded twice (`table_mouse::selection_rects`, used
by the canvas). A selection that starts and ends in one table keeps its text
highlight, so selecting all the text of a cell doesn't look like selecting the
cell; a nested table in a shaded row shades with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typing: Shift+Right at a cell's end selects the cell first (#971)
Shift+Right out of the cell the selection is in selected that cell and the
next one at once. As in Word, the first step out of the cell now takes the
cell alone (its end-of-cell mark) and the next press goes on into the next
cell; in an empty cell the first press selects it. Shift+Left in
right-to-left text, the forward step there, does the same. Pinned in
`tests_typing.rs` and `docs/typing-parity.md`; progress docs for all three
table fixes of #971.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat-ink-to-shape-arrows: WIP
* Draw: Ink to Shape recognises arrows (gap 12)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ink to Shape arrows: hand-drawn shafts, more tests, docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ink to Shape arrows: the tip is the first near-farthest point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ink to Shape arrows: step to the real tip on dense strokes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1445: Save in the close prompt that can't write the file says so instead of looking like Cancel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1445: a failed save from the close prompt brings the prompt back with the reason, Save As included
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1315: web Save before New/Open waits for the save picker; cancel or failure keeps the document
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1315: overlapping saves hold the replacement; cancel and waiting messages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1252: dim highlights on a dark page so the text on them reads
On a dark page (View > Switch Modes) highlight and character-shading fills
were lightness-inverted like every other colour, which leaves saturated
hues such as yellow bright under white text. They are now marked as text
backgrounds (Draw::Fill text_bg) and dimmed toward black keeping their hue
(RenderOptions::ink_bg; yellow -> ~#4a4a10), and text drawn on them
(Draw::Glyphs on_text_bg) takes the lighter of its colour and its inverted
colour (RenderOptions::ink_text), so black and Automatic text draw white
with WCAG contrast >= 4.5 on all 16 highlight colours. Light page, print
and PDF are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1252: text on a dimmed highlight always reaches 4.5:1; lines too
Glyphs and lines now carry the text background they sit on
(Draw::Glyphs/Draw::Line text_bg: Option<Rgb>) instead of a bool. On a
dark page RenderOptions::ink_text takes the lighter of the colour and its
inversion, and falls back to white when that is under 4.5:1 against the
dimmed background (pure blue on a dim olive was 1.34:1). Underlines and
strikethroughs on a highlight use the same rule. The dim constants are
crate-private and documented as assuming the default dark paper grey.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1252: docs: the 4.5:1 rule for text and lines on highlights
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #980: full screen on F11 and View › Window › Full Screen
view.fullScreen toggles OS-level full screen (ViewportCommand::Fullscreen),
on F11 and ⌃⌘F (macOS); Escape leaves it when nothing else takes Escape.
The window's own full screen changes (WM, macOS button) keep the flag in
step. Disabled on the web (the browser's own F11). Next Field moves from
F11 to Word's other keys, Alt+F1 / Alt+Shift+F1 (Shift+F11 kept).
Ribbon button with a drawn icon, keytip, translations in all catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #980: Escape collapses a selection before leaving full screen; F11 only off macOS
In full screen, Escape left it even when it had a selection to collapse
or a drag to cancel; it now leaves only when nothing else takes Escape.
The shortcut read Ctrl+Ctrl+F on Windows and Linux: macOS gets ⌃⌘F and
F11, others F11. Docs: Alt+F1 may be taken by GNOME/KDE; keyboard
counts re-measured from the registry (152 commands, 172 combinations).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #860: Language menu opens Set Proofing Language… and Language Preferences…
Review › Language (and the status bar's language menu) offered only four
languages since #346 replaced the button that opened the Language dialog,
so French and the other 25 languages, and Set As Default, were out of
reach. The menu now ends with Set Proofing Language… (the Language dialog)
and Language Preferences… (Options › Language); a language outside the
short list shows by name and checked, and the status bar names it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #860: other regions of a language show by name, and no language shows twice
A tag outside the Language dialog's list (fr-CA, en-AU, de-AT) shows as the
language and region (French (Canada)) instead of the raw tag; pt-AO is
Portuguese (Angola), not Brazil. A current language named like a short-list
entry checks that entry rather than showing again above the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #960: double-click the margin to create and edit a header or footer
Double-clicking the top or bottom margin in Print Layout now edits the
header/footer that page shows, creating it (first page, even page or
default slot) when the page has none. insert.editHeader/editFooter take
{"page"}; insert.closeHeader takes away a created one left empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #960: making a header by double-click is an undoable, protected edit
insert.editHeader/editFooter stay pure and hand creation to the new
mutating insert.createHeader/createFooter; closeHeader removes empty new
ones through insert.removeEmptyHeaders. An empty new header is never
saved, one an undo brings back goes on the next close, and a new header
goes in the first section (linked to later ones, as in Word).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1002: equation pictures share one texture, keeping theme colours on Windows
The Equation tab's gallery tiles, structure buttons and symbols were a
texture each: hundreds of small textures, created as galleries opened and
all freed at once when the preview cache filled. On Windows (DirectX 12)
tiles then showed other textures' memory (inverted pictures, red and teal
fills, damaged interface letters). They are now packed into one atlas
texture written in place, the way egui keeps its glyphs; it starts over
when full (at most once a frame).
Tests: every equation picture composited in both themes is only the
theme's greys; all pictures share one texture, none freed while browsing,
none overlapping; the atlas packs rows and starts over when full.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1002: the theme test looks for paper anywhere, not in the corner (Windows fonts reach it)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1002: equation atlas uses up to four pages and never wipes a page drawn from this frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #816: copied equations reach other programs as MathML
A copy or cut whose selection holds an equation now also puts HTML on the
system clipboard, with each equation as Presentation MathML (wrapped in
<semantics> with the linear format as an application/x-unicodemath
annotation), so Word, LibreOffice, Google Docs and browsers paste a
formatted equation instead of `y=2x_s`.
- formats: new mathml module, a writer from the equation tree (runs,
fractions, scripts and pre-scripts, radicals, n-ary operators, delimiters,
functions, limits, accents, bars, border boxes, group characters, equation
arrays, matrices, phantoms) and a bounded reader (our annotation, else the
presentation elements, else a TeX annotation). HTML export writes
equations as MathML; HTML import (open and paste) reads <math> back.
- engine: edit.copy/edit.cut (and column copy) return `html` and keep
Session::clipboard_html when the fragment holds an equation.
- ui-egui: Services::set_clipboard_html; copy sites put HTML with the text
through it, falling back to egui's text copy.
- wordcraft: arboard set().html() with the text alternative; on Wayland
without data-control the text-only path is kept.
OMML on the clipboard (Word's "Office Open XML") needs custom clipboard
formats arboard can't set; left as a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #816: honest clipboard docs; MathML reader fixes (token cap, function names, column vectors)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #822: a chosen interface size is absolute; Auto keeps the display scale
On a display the system reports at 200 % (a bare window manager on X11,
where winit guesses the scale from the monitor's DPI), "100%" drew at
200 %. As in PhotoCraft and PdfCraft (pdfcraft#1005), a chosen size now
divides the display scale back out, so 100% is one screen pixel per
point. The new default, Auto, keeps the display scale and its menu entry
shows the size it draws at. Sizes run 75-300 %; ui.json moves to
uiScale and migrates the old interfaceSize (100 -> Auto).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #822: an old interface size becomes the size it drew at; a valid uiScale wins
A pre-#822 interfaceSize was on top of the display scale, so it is kept
until the first frame knows that scale and becomes old x native
(125% on a 2x display stays at 250%). Note that on the web a chosen
size cancels browser zoom.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #822: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
egui drops the page's keyboard focus on any click elsewhere. Command
buttons handed it back (#686), but gallery tiles, tab names, empty ribbon
space, View check boxes and the status bar's items did not, so the caret
vanished and typing went nowhere. After a click on background chrome that
leaves nothing focused (no text field, dialog, popup, Backstage or comment
balloon), the canvas takes focus back, as in Word. Tabs, style tiles and
status-bar items now report their name and state to screen readers.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #299: caret contrasts with the paper, page colour, shading or highlight under it
The caret was black (white only in Switch Modes' dark page), so it vanished
on a dark page colour, dark shading or a dark highlight. It now takes black
or white by WCAG contrast with the background at the caret
(wordcraft_render::background_at), cached per layout/view/position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #299: shapes colour the caret's background only inside their outline; look up only when drawn
background_at hit-tests a filled shape's real outline (shape_path), so an ellipse's corner
shows what lies beneath. The caret's colour is worked out only when the caret (or IME text)
is drawn, and the unused Tokens::caret is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #299: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1129: no document behind the Start screen; Back shows an empty window
Launched with nothing to open, the Start screen no longer has a blank
document behind it. Esc or Back shows an empty window (no page, ribbon
greyed out but for Help, Info/Save/Save As/Print/Export off in File,
title "WordCraft") until Blank document, a template or a file is
opened; closing asks nothing. Commands that need a document say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1129: explicit no-document allow-list; control inspect/render need a document; fonts can be added
works_without_document lists the ui.* commands that make sense with no document instead of
letting every ui.* command through; font.add/list/remove work so a font file dropped on the
empty window is added. The control channel's document.inspect and ui.render answer that no
document is open instead of reading the hidden placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A click in the left margin beside body text selects that line (trailing
space, and the paragraph mark on a paragraph's last line), a double-click
the paragraph, a triple-click or Ctrl/Cmd+click the document; dragging
there selects whole lines and Shift+click extends by lines. The pointer
shows the arrow there. Layout gains selection_bar_at, body_line_at and
line_span; select.line takes an optional pos; select.dragUnit takes the
units line and document.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Closing an edited document asked Save / Don't Save / Cancel with no button
focused, so the arrow keys did nothing and only the mouse could pick Don't
Save or Cancel. Save now has focus when the prompt opens, as in Word: Enter
saves, and the arrow keys (egui's focus navigation) or Tab move between the
buttons.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Find, Replace and Go To work like Word (#712)
Before: Ctrl+F opened both the Navigation pane and a Find window,
Advanced Find opened the same window, Ctrl+H failed with nothing to
replace, and Go To was a separate little window that only took a page.
After, as in Word:
- Ctrl+F and Home > Find open only the Navigation pane, with its search
box focused so typing searches; selected text is searched for.
- Find, Replace and Go To are one Find and Replace dialog with three
tabs: Advanced Find opens Find, Ctrl+H (edit.findReplace, which never
marks the document edited) opens Replace, Ctrl+G / F5 open Go To.
- Go To moves to a page (a number, or +2 / -1 from the page the caret
is on; empty, Next and Previous step a page) or picks a bookmark.
edit.goto takes pageBy for scripts and agents.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Go To: list bookmarks only while Bookmark is chosen (#712)
Finding a document's bookmarks walks every paragraph; the Go To tab did it every frame even
while it showed pages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Rulers: table column and row markers; indents in a cell measured from it (#700)
With the caret in a table, the rulers show the table, as Word's do:
- The horizontal ruler marks the column borders of the caret's row (past
the table's left edge); the vertical ruler marks the bottom of each of
the table's rows on the page. Each marker has the resize pointer, a
tooltip and a name for screen readers.
- Dragging a marker drags that border exactly like a drag on the page
(#600): a dashed guide, Escape cancels, one undo step on release
through `table.resizeDrag`. On the horizontal ruler only the column
left of the border changes and the columns right of it move along, so
the table's width changes (Word's ruler); a row marker sets the row's
height. The markers follow any drag of the table's borders, on the
ruler or on the page.
- In a cell, the indent markers and tabs are measured from the cell's
text area (its box less its margins); they were placed as if the
paragraph started at the page's margin.
- Layout: `cell_margins` (a cell's own margins, else the table's, else
Word's), so the ruler uses what the layout does.
- UI: crates/ui-egui/src/table_ruler.rs; table_resize.rs starts a drag
from a ruler marker (`grab`) and tells the ruler where a dragged border
is (`moved`), sharing that computation with the dashed guide.
- Tests: layout (cell_margins matches where each cell's text starts);
kittest with real pointer events: a column marker drag (the marker
follows, one undo step, the columns right of it keep their widths), a
row marker drag, and an 18 pt drag of the left-indent marker in a cell
indenting the paragraph 18 pt.
Progress docs updated: ui-parity (rulers 70% -> 85%), gaps (the ruler's
column markers are off the list), target-app-parity, ROADMAP. No new
commands or labels (the tooltips reuse "Table Column Width" and "Table
Row Height"). `cargo xtask ci` passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rulers: Shift works the other way round on the ruler; row markers beside a vertical merge
Checked against Word (2019, Windows): on the ruler a column marker drag
changes only the column on its left and the columns right of it move
along (as before), and with Shift the neighbour makes up the difference,
so the table keeps its width; on the page Shift does the opposite. The
ruler now follows that: `ResizeDrag::plan` takes the Shift key and flips
it for a drag started on a ruler marker.
Since #669 a vertically merged cell's box spans all its rows, so a row
marker took the merge's bottom when the merge was the row's leftmost cell.
The marker now sits on the row's shortest box. (`Placed::Cell` patterns
take `..` for #669's new field.)
- Test (kittest, real pointer events): a Shift drag of a column marker
on the ruler gives the neighbour's width to the column on the left and
keeps the table's width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tables: Ctrl, Alt, left-edge and selected-cells border drags (#711)
Word's border drags that #600 left out:
- Ctrl while dragging a column border: the columns right of it share the
change in proportion to their widths, so the table keeps its width
(none narrower than the minimum). Ctrl wins over Shift; on the table's
right edge only the last column changes.
- The table's left edge can be dragged, from just outside it: the
table's indent moves and the first column makes up the difference, so
the other borders stay. A centred or right-aligned table becomes
left-aligned where the edge was dropped; a floating table moves its
horizontal offset. The horizontal ruler has a marker on it too.
- Alt while dragging a column border: the horizontal ruler shows each
column's width as it is going to be.
- With the selection across two or more cells, dragging one of their
column borders (in one of their rows) moves it only in their rows; the
grid gets a line for the new border and the cells' spans follow. A
border of a cell merged down past the selection moves in the whole
column. Text selected within one cell isn't a cell selection (it can't
be told from one selected cell yet), so it moves the whole column.
- Engine: `Share` (who makes up a column border's move),
`Edge::Column(0)`, `ResizeTarget::limit_to_selection`/`only`/
`planned_x`, `Plan::indent_by`/`cells`; `table.resizeDrag` takes
`"ctrl"`. `plan` still takes a bool for Shift. Each drag is one command
and one undo step.
- UI: table_resize.rs (keys, a guide down the rows that move,
`measuring`), table_ruler.rs (left-edge marker, `widths`).
- Tests: engine (Ctrl in proportion and at the minimum; the left edge,
also of a centred table; selected cells' rows only, with the grid and
spans; a merged cell past the selection); kittest with real pointer
events (a Ctrl drag, the left edge and its ruler marker, Alt's widths
mid-drag).
Progress docs updated: ui-parity (tables on the page 80% -> 90%),
target-app-parity, ROADMAP. No new commands or labels. `cargo xtask ci`
passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tables: a centred or right-aligned table keeps its alignment when its left edge is dragged
Checked against Word (2019, Windows): dragging the left edge of a centred
table widens the first column by the whole drag and the table stays
centred (each side moves half); a right-aligned table keeps its right edge
and grows to the left. Only a left-aligned table's indent moves. #711 made
such a table left-aligned where the edge was dropped; now `move_left_edge`
leaves a centred or right-aligned table's alignment and indent alone and
says whether it moved anything, and the drop-point correction runs only
then.
The ruler's Shift (from #710) now goes through `share`: on a ruler marker
Shift makes the neighbour take up the change, without it only the column
on the left changes.
- Tests: the left-edge test checks the centred table (first column +20 pt,
the others unchanged, still centred) and a right-aligned one (only the
left edge moves); a kittest Shift drag on the ruler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Search: Alt+Q opens it from anywhere, like Word (#819)
- Alt+Q (and Alt, then Q on the keytips) opens the Search box with its
text field focused, from the page or anywhere else; the key's own "q"
is never typed into the box or the document. The title-bar box shows
its Q keytip and its shortcut in the tooltip.
- An empty box suggests common commands; every word of a query has to
match (so "insert table" finds Insert > Tables > Table, as the box's
own hint promises); Enter runs the top match; "Find ... in the
document" opens the Navigation pane on the results. Matching is
about 2x faster than before.
- tools.search lists matching commands for scripts and agents.
Observed in Word for Windows (Microsoft 365): Alt+Q and Alt, Q focus
Search with nothing typed; multi-word queries match commands; the list
also offers finding the words in the document.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Search: Bulgarian, Czech, German, French, Hungarian, Italian and Polish strings
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Rulers: table column and row markers; indents in a cell measured from it (#700)
With the caret in a table, the rulers show the table, as Word's do:
- The horizontal ruler marks the column borders of the caret's row (past
the table's left edge); the vertical ruler marks the bottom of each of
the table's rows on the page. Each marker has the resize pointer, a
tooltip and a name for screen readers.
- Dragging a marker drags that border exactly like a drag on the page
(#600): a dashed guide, Escape cancels, one undo step on release
through `table.resizeDrag`. On the horizontal ruler only the column
left of the border changes and the columns right of it move along, so
the table's width changes (Word's ruler); a row marker sets the row's
height. The markers follow any drag of the table's borders, on the
ruler or on the page.
- In a cell, the indent markers and tabs are measured from the cell's
text area (its box less its margins); they were placed as if the
paragraph started at the page's margin.
- Layout: `cell_margins` (a cell's own margins, else the table's, else
Word's), so the ruler uses what the layout does.
- UI: crates/ui-egui/src/table_ruler.rs; table_resize.rs starts a drag
from a ruler marker (`grab`) and tells the ruler where a dragged border
is (`moved`), sharing that computation with the dashed guide.
- Tests: layout (cell_margins matches where each cell's text starts);
kittest with real pointer events: a column marker drag (the marker
follows, one undo step, the columns right of it keep their widths), a
row marker drag, and an 18 pt drag of the left-indent marker in a cell
indenting the paragraph 18 pt.
Progress docs updated: ui-parity (rulers 70% -> 85%), gaps (the ruler's
column markers are off the list), target-app-parity, ROADMAP. No new
commands or labels (the tooltips reuse "Table Column Width" and "Table
Row Height"). `cargo xtask ci` passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rulers: Shift works the other way round on the ruler; row markers beside a vertical merge
Checked against Word (2019, Windows): on the ruler a column marker drag
changes only the column on its left and the columns right of it move
along (as before), and with Shift the neighbour makes up the difference,
so the table keeps its width; on the page Shift does the opposite. The
ruler now follows that: `ResizeDrag::plan` takes the Shift key and flips
it for a drag started on a ruler marker.
Since #669 a vertically merged cell's box spans all its rows, so a row
marker took the merge's bottom when the merge was the row's leftmost cell.
The marker now sits on the row's shortest box. (`Placed::Cell` patterns
take `..` for #669's new field.)
- Test (kittest, real pointer events): a Shift drag of a column marker
on the ruler gives the neighbour's width to the column on the left and
keeps the table's width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>