@pixerate/editor 0.22.0 → 0.24.0

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.
Files changed (188) hide show
  1. package/README.md +175 -2
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/dist/agent/index.cjs +87 -111
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +70 -211
  6. package/dist/agent/index.d.ts +70 -211
  7. package/dist/agent/index.js +78 -102
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
  10. package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
  11. package/dist/canvas/index.cjs +2075 -86
  12. package/dist/canvas/index.cjs.map +1 -1
  13. package/dist/canvas/index.d.cts +608 -186
  14. package/dist/canvas/index.d.ts +608 -186
  15. package/dist/canvas/index.js +2060 -71
  16. package/dist/canvas/index.js.map +1 -1
  17. package/dist/chunk-26BVFKHU.cjs +453 -0
  18. package/dist/chunk-26BVFKHU.cjs.map +1 -0
  19. package/dist/chunk-2H7ZHPEJ.js +762 -0
  20. package/dist/chunk-2H7ZHPEJ.js.map +1 -0
  21. package/dist/{chunk-DENSCKFC.js → chunk-3CN2WD3X.js} +1730 -189
  22. package/dist/chunk-3CN2WD3X.js.map +1 -0
  23. package/dist/{chunk-JJDLPEWL.js → chunk-3LV6RRPH.js} +2 -2
  24. package/dist/{chunk-MJZBNWG7.js → chunk-5S3Z4PPF.js} +78 -557
  25. package/dist/chunk-5S3Z4PPF.js.map +1 -0
  26. package/dist/chunk-664LQYFJ.js +115 -0
  27. package/dist/chunk-664LQYFJ.js.map +1 -0
  28. package/dist/chunk-7DWMJCAT.js +453 -0
  29. package/dist/chunk-7DWMJCAT.js.map +1 -0
  30. package/dist/chunk-ARRTP33I.js +101 -0
  31. package/dist/chunk-ARRTP33I.js.map +1 -0
  32. package/dist/chunk-CPOKFNBG.cjs +101 -0
  33. package/dist/chunk-CPOKFNBG.cjs.map +1 -0
  34. package/dist/{chunk-AA4ZEVCC.cjs → chunk-D27YFC46.cjs} +101 -580
  35. package/dist/chunk-D27YFC46.cjs.map +1 -0
  36. package/dist/chunk-D6FH2YAX.cjs +131 -0
  37. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  38. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  39. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  40. package/dist/chunk-FODNLX5N.js +749 -0
  41. package/dist/chunk-FODNLX5N.js.map +1 -0
  42. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  43. package/dist/chunk-FPENXAEJ.js.map +1 -0
  44. package/dist/chunk-G2I4QJOT.cjs +762 -0
  45. package/dist/chunk-G2I4QJOT.cjs.map +1 -0
  46. package/dist/chunk-HLIMTC6N.js +1 -0
  47. package/dist/chunk-HLIMTC6N.js.map +1 -0
  48. package/dist/{chunk-QGHGJ56O.js → chunk-ITBSKI26.js} +1483 -19
  49. package/dist/chunk-ITBSKI26.js.map +1 -0
  50. package/dist/{chunk-6M3BCTBD.cjs → chunk-MA2F5KHF.cjs} +470 -242
  51. package/dist/chunk-MA2F5KHF.cjs.map +1 -0
  52. package/dist/{chunk-SAZRENEZ.cjs → chunk-MCSJFWAO.cjs} +1733 -192
  53. package/dist/chunk-MCSJFWAO.cjs.map +1 -0
  54. package/dist/chunk-MKA3AHLV.cjs +256 -0
  55. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  56. package/dist/chunk-OLZJXL4R.cjs +749 -0
  57. package/dist/chunk-OLZJXL4R.cjs.map +1 -0
  58. package/dist/chunk-OXSV64JX.js +689 -0
  59. package/dist/chunk-OXSV64JX.js.map +1 -0
  60. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  61. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  62. package/dist/chunk-P6ODPZ4W.js +256 -0
  63. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  64. package/dist/chunk-PGNZKFQS.js +685 -0
  65. package/dist/chunk-PGNZKFQS.js.map +1 -0
  66. package/dist/chunk-PL4JB3BF.cjs +1 -0
  67. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  68. package/dist/{chunk-GVSMRPHB.cjs → chunk-SBCZ6RN5.cjs} +1568 -104
  69. package/dist/chunk-SBCZ6RN5.cjs.map +1 -0
  70. package/dist/{chunk-CL37JGFW.cjs → chunk-SML64VJG.cjs} +4 -4
  71. package/dist/{chunk-CL37JGFW.cjs.map → chunk-SML64VJG.cjs.map} +1 -1
  72. package/dist/chunk-TNNJXF46.cjs +115 -0
  73. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  74. package/dist/chunk-U5HZSFCS.js +131 -0
  75. package/dist/chunk-U5HZSFCS.js.map +1 -0
  76. package/dist/{chunk-RU52I7QO.js → chunk-U5KWZU4X.js} +388 -160
  77. package/dist/chunk-U5KWZU4X.js.map +1 -0
  78. package/dist/chunk-XNORY7M6.cjs +685 -0
  79. package/dist/chunk-XNORY7M6.cjs.map +1 -0
  80. package/dist/commands/index.cjs +860 -0
  81. package/dist/commands/index.cjs.map +1 -0
  82. package/dist/commands/index.d.cts +244 -0
  83. package/dist/commands/index.d.ts +244 -0
  84. package/dist/commands/index.js +860 -0
  85. package/dist/commands/index.js.map +1 -0
  86. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  87. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  88. package/dist/compare/index.cjs +15 -0
  89. package/dist/compare/index.cjs.map +1 -0
  90. package/dist/compare/index.d.cts +58 -0
  91. package/dist/compare/index.d.ts +58 -0
  92. package/dist/compare/index.js +15 -0
  93. package/dist/compare/index.js.map +1 -0
  94. package/dist/{controller-B7GIybc4.d.cts → controller-BhLLAoi8.d.cts} +119 -5
  95. package/dist/{controller-B7GIybc4.d.ts → controller-BhLLAoi8.d.ts} +119 -5
  96. package/dist/documents-BQjzJNe2.d.ts +76 -0
  97. package/dist/documents-KRTPhvst.d.cts +76 -0
  98. package/dist/extensions/index.cjs +53 -5
  99. package/dist/extensions/index.cjs.map +1 -1
  100. package/dist/extensions/index.d.cts +7 -3
  101. package/dist/extensions/index.d.ts +7 -3
  102. package/dist/extensions/index.js +52 -4
  103. package/dist/generation/index.cjs +1373 -0
  104. package/dist/generation/index.cjs.map +1 -0
  105. package/dist/generation/index.d.cts +554 -0
  106. package/dist/generation/index.d.ts +554 -0
  107. package/dist/generation/index.js +1373 -0
  108. package/dist/generation/index.js.map +1 -0
  109. package/dist/grammar/index.cjs +48 -3
  110. package/dist/grammar/index.cjs.map +1 -1
  111. package/dist/grammar/index.d.cts +322 -100
  112. package/dist/grammar/index.d.ts +322 -100
  113. package/dist/grammar/index.js +54 -9
  114. package/dist/history/index.cjs +12 -2
  115. package/dist/history/index.cjs.map +1 -1
  116. package/dist/history/index.d.cts +211 -7
  117. package/dist/history/index.d.ts +211 -7
  118. package/dist/history/index.js +13 -3
  119. package/dist/image-editor/index.cjs +2244 -38
  120. package/dist/image-editor/index.cjs.map +1 -1
  121. package/dist/image-editor/index.d.cts +742 -4
  122. package/dist/image-editor/index.d.ts +742 -4
  123. package/dist/image-editor/index.js +2222 -16
  124. package/dist/image-editor/index.js.map +1 -1
  125. package/dist/{index-CCr_z9kF.d.cts → index-BhJBSm2d.d.cts} +370 -4
  126. package/dist/{index-Ck6ajwXh.d.ts → index-fJrlkJCd.d.ts} +370 -4
  127. package/dist/index.cjs +193 -98
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.d.cts +17 -48
  130. package/dist/index.d.ts +17 -48
  131. package/dist/index.js +198 -103
  132. package/dist/index.js.map +1 -1
  133. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  134. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  135. package/dist/persistence/index.cjs +676 -0
  136. package/dist/persistence/index.cjs.map +1 -0
  137. package/dist/persistence/index.d.cts +347 -0
  138. package/dist/persistence/index.d.ts +347 -0
  139. package/dist/persistence/index.js +676 -0
  140. package/dist/persistence/index.js.map +1 -0
  141. package/dist/registry-BDe-vlv3.d.ts +171 -0
  142. package/dist/registry-D32LpIc4.d.cts +354 -0
  143. package/dist/registry-DnzpgY27.d.cts +171 -0
  144. package/dist/registry-evKTP4ls.d.ts +354 -0
  145. package/dist/schema-DYDGdYL9.d.cts +41 -0
  146. package/dist/schema-DYDGdYL9.d.ts +41 -0
  147. package/dist/serializers/index.cjs +4 -3
  148. package/dist/serializers/index.cjs.map +1 -1
  149. package/dist/serializers/index.d.cts +2 -1
  150. package/dist/serializers/index.d.ts +2 -1
  151. package/dist/serializers/index.js +3 -2
  152. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  153. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  154. package/dist/spreadsheet/index.cjs +53 -3
  155. package/dist/spreadsheet/index.cjs.map +1 -1
  156. package/dist/spreadsheet/index.d.cts +247 -18
  157. package/dist/spreadsheet/index.d.ts +247 -18
  158. package/dist/spreadsheet/index.js +58 -8
  159. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  160. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  161. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-BK5sVf4_.d.cts} +18 -136
  162. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-CDjCrwKe.d.ts} +18 -136
  163. package/dist/{types-CJ4U-HHI.d.ts → types-DX4Mh4SL.d.cts} +1 -1
  164. package/dist/{types-CJ4U-HHI.d.cts → types-DX4Mh4SL.d.ts} +1 -1
  165. package/dist/types-GT5a5YAX.d.cts +86 -0
  166. package/dist/types-GT5a5YAX.d.ts +86 -0
  167. package/package.json +23 -2
  168. package/dist/chunk-2TWGLC5K.cjs +0 -262
  169. package/dist/chunk-2TWGLC5K.cjs.map +0 -1
  170. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  171. package/dist/chunk-6M3BCTBD.cjs.map +0 -1
  172. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  173. package/dist/chunk-AXV2ZW34.cjs +0 -118
  174. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  175. package/dist/chunk-B2ZRLN4S.js +0 -118
  176. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  177. package/dist/chunk-DENSCKFC.js.map +0 -1
  178. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  179. package/dist/chunk-KAADNAPF.js +0 -262
  180. package/dist/chunk-KAADNAPF.js.map +0 -1
  181. package/dist/chunk-MJZBNWG7.js.map +0 -1
  182. package/dist/chunk-OYXSKQFR.js.map +0 -1
  183. package/dist/chunk-QGHGJ56O.js.map +0 -1
  184. package/dist/chunk-RU52I7QO.js.map +0 -1
  185. package/dist/chunk-SAZRENEZ.cjs.map +0 -1
  186. package/dist/types-BZgcDsC5.d.cts +0 -42
  187. package/dist/types-BZgcDsC5.d.ts +0 -42
  188. /package/dist/{chunk-JJDLPEWL.js.map → chunk-3LV6RRPH.js.map} +0 -0
