@openpresentation/opf-editor 0.24.3 → 0.25.1

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.
package/README.md CHANGED
@@ -40,7 +40,8 @@ The release after 0.14.2 moves the editor onto core 0.15 (a breaking, clean spec
40
40
  - **Save and export embed.** `prepareSave`/`saveDocument`, `serializeOpfTransfer` (presentation and slide scope) and `convert` (`/export`) call core's `embed`; the playground's OPF download and PPTX export do too.
41
41
  - **Paste** (`prepareOpfImport`, insert mode) uses core's `copySlides`: groups match by `source`, identical records are reused, and the result's `renamed` (`custom-conflict`, `catalog-revision`, `group-name`) and `addedGroups` tell the user what changed.
42
42
  - **Update from catalog** is an explicit action: `checkCatalogUpdates(presentation, { catalogs })` lists the embedded records whose catalog record differs, and `applyCatalogUpdate(editor, { refs })` applies only the approved ones as one undoable step. The design panel's `catalog` section runs both and lists the `opf/unresolved-reference` and `opf/undeclared-catalog` findings (`referenceFindings(editor.validation)`).
43
- - **Editing a catalog's record forks it.** An edit of a layout, theme, colour scheme or font scheme embedded under `catalogs.default` or a named group (from a panel, Properties, Source or any `applyPatch`) moves it into `catalogs.custom` as `<id>-custom` (or the id in `meta.forkIds["<group>:<kind>:<id>"]`), rewrites every reference to it and drops the original, in the same undo step. The change and the session event carry `forked` and a `notice` ("… is now this presentation's own; it no longer receives catalog updates"); approved catalog updates and `moveToCustom` change records in place (`meta.catalogRecordEdit: "in-place"`). `moveToCustom(editor, { group, kind, id }, { id })` is the explicit form, offered for `opf/catalog-record-not-in-source`.
43
+ - **Editing a catalog's record forks it.** An edit of a layout, theme, colour scheme or font scheme embedded under `catalogs.default` or a named group (from a panel, Properties, Source or any `applyPatch`) moves it into `catalogs.custom` as `<id>-custom` (or the id in `meta.forkIds["<group>:<kind>:<id>"]`), rewrites every reference to it and drops the original, in the same undo step. The change and the session event carry `forked` and a `notice` ("… is now this presentation's own; it no longer receives catalog updates"); approved catalog updates and `moveToCustom` change records in place (`meta.catalogRecordEdit: "in-place"`). `moveToCustom(editor, { group, kind, id }, { id })` is the explicit form, offered for `opf/catalog-record-not-in-source`. Opening a document is not an edit: use `replaceDocument` (next bullet).
44
+ - **Opening a document: `replaceDocument`.** `editor.replaceDocument(input, meta?)` puts exactly `input` (a parsed document, JSON text or bytes) in the session as ONE undoable change: Undo restores the previous document, Redo the new one, and nothing under `catalogs.default` or a named group is forked into `catalogs.custom`, even when the two documents embed the same record ids with different content (two examples published at different times). Use it for a host's example menu, File > Open, "start over" and restoring a stored copy; keep `applyPatch` for edits, because an `applyPatch` that replaces the root IS read as an edit of the records the documents share and forks them. It validates like any edit (`meta.rejectInvalid` or the session option refuses an invalid document and changes nothing), accepts the usual `meta` (`source` defaults to `"replace-document"`; the `patch` event carries `meta.replaceDocument: true`) and returns the change; it always records one undo step, also for an equal document. The editor's own File > Open (the playground's Import dialog, "Open as a presentation"), the gallery handoff and `createPersistence`'s `restore()` use it.
44
45
  - **Backgrounds** take the flat image form `{ type: "image", src, alt, fit, focus, opacity, recolor, overlay }` (fits `cover`, `contain`, `stretch`, `tile`) and the image-source shorthand, on the deck and on a slide. **Image blocks** carry their own fit, focus, treatments and placement (`/image-options`, the panel's `image` section): a full-bleed photo is a background, a picture beside the content is a placed image block. `design.imageFit` is the default fit of image blocks.
45
46
 
46
47
  Editor 0.15.0 uses core `^0.15.1` and the optional renderer peer `^0.15.0`. Install renderer and PPTX 0.15 together for the coordinated canvas and export workflow. The shared APIs are core's `validate`, `Finding`, `resolveSlideContext` and the catalog helpers above, and the renderer's `loadFonts()` handle passed as `fonts` to the canvas, export, composition and pagination. Historical version changes are recorded in `CHANGELOG.md`.
@@ -109,8 +110,9 @@ const pane = createSourcePane(container, {
109
110
  pane.setFormat('yaml'); pane.focusPath('/slides/1/title'); pane.getText(); pane.destroy();
110
111
  ```
111
112
 
112
- Methods: `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format; exact for JSON, best effort for YAML and Markdown), `getText()`, `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
113
+ Methods: `replaceDocument(document, { source?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format; exact for JSON, best effort for YAML and Markdown), `getText()`, `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
113
114
 
115
+ - **Opening a document: `pane.replaceDocument(document, { source, name })`.** Loading an example or a file into the pane (never by writing its text into the code editor, which the pane applies as a typed edit and forks the catalog records the two documents share) calls `editor.replaceDocument`: one undoable change, no catalog record forked. `document` is a parsed document or JSON text; `source` is the JSON text it was read from (shown byte for byte; `document` itself when that is text) and `name` the file name. In YAML or Markdown the pane shows the document written in that format. Undo and Redo bring back each document's text exactly in the format shown (the opened JSON as written, the replaced document's text as it was, including YAML comments), so one Undo returns to the document and the text the visitor had. The pane keeps the last 8 such texts. A refused document throws (`invalid-opf-edit`, with `issues`) and changes nothing, and works in a read-only pane. A diff typed or pasted into the pane is still an edit, and editing a catalog record in it forks the record.
114
116
  - **JSON is canonical** and keeps the author's bytes: a YAML or Markdown edit rewrites only the changed JSON tokens (`exact-source`), and Undo restores the exact spelling.
115
117
  - **YAML and Markdown** are core's browser-safe converters, `@openpresentation/opf/yaml` and `/markdown`. Switching format writes the session's document in the new format. An edit parses back to the document and reaches the session as one `replace` patch, after a short delay (`applyDelay`, default 250 ms) so one burst of typing is one undo step. A syntax error, or an OPF rule the session refuses, is listed with its line and never reaches the document; Undo, Redo and outside edits replace a draft that did not parse.
116
118
  - **Nothing is lost silently.** Markdown writes what its dialect has no syntax for (a `design`, region keys, nested groups) as YAML in `opf-slide` and `opf-block` fences; each such part is a warning finding with its JSON Pointer and reason. Comments in YAML cannot survive a rewrite from the document: Format and outside edits drop them and say how many. A document that is not valid OPF cannot be written as YAML or Markdown; the pane stays in JSON and says so.
@@ -121,6 +123,67 @@ Methods: `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (
121
123
 
122
124
  Icons are inline paths from [Lucide](https://lucide.dev) (ISC license). The harness is `examples/source-pane.html` (built by `npm run build:playground`); `npm run test:source-pane-browser` drives it.
123
125
 
126
+ ## Layout editor (RR-99)
127
+
128
+ `createLayoutEditor` from `@openpresentation/opf-editor/layout-editor` is a Figma-style GUI for building one layout record: a `direction` plus an ordered `regions` list of regions and splits (FA-33). It is plain DOM, styled only by `--opf-ui-*` tokens (`src/styles/layout-editor.css`, shipped in `styles.css`), and makes no network request. It is built from four parts that are exported separately, so a host can arrange them differently:
129
+
130
+ | Part | Export | Prefix |
131
+ | --- | --- | --- |
132
+ | Tool palette | `createLayoutToolPalette` | `opf-lx-palette`, `opf-lx-tool` |
133
+ | Canvas: a 16:9 slide frame with the region tree as labeled boxes | `createLayoutCanvas` | `opf-lx-canvas`, `opf-lx-node` |
134
+ | Layers: the tree of splits and regions, with the findings | `createLayoutTree` | `opf-lx-layers`, `opf-lx-tree` |
135
+ | Inspector: the properties of the selection | `createLayoutInspector` | `opf-lx-inspector` |
136
+
137
+ ```js
138
+ import { createLayoutEditor } from '@openpresentation/opf-editor/layout-editor';
139
+ import { toSvg } from '@openpresentation/opf-render/svg';
140
+
141
+ const layoutEditor = createLayoutEditor(container, {
142
+ record, // { id, name, direction, regions: [...] }; default: a title and one content region
143
+ onChange: (record) => save(record), // after each edit, undo and redo (not after setRecord)
144
+ density: 'comfortable', // 'touch' makes the controls 40px
145
+ toSvg, fonts, // optional: Preview draws the sample slide with the host's renderer and fonts handle
146
+ });
147
+ layoutEditor.getRecord(); // a copy of the record
148
+ layoutEditor.setRecord(other); // load a layout; clears the selection and the history
149
+ layoutEditor.getFindings(); // core's findings: { ruleId, severity, message, path, node, field }
150
+ layoutEditor.saveToDeck(session); // writes catalogs.custom.layouts through saveCustomLayout, one undoable edit
151
+ layoutEditor.applyToSlide(session, 0); // saves, then makes it slide 0's layout, in one undoable edit
152
+ ```
153
+
154
+ To arrange the parts yourself, create one model and pass it to each part; `saveLayoutToDeck(session, model)` and `applyLayoutToSlide(session, model, slideIndex)` are the deck functions the composed editor calls.
155
+
156
+ ```js
157
+ import { createLayoutModel, createLayoutToolPalette, createLayoutCanvas, createLayoutTree, createLayoutInspector } from '@openpresentation/opf-editor/layout-editor';
158
+ const model = createLayoutModel({ record });
159
+ createLayoutToolPalette(paletteHost, { model, orientation: 'horizontal' });
160
+ createLayoutCanvas(canvasHost, { model });
161
+ createLayoutTree(layersHost, { model });
162
+ createLayoutInspector(inspectorHost, { model });
163
+ ```
164
+
165
+ **Tools** (Figma-style shortcuts; the palette buttons have the same names): Select (V), Split into a row (R), Split into a column (C), Insert region (I), Heading regions (T), Hand (H, or hold Space) and Preview (P).
166
+ - **Select:** click a region to select it; click again inside a selected split to select what is inside. Esc selects the parent. Shift-click adds a sibling. Double-click or F2 renames in place.
167
+ - **Split into a row or column:** click a region. The original keeps its name and rules and the new one is called `region-N`; the tool returns to Select. In a list that already runs along that axis the new region is a sibling that shares the original's size, otherwise a split holds both. With a split tool active, Enter splits the selected region.
168
+ - **Insert region:** slots appear between siblings and at a split's edges; click one (or Tab to it and press Enter).
169
+ - **Heading regions:** a menu to add or remove the `title` and `subtitle` regions. Without a title region the engine adds an implicit title row, drawn as a dashed band.
170
+ - **Hand:** pan a zoomed slide (zoom buttons, Ctrl or Cmd with the wheel).
171
+ - **Preview:** placeholder blocks of each region's accepted kinds. With the host's `toSvg` (and its `fonts` handle) the sample slide is drawn by the renderer behind the region outlines; without them a wireframe is drawn. A layout with an error finding shows the wireframe.
172
+
173
+ **Direct manipulation on the canvas:**
174
+ - **Resize:** drag the divider between siblings. With other siblings the pair keeps its joint size and snaps to whole numbers and halves; alone in its list it snaps to the nearest whole-number ratio. The ratio shows while dragging ("2 : 1"); Shift skips snapping. Double-click a divider to make both sides equal. A divider next to an `auto` size is not a share and is fixed.
175
+ - **Reorder and move:** drag a region (or several selected siblings) before or after a sibling, or into another split, with a drop indicator; a refused drop (too deep, a split into itself) shows the reason. The same drag works in the layers panel.
176
+ - **Delete:** Delete or Backspace. A split left with one child collapses into it, and the child takes the split's size and inherited anchor. The last region of a layout is kept.
177
+ - **Context menu** (right click, the menu key or Shift+F10): wrap in row, wrap in column, unwrap, duplicate, delete, rename.
178
+
179
+ **Inspector:** with nothing selected, the layout (`id`, `name`, `summary`, `direction`, a default `anchor` that inherits down, `overflowRegion` as a picker over the body regions, and the design hints the record carries, which can be removed). A split has a `direction` toggle, `size` and `anchor`. A region has `name` (unique), `size` (a number, or `auto` only inside a column), `accepts` as chips (none selected means every kind), `role`, `flow`, `max`, `anchor` (shown as "Inherited" with its source while unset), `bleed`, `listColumns` and `empty`. A heading region takes only a size.
180
+
181
+ **Validation and history:** every edit is checked live by core (`opf/layout-template` and `opf/layout-region`, plus the schema); the finding's region is highlighted on the canvas and in the layers panel, its message is under the field, and the findings are listed under the layers (click one to select its region). Edits that would break the layout (more than three levels, more than twelve entries, a grid over 12 by 12 tracks) are refused with the reason. The editor has its own undo and redo (Cmd or Ctrl+Z, Shift+Z; the buttons in the top bar); one drag is one step and typing a value is one step. `saveToDeck` refuses a layout with an error finding and an id the deck already has, unless `replace: true`.
182
+
183
+ **Keyboard:** arrows move between siblings, Enter goes into a split and Esc goes up; `[` and `]` move a region earlier or later (at the end of a split it leaves the split); Shift with a bracket moves it into the neighboring split; Cmd or Ctrl+D duplicates; Delete deletes. A divider is `role="separator"` with `aria-valuenow` (the first side's share of the pair, in percent) and `aria-valuetext` ("2 : 1"); Arrow keys resize it by half a share (Shift: one), Enter makes the sides equal. The layers panel is `role="tree"` (Up and Down, Right to open or enter, Left to close or go to the parent, Home, End, F2). Every action is also a button in the inspector. Touch density makes controls 40px and the divider hit area wider; below 760px the palette is a row above the canvas and Layers and Inspector are tabs under it.
184
+
185
+ The headless operations the editor runs are in `@openpresentation/opf-editor/layout-tree` (`splitRegion`, `wrapNodes`, `unwrapNode`, `moveNodes`, `deleteNodes`, `duplicateNodes`, `resizeSizes`, `layoutBoxes` and more): pure functions that return a new record and the paths to select, or throw a `LayoutOperationError` with the reason, and keep a core-valid record core-valid. `npm run test:layout-editor-browser` drives `examples/layout-editor.html` (built by `npm run build:playground`); the playground's Layouts view mounts the same editor on the presentation's custom layouts. Icons are inline paths from [Lucide](https://lucide.dev) (ISC license).
186
+
124
187
  ## Scope
125
188
 
126
189
  - Package: `@openpresentation/opf-editor`
@@ -508,9 +571,9 @@ const manager = createSlideManager(document.querySelector("#slide-list"), {
508
571
  editor.subscribe(() => manager.render());
509
572
  ```
510
573
 
511
- OPF 0.19 layouts have a grid (`areas`) and named content regions. The slide manager's `openLayoutPicker("apply")` groups the 28 built-ins by intent and applies one to the current slide; `openLayoutPicker()` adds a slide. Pass `renderLayoutPreview(presentation, slideIndex, choice)` to render the current slide with each candidate through the shared renderer. The picker shows the record's `preview` when supplied, otherwise a diagram of its areas. It retains content when switching.
574
+ OPF layouts are one ordered list of named content regions, nested with splits for two dimensions (a record has a `direction` and `regions`). The slide manager's `openLayoutPicker("apply")` groups the 28 built-ins by intent and applies one to the current slide; `openLayoutPicker()` adds a slide. Pass `renderLayoutPreview(presentation, slideIndex, choice)` to render the current slide with each candidate through the shared renderer. The picker shows the record's `preview` when supplied, otherwise a diagram of its regions and splits. It retains content when switching.
512
575
 
513
- `@openpresentation/opf-editor/custom-layout-editor` exports `openCustomLayoutEditor(editor)`, the playground's 12 by 12 grid dialog. Draw named rectangular areas, choose the content kinds, role and flow of each body region, and save the record under `catalogs.custom.layouts` in one undoable transaction. The headless `layoutFromGrid`, `prepareCustomLayout`, `saveCustomLayout` and `pinBlockToRegion` operations are in `/layout-templates`. Canvas Arrange mode shows every template region; empty ones say “Drop content here”, and a top-level block can be dragged or chosen by keyboard for a validated region pin. A full or incompatible region refuses the edit instead of silently ignoring the pin. The host registers the 0.19 catalog with the session; the editor ships no catalog data.
576
+ Layouts are built with the layout editor (see [Layout editor](#layout-editor-rr-99)); it replaces the earlier `openCustomLayoutEditor` dialog. The headless `layoutFromRegions`, `prepareCustomLayout`, `saveCustomLayout` (with `{ replace: true }` to edit a layout the deck already has) and `pinBlockToRegion` operations are in `/layout-templates`. Canvas Arrange mode shows every template region; empty ones say “Drop content here”, and a top-level block can be dragged or chosen by keyboard for a validated region pin. A full or incompatible region refuses the edit instead of silently ignoring the pin. The host registers the 0.19 catalog with the session; the editor ships no catalog data.
514
577
 
515
578
  `@openpresentation/opf-editor/outline` and `/outline-view` edit the deck as an outline: a row per slide title, subtitle, text paragraph and list item, and a read-only row for content that is not text (a chart, a table, an image) or text that carries formatting, so nothing is flattened away. Typing commits as one change when the row loses focus or on Enter. Enter adds a line (after a title: a slide), Alt+Up and Alt+Down move a bullet with the bullets nested under it, or a slide with its text, Alt+Shift+Right demotes and Alt+Shift+Left promotes. Demoting a bullet nests it (`level`); promoting a top-level bullet turns it into a new slide that takes the bullets after it; demoting a plain slide makes it a bullet of the slide before it (refused, with the reason, when that would drop content such as notes, blocks or a design). Backspace on an empty line removes it. Tab keeps moving focus, so the outline is no keyboard trap.
516
579
 
@@ -861,7 +924,7 @@ bindPresentShortcut(document, () => presenter.start(current)); // F5
861
924
  - **Accessibility.** The element is `role="dialog"` with `aria-modal="true"` and an `aria-label` ("Presentation"; `labels.dialog`). Focus moves into it on start and stays in it (Tab cycles the dialog and the exit button). An `aria-live="polite"` region announces "Slide 3 of 12" (`labels.position`).
862
925
  - **Fonts.** Like the canvas, the presenter waits for the faces the deck needs (`fonts.pending` and `fonts.ensure`) and shows "Loading fonts…" meanwhile.
863
926
 
864
- `createPresentButton(container, { onPresent, disabled, density })` mounts an icon-only button (the Lucide Presentation icon, inlined) named "Present" (`aria-label`), titled "Present (F5)" and styled like the Export menu's trigger. `disabled` is `true` or a reason string (the title); the button stays focusable (`aria-disabled`) and does nothing. It returns `{ element, setDisabled(reason), setDensity(density), destroy() }`. `bindPresentShortcut(target, onPresent)` binds F5 (and prevents the browser's reload); it ignores F5 while typing in inputs, selects and editors (CodeMirror, contenteditable), in menus and dialogs, and with a modifier key, and returns a function that removes the listener. `examples/playground.js` wires all three next to the Export menu.
927
+ `createPresentButton(container, { onPresent, disabled, density, variant })` mounts an icon-only button (the Lucide Presentation icon, inlined) named "Present" (`aria-label`), titled "Present (F5)". `variant` is `"quiet"` (the default: a transparent icon in `--opf-ui-muted-fg` that turns `--opf-ui-fg` on `--opf-ui-muted` on hover, like the toolbar's undo and redo, so a lone icon beside a primary Export does not compete with it) or `"primary"` (the filled accent look of the Export menu's trigger). `disabled` is `true` or a reason string (the title); the button stays focusable (`aria-disabled`) and does nothing. It returns `{ element, setDisabled(reason), setDensity(density), setVariant(variant), destroy() }`. `bindPresentShortcut(target, onPresent)` binds F5 (and prevents the browser's reload); it ignores F5 while typing in inputs, selects and editors (CodeMirror, contenteditable), in menus and dialogs, and with a modifier key, and returns a function that removes the listener. `examples/playground.js` wires all three next to the Export menu.
865
928
 
866
929
  Tests: `npm run test:presenter-browser` and `npm run test:present-button-browser`.
867
930
 
@@ -890,7 +953,7 @@ input.onchange = async () => {
890
953
  - **Findings.** The conversion's findings for YAML and Markdown, in core's `Finding` shape: `path` (a JSON Pointer such as `/slides/1/bogus`), `message`, `severity` and `location` (line and column in the file). A deck that reads but has OPF errors is returned with them, so a host can show them; JSON gives none (the editor session validates it).
891
954
  - **Errors.** Text that cannot be read at all throws `DeckFileError` with a `code` and a clear message: `unreadable`, `file-too-large` (over 20 MB), `empty-file`, `unsupported-file` (a .pptx or other binary file), `invalid-json`, `invalid-yaml`, `invalid-markdown` (with the syntax `findings` and the line) and `not-a-deck` (JSON without a `slides` list, such as one slide or a block, which a host can send to its paste path as the playground does).
892
955
 
893
- The source pane has no file-opening path of its own, so it keeps no second copy of this. The playground's Import dialog uses `readDeckFile` and `DECK_FILE_ACCEPT` for its file tab and for dropped files (`examples/transfer-controls.js`). Icons in the presenter button are inlined [Lucide](https://lucide.dev) paths (ISC license).
956
+ Put the opened document in the session with `editor.replaceDocument(document)` (or `pane.replaceDocument(document, { source })` when a source pane is mounted), not `applyPatch`: opening is one undoable change that forks no catalog record. The source pane has no file-opening path of its own, so it keeps no second copy of this. The playground's Import dialog uses `readDeckFile` and `DECK_FILE_ACCEPT` for its file tab and for dropped files (`examples/transfer-controls.js`). Icons in the presenter button are inlined [Lucide](https://lucide.dev) paths (ISC license).
894
957
 
895
958
  ## Optional React Bindings
896
959
 
package/dist/galleries.js CHANGED
@@ -184,9 +184,9 @@ function attachDefinition(presentation, descriptor, gallerySource) {
184
184
  groups[group][kind] ??= {};
185
185
  let record;
186
186
  if (kind === "layouts") {
187
- if (!Array.isArray(source.areas)) return;
187
+ if (!Array.isArray(source.regions)) return;
188
188
  record = {name: source.label ?? source.name ?? id};
189
- for (const key of ['summary', 'description', 'areas', 'columns', 'rows', 'regions', 'overflowRegion', 'design', 'composition', 'tags', 'preview'])
189
+ for (const key of ['summary', 'description', 'direction', 'anchor', 'regions', 'overflowRegion', 'design', 'composition', 'tags', 'preview'])
190
190
  if (source[key] !== undefined) record[key] = structuredClone(source[key]);
191
191
  } else if (kind === "fontSchemes") record = fontSchemeRecord(source, id);
192
192
  else record = embeddedRecord(source);
package/dist/index.d.ts CHANGED
@@ -91,6 +91,14 @@ export interface EditorSession {
91
91
  setGroupComposition(path: string, composition: Composition, meta?: Record<string, unknown>): EditorChange;
92
92
  setCatalog(path: string | string[], catalogKind: string, id: string, meta?: Record<string, unknown>): EditorChange;
93
93
  applyPatch(operations: JsonPatchOperation[], meta?: Record<string, unknown>): EditorChange;
94
+ /**
95
+ * Open a different document in the session: the whole document is replaced as one undoable change, and the replacement is never
96
+ * read as an edit of the catalog records the two documents share, so nothing under `catalogs.default` or a named group is forked
97
+ * into `catalogs.custom`. `input` is a parsed document, JSON text or bytes. It is validated like any edit (`meta.rejectInvalid` or
98
+ * the session option refuses an invalid document and changes nothing). It always records its one change, also for an equal document.
99
+ * `meta.source` defaults to `"replace-document"`; the `patch` event carries `meta.replaceDocument: true`.
100
+ */
101
+ replaceDocument(input: unknown, meta?: Record<string, unknown>): EditorChange;
94
102
  /** The undo and redo stacks as plain data (oldest entry first), for hosts that persist work. */
95
103
  exportHistory(): EditorHistory;
96
104
  /** Replace the document and, optionally, the history in one step (one `restore` event). A history that does not replay against the document throws `invalid-history` before anything changes. */
package/dist/index.js CHANGED
@@ -314,6 +314,23 @@ export function createEditorSession(input, options = {}) {
314
314
  return setCatalogId(editor, path, catalogKind, id, meta);
315
315
  },
316
316
  applyPatch: commitPatch,
317
+ /**
318
+ * Open a different document in the session (a host's example menu, File > Open, "start over"): the whole document is replaced as ONE
319
+ * undoable change (Undo restores the previous document, Redo the new one). Unlike an `applyPatch` that replaces the root, this is
320
+ * never read as the user editing the catalog records the two documents share: records under `catalogs.default` or a named group
321
+ * keep the ids and content the new document carries, and nothing is forked into `catalogs.custom`. `input` is a parsed document,
322
+ * JSON text or bytes. The document is validated like any edit (`meta.rejectInvalid` or the session option refuses an invalid one
323
+ * and changes nothing). It always records its one change, also when the new document equals the open one (Undo then returns to the
324
+ * same document, as for any other replace). `meta.source` defaults to `"replace-document"`; the event is a `patch` whose `meta.replaceDocument` is true.
325
+ */
326
+ replaceDocument(input, meta = {}) {
327
+ return commitPatch([{ op: "replace", path: "", value: parseInput(input) }], {
328
+ ...meta,
329
+ source: meta.source ?? "replace-document",
330
+ replaceDocument: true,
331
+ catalogRecordEdit: "in-place"
332
+ });
333
+ },
317
334
  /**
318
335
  * The undo and redo stacks as plain data (oldest entry first): `{ undo: [{ patches, inversePatches, meta }], redo: [...] }`.
319
336
  * Together with `presentation` this is everything `restoreState` needs to bring a session back, for hosts that persist work.