The root file is read at every session start and had grown to 87 KB, so every session paid for knowledge it did not use. Move the per-directory detail into nested CLAUDE.md files, which load when a file in that directory is touched, and the five deep-dive topics into docs/. The root keeps the commands, the hard rules and an index of where the rest lives. Content moved verbatim — the probed Word/LibreOffice behaviour, sentinel order and shared constants are what the code cannot say. CI fails the root over 120 lines, since a prose budget alone is what let it grow.
5.5 KiB
5.5 KiB
Frames: images and text boxes
Covers src/lib/editor/extensions/image.ts and textBox.ts — node views, resize/rotate
handles, wrap modes, frame offsets — and their ODF/DOCX legs.
image.ts(Image) — inline, as-character image atom (Word's "in line with text"). Attrs:src(data-URI),alt,width/height(unscaled doc px @96dpi, likerowHeight),rotation(CW degrees). The node view nests an<img>in a rotated "rotor" inside an axis-aligned wrapper that reserves the rotated bounding box (so text reflows around it). The rotor's transform makes it a stacking context, so a selected.image-nodeis given az-indexabove the header/footer layer to keep handles that poke into a margin grabbable. It has eight zoom-aware resize handles — corners aspect-locked, edges single-axis (width- or height-only) — plus a rotation-arrow grip, and shows a live "Width … × Height …" (cm) badge while resizing; resize deltas are un-rotated onto the image's own axes so handles track the pointer at any angle (drag math mirrorstableRowResize.ts; width cap = the cell or page-text-column, height cap = the page text height).setImagecommand inserts it; the toolbar button (ToolbarExpanded), drag-drop and paste (Editor.svelte) read the file viaFileReader→ data-URI and clamp the initial size to the page text box. Text wrap: thewrapattr ('inline'|'left'|'right'|'topBottom') makes the image a floating frame —ImageToolbar.svelte(a per-image floating toolbar wired inEditor.sveltelikeTableToolbar) sets the mode;left/rightfloat the wrapper at its anchor paragraph (text flows on the open side via CSSfloat),topBottomis a full-widthfloat(text only above/below). All modes usefloatdeliberately: an in-flowdisplay:blockon an inline-atom node view splits the paragraph's inline content into anonymous block boxes, which desyncs ProseMirror's inline view descriptor and makes it drop the following page-break spacer widget; a float is out-of-flow and avoids that. Dragging an inline image runs ProseMirror's native node move (thedropCursorplugin shows the caret). Dragging a floating image runs a custom drag (ImageView.startReposition) that live re-anchors the node to the text position under the cursor, so the float and text reflow in real time as you drag; it's rAF-throttled and only moves when the cursor's line changes (a float can't move within a line), and stays a single undo step (first move records history, later moves areaddToHistory:falseand get rebased on undo). A paragraph that contains an image paginates atomically (pageBreaks.ts— pushed whole to the next page, spacer placed before it) so a page break never splits the paragraph next to the image's float (where ProseMirror drops the spacer widget). Frame offsets:wrapOffsetis the frame's left edge in the text column (Word'spositionHposOffset, ODFsvg:x), rendered as the float's near/far margin against the live column vars — an indented anchor paragraph can't skew it — and shared withtextBox.tsthroughframeMargins.wrapOffsetY(positionV,svg:y) round-trips but is not rendered: a line box avoids a float's whole margin box, so a top margin would make dead space where Word flows text (measured on the thesis fixture: 131mm off became 187mm, and the document grew a page). Browsers can't wrap text around a freely-positioned box (CSS Exclusions are unimplemented), so a frame's vertical place stays the anchor paragraph's. Inline images stay as-char. Works inside table cells. Not available while editing a header/footer (the HF schema has no image node). Images live as data-URIs in the autosaved JSON;autosave.tswarns once if that exceeds the localStorage quota.textBox.ts(TextBox) — block-level text box / basic shape with editable block content (content: '(paragraph|heading|bulletList|orderedList)+',isolating). Attrs:width/height(px @96dpi; height renders as min-height — content grows the box),rotation(CW deg),wrap(image'sWrapMode;inlinerenders in-flow, floats via CSSfloatlikeImageView.applyWrap),shapeKind('textbox'|'roundRect'|'ellipse'— ellipse renders viaborder-radius:50%),fillColor/strokeColor(null= transparent/no border),strokeWidthPt. Padding is the fixedTEXTBOX_PADDING_CM(0.15cm). The node has its own group (group: 'textBox') so only the document admits it (extensions.tswidens Document to'(block | textBox)+') — never in table cells, lists, header/footer, or other boxes; imports found there are flattened with a warning.TextBoxViewmirrorsImageView(wrapper reserving the rotated bbox → rotor carrying fill/stroke/rotation →contentDOM); the rotor is out of flow and auto-grows, so a ResizeObserver refits the wrapper. Click model (Word-like):stopEvent/isFrameHitclaim mouse events on a ~6px frame ring (rotation-/zoom-aware) → NodeSelection + handles (the sharedimage-*handle classes/CSS); clicks further inside pass to ProseMirror (caret). The box is browser-draggableonly while node-selected, so PM's native block drag moves it without hijacking text selection. Commands:insertTextBox()(after the current top-level block, cursor inside),setTextBoxAttrs()(NodeSelection or cursor inside; helperfindTextBox).TextBoxToolbar.svelte(wired inEditor.sveltelike the image toolbar, also shown while the caret is inside a box) sets wrap/shape/fill/stroke.trailingNode.tsappends a paragraph after a trailing box;pageBreaks.tspaginates.textbox-nodeatomically.