package/README.md CHANGED
@@ -119,6 +119,26 @@ editor.commands.acceptChange(id); // rejectChange, acceptAllChanges, rejectAllCh
119
119
  - **Serializers** render tracked content as if all changes were accepted: `getEditorText`, `htmlToPlainText` and `getEditorMarkdown` drop deleted text and use the new formatting and block types by default. Pass `{ trackedChanges: "original" }` (as if rejected) or `"all"` (both). `resolveTrackedChanges(doc, view)` returns a resolved document. TipTap's own `editor.getText()` still includes deleted text.
120
120
  - Both extensions are also available as `createRichTextPreset({ comments: true, trackChanges: true })`. Mark styles use Tailwind classes; override them via `commentClass` / `activeClass` and `insertionHTMLAttributes` / `deletionHTMLAttributes` / `formatChangeHTMLAttributes` / `blockChangeClass`.
121
121
 
122
+ ## Find and replace
123
+
124
+ ```ts
125
+ import { createRichTextPreset, getFindReplaceState } from "@pixerate/editor";
126
+
127
+ const editor = new Editor({ extensions: [...createRichTextPreset({ findReplace: true })] }); // or FindReplace.configure({...})
128
+
129
+ editor.commands.setSearch({ query: "([A-Z][a-z]@), ([A-Z][a-z]@)", replacement: "\\2 \\1", options: { wildcards: true } });
130
+ editor.commands.findNext(); // findPrevious, replaceCurrent, replaceAll (one undo step), clearSearch
131
+ editor.commands.openFindReplace({ mode: "replace" }); // what Mod+F / Mod+H do; closeFindReplace hides it
132
+ getFindReplaceState(editor); // { open, mode, query, replacement, options, matches, currentIndex, error, truncated, lastReplaceCount }
133
+ ```
134
+
135
+ - **Options** (`FindOptions`): `matchCase`, `wholeWord`, `matchPrefix`, `matchSuffix`, `ignorePunctuation`, `ignoreWhitespace`, `wildcards`, `regex`, `literal` (no `^` codes).
136
+ - **Special characters** (Word): `^p` paragraph mark (a pattern with `^p` reads sibling paragraphs together; `^p` in the replacement splits the paragraph), `^t` tab, `^l` line break, `^?` any character, `^#` digit, `^$` letter, `^w` white space, `^^` caret, `^+` / `^=` em / en dash, `^s` non-breaking space, `^13` / `^u8212` character codes. In Replace with: `^&` found text, `^c` clipboard (`getClipboardText` option).
137
+ - **Wildcards** (Word): `?`, `*` (shortest run within a paragraph), `[a-z]`, `[!x]`, `{n}`, `{n,}`, `{n,m}`, `@` (one or more), `<` / `>` word start / end, `( )` groups used as `\1`–`\9` in Replace with. Wildcard search always matches case.
138
+ - **Bounded**: special and wildcard patterns run on a linear-time matcher (no catastrophic backtracking); every search also has a time budget (`timeLimitMs`, 250 ms) and match cap (`maxMatches`, 10 000), reported as `truncated`. Raw `regex` mode uses JavaScript's `RegExp`, rejects nested repetition such as `(a+)+` and is only bounded between matches. Invalid patterns are reported as `error: { code, message }`, never thrown.
139
+ - **Track changes**: tracked-deleted text is skipped (a match never spans a deletion, but word boundaries see the live text), and replacements made while tracking become tracked deletions and insertions.
140
+ - Matches are decorated with `pixerate-find-match` and the current one also with `pixerate-find-match-current` (`matchClass` / `currentMatchClass`); style them in your CSS. `createFindReplaceCommands(editor)` returns command-registry specs (`findReplace.open`, `.next`, `.find`, `.replaceAll`, …).
141
+
122
142
  ## File import and rich clipboard
123
143
 
124
144
  An import adapter registry maps file types (MIME types and extensions) to converters. Built-in `document` adapters cover markdown (`.md`, `.markdown`, `.mdown`, `.mkdn`, `.mkd`, `.Rmd`, `.md.txt`), HTML (sanitized with the same allowlist as markdown), plain text (`.txt`), RTF (best-effort text extraction) and DOCX (`.docx`, `.docm`, `.dotx`). The longest matching extension wins, then exact MIME types, then `priority`, then the most recent registration.
@@ -169,6 +189,84 @@ await registry.callTool("insert_text", { text: "Hello" }); // { content: [...],
169
189
 
170
190
  `createEditorMcpToolProvider` exposes the static tools (`get_editor_state`, `edit_canvas`, `edit_prompt`, `view_editor`) through the registry, and `planToolCall` turns any tool call into planner steps (`step:tool_call` for registry tools).
171
191
 
192
+ ## Commands, shortcuts and the command palette
193
+
194
+ `@pixerate/editor/commands` gives every action one id that menus, keyboard shortcuts, the command palette and agents all run, so they share enablement and parameter validation. It has no third-party imports.
195
+
196
+ ```ts
197
+ import {
198
+ CommandRegistry,
199
+ createHistoryCommands,
200
+ createArrangeCommands,
201
+ createCommandToolProvider,
202
+ formatShortcut,
203
+ } from "@pixerate/editor/commands";
204
+
205
+ const commands = new CommandRegistry({ history }); // `history` makes `undoable` commands one undo step
206
+ commands.register(createHistoryCommands(history)); // history.undo (Mod+Z), history.redo (Mod+Shift+Z, Mod+Y off macOS)
207
+ commands.register(createArrangeCommands(arrange, { getSelectedIds })); // canvas.alignLeft (Alt+A), … scoped to "canvas"
208
+ commands.register({
209
+ id: "doc.insertDate",
210
+ label: "Insert Date",
211
+ group: "Insert",
212
+ shortcut: "Mod+Shift+D",
213
+ params: { type: "object", properties: { format: { type: "string" } } },
214
+ enabled: (ctx) => (ctx.editable ? true : "The document is read-only."),
215
+ run: (ctx, { format }) => ctx.insertDate(format),
216
+ });
217
+
218
+ commands.execute("doc.insertDate", { fmt: "iso" });
219
+ // { ok: false, code: "invalid_params", message: 'Unknown parameter "fmt" for command "doc.insertDate"; expected "format".' }
220
+ commands.list({ scope: "canvas" }); // entries with shortcuts, enabled and disabledReason
221
+ window.addEventListener("keydown", (e) => commands.handleKeyDown(e, { scopes: ["canvas"] }));
222
+ formatShortcut("Mod+Shift+Z", "mac"); // "⇧⌘Z" ("Ctrl+Shift+Z" on Windows / Linux)
223
+ ```
224
+
225
+ - **Results**: `execute` never throws. It returns `{ ok: true, result }` or `{ ok: false, code: "unknown_command" | "disabled" | "invalid_params" | "failed", message }`. `enabled(ctx, params)` returns `true` or a reason string, and receives the params the caller passed, so a command that acts on the selection can still run on ids passed in params. `executeAsync` awaits async commands.
226
+ - **Shortcuts** (`registry.shortcuts`, a `ShortcutMap`): chords are platform-neutral (`Mod` is ⌘ on macOS and Ctrl elsewhere) and match `event.code`, so Option/Alt chords work on macOS. Letter keys follow Latin layouts such as AZERTY. Bindings can be scoped (`scope: "canvas"`), and scoped bindings win over global ones. Events from inputs and editors are ignored unless `allowInEditable` is set. `setOverride` / `getOverrides` store user remaps, and `findConflicts(platform)` reports chords that collide on a platform (e.g. `Mod+Y` and `Ctrl+Y` collide off macOS).
227
+ - **Palette**: `rankCommands(query, entries, { recents })` does subsequence fuzzy ranking over label, group, keywords and id. Exact matches come first, and an empty query lists recent commands first. `createCommandPaletteController(registry)` holds the palette state that the React and Svelte `<CommandPalette>` components render.
228
+ - **Agents**: `createCommandToolProvider(registry)` adds `list_commands`, `run_command` and `run_commands` to a `ToolRegistry`, with complete MCP annotations. `run_commands` runs steps in order but is **not atomic**: steps that succeeded before a failure stay applied. To get all-or-nothing behaviour, register one `undoable` command that runs the steps.
229
+
230
+ ## Generation models, planning and provenance
231
+
232
+ `@pixerate/editor/generation` describes generative models as data, checks a request against a model before it is sent, and records what produced each output. It reaches only the grammar and the agent tool-registry modules, so it has no third-party imports.
233
+
234
+ ```ts
235
+ import {
236
+ planGenerationRequest,
237
+ capabilitiesToJsonSchema,
238
+ createGenerationToolProvider,
239
+ createProvenance,
240
+ recreateRequest,
241
+ estimateJobProgress,
242
+ expectedDurationFor,
243
+ type ModelCapabilities,
244
+ } from "@pixerate/editor/generation";
245
+
246
+ const model: ModelCapabilities = {
247
+ id: "img-xl",
248
+ media: "image",
249
+ prompt: { maxLength: 1000 }, // counted after templates/variables resolve (countResolved defaults to true)
250
+ negativePrompt: true,
251
+ aspectRatios: { options: ["9:16", "1:1", "16:9"], default: "1:1" },
252
+ quality: { options: ["draft", "standard", "high"] },
253
+ count: { max: 4, options: [1, 2, 4] },
254
+ references: { roles: { style_ref: { max: 2 }, character_ref: { max: 1 } }, maxTotal: 2 },
255
+ params: { guidance: { type: "number", min: 1, max: 10, step: 0.5 } },
256
+ timing: { baseMs: 12_000, perItemMs: 4_000 },
257
+ };
258
+
259
+ const plan = planGenerationRequest({ aspectRatio: "21:9", count: 3 }, model, { strategy: "downgrade", resolvedPrompt });
260
+ // plan.request: { model: "img-xl", aspectRatio: "16:9", count: 2 }
261
+ // plan.adjustments: [{ field: "aspectRatio", from: "21:9", to: "16:9", kind: "snapped", reason }, { field: "count", … }]
262
+ ```
263
+
264
+ - **Strategies**: `nearest` (default) snaps enum values to the closest option, clamps and rounds ranges, truncates text and reference lists, and drops unsupported fields and references, reporting each change. Ties go to the higher value, or to the lower one with `downgrade`. `upgrade` also breaks ties upwards but does not drop references the model cannot use: that is an error, as is a text-only model given image references. `error` changes nothing and reports every mismatch. Missing required references, unsupported modes (`edit` / `inpaint`) and a model mismatch are always errors. Planning is pure and deterministic.
265
+ - **Schema and agents**: `capabilitiesToJsonSchema(model)` builds a strict request schema (enums, `minimum` / `maximum`, `maxItems`, and descriptions for per-role limits) that `validateJsonSchema` checks. `createGenerationToolProvider({ models, generate, estimate? })` adds `list_models`, `plan_generation` and `estimate_generation` (read-only) and `generate` (destructive) to a `ToolRegistry`. `generate` requires an `idempotencyKey`: a retry with the same key and arguments returns the first result (or joins it while it is still running) instead of generating again. `mergeCapabilities(remote, overlay)` keeps server capability values and takes only presentation fields (`name`, `description`, `badges`, `icon`, `group`) and overlay-only models from the overlay.
266
+ - **Provenance**: `createProvenance({ rawPrompt, templates, bindings, values, model, plan })` pins the exact bodies and versions of every template the prompt reached, and records the variable values, the resolved prompt with its source map, the planned settings, adjustments and references. `serializeProvenance` / `parseProvenance` are versioned. `recreateRequest(provenance)` returns the request that was sent, and `replayProvenancePrompt` re-resolves the prompt from the pinned bodies.
267
+ - **Lookups and progress**: `createBatchResolver({ fetch, maxBatch, delayMs })` batches and dedupes lookups; a failed batch resolves its keys as empty instead of retrying. `estimateJobProgress({ startedAt, now, expectedMs })` returns a time-based percent that stays at or below 95 until `done`, and a label such as `~2m left` or `Almost done…`. `expectedDurationFor(model, request)` uses the model's `timing`.
268
+ - **Forms**: `getGenerationControls(model)` and `getReferenceSlots(model, request)` describe the controls that the React and Svelte `<GenerationSettings>` render.
269
+
172
270
  ### Document structure and autocorrect
173
271
 
174
272
  Opt-in preset options add Word/Pages-style document structure:
@@ -195,13 +293,88 @@ Each module is also available as a subpath import:
195
293
  | `@pixerate/editor/grammar` | Tokenizer, resolver, sourcemaps. No TipTap or DOM dependencies, so it is safe for Node.js, workers and serverless functions. |
196
294
  | `@pixerate/editor/serializers` | Plain text, markdown and mention serializers |
197
295
  | `@pixerate/editor/extensions` | Headless TipTap extensions |
198
- | `@pixerate/editor/history` | `HistoryManager`: a command-pattern undo/redo stack |
296
+ | `@pixerate/editor/history` | `HistoryManager`: a command-pattern undo/redo stack with gesture merge keys, undo marks, a byte budget and a History-panel listing; `DirtyTracker` / `bindDirtyToHistory` |
199
297
  | `@pixerate/editor/spreadsheet` | `SpreadsheetController`, formula parser/evaluator and dependency graph, formula function registry (`registerFormulaFunction`, `listFormulaFunctions`, `searchFormulaFunctions`, `getFormulaSignature`) and autocomplete helpers (`getFormulaHints`, `applyFormulaSuggestion`) |
200
298
  | `@pixerate/editor/image-editor` | `ImageEditorController` and Canvas 2D renderer: crop, adjustments, annotations, depth masks and AI hooks. Needs a DOM/canvas environment. |
