@sobree/core 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,22 +20,6 @@ export type { ApiRangeType, BlockInfo, ChangePayload, CommandBus, CommandDefinit
20
20
  export type { CellRef, InsertAt, InsertColumnOpts, InsertRowOpts, MergeCellsOpts, } from './types';
21
21
  export { runsLength } from '../doc/runs';
22
22
  export type { RunPropertiesPatch };
23
- /**
24
- * Public editor surface.
25
- *
26
- * Two entry points for every operation:
27
- * - Core methods take `BlockRef` / `InlinePosition` / `Range` and
28
- * return `EditResult`. These are the wire-callable API — same
29
- * contract for in-process toolbars, headless Y peers (HeadlessSobree),
30
- * and future MCP wrappers.
31
- * - "AtSelection" sugar reads the live DOM selection, builds the
32
- * position/range for you, and delegates to the core. Use these in
33
- * in-process UI code.
34
- *
35
- * Mutations enforce optimistic locking via block `version` numbers.
36
- * Conflicts return `{ ok: false, error: { code: "optimistic-lock", … } }`
37
- * rather than throwing.
38
- */
39
23
  export declare class Editor {
40
24
  readonly host: HTMLElement;
41
25
  readonly selection: EditorSelection;
@@ -160,6 +144,21 @@ export declare class Editor {
160
144
  * and `emitChangeNow` sync only when this flag is set.
161
145
  */
162
146
  private domDirty;
147
+ /**
148
+ * Ids of editable textbox frames whose DOM the user has edited since
149
+ * the last sync. Frames live in the floating overlay (outside the
150
+ * body content hosts), so they need their own read-back path —
151
+ * `syncFromDom` re-serialises each dirty frame into
152
+ * `anchoredFrames[id].content.body`.
153
+ */
154
+ private readonly dirtyFrameIds;
155
+ /**
156
+ * Set by `syncFromDom` when the pending change was a pure live frame
157
+ * keystroke; read (and reset) by `emitChangeNow` into the change
158
+ * payload's `liveFrameEdit`. Lets the host skip the overlay repaint
159
+ * that would clobber the caret, while still repainting on undo/remote.
160
+ */
161
+ private pendingLiveFrameEdit;
163
162
  /**
164
163
  * Kernel seam handed to the behaviour modules (`ops/*`, `query`). Built
165
164
  * once in the constructor; closes over this instance's privates so the
@@ -482,6 +481,32 @@ export declare class Editor {
482
481
  */
483
482
  private ensureCurrent;
484
483
  private syncFromDom;
484
+ /**
485
+ * Re-read the DOM of each dirty editable textbox frame into the AST.
486
+ * The frame element IS the serialization host (the block renderer paints
487
+ * its body directly into it), so `serializeHostsToDocument([el])` yields
488
+ * the same `Block[]` shape as a body host. Matched to the AST frame by
489
+ * its stable `data-anchor-id`. Pure body swap — geometry/anchor untouched.
490
+ */
491
+ private syncFramesFromDom;
492
+ /**
493
+ * The id of the editable textbox frame the caret currently sits in, or
494
+ * null when the selection is in ordinary body flow. Used to route an
495
+ * `input` event to the frame read-back instead of the body read-back.
496
+ */
497
+ private editedFrameId;
498
+ /**
499
+ * Toggle a mark on the caret inside an editable textbox frame, natively
500
+ * (`document.execCommand`), so the body-selection mark path doesn't have
501
+ * to understand frame coordinates. The resulting `<b>`/`<i>`/`<u>` tags
502
+ * round-trip through the frame read-back (the inline serializer maps them
503
+ * to run properties). Returns false when the caret isn't in a frame, so
504
+ * the mark command falls back to the body path.
505
+ */
506
+ applyFrameMark(tag: string): boolean;
507
+ /** Active state of `tag` at a frame caret (toolbar highlight), or null
508
+ * when the caret isn't in a frame. */
509
+ frameMarkActive(tag: string): boolean | null;
485
510
  /**
486
511
  * Schedule a DOM-driven change emit. Called from the `input` listener
487
512
  * when the user types — the DOM is the source of truth and we sync the
@@ -94,6 +94,15 @@ export interface ChangePayload {
94
94
  document: SobreeDocument;
95
95
  revision: number;
96
96
  documentVersion: number;
97
+ /**
98
+ * True when this change came from a live keystroke inside an editable
99
+ * textbox frame — the frame's DOM already holds the edit, so the host
100
+ * can skip repainting the floating overlay (which would clobber the
101
+ * caret). Absent / false for body edits, API mutations, undo/redo, and
102
+ * remote (Y.Doc-driven) changes — those re-render from the AST, so the
103
+ * overlay IS stale and must repaint.
104
+ */
105
+ liveFrameEdit?: boolean;
97
106
  }
98
107
  /** Summary of a top-level block, for `getBlocks()` and list-style UIs. */
99
108
  export interface BlockInfo {
@@ -171,6 +180,18 @@ export interface EditorLike {
171
180
  expect?: Record<string, number>;
172
181
  }): EditResult<void>;
173
182
  readonly selection: EditorSelectionLike;
183
+ /**
184
+ * When the caret sits in an editable textbox frame, toggle a mark
185
+ * (`"strong"`/`"em"`/`"u"`/`"s"`/`"sup"`/`"sub"`) natively on that
186
+ * frame's selection — the frame read-back captures the result. Returns
187
+ * `true` when handled (caret was in a frame), `false` otherwise so the
188
+ * caller falls back to the body mark path. Optional: implemented by the
189
+ * DOM Editor, absent on headless peers (which have no frame DOM).
190
+ */
191
+ applyFrameMark?(tag: string): boolean;
192
+ /** Active state of `tag` at the frame caret, or `null` when the caret
193
+ * isn't in a frame (caller uses the body `isMarkActive`). */
194
+ frameMarkActive?(tag: string): boolean | null;
174
195
  }
175
196
  export type EditorEvent = "change" | "selection" | "keydown" | "track-changes-change";
176
197
  export type EditorEventPayload = {
@@ -16,6 +16,16 @@ export interface AnchorLayerContext {
16
16
  * stacked text (test/headless fallback).
17
17
  */
18
18
  renderBody?: (blocks: Block[], host: HTMLElement) => void;
19
+ /**
20
+ * When true, textbox frames become editable islands: the frame turns
21
+ * into its own `contentEditable` host (`pointer-events:auto`) so the
22
+ * user can click in and type. The editor wires the read-back that
23
+ * serialises the frame's DOM into `anchoredFrames[id].content.body`.
24
+ * False / absent → display-only overlay (read-only mode, decorations,
25
+ * headless). Pictures, shapes, and groups stay non-interactive
26
+ * regardless — only textbox prose is editable.
27
+ */
28
+ editable?: boolean;
19
29
  }
20
30
  /**
21
31
  * Build the per-page anchor layer. Returns a single `<div>` whose