@pixerate/editor 0.23.0 → 0.25.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 (187) hide show
  1. package/README.md +167 -2
  2. package/THIRD_PARTY_NOTICES.md +80 -0
  3. package/dist/agent/index.cjs +10 -8
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +4 -3
  6. package/dist/agent/index.d.ts +4 -3
  7. package/dist/agent/index.js +4 -2
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +4437 -411
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +934 -4
  12. package/dist/canvas/index.d.ts +934 -4
  13. package/dist/canvas/index.js +4309 -283
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/chunk-4BVP64JJ.cjs +1406 -0
  16. package/dist/chunk-4BVP64JJ.cjs.map +1 -0
  17. package/dist/chunk-664LQYFJ.js +115 -0
  18. package/dist/chunk-664LQYFJ.js.map +1 -0
  19. package/dist/chunk-7WISYZFO.cjs +740 -0
  20. package/dist/chunk-7WISYZFO.cjs.map +1 -0
  21. package/dist/chunk-AL6JMGWW.cjs +783 -0
  22. package/dist/chunk-AL6JMGWW.cjs.map +1 -0
  23. package/dist/chunk-BVP3TSLH.cjs +69 -0
  24. package/dist/chunk-BVP3TSLH.cjs.map +1 -0
  25. package/dist/{chunk-RIKBJLS2.cjs → chunk-C7P22VIP.cjs} +2001 -279
  26. package/dist/chunk-C7P22VIP.cjs.map +1 -0
  27. package/dist/chunk-D6FH2YAX.cjs +131 -0
  28. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  29. package/dist/chunk-ECCCYKGL.js +740 -0
  30. package/dist/chunk-ECCCYKGL.js.map +1 -0
  31. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  32. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  33. package/dist/{chunk-QBJ22YZR.js → chunk-FH6F67DN.js} +1999 -277
  34. package/dist/chunk-FH6F67DN.js.map +1 -0
  35. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  36. package/dist/chunk-FPENXAEJ.js.map +1 -0
  37. package/dist/chunk-HLIMTC6N.js +1 -0
  38. package/dist/chunk-HLIMTC6N.js.map +1 -0
  39. package/dist/chunk-HRMEDT2L.js +1406 -0
  40. package/dist/chunk-HRMEDT2L.js.map +1 -0
  41. package/dist/{chunk-QGHGJ56O.js → chunk-IXHSYOVY.js} +2518 -122
  42. package/dist/chunk-IXHSYOVY.js.map +1 -0
  43. package/dist/chunk-MKA3AHLV.cjs +256 -0
  44. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  45. package/dist/chunk-NAXVVS5I.js +783 -0
  46. package/dist/chunk-NAXVVS5I.js.map +1 -0
  47. package/dist/chunk-O5RYH2PG.js +69 -0
  48. package/dist/chunk-O5RYH2PG.js.map +1 -0
  49. package/dist/chunk-OXSV64JX.js +689 -0
  50. package/dist/chunk-OXSV64JX.js.map +1 -0
  51. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  52. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  53. package/dist/chunk-P6ODPZ4W.js +256 -0
  54. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  55. package/dist/chunk-PL4JB3BF.cjs +1 -0
  56. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  57. package/dist/{chunk-GVSMRPHB.cjs → chunk-PO6A6QQH.cjs} +2607 -211
  58. package/dist/chunk-PO6A6QQH.cjs.map +1 -0
  59. package/dist/{chunk-JJDLPEWL.js → chunk-QGYDWPRM.js} +2 -2
  60. package/dist/chunk-RR7F5GUM.js +5726 -0
  61. package/dist/chunk-RR7F5GUM.js.map +1 -0
  62. package/dist/chunk-TNNJXF46.cjs +115 -0
  63. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  64. package/dist/chunk-U5HZSFCS.js +131 -0
  65. package/dist/chunk-U5HZSFCS.js.map +1 -0
  66. package/dist/{chunk-MJZBNWG7.js → chunk-UAKDG62Z.js} +309 -581
  67. package/dist/chunk-UAKDG62Z.js.map +1 -0
  68. package/dist/{chunk-CL37JGFW.cjs → chunk-V3CHHWOE.cjs} +4 -4
  69. package/dist/{chunk-CL37JGFW.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
  70. package/dist/chunk-WSQKHXW7.cjs +5726 -0
  71. package/dist/chunk-WSQKHXW7.cjs.map +1 -0
  72. package/dist/{chunk-AA4ZEVCC.cjs → chunk-X4N4WUKB.cjs} +336 -608
  73. package/dist/chunk-X4N4WUKB.cjs.map +1 -0
  74. package/dist/commands/index.cjs +74 -1
  75. package/dist/commands/index.cjs.map +1 -1
  76. package/dist/commands/index.d.cts +38 -353
  77. package/dist/commands/index.d.ts +38 -353
  78. package/dist/commands/index.js +73 -0
  79. package/dist/commands/index.js.map +1 -1
  80. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  81. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  82. package/dist/compare/index.cjs +15 -0
  83. package/dist/compare/index.cjs.map +1 -0
  84. package/dist/compare/index.d.cts +58 -0
  85. package/dist/compare/index.d.ts +58 -0
  86. package/dist/compare/index.js +15 -0
  87. package/dist/compare/index.js.map +1 -0
  88. package/dist/controller-BIJiNoVs.d.cts +1842 -0
  89. package/dist/controller-BIJiNoVs.d.ts +1842 -0
  90. package/dist/documents-CHT1U5Vs.d.ts +86 -0
  91. package/dist/documents-Gp7wb0Me.d.cts +86 -0
  92. package/dist/extensions/index.cjs +100 -5
  93. package/dist/extensions/index.cjs.map +1 -1
  94. package/dist/extensions/index.d.cts +16 -4
  95. package/dist/extensions/index.d.ts +16 -4
  96. package/dist/extensions/index.js +100 -5
  97. package/dist/generation/index.cjs +2317 -0
  98. package/dist/generation/index.cjs.map +1 -0
  99. package/dist/generation/index.d.cts +1015 -0
  100. package/dist/generation/index.d.ts +1015 -0
  101. package/dist/generation/index.js +2317 -0
  102. package/dist/generation/index.js.map +1 -0
  103. package/dist/grammar/index.cjs +48 -3
  104. package/dist/grammar/index.cjs.map +1 -1
  105. package/dist/grammar/index.d.cts +322 -100
  106. package/dist/grammar/index.d.ts +322 -100
  107. package/dist/grammar/index.js +54 -9
  108. package/dist/image-editor/index.cjs +4392 -481
  109. package/dist/image-editor/index.cjs.map +1 -1
  110. package/dist/image-editor/index.d.cts +1249 -97
  111. package/dist/image-editor/index.d.ts +1249 -97
  112. package/dist/image-editor/index.js +4281 -370
  113. package/dist/image-editor/index.js.map +1 -1
  114. package/dist/index-7a6bHTLy.d.ts +1754 -0
  115. package/dist/index-Csd5qI8u.d.cts +1754 -0
  116. package/dist/index.cjs +1682 -99
  117. package/dist/index.cjs.map +1 -1
  118. package/dist/index.d.cts +118 -21
  119. package/dist/index.d.ts +118 -21
  120. package/dist/index.js +1630 -47
  121. package/dist/index.js.map +1 -1
  122. package/dist/mention-text-CDnCFd3c.d.cts +53 -0
  123. package/dist/mention-text-CDnCFd3c.d.ts +53 -0
  124. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  125. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  126. package/dist/model-C1zApQM8.d.cts +219 -0
  127. package/dist/model-C1zApQM8.d.ts +219 -0
  128. package/dist/persistence/index.cjs +676 -0
  129. package/dist/persistence/index.cjs.map +1 -0
  130. package/dist/persistence/index.d.cts +230 -0
  131. package/dist/persistence/index.d.ts +230 -0
  132. package/dist/persistence/index.js +676 -0
  133. package/dist/persistence/index.js.map +1 -0
  134. package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
  135. package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.cts} +12 -1
  136. package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.ts} +12 -1
  137. package/dist/registry-D32LpIc4.d.cts +354 -0
  138. package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
  139. package/dist/registry-evKTP4ls.d.ts +354 -0
  140. package/dist/schema-DYDGdYL9.d.cts +41 -0
  141. package/dist/schema-DYDGdYL9.d.ts +41 -0
  142. package/dist/serializers/index.cjs +12 -3
  143. package/dist/serializers/index.cjs.map +1 -1
  144. package/dist/serializers/index.d.cts +40 -12
  145. package/dist/serializers/index.d.ts +40 -12
  146. package/dist/serializers/index.js +12 -3
  147. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  148. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  149. package/dist/spreadsheet/index.cjs +161 -3
  150. package/dist/spreadsheet/index.cjs.map +1 -1
  151. package/dist/spreadsheet/index.d.cts +611 -20
  152. package/dist/spreadsheet/index.d.ts +611 -20
  153. package/dist/spreadsheet/index.js +168 -10
  154. package/dist/storage-BpFfspHm.d.cts +120 -0
  155. package/dist/storage-BpFfspHm.d.ts +120 -0
  156. package/dist/tokens/index.cjs +115 -0
  157. package/dist/tokens/index.cjs.map +1 -0
  158. package/dist/tokens/index.d.cts +212 -0
  159. package/dist/tokens/index.d.ts +212 -0
  160. package/dist/tokens/index.js +115 -0
  161. package/dist/tokens/index.js.map +1 -0
  162. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  163. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  164. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-D_VAPELi.d.cts} +110 -144
  165. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-Db_fGEvx.d.ts} +110 -144
  166. package/package.json +21 -1
  167. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  168. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  169. package/dist/chunk-AXV2ZW34.cjs +0 -118
  170. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  171. package/dist/chunk-B2ZRLN4S.js +0 -118
  172. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  173. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  174. package/dist/chunk-K3R5NG2D.js +0 -3100
  175. package/dist/chunk-K3R5NG2D.js.map +0 -1
  176. package/dist/chunk-MJZBNWG7.js.map +0 -1
  177. package/dist/chunk-OYXSKQFR.js.map +0 -1
  178. package/dist/chunk-QBJ22YZR.js.map +0 -1
  179. package/dist/chunk-QGHGJ56O.js.map +0 -1
  180. package/dist/chunk-RIKBJLS2.cjs.map +0 -1
  181. package/dist/chunk-XDMXXC4Z.cjs +0 -3100
  182. package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
  183. package/dist/controller-pePCJLT1.d.cts +0 -959
  184. package/dist/controller-pePCJLT1.d.ts +0 -959
  185. package/dist/index-CCr_z9kF.d.cts +0 -899
  186. package/dist/index-Ck6ajwXh.d.ts +0 -899
  187. /package/dist/{chunk-JJDLPEWL.js.map → chunk-QGYDWPRM.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.
@@ -207,6 +227,46 @@ formatShortcut("Mod+Shift+Z", "mac"); // "⇧⌘Z" ("Ctrl+Shift+Z" on Windows /
207
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.
208
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.
209
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
+
210
270
  ### Document structure and autocorrect
211
271
 
212
272
  Opt-in preset options add Word/Pages-style document structure:
@@ -224,6 +284,76 @@ const extensions = createRichTextPreset({
224
284
 
225
285
  `Autocorrect` takes a `rules` list (`defaultAutocorrectRules`, or compose `smartQuoteRules`, `symbolRules`, `fractionRules`, `capitalizeSentenceRules`, `twoInitialCapitalsRules`, `smilieAutocorrectRules` and your own `{ id, find, replace }` rules). Undo right after a correction restores what was typed; nothing is corrected in code or inside prompt tokens. It replaces the Typography extension in the preset. `getDocumentStructureCommandItems(editor, query)` returns ready-made slash-menu/toolbar items for the installed extensions.
226
286
 
287
+ ## Image selections and masks
288
+
289
+ `@pixerate/editor/image-editor` has Photoshop-style selections. A selection is a `SelectionMask`: one 8-bit coverage value per source pixel (natural image pixels, the same space as the crop box), with `bounds`, treated as immutable.
290
+
291
+ ```ts
292
+ const editor = new ImageEditorController({ image });
293
+ await editor.magicWand({ x: 120, y: 80 }, { tolerance: 32, contiguous: true, mode: "add" });
294
+ editor.selectEllipse({ x: 40, y: 40, width: 200, height: 120 }, { mode: "subtract", feather: 2 });
295
+ editor.modifySelection("grow", 4); // grow | shrink | feather | border | smooth
296
+
297
+ // A coarse AI segmentation (any size, white = selected), snapped to the image's edges.
298
+ await editor.loadSelectionFromImage(maskDataUrl);
299
+ await editor.refineSelection({ radius: 12, smartRadius: true, contrast: 20 });
300
+
301
+ // Use it as an adjustment layer mask; density and feather stay editable.
302
+ const id = editor.addAdjustmentLayer("hueSaturation");
303
+ editor.applySelectionAsLayerMask(id, { feather: 3 });
304
+ editor.setAdjustmentLayerMaskProps(id, { density: 0.6 });
305
+ ```
306
+
307
+ - **Tools**: `magicWand` (scanline span fill with an explicit stack; RGB or perceptual ΔE tolerance; contiguous or global; optional `sampleMerged`), `colorRange` (samples, fuzziness, localized clusters), `toneRange`, `hueRange`, `rectSelection`, `ellipseSelection`, `polygonSelection` (anti-aliased).
308
+ - **Modes**: `replace`, `add` (max), `subtract`, `intersect` (min), on 8-bit values; Shift / Alt map to add / subtract (`selectionModeFromModifiers`).
309
+ - **Modify**: grow / shrink through an exact Euclidean distance transform, border, smooth (box + threshold), feather (Gaussian, σ = radius, three box passes above σ 8). All linear in the pixel count.
310
+ - **Refine Edge**: `refineSelection(pixels, mask, { radius, smartRadius, smooth, feather, contrast, shiftEdge })`, a colour guided filter in a band around the edge, confined to the mask's bounds plus a halo.
311
+ - **Layer masks**: `AdjustmentLayer.mask` (`{ width, height, rle, density?, feather?, enabled? }`) scales the layer's opacity per pixel; it is resampled through the crop / rotation / flips like the image. `selectionFromLayerMask` loads it back.
312
+ - **Document**: `toJSON()` adds `selection` (run-length encoded, `r1:` + base64) only when something is selected, and layers only carry `mask` when they have one, so selection- and mask-free documents serialize exactly as before. Selections are part of undo history. `renderMask()` / `toMaskDataURL()` include the selection for inpainting.
313
+ - **Viewport**: marching ants by default, `quickMask` tints the unselected area; `RenderOptions.selectionDisplay` overrides, `draftSelectionPath` shows a shape being drawn. `SelectionGesture` turns pointer events into selections for UIs.
314
+ - **Agent tools**: `createImageEditorToolProvider` adds `get_selection`, `select_region`, `select_by_color`, `modify_selection`, `refine_selection` and `apply_selection_as_mask`.
315
+ - **Pixels**: the tools read the source image through a canvas; pass `setSamplePixels(imageData)` (or `{ pixels }` per call) where there is no canvas or the image is cross-origin.
316
+
317
+ ## Spreadsheet sheets, defined names and LAMBDA
318
+
319
+ A `SpreadsheetController` is a workbook. Created from one `document` it has a single sheet and behaves as before (`controller.document` is the active sheet); pass `workbook` for several sheets.
320
+
321
+ ```ts
322
+ import { SpreadsheetController } from "@pixerate/editor/spreadsheet";
323
+
324
+ const sheet = new SpreadsheetController({ document: { name: "Sales" } });
325
+ sheet.addSheet({ name: "Summary" }); // activates it; returns its id
326
+ sheet.defineName("Revenue", "=Sales!$F$2:$F$11");
327
+ sheet.defineName("WithTax", "=LAMBDA(x, x*1.2)");
328
+ sheet.setCellValue(sheet.document.rows[0].id, sheet.document.columns[0].id, "=WithTax(SUM(Revenue))");
329
+ sheet.renameSheet("Sales", "Sales 2026"); // formulas become ='Sales 2026'!…
330
+ sheet.getWorkbook(); // { sheets, activeSheetId, names } to save
331
+ ```
332
+
333
+ Formulas reference other sheets (`Sheet2!A1`, `'My Sheet'!A1:B2`, `Data!A1#`) and 3-D ranges (`SUM(Sheet1:Sheet3!A1)`). Deleting a sheet turns references to it into `#REF!`; inserting or deleting rows and columns rewrites references from every sheet. Names can be workbook- or sheet-scoped (`{ sheet }`), are validated like Excel (`validateDefinedName`), and `renameName` rewrites the formulas using them. `LAMBDA` values can be called inline (`=LAMBDA(x, x+1)(2)`), stored with `LET` or a name (recursion is capped at 1000 calls with `#NUM!`), or passed to `MAP`, `REDUCE`, `SCAN`, `BYROW`, `BYCOL` and `MAKEARRAY`. Undo restores the sheet a step changed (switching to it) or, for sheet and name operations, the whole workbook.
334
+
335
+ ## Spreadsheet conditional formatting and data validation
336
+
337
+ Rules live in the sheet's `document.table` (`conditionalFormats`, highest priority first, and `dataValidations`), so they are saved, undone and redone with the sheet, and their ranges and formulas follow inserted and deleted rows and columns.
338
+
339
+ ```ts
340
+ import { SpreadsheetController, createConditionalFormatRule } from "@pixerate/editor/spreadsheet";
341
+
342
+ const sheet = new SpreadsheetController({ document });
343
+ const all = { startRow: 1, startCol: 0, endRow: 20, endCol: 3 };
344
+ sheet.addConditionalFormat({ type: "expression", formula: "=$D2>100", style: { fill: "#c6efce" }, ranges: [all] });
345
+ sheet.addConditionalFormat(createConditionalFormatRule({ kind: "dataBar", ranges: [{ ...all, startCol: 3, endCol: 3 }] }));
346
+ sheet.getConditionalFormat(4, 3); // { fill, dataBar: { fraction, color, gradient }, ... } or undefined
347
+
348
+ sheet.addDataValidation({ ranges: [{ ...all, endCol: 0 }], type: "list", formula1: "North,South,East,West", errorStyle: "warning" });
349
+ sheet.startEditing(1, 0, "Mars");
350
+ const result = sheet.commitEditing(); // { status: "confirm", prompt } — the editor stays open
351
+ sheet.resolveValidationPrompt("cancel"); // or "accept" (warning / information) / "retry"
352
+ sheet.findInvalidCells(); // "circle invalid data"
353
+ ```
354
+
355
+ `evaluateConditionalFormats(sheet, rules)` and `validateCellInput(sheet, rules, row, col, input)` also work on any grid through the small `ConditionalFormatSheet` / `DataValidationSheet` interfaces. Rule formulas are written for the top-left cell of the first range and shift for other cells; custom validation formulas see the value being entered. `setCellValue`, paste and import never validate (as in Excel); `commitEditing` and `commitCellInput` do.
356
+
227
357
  ## Other modules
228
358
 
229
359
  Each module is also available as a subpath import:
@@ -235,12 +365,47 @@ Each module is also available as a subpath import:
235
365
  | `@pixerate/editor/extensions` | Headless TipTap extensions |
236
366
  | `@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` |
237
367
  | `@pixerate/editor/spreadsheet` | `SpreadsheetController`, formula parser/evaluator and dependency graph, formula function registry (`registerFormulaFunction`, `listFormulaFunctions`, `searchFormulaFunctions`, `getFormulaSignature`) and autocomplete helpers (`getFormulaHints`, `applyFormulaSuggestion`) |
238
- | `@pixerate/editor/image-editor` | `ImageEditorController` and Canvas 2D renderer: crop, adjustments, annotations, depth masks and AI hooks. Needs a DOM/canvas environment. |
368
+ | `@pixerate/editor/image-editor` | `ImageEditorController` and Canvas 2D renderer: crop, adjustments, annotations, depth masks, selections and layer masks, and AI hooks. Needs a DOM/canvas environment (the selection algorithms themselves are pure). |
239
369
  | `@pixerate/editor/canvas` | Node-graph layout (needs the optional `@dagrejs/dagre` peer), placement, trajectories, displacement, clipboard and streaming layout helpers |
240
370
  | `@pixerate/editor/agent` | MCP tool schemas, a planner for agent-driven canvas edits, and the dynamic `ToolRegistry` with editor/spreadsheet tool providers |
241
371
  | `@pixerate/editor/commands` | `CommandRegistry`, `ShortcutMap`, palette ranking, `createCommandToolProvider` and built-in history / arrange / spreadsheet commands (no third-party imports) |
372
+ | `@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) |
373
+ | `@pixerate/editor/generation` | Model capability descriptors, `planGenerationRequest`, `capabilitiesToJsonSchema`, `createGenerationToolProvider`, provenance, `createBatchResolver` and job-progress estimates (no third-party imports) |
374
+
375
+ 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.
376
+
377
+ ## Autosave, crash recovery and document migration
378
+
379
+ ```ts
380
+ import {
381
+ createAutosaveController,
382
+ createIndexedDbAdapter,
383
+ createRevisionTracker,
384
+ formatRecoveryMessage,
385
+ } from "@pixerate/editor/persistence";
386
+
387
+ const revisions = createRevisionTracker({ history, dirty: bindDirtyToHistory({ history, getValue }) });
388
+ const autosave = createAutosaveController({
389
+ storage: createIndexedDbAdapter("my-app"), // or createLocalStorageAdapter / createOpfsAdapter / createMemoryStorage
390
+ key: "doc-42",
391
+ getSnapshot: () => doc,
392
+ revision: revisions.revision,
393
+ savedRevision: revisions.savedRevision,
394
+ name: "Quarterly plan",
395
+ });
396
+ revisions.subscribe(() => autosave.notify());
397
+
398
+ const recovered = await autosave.checkRecovery(); // { snapshot, meta: { time, name, revision } } | null
399
+ if (recovered && confirm(formatRecoveryMessage(recovered.meta))) load(recovered.snapshot);
400
+ else if (recovered) await autosave.discardRecovery();
401
+ autosave.ready(); // hydration gate: nothing is written before this
402
+ addEventListener("pagehide", () => autosave.flushNow());
403
+ // after an explicit save: autosave.markClean() retires the snapshot, but only one this session wrote
404
+ ```
405
+
406
+ 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.
242
407
 
243
- The root entry (`@pixerate/editor`) re-exports everything except canvas, agent, commands and image editor, including TipTap. Import from the narrowest subpath you need: `grammar`, `history`, `spreadsheet`, `commands` 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.
408
+ `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`.
244
409
 
245
410
  ## History gestures, marks and dirty tracking
246
411
 
@@ -8,12 +8,29 @@ All sources below are from the ArtCraft "craft" apps, each licensed **MIT OR Apa
8
8
  © 2026 ArtCraft Team and the respective app's contributors. They are used here under the MIT licence
9
9
  reproduced at the end of this file. Paths are relative to each source repository.
10
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
+
11
21
  ## figcraft
12
22
 
13
23
  © 2026 ArtCraft Team and the FigCraft contributors.
14
24
 
15
25
  - `crates/engine/src/distance.rs` (`between`, `inside`, `to_guide`, `label`) → `packages/core/src/canvas/measure.ts`
16
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
+ - `crates/doc/src/styles.rs` (`Collection`, `Mode`, `Variable`, `VariableValue`, `Variables::resolve`, `default_mode`, `current_mode`, `find_mode`, `scopes_for`, `normalize_scopes`, `in_scope`) → `packages/core/src/tokens/types.ts`, `packages/core/src/tokens/model.ts`
30
+ - `crates/engine/src/cmd/variables.rs` (`alias_target` cycle check, `forget_variables` alias detaching, `remove_mode`, `move_mode`, `rename_collection`, `delete_collection`, `field_scope`) → `packages/core/src/tokens/model.ts`, `packages/core/src/canvas/tokenBindings.ts`
31
+ - `crates/doc/src/lib.rs` (`modes_for`: modes inherited down the tree) → `getEffectiveTokenModes` in `packages/core/src/canvas/tokenBindings.ts`
32
+ - `crates/engine/src/variables_io_tests.rs` (behaviour of the variables JSON import / export: merging by name, junk and loop handling, CSS export per mode) → `packages/core/src/tokens/w3c.ts`, `packages/core/src/tokens/css.ts`
33
+ - `crates/engine/src/{variable_tests.rs,variables_behaviour_tests.rs,variables_io_tests.rs}` (test cases re-expressed) → `packages/core/tests/tokens.test.ts`, `packages/core/tests/canvas-tokens.test.ts`
17
34
 
18
35
  ## filmcraft
19
36
 
@@ -24,6 +41,9 @@ reproduced at the end of this file. Paths are relative to each source repository
24
41
  - `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`
25
42
  - `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`
26
43
  - `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`
44
+ - `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`
45
+ - `apps/filmcraft-web/src/recovery.rs` (snapshot-then-dirty-meta writes, recovery check of a dirty meta) → `packages/core/src/persistence/autosave.ts`
46
+ - `apps/filmcraft-web/tests/recovery_policy.rs` (test cases re-expressed) → `packages/core/tests/persistence-policy.test.ts`
27
47
 
28
48
  ## gridcraft
29
49
 
@@ -33,6 +53,19 @@ reproduced at the end of this file. Paths are relative to each source repository
33
53
  - `crates/core/src/date.rs` (1900 date-system serials, days in month, civil-day arithmetic) → `packages/core/src/spreadsheet/formula/parse-input.ts`
34
54
  - `crates/core/src/value.rs` (Excel comparison, 15-digit number comparison, sort order, number-to-text coercion) → `packages/core/src/spreadsheet/formula/values.ts`
35
55
  - Test cases from the files above, re-expressed → `packages/core/tests/spreadsheet-typed-input.test.ts`
56
+ - `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/`
57
+ - `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`
58
+ - `crates/calc/src/eval.rs` (LET/LAMBDA environments and closures, lambda calls and argument counts, ISOMITTED, MAP/REDUCE/SCAN/BYROW/BYCOL/MAKEARRAY, functions passed by name, defined-name evaluation, recursion depth limit) → `packages/core/src/spreadsheet/formula/evaluator.ts`, `packages/core/src/spreadsheet/formula/lambda.ts`, `packages/core/src/spreadsheet/formula/functions/lambda.ts`
59
+ - `crates/model/src/workbook.rs` (sheet name rules, defined-name lookup by scope) and `crates/engine/src/cmd/formulas.rs` (defined-name validation, R1C1 look-alikes, renaming names in formulas) → `packages/core/src/spreadsheet/workbook.ts`, `packages/core/src/spreadsheet/controller.ts`
60
+ - `crates/formula/src/adjust.rs` and `crates/formula/src/printer.rs` (rename/delete sheet rewriting, 3-D span shrinking, sheet-name quoting) and `crates/engine/src/cmd/sheet.rs` (add, rename, move, copy and delete sheets with their names) → `packages/core/src/spreadsheet/formula/references.ts`, `packages/core/src/spreadsheet/controller.ts`
61
+ - `crates/calc/src/tests.rs`, `crates/formula/src/tests.rs`, `crates/engine/src/tests.rs` and `crates/model/src/workbook.rs` tests (test cases re-expressed) → `packages/core/tests/spreadsheet-lambda.test.ts`, `packages/core/tests/spreadsheet-workbook.test.ts`
62
+ - `crates/model/src/features.rs` (conditional formatting and data validation rule model, icon-set thresholds) and `crates/engine/src/cf.rs` (rule evaluation: priority, stop if true, colour scales, data bars, icon sets, top/bottom, averages, duplicates, text and date rules, relative rule formulas) → `packages/core/src/spreadsheet/conditional-format/`
63
+ - `crates/engine/src/cmd/data.rs` (validation checks, list items, circle invalid data, removing rule cells) and `crates/engine/src/cmd/sheet.rs` (rule ranges following inserted / deleted rows and columns) → `packages/core/src/spreadsheet/validation/`, `packages/core/src/spreadsheet/rule-ranges.ts`
64
+ - Test cases from `crates/engine/src/cf.rs` and `crates/engine/src/tests.rs` (icon-set thresholds, relative validation formulas), re-expressed → `packages/core/tests/spreadsheet-conditional-format.test.ts`, `packages/core/tests/spreadsheet-data-validation.test.ts`
65
+ **Apache POI.** The rows of `packages/core/tests/fixtures/poi-number-format-approx.ts` (format, value and Excel's
66
+ `TEXT()` result) come from Apache POI's `test-data/spreadsheet/NumberFormatApproxTests.xlsx`, as collected in
67
+ GridCraft. Apache POI is © The Apache Software Foundation, licensed under the Apache License, Version 2.0
68
+ (https://www.apache.org/licenses/LICENSE-2.0).
36
69
 
37
70
  ## lightcraft
38
71
 
@@ -43,6 +76,17 @@ reproduced at the end of this file. Paths are relative to each source repository
43
76
  - `crates/pipeline/src/tone.rs` (`ToneMap::display` S-curve anchored at 0, grey and 1) → `packages/core/src/image-editor/color.ts`
44
77
  - `crates/pipeline/src/local.rs` (relative white balance with neutral-luminance normalisation) → `packages/core/src/image-editor/color.ts`
45
78
  - `crates/develop/src/settings.rs` and `docs/process-versions.md` (process version semantics) → `packages/core/src/image-editor/color.ts`
79
+ - `crates/develop/src/controls.rs` (`ControlSpec` table: id, label, section, range, default, step, decimals, track style; clamping) → `packages/core/src/image-editor/controls.ts`
80
+
81
+ ## pdfcraft
82
+
83
+ © 2026 ArtCraft Team and the PDFCraft contributors.
84
+
85
+ - `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`
86
+ - 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`
87
+ - `crates/export/src/lib.rs` (`body_size`: body text size by character count; `level`: heading levels relative to it, bold body-size headings; `tables`: grid columns from aligned cell edges, rows from overlapping spans, spanning header and totals rows next to a run, the 63-column cap) → `packages/core/src/import/pdf-layout.ts`
88
+ - `crates/edit/src/text_regions.rs` (`group_layout`: runs joined on a baseline across small gaps, rows grouped into blocks by pitch and alignment, line-end hyphens removed when joining rows) → `packages/core/src/import/pdf-layout.ts`
89
+ - Test cases from `crates/export/src/lib.rs` (`grid_text_becomes_a_real_table_in_every_format`, `a_staircase_of_blocks_is_not_a_huge_table`, `two_column_body_text_is_not_mistaken_for_a_table`, `word_does_not_invent_borders_for_aligned_invoice_fields`), re-expressed → `packages/core/tests/pdf-layout.test.ts`
46
90
 
47
91
  ## photocraft
48
92
 
@@ -50,6 +94,17 @@ reproduced at the end of this file. Paths are relative to each source repository
50
94
 
51
95
  - `crates/ops/src/lib.rs` (`History { max_states, max_bytes }`, `trim`, `unique_bytes`) → `packages/core/src/history/HistoryManager.ts`
52
96
  - `crates/ui-egui/src/palette.rs` (`fuzzy_score`, `exact_bonus`, `push_recent`) → `packages/core/src/commands/palette.ts`
97
+ - `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`
98
+ - `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`
99
+ - `crates/color/src/blend.rs` (separable and non-separable blend modes, hardened `clip_color`, `set_lum`, `composite`) → `packages/core/src/image-editor/blend.ts`
100
+ - `crates/color/src/convert.rs` (`D50`, `SRGB_TO_XYZ_D50`, `XYZ_D50_TO_SRGB`) → `packages/core/src/image-editor/adjustment-layers.ts`
101
+ - `crates/algo/src/histogram.rs` (bounded RGB histogram, transparent exclusion, exact-endpoint clipping) → `packages/core/src/image-editor/histogram.ts`
102
+ - 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`
103
+ - `crates/algo/src/selection.rs` (`SelectionMode` / `combine`, `magic_wand` / `wand_region` span fill and anti-aliasing, `color_range_samples`, `tone_range`, `hue_range`, `expand` / `contract` / `border`, anti-aliased `polygon`) → `packages/core/src/image-editor/selection/mask.ts`, `shapes.ts`, `wand.ts`, `modify.ts`
104
+ - `crates/algo/src/selection/distance.rs` (Felzenszwalb–Huttenlocher `edt`) and `crates/algo/src/selection_blur.rs` (Smooth: box + threshold; Feather: Gaussian with σ = radius) → `packages/core/src/image-editor/selection/modify.ts`
105
+ - `crates/algo/src/matting.rs` (`RefineParams`, `box_mean`, `guided_filter_color`, `edge_width`, `refine_buffer` band / smooth / feather / contrast / `shift_mask`) → `packages/core/src/image-editor/selection/refine.ts`, `modify.ts`
106
+ - `crates/engine/src/layer_mask_props_cmds.rs` and `crates/compose/src/masks.rs` (non-destructive mask density and feather) → `packages/core/src/image-editor/selection/layer-mask.ts`
107
+ - Test cases from `crates/algo/src/selection.rs`, `crates/algo/src/selection_blur.rs`, `crates/algo/src/matting.rs` and `crates/algo/src/tests_selection_mix.rs`, re-expressed → `packages/core/tests/image-editor-selection.test.ts`, `packages/core/tests/image-editor-selection-controller.test.ts`
53
108
 
54
109
  ## soundcraft
55
110
 
@@ -67,8 +122,33 @@ reproduced at the end of this file. Paths are relative to each source repository
67
122
  - `crates/pathops/src/fit.rs` (`fit_cubics`, `fit_single`, `fit_single_from`, `generate`, `newton`, `chord_params`, `is_straight`) → `packages/core/src/canvas/curveFit.ts`
68
123
  - `crates/pathops/src/edit.rs` (`simplify_with` corner splitting) → `packages/core/src/canvas/curveFit.ts`
69
124
  - 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`
125
+ - `crates/pathops/src/boolean.rs` (`BoolOp::apply`, fill-rule `inside`, `Tidy::keeping` / `exact_merge`: rebuilding split pieces as exact sub-curves of the input curves, `is_sliver` mean-width sliver test) → `packages/core/src/canvas/pathops/boolean.ts`
126
+ - `crates/pathops/src/offset.rs` (`offset_path`: fill ∪ / − a `2·|d|` stroke ring, open subpaths outlined with butt caps, spurious-contour removal by `deep_point` distance; `stroke_region`) → `packages/core/src/canvas/pathops/stroke.ts`
127
+ - `crates/effects/src/stroke/dash.rs` (exact dash plans: odd patterns doubled, offset into the pattern, restart per subpath, joining across the start of a closed subpath, zero-length dots, invalid / too-fine patterns drawn solid) → `packages/core/src/canvas/pathops/stroke.ts`
128
+ - `crates/pathops/src/boolean.rs` (`try_merge` refit of tangent-continuous runs) → `mergeSmoothRuns` in `packages/core/src/canvas/pathops/stroke.ts`
129
+ - Test cases from `crates/pathops/tests/ops.rs`, `tests/props.rs`, `tests/prop_pathops.rs`, `crates/pathops/src/offset.rs` and `crates/engine/src/tests_outlinestroke.rs`, re-expressed → `packages/core/tests/canvas-pathops.test.ts`
130
+ ---
131
+
132
+ ## wordcraft
133
+
134
+ © 2026 ArtCraft Team and the WordCraft contributors.
135
+
136
+ - `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`
137
+ - `crates/formats/src/sheet.rs` (`field_names`: blank headers as `Column N`, repeated headers numbered) → `packages/core/src/grammar/data-source.ts`
138
+ - `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`
139
+ - 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`
140
+ - `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`
141
+ - `crates/engine/src/cmd/fmt_revisions.rs` (`compare_formatting`: equal text compared for formatting changes) → `packages/core/src/compare/documents.ts`
142
+ - 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`
70
143
 
71
144
  ---
145
+ - `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`
146
+ - `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`
147
+ - `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`
148
+ - `crates/engine/src/cmd/moves.rs` (`move_sized`; `cut_tracked` / `take_paste`: a tracked cut gets its own revision and the paste right after it, by the same author and of the same text, links both ends with a move name; `resolve`: accepting a move removes its source and keeps its destination, rejecting restores the source and removes the destination, a moved paragraph's emptied block goes with it) and `crates/engine/src/cmd/review.rs` (moves listed as one change, accepting or rejecting either end resolves both) → `packages/core/src/extensions/track-changes.ts`
149
+ - Test cases from `crates/engine/src/cmd/moves.rs` (`a_tracked_cut_and_paste_of_a_paragraph_is_a_move`, `accepting_one_end_of_a_move_accepts_both`, `rejecting_a_move_puts_the_text_back`), re-expressed → `packages/core/tests/track-changes-moves.test.ts`
150
+ - `crates/doc/src/styles.rs` (`Style` with `based_on` / `next` / `linked`, the built-in style set's shape, `gallery`, `chain` (bounded, cycle-safe), `find`, `upsert`, `remove` (rebasing dependants, dropping `next` / `linked` references), `new_id`) and `crates/doc/src/resolve.rs` (cascade: document defaults → paragraph style chain → character style chain → direct formatting) → `packages/core/src/extensions/paragraph-styles/registry.ts`
151
+ - Test cases from `crates/doc/src/styles.rs` (`builtins_present`, `chain_is_cycle_safe`, `new_ids_unique`) and `crates/doc/src/resolve.rs` (`heading_resolves`, `char_style_then_direct`), re-expressed → `packages/core/tests/paragraph-styles.test.ts`
72
152
 
73
153
  ## MIT License
74
154
 
@@ -5,12 +5,14 @@ var _chunkCPOKFNBGcjs = require('../chunk-CPOKFNBG.cjs');
5
5
 
6
6
 
7
7
 
8
- var _chunkAA4ZEVCCcjs = require('../chunk-AA4ZEVCC.cjs');
8
+ var _chunk7WISYZFOcjs = require('../chunk-7WISYZFO.cjs');
9
+ require('../chunk-BVP3TSLH.cjs');
10
+ require('../chunk-X4N4WUKB.cjs');
9
11
 
10
12
 
11
13
 
12
14
 
13
- var _chunkRIKBJLS2cjs = require('../chunk-RIKBJLS2.cjs');
15
+ var _chunkC7P22VIPcjs = require('../chunk-C7P22VIP.cjs');
14
16
 
15
17
 
16
18
 
@@ -700,7 +702,7 @@ function createToolRegistry(options) {
700
702
  var toEditor = (target) => "commands" in target ? target : target.editor;
701
703
  function replaceWithText(editor, from, to, text) {
702
704
  const { state, view } = editor;
703
- const tr = /[\r\n]/.test(text) ? state.tr.replace(from, to, _chunkAA4ZEVCCcjs.plainTextToSlice.call(void 0, text, state.schema)) : text ? state.tr.insertText(text, from, to) : state.tr.delete(from, to);
705
+ const tr = /[\r\n]/.test(text) ? state.tr.replace(from, to, _chunk7WISYZFOcjs.plainTextToSlice.call(void 0, text, state.schema)) : text ? state.tr.insertText(text, from, to) : state.tr.delete(from, to);
704
706
  view.dispatch(tr.scrollIntoView());
705
707
  }
706
708
  function selectionInfo(editor) {
@@ -733,7 +735,7 @@ function createRichTextToolProvider(target, options) {
733
735
  },
734
736
  handler: ({ format = "text" }) => {
735
737
  const ed = editor();
736
- const content = format === "html" ? ed.getHTML() : format === "markdown" ? _chunkAA4ZEVCCcjs.getEditorMarkdown.call(void 0, ed) : _chunkAA4ZEVCCcjs.getEditorText.call(void 0, ed);
738
+ const content = format === "html" ? ed.getHTML() : format === "markdown" ? _chunk7WISYZFOcjs.getEditorMarkdown.call(void 0, ed) : _chunk7WISYZFOcjs.getEditorText.call(void 0, ed);
737
739
  return { format, content, selection: selectionInfo(ed), size: ed.state.doc.content.size };
738
740
  }
739
741
  };
@@ -815,7 +817,7 @@ var CELL_REF_REGEX = /^\$?([A-Za-z]{1,3})\$?(\d{1,7})$/;
815
817
  function parseCell(ref) {
816
818
  const match = CELL_REF_REGEX.exec(ref.trim());
817
819
  if (!match) throw new Error(`Invalid cell reference "${ref}". Use A1 notation, e.g. "B3".`);
818
- return { col: _chunkRIKBJLS2cjs.colNameToIndex.call(void 0, match[1].toUpperCase()), row: parseInt(match[2], 10) - 1 };
820
+ return { col: _chunkC7P22VIPcjs.colNameToIndex.call(void 0, match[1].toUpperCase()), row: parseInt(match[2], 10) - 1 };
819
821
  }
820
822
  function createSpreadsheetToolProvider(controller, options) {
821
823
  const lookup = (row, col) => {
@@ -863,14 +865,14 @@ function createSpreadsheetToolProvider(controller, options) {
863
865
  const cell = controller.getCell(found.rowDef.id, found.colDef.id);
864
866
  valueRow.push(_nullishCoalesce(cell.error, () => ( cell.value)));
865
867
  rawRow.push(cell.raw);
866
- const error = _nullishCoalesce(cell.error, () => ( (_chunkRIKBJLS2cjs.isFormulaError.call(void 0, cell.value) ? cell.value : void 0)));
867
- if (error) errors.push({ cell: `${_chunkRIKBJLS2cjs.indexToColName.call(void 0, c)}${r + 1}`, error });
868
+ const error = _nullishCoalesce(cell.error, () => ( (_chunkC7P22VIPcjs.isFormulaError.call(void 0, cell.value) ? cell.value : void 0)));
869
+ if (error) errors.push({ cell: `${_chunkC7P22VIPcjs.indexToColName.call(void 0, c)}${r + 1}`, error });
868
870
  }
869
871
  values.push(valueRow);
870
872
  raw.push(rawRow);
871
873
  }
872
874
  return {
873
- range: values.length && values[0].length ? `${_chunkRIKBJLS2cjs.indexToColName.call(void 0, c0)}${r0 + 1}:${_chunkRIKBJLS2cjs.indexToColName.call(void 0, c1)}${r1 + 1}` : null,
875
+ range: values.length && values[0].length ? `${_chunkC7P22VIPcjs.indexToColName.call(void 0, c0)}${r0 + 1}:${_chunkC7P22VIPcjs.indexToColName.call(void 0, c1)}${r1 + 1}` : null,
874
876
  values,
875
877
  raw,
876
878
  errors,