201
299
  | `@pixerate/editor/canvas` | Node-graph layout (needs the optional `@dagrejs/dagre` peer), placement, trajectories, displacement, clipboard and streaming layout helpers |
202
300
  | `@pixerate/editor/agent` | MCP tool schemas, a planner for agent-driven canvas edits, and the dynamic `ToolRegistry` with editor/spreadsheet tool providers |
301
+ | `@pixerate/editor/commands` | `CommandRegistry`, `ShortcutMap`, palette ranking, `createCommandToolProvider` and built-in history / arrange / spreadsheet commands (no third-party imports) |
302
+ | `@pixerate/editor/persistence` | Autosave and crash recovery (`createAutosaveController`, `createRecoveryPolicy`, `createRevisionTracker`), storage adapters (memory, localStorage, IndexedDB, OPFS) and document migration (`migrateDocument`, `loadSections`, `LoadReport`) (no third-party imports) |
303
+ | `@pixerate/editor/generation` | Model capability descriptors, `planGenerationRequest`, `capabilitiesToJsonSchema`, `createGenerationToolProvider`, provenance, `createBatchResolver` and job-progress estimates (no third-party imports) |
304
+
305
+ The root entry (`@pixerate/editor`) re-exports everything except canvas, agent, commands, persistence, generation and image editor, including TipTap. Import from the narrowest subpath you need: `grammar`, `history`, `spreadsheet`, `commands`, `persistence` and `image-editor` have no third-party imports, so a spreadsheet-only app bundles no TipTap, markdown or dagre code. The package is marked `sideEffects: false`, so unused root exports are also tree-shaken.
306
+
307
+ ## Autosave, crash recovery and document migration
308
+
309
+ ```ts
310
+ import {
311
+ createAutosaveController,
312
+ createIndexedDbAdapter,
313
+ createRevisionTracker,
314
+ formatRecoveryMessage,
315
+ } from "@pixerate/editor/persistence";
316
+
317
+ const revisions = createRevisionTracker({ history, dirty: bindDirtyToHistory({ history, getValue }) });
318
+ const autosave = createAutosaveController({
319
+ storage: createIndexedDbAdapter("my-app"), // or createLocalStorageAdapter / createOpfsAdapter / createMemoryStorage
320
+ key: "doc-42",
321
+ getSnapshot: () => doc,
322
+ revision: revisions.revision,
323
+ savedRevision: revisions.savedRevision,
324
+ name: "Quarterly plan",
325
+ });
326
+ revisions.subscribe(() => autosave.notify());
327
+
328
+ const recovered = await autosave.checkRecovery(); // { snapshot, meta: { time, name, revision } } | null
329
+ if (recovered && confirm(formatRecoveryMessage(recovered.meta))) load(recovered.snapshot);
330
+ else if (recovered) await autosave.discardRecovery();
331
+ autosave.ready(); // hydration gate: nothing is written before this
332
+ addEventListener("pagehide", () => autosave.flushNow());
333
+ // after an explicit save: autosave.markClean() retires the snapshot, but only one this session wrote
334
+ ```
335
+
336
+ Snapshots are written `debounceMs` after the last edit and at least every `interval` while edits keep coming; failed writes retry with exponential backoff (`retryBaseMs` doubling to `retryMaxMs`). `createRecoveryPolicy` is the pure, clock-injected state machine behind it.
337
+
338
+ `migrateDocument(json, { currentVersion, steps: { 1: (doc) => … } })` upgrades stored JSON one version at a time and returns a `LoadReport`: `migratedFrom`, `readOnly` (a newer version, or a failed step: show it but never save over it) and `unknownKeys`. `loadSections(doc, { key: { parse, defaults } })` falls back to defaults one section at a time and keeps unknown keys; `loadDocument` does both. `loadImageEditorDocument(json)` (`@pixerate/editor/image-editor`) applies them to `SerializedImageEditorState`.
339
+
340
+ ## History gestures, marks and dirty tracking
341
+
342
+ ```ts
343
+ import { HistoryManager, bindDirtyToHistory } from "@pixerate/editor/history";
344
+
345
+ const history = new HistoryManager({ maxDepth: 200, maxBytes: 32 << 20, sizeOf: (c) => c.byteSize ?? 0 });
346
+
347
+ // A drag is one undo step, however long it lasts. Any other recorded command,
348
+ // undo, redo or endGesture() ends it.
349
+ history.executeMerged(setOpacity(0.4), { key: "opacity-slider" });
350
+ history.executeMerged(setOpacity(0.5), { key: "opacity-slider" });
351
+
352
+ // Hold a gesture: every merged execute until endGesture() joins one step.
353
+ history.beginGesture("automation-pass");
354
+ /* ... */
355
+ history.endGesture();
356
+
357
+ // Escape-cancel: take back everything since the drag started, with no redo left behind.
358
+ const mark = history.mark();
359
+ /* ... drag ... */
360
+ history.revertTo(mark); // returns the number of steps reverted (0 if the mark is gone)
361
+
362
+ // History panel
363
+ history.getEntries(); // [{ id, label, index, current, undone }]
364
+ history.goTo(2); // undo / redo until entry 2 is current; goTo(-1) undoes everything
365
+
366
+ // Dirty vs saved: undoing back to the saved state is clean again.
367
+ const dirty = bindDirtyToHistory({
368
+ history,
369
+ getValue: () => doc,
370
+ normalize: ({ selection, viewport, ...rest }) => rest, // selection/zoom never dirty
371
+ });
372
+ dirty.suppress(() => loadFromDisk()); // programmatic loads don't mark dirty (re-entrant)
373
+ dirty.markClean(); // after saving
374
+ dirty.dirty; // feed to useNavigationGuard / createNavigationGuard
375
+ ```
203
376
 
