Files
edentext/docs/architecture/frames.md
T
Steffen Becker 60980fa4d6 docs: load CLAUDE.md detail only where it is needed
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.
2026-08-08 15:33:46 +02:00

5.5 KiB
Raw Blame History

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, like rowHeight), 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-node is given a z-index above 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 mirrors tableRowResize.ts; width cap = the cell or page-text-column, height cap = the page text height). setImage command inserts it; the toolbar button (ToolbarExpanded), drag-drop and paste (Editor.svelte) read the file via FileReader → data-URI and clamp the initial size to the page text box. Text wrap: the wrap attr ('inline'|'left'|'right'|'topBottom') makes the image a floating frame — ImageToolbar.svelte (a per-image floating toolbar wired in Editor.svelte like TableToolbar) sets the mode; left/right float the wrapper at its anchor paragraph (text flows on the open side via CSS float), topBottom is a full-width float (text only above/below). All modes use float deliberately: an in-flow display:block on 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 (the dropCursor plugin 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 are addToHistory:false and 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: wrapOffset is the frame's left edge in the text column (Word's positionH posOffset, ODF svg:x), rendered as the float's near/far margin against the live column vars — an indented anchor paragraph can't skew it — and shared with textBox.ts through frameMargins. 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.ts warns 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's WrapMode; inline renders in-flow, floats via CSS float like ImageView.applyWrap), shapeKind ('textbox'|'roundRect'|'ellipse' — ellipse renders via border-radius:50%), fillColor/strokeColor (null = transparent/no border), strokeWidthPt. Padding is the fixed TEXTBOX_PADDING_CM (0.15cm). The node has its own group (group: 'textBox') so only the document admits it (extensions.ts widens Document to '(block | textBox)+') — never in table cells, lists, header/footer, or other boxes; imports found there are flattened with a warning. TextBoxView mirrors ImageView (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/isFrameHit claim mouse events on a ~6px frame ring (rotation-/zoom-aware) → NodeSelection + handles (the shared image-* handle classes/CSS); clicks further inside pass to ProseMirror (caret). The box is browser-draggable only 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; helper findTextBox). TextBoxToolbar.svelte (wired in Editor.svelte like the image toolbar, also shown while the caret is inside a box) sets wrap/shape/fill/stroke. trailingNode.ts appends a paragraph after a trailing box; pageBreaks.ts paginates .textbox-node atomically.