204
- The root entry (`@pixerate/editor`) re-exports everything except canvas, agent and image editor, including TipTap. Import from the narrowest subpath you need: `grammar`, `history`, `spreadsheet` and `image-editor` have no third-party imports, so a spreadsheet-only app bundles no TipTap, markdown or dagre code. The package is marked `sideEffects: false`, so unused root exports are also tree-shaken.
377
+ Tag-only `executeMerged(cmd)` keeps its 300 ms time-window coalescing. React exposes the same API through `useHistory()` (`entries`, `goTo`, `mark`, `revertTo`, `beginGesture`, `endGesture`) and `useHistoryDirty()`; Svelte through `createHistory()` and `createHistoryDirty()`.
205
378
 
206
379
  ## License
207
380
 
@@ -0,0 +1,139 @@
1
+ # Third-Party Notices
2
+
3
+ Portions of this repository are ported from third-party source code. Each ported file carries a
4
+ header comment naming its source. The ported algorithms were re-expressed in TypeScript; test cases
5
+ were re-expressed as vitest cases rather than copied.
6
+
7
+ All sources below are from the ArtCraft "craft" apps, each licensed **MIT OR Apache-2.0**,
8
+ © 2026 ArtCraft Team and the respective app's contributors. They are used here under the MIT licence
9
+ reproduced at the end of this file. Paths are relative to each source repository.
10
+
11
+ ## craft-libs
12
+
13
+ © 2026 ArtCraft Team and the craft-libs contributors.
14
+
15
+ - `crates/settings/src/migrate.rs` (`upgrade`: step-wise migration to a target version, missing version taken as current, newer version left alone) → `migrateDocument` in `packages/core/src/persistence/migrate.ts`
16
+ - `crates/settings/src/document.rs` (`deserialize_sections`: per-section fallback to defaults) → `loadSections` in `packages/core/src/persistence/migrate.ts`
17
+ - `crates/settings/src/error.rs` (`LoadReport`, `has_problems`, `SectionError`, read-only reasons) → `packages/core/src/persistence/migrate.ts`
18
+ - `crates/settings/src/store.rs` (debounced save, `retry_delay` exponential backoff with cap) → `packages/core/src/persistence/autosave.ts`, `exponentialBackoff` in `packages/core/src/persistence/recoveryPolicy.ts`
19
+ - Test cases from `crates/settings/tests/migrate.rs`, `tests/recovery.rs` and `tests/store.rs`, re-expressed → `packages/core/tests/persistence-migrate.test.ts`, `packages/core/tests/persistence-policy.test.ts`
20
+
21
+ ## figcraft
22
+
23
+ © 2026 ArtCraft Team and the FigCraft contributors.
24
+
25
+ - `crates/engine/src/distance.rs` (`between`, `inside`, `to_guide`, `label`) → `packages/core/src/canvas/measure.ts`
26
+ - `crates/engine/src/measure_tests.rs` (test cases re-expressed) → `packages/core/tests/canvas-measure.test.ts`
27
+ - `crates/layout/src/lib.rs` (`layout_frame`, `distribute`, `main_offsets`, `fit_hug_text`, `enforce_limits`, `item_baseline`, `resize_child`, `grid_place`, `resolve_tracks`, `track_content`, `grid_geometry`, `layout_grid`, `apply_constraints`, `layout_page`, `layout_up`, `coherent_sizing`, `clamp_size`) → `packages/core/src/canvas/autoLayout.ts`
28
+ - `crates/layout/src/{tests.rs,behaviour_tests.rs,nested_tests.rs,content_align_tests.rs}` (test cases re-expressed) → `packages/core/tests/canvas-auto-layout.test.ts`
29
+
30
+ ## filmcraft
31
+
32
+ © 2026 ArtCraft Team and the FilmCraft contributors.
33
+
34
+ - `crates/engine/src/lib.rs` (`Session::edit_merged`, `undo_mark`, `revert_to`) → `packages/core/src/history/HistoryManager.ts`
35
+ - `crates/engine/src/commands.rs` (command specs, lookup by id, enablement with reasons) → `packages/core/src/commands/registry.ts`
36
+ - `crates/engine/src/lib.rs` (`Session::execute`, `enabled_for`: re-checking enablement against targets named in params, catching command failures) → `packages/core/src/commands/registry.ts`
37
+ - `crates/engine/src/shortcuts.rs` and `crates/engine/src/shortcuts_tests.rs` (platform-neutral chords, `Mod` resolution per platform, scoped bindings, conflict detection, per-platform conflict test) → `packages/core/src/commands/chords.ts`, `packages/core/src/commands/shortcuts.ts`, `packages/core/tests/commands.test.ts`
38
+ - `crates/automation/src/server.rs` (`command_list` / `command_run` / `command_batch` tools, complete MCP annotations, unknown-argument errors naming the accepted keys) → `packages/core/src/commands/agentTools.ts`, `packages/core/src/commands/registry.ts`
39
+ - `apps/filmcraft-web/src/recovery_policy.rs` (`Policy`, `Action`, `tick` / `written` / `cleaned`: snapshot at most once per interval and revision, retry failed writes, retire only this session's snapshot) → `packages/core/src/persistence/recoveryPolicy.ts`
40
+ - `apps/filmcraft-web/src/recovery.rs` (snapshot-then-dirty-meta writes, recovery check of a dirty meta) → `packages/core/src/persistence/autosave.ts`
41
+ - `apps/filmcraft-web/tests/recovery_policy.rs` (test cases re-expressed) → `packages/core/tests/persistence-policy.test.ts`
42
+
43
+ ## gridcraft
44
+
45
+ © 2026 ArtCraft Team and the GridCraft contributors.
46
+
47
+ - `crates/core/src/parse.rs` (typed cell input parsing) → `packages/core/src/spreadsheet/formula/parse-input.ts`
48
+ - `crates/core/src/date.rs` (1900 date-system serials, days in month, civil-day arithmetic) → `packages/core/src/spreadsheet/formula/parse-input.ts`
49
+ - `crates/core/src/value.rs` (Excel comparison, 15-digit number comparison, sort order, number-to-text coercion) → `packages/core/src/spreadsheet/formula/values.ts`
50
+ - Test cases from the files above, re-expressed → `packages/core/tests/spreadsheet-typed-input.test.ts`
51
+ - `crates/numfmt/src/lib.rs`, `parse.rs`, `render.rs`, `decimal.rs`, `builtin.rs` (Excel number format parsing and rendering, 15-digit decimal rounding, General format, `TEXT()`, built-in formats 0–49, parse cache) and `crates/core/src/date.rs` (serial to date) → `packages/core/src/spreadsheet/numfmt/`
52
+ - `crates/numfmt/src/tests.rs` and `crates/numfmt/tests/poi_number_format_approx.rs` (test cases re-expressed) → `packages/core/tests/number-format.test.ts`, `packages/core/tests/fixtures/poi-number-format-approx.ts`
53
+ **Apache POI.** The rows of `packages/core/tests/fixtures/poi-number-format-approx.ts` (format, value and Excel's
54
+ `TEXT()` result) come from Apache POI's `test-data/spreadsheet/NumberFormatApproxTests.xlsx`, as collected in
55
+ GridCraft. Apache POI is © The Apache Software Foundation, licensed under the Apache License, Version 2.0
56
+ (https://www.apache.org/licenses/LICENSE-2.0).
57
+
58
+ ## lightcraft
59
+
60
+ © 2026 ArtCraft Team and the LightCraft contributors.
61
+
62
+ - `crates/color/src/cct.rs` (Kim et al. Planckian locus, temperature / tint ↔ xy, white-balance matrix) → `packages/core/src/image-editor/color.ts`
63
+ - `crates/color/src/lib.rs` (Bradford adaptation, RGB ↔ XYZ from primaries) → `packages/core/src/image-editor/color.ts`
64
+ - `crates/pipeline/src/tone.rs` (`ToneMap::display` S-curve anchored at 0, grey and 1) → `packages/core/src/image-editor/color.ts`
65
+ - `crates/pipeline/src/local.rs` (relative white balance with neutral-luminance normalisation) → `packages/core/src/image-editor/color.ts`
66
+ - `crates/develop/src/settings.rs` and `docs/process-versions.md` (process version semantics) → `packages/core/src/image-editor/color.ts`
67
+ - `crates/develop/src/controls.rs` (`ControlSpec` table: id, label, section, range, default, step, decimals, track style; clamping) → `packages/core/src/image-editor/controls.ts`
68
+
69
+ ## pdfcraft
70
+
71
+ © 2026 ArtCraft Team and the PDFCraft contributors.
72
+
73
+ - `crates/compare/src/lib.rs` (`diff` with common prefix/suffix stripping, `myers` keeping only the −d..d frontier, patience `anchors` with longest increasing subsequence, recursion depth limit, grouping into inserted/deleted/replaced) → `packages/core/src/compare/diff.ts`
74
+ - Test cases from the file above (`long_documents_with_scattered_edits`, `very_different_documents_finish`, `insertions_deletions_and_replacements`), re-expressed → `packages/core/tests/compare-diff.test.ts`
75
+
76
+ ## photocraft
77
+
78
+ © 2026 ArtCraft Team and the PhotoCraft contributors.
79
+
80
+ - `crates/ops/src/lib.rs` (`History { max_states, max_bytes }`, `trim`, `unique_bytes`) → `packages/core/src/history/HistoryManager.ts`
81
+ - `crates/ui-egui/src/palette.rs` (`fuzzy_score`, `exact_bonus`, `push_recent`) → `packages/core/src/commands/palette.ts`
82
+ - `crates/doc/src/adjust.rs` (`Adjustment` model, `HueRange::neutral` / `weight`, `LevelsChannel` defaults) → `packages/core/src/image-editor/types.ts`, `packages/core/src/image-editor/adjustment-layers.ts`
83
+ - `crates/compose/src/adjust.rs` (`curve_lut`, `levels_q`, `tone_luts_q`, `hue_saturation`, `range_lightness`, `hue_range_tables`, `rgb_to_hsl` / `hsl_to_rgb`, `vibrance_px` / `vibrance_lin`, `color_balance_channel`, `black_white_gray`, `photo_filter_matrix`, `posterize`, Threshold) → `packages/core/src/image-editor/adjustment-layers.ts`
84
+ - `crates/color/src/blend.rs` (separable and non-separable blend modes, hardened `clip_color`, `set_lum`, `composite`) → `packages/core/src/image-editor/blend.ts`
85
+ - `crates/color/src/convert.rs` (`D50`, `SRGB_TO_XYZ_D50`, `XYZ_D50_TO_SRGB`) → `packages/core/src/image-editor/adjustment-layers.ts`
86
+ - `crates/algo/src/histogram.rs` (bounded RGB histogram, transparent exclusion, exact-endpoint clipping) → `packages/core/src/image-editor/histogram.ts`
87
+ - Test cases from `crates/compose/src/adjust.rs`, `crates/compose/src/tests.rs`, `crates/doc/src/adjust.rs`, `crates/color/src/blend.rs` and `crates/algo/src/histogram.rs`, re-expressed → `packages/core/tests/image-editor-adjustment-layers.test.ts`, `packages/core/tests/image-editor-blend-histogram.test.ts`
88
+
89
+ ## soundcraft
90
+
91
+ © 2026 ArtCraft Team and the SoundCraft contributors.
92
+
93
+ - `crates/engine/src/lib.rs` (`Engine::execute_merged`, `hold_merge`, `end_merge`) → `packages/core/src/history/HistoryManager.ts`
94
+ - `crates/engine/src/lib.rs` (`Engine::is_dirty`, `same_document`, `mark_clean`) → `packages/core/src/dirty.ts`
95
+
96
+ ## vectorcraft
97
+
98
+ © 2026 ArtCraft Team and the VectorCraft contributors.
99
+
100
+ - `crates/tools/src/guides.rs` (`snap_scale`) → `packages/core/src/canvas/resizeSnap.ts`
101
+ - `crates/geom/src/snap.rs` (`snap_to_grid`) → `snapToGrid` in `packages/core/src/canvas/guides.ts`
102
+ - `crates/pathops/src/fit.rs` (`fit_cubics`, `fit_single`, `fit_single_from`, `generate`, `newton`, `chord_params`, `is_straight`) → `packages/core/src/canvas/curveFit.ts`
103
+ - `crates/pathops/src/edit.rs` (`simplify_with` corner splitting) → `packages/core/src/canvas/curveFit.ts`
104
+ - Test cases from the files above and `crates/pathops/tests/ops.rs`, re-expressed → `packages/core/tests/canvas-resize-snap.test.ts`, `packages/core/tests/canvas-curve-fit.test.ts`
105
+ ---
106
+
107
+ ## wordcraft
108
+
109
+ © 2026 ArtCraft Team and the WordCraft contributors.
110
+
111
+ - `crates/engine/src/cmd/mailings.rs` (`parse_csv` delimiter auto-detection and blank-row skipping, `table_from_rows`, `header_split` padding / truncation and record caps, `address_map` field aliases) → `packages/core/src/grammar/data-source.ts`
112
+ - `crates/formats/src/sheet.rs` (`field_names`: blank headers as `Column N`, repeated headers numbered) → `packages/core/src/grammar/data-source.ts`
113
+ - `crates/engine/src/cmd/mailings.rs` (`condition` numeric-or-text comparison with `= <> < > <= >=`, `finish` one copy per record with SKIPIF and a merge cap) → `packages/core/src/grammar/variables.ts`, `packages/core/src/grammar/merge.ts`
114
+ - Test cases from `crates/engine/src/cmd/mailings.rs` (`csv_parsing`, `merge_end_to_end`, `recipients_from_fields_and_array_rows`, `skipif_drops_matching_records`, `if_rule_previews_its_result`), re-expressed → `packages/core/tests/mail-merge.test.ts`
115
+ - `crates/engine/src/cmd/tools.rs` (`compare`: block-level diff of case-folded / white-space-collapsed paragraph keys, pairing the i-th deleted with the i-th inserted paragraph for word or character diffs, whole insertions/deletions otherwise, `MAX_CHAR_DIFF` fallback, options not honoured; `tokens`, `word_diff`) → `packages/core/src/compare/documents.ts`
116
+ - `crates/engine/src/cmd/fmt_revisions.rs` (`compare_formatting`: equal text compared for formatting changes) → `packages/core/src/compare/documents.ts`
117
+ - Test cases from `crates/engine/src/cmd/tools.rs` (`compare_marks_changes`, `compare_options_ignore_case_and_white_space`), re-expressed → `packages/core/tests/compare-documents.test.ts`
118
+
119
+ ---
120
+ - `crates/engine/src/cmd/find_pattern.rs` (`compile`, `special`, `wildcards`, `count`, `class`, `find_code` / `replace_code` / `numeric_code`, `parse_replace`, `expand`) → `packages/core/src/extensions/find-replace/pattern.ts`
121
+ - `crates/engine/src/cmd/edit.rs` (`search_full`: live-text haystack with a position map and cuts at tracked deletions, joining paragraphs only for `^p`, stepping past empty matches, match cap; `replace_all`: last-to-first, tracked path, paragraph splits) → `packages/core/src/extensions/find-replace/search.ts`, `packages/core/src/extensions/find-replace/extension.ts`
122
+ - `crates/engine/src/tests.rs` (`find_replace`, `find_replace_special_codes`, `find_replace_wildcards`, `hostile_find_patterns_are_bounded`, `replace_under_track_changes_skips_deleted_text`, `find_reads_the_text_around_tracked_deletions`, `replace_all_is_tracked`, `replace_all_splits_paragraphs_at_a_paragraph_mark_in_the_replacement`; re-expressed) → `packages/core/tests/find-replace.test.ts`
123
+
124
+ ## MIT License
125
+
126
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
127
+ associated documentation files (the "Software"), to deal in the Software without restriction,
128
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
129
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
130
+ furnished to do so, subject to the following conditions:
131
+
132
+ The above copyright notice and this permission notice shall be included in all copies or
133
+ substantial portions of the Software.
134
+
135
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
136
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
137
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
138
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
139
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.