@pixerate/editor-svelte 0.21.0 → 0.23.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 (117) hide show
  1. package/README.md +6 -1
  2. package/dist/MergePreview.svelte +201 -0
  3. package/dist/MergePreview.svelte.d.ts +25 -0
  4. package/dist/MergePreview.svelte.d.ts.map +1 -0
  5. package/dist/TemplateRenderer.svelte +4 -10
  6. package/dist/TemplateRenderer.svelte.d.ts.map +1 -1
  7. package/dist/canvas/CanvasGuidesLayer.svelte +32 -2
  8. package/dist/canvas/CanvasGuidesLayer.svelte.d.ts +2 -0
  9. package/dist/canvas/CanvasGuidesLayer.svelte.d.ts.map +1 -1
  10. package/dist/canvas/createCanvasArrange.svelte.d.ts +16 -1
  11. package/dist/canvas/createCanvasArrange.svelte.d.ts.map +1 -1
  12. package/dist/canvas/createCanvasArrange.svelte.js +19 -2
  13. package/dist/canvas/createCanvasAutoLayout.svelte.d.ts +68 -0
  14. package/dist/canvas/createCanvasAutoLayout.svelte.d.ts.map +1 -0
  15. package/dist/canvas/createCanvasAutoLayout.svelte.js +78 -0
  16. package/dist/canvas/createCanvasMeasure.svelte.d.ts +53 -0
  17. package/dist/canvas/createCanvasMeasure.svelte.d.ts.map +1 -0
  18. package/dist/canvas/createCanvasMeasure.svelte.js +80 -0
  19. package/dist/canvas/index.d.ts +4 -0
  20. package/dist/canvas/index.d.ts.map +1 -1
  21. package/dist/canvas/index.js +6 -0
  22. package/dist/commands/CommandPalette.svelte +129 -0
  23. package/dist/commands/CommandPalette.svelte.d.ts +13 -0
  24. package/dist/commands/CommandPalette.svelte.d.ts.map +1 -0
  25. package/dist/commands/commandShortcuts.svelte.d.ts +30 -0
  26. package/dist/commands/commandShortcuts.svelte.d.ts.map +1 -0
  27. package/dist/commands/commandShortcuts.svelte.js +37 -0
  28. package/dist/commands/index.d.ts +7 -0
  29. package/dist/commands/index.d.ts.map +1 -0
  30. package/dist/commands/index.js +3 -0
  31. package/dist/commands/palette.svelte.d.ts +34 -0
  32. package/dist/commands/palette.svelte.d.ts.map +1 -0
  33. package/dist/commands/palette.svelte.js +61 -0
  34. package/dist/find-replace/FindReplaceBar.svelte +203 -0
  35. package/dist/find-replace/FindReplaceBar.svelte.d.ts +15 -0
  36. package/dist/find-replace/FindReplaceBar.svelte.d.ts.map +1 -0
  37. package/dist/find-replace/findReplace.svelte.d.ts +27 -0
  38. package/dist/find-replace/findReplace.svelte.d.ts.map +1 -0
  39. package/dist/find-replace/findReplace.svelte.js +79 -0
  40. package/dist/find-replace/index.d.ts +6 -0
  41. package/dist/find-replace/index.d.ts.map +1 -0
  42. package/dist/find-replace/index.js +2 -0
  43. package/dist/generation/GenerationSettings.svelte +174 -0
  44. package/dist/generation/GenerationSettings.svelte.d.ts +23 -0
  45. package/dist/generation/GenerationSettings.svelte.d.ts.map +1 -0
  46. package/dist/generation/JobProgress.svelte +53 -0
  47. package/dist/generation/JobProgress.svelte.d.ts +17 -0
  48. package/dist/generation/JobProgress.svelte.d.ts.map +1 -0
  49. package/dist/generation/index.d.ts +10 -0
  50. package/dist/generation/index.d.ts.map +1 -0
  51. package/dist/generation/index.js +4 -0
  52. package/dist/generation/plan.svelte.d.ts +16 -0
  53. package/dist/generation/plan.svelte.d.ts.map +1 -0
  54. package/dist/generation/plan.svelte.js +26 -0
  55. package/dist/generation/progress.svelte.d.ts +25 -0
  56. package/dist/generation/progress.svelte.d.ts.map +1 -0
  57. package/dist/generation/progress.svelte.js +36 -0
  58. package/dist/history/createHistoryShortcuts.d.ts +1 -1
  59. package/dist/history/createHistoryShortcuts.d.ts.map +1 -1
  60. package/dist/history/createHistoryShortcuts.js +10 -32
  61. package/dist/history/history.svelte.d.ts +13 -4
  62. package/dist/history/history.svelte.d.ts.map +1 -1
  63. package/dist/history/history.svelte.js +10 -0
  64. package/dist/history/historyDirty.svelte.d.ts +31 -0
  65. package/dist/history/historyDirty.svelte.d.ts.map +1 -0
  66. package/dist/history/historyDirty.svelte.js +30 -0
  67. package/dist/history/index.d.ts +1 -0
  68. package/dist/history/index.d.ts.map +1 -1
  69. package/dist/history/index.js +1 -0
  70. package/dist/image-editor/AdjustmentLayersPanel.svelte +309 -0
  71. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +11 -0
  72. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts.map +1 -0
  73. package/dist/image-editor/ControlSlider.svelte +39 -0
  74. package/dist/image-editor/ControlSlider.svelte.d.ts +13 -0
  75. package/dist/image-editor/ControlSlider.svelte.d.ts.map +1 -0
  76. package/dist/image-editor/CurvesEditor.svelte +105 -0
  77. package/dist/image-editor/CurvesEditor.svelte.d.ts +13 -0
  78. package/dist/image-editor/CurvesEditor.svelte.d.ts.map +1 -0
  79. package/dist/image-editor/Histogram.svelte +49 -0
  80. package/dist/image-editor/Histogram.svelte.d.ts +12 -0
  81. package/dist/image-editor/Histogram.svelte.d.ts.map +1 -0
  82. package/dist/image-editor/ImageEditor.svelte +38 -107
  83. package/dist/image-editor/ImageEditor.svelte.d.ts.map +1 -1
  84. package/dist/image-editor/imageEditorState.svelte.d.ts +16 -1
  85. package/dist/image-editor/imageEditorState.svelte.d.ts.map +1 -1
  86. package/dist/image-editor/imageEditorState.svelte.js +44 -0
  87. package/dist/image-editor/index.d.ts +12 -0
  88. package/dist/image-editor/index.d.ts.map +1 -1
  89. package/dist/image-editor/index.js +4 -0
  90. package/dist/index.d.ts +11 -3
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +7 -1
  93. package/dist/merge.svelte.d.ts +51 -0
  94. package/dist/merge.svelte.d.ts.map +1 -0
  95. package/dist/merge.svelte.js +117 -0
  96. package/dist/persistence/RecoveryPrompt.svelte +57 -0
  97. package/dist/persistence/RecoveryPrompt.svelte.d.ts +18 -0
  98. package/dist/persistence/RecoveryPrompt.svelte.d.ts.map +1 -0
  99. package/dist/persistence/autosave.svelte.d.ts +63 -0
  100. package/dist/persistence/autosave.svelte.d.ts.map +1 -0
  101. package/dist/persistence/autosave.svelte.js +110 -0
  102. package/dist/persistence/index.d.ts +6 -0
  103. package/dist/persistence/index.d.ts.map +1 -0
  104. package/dist/persistence/index.js +2 -0
  105. package/dist/review/documentCompare.svelte.d.ts +21 -0
  106. package/dist/review/documentCompare.svelte.d.ts.map +1 -0
  107. package/dist/review/documentCompare.svelte.js +23 -0
  108. package/dist/review/index.d.ts +1 -0
  109. package/dist/review/index.d.ts.map +1 -1
  110. package/dist/review/index.js +1 -0
  111. package/dist/spreadsheet/SpreadsheetContextMenu.svelte +6 -3
  112. package/dist/spreadsheet/SpreadsheetContextMenu.svelte.d.ts.map +1 -1
  113. package/dist/spreadsheet/SpreadsheetEditor.svelte +7 -2
  114. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts.map +1 -1
  115. package/dist/spreadsheet/spreadsheet.svelte.d.ts +2 -0
  116. package/dist/spreadsheet/spreadsheet.svelte.d.ts.map +1 -1
  117. package/package.json +26 -2
package/README.md CHANGED
@@ -69,13 +69,18 @@ Requires **Svelte 5** (the components use runes). Components ship as `.svelte` s
69
69
 
70
70
  `ReviewToolbar` steps through insertions, deletions, formatting and block changes and shows the current one's author and description (e.g. "Ada: Formatted: Bold"); hide it with `showDescription={false}` or restyle it with `descriptionClass`. Put `<CommentButton {editor} />` inside a `BubbleMenu` to comment on the selection. `createComments(editorOrStore)` and `createTrackChanges(editor)` are rune (and Svelte store) adapters for custom UIs.
71
71
 
72
+ `<FindReplaceBar {editor} />` is a Word-style find bar for editors with the `FindReplace` extension (`createRichTextPreset({ findReplace: true })`): it appears on Mod+F / Mod+H (or `editor.commands.openFindReplace()`), with find and replace fields, option toggles, a "3 of 12" counter, previous/next, Replace and Replace all; Enter / Shift+Enter step, Mod+Enter in the replace field replaces all, Escape closes (`onclose`). `createFindReplace(editor)` is the rune (and Svelte store) adapter for custom UIs.
73
+
72
74
  ## More exports
73
75
 
74
76
  | Import | Exports |
75
77
  | :--- | :--- |
76
- | `@pixerate/editor-svelte` | `EditableTextNodeEditor`, `BubbleMenu`, `TemplateRenderer`, `initiateEditor`, `createReactiveEditor`, `createHistory`, `createHistoryShortcuts`, `navigationGuard` (action), `createNavigationGuard`, `SpreadsheetEditor`, `FormulaBar`, `FormulaHints`, `createReactiveSpreadsheet`, `DirtyTracker` |
78
+ | `@pixerate/editor-svelte` | `EditableTextNodeEditor`, `BubbleMenu`, `TemplateRenderer`, `initiateEditor`, `createReactiveEditor`, `createHistory`, `createHistoryDirty`, `createHistoryShortcuts`, `navigationGuard` (action), `createNavigationGuard`, `SpreadsheetEditor`, `FormulaBar`, `FormulaHints`, `createReactiveSpreadsheet`, `DirtyTracker` |
77
79
  | `@pixerate/editor-svelte/spreadsheet` | `SpreadsheetEditor`, `FormulaBar`, `FormulaHints`, `SpreadsheetContextMenu`, `SpreadsheetGroupRow`, `createReactiveSpreadsheet` (also re-exported from the root entry) |
78
80
  | `@pixerate/editor-svelte/image-editor` | `ImageEditor`, `createReactiveImageEditor`: crop, rotate/flip, adjustments, annotations, depth masks, and `onInpaint` / `onRemoveBackground` AI hooks |
81
+ | `@pixerate/editor-svelte/commands` | `CommandPalette`, `createCommandPalette` (`hotkey: "Mod+K"`; call `destroy()` when done), `createCommandShortcuts`, `useRegisterCommands` over a `CommandRegistry` from `@pixerate/editor/commands` (also re-exported from the root entry) |
82
+ | `@pixerate/editor-svelte/persistence` | `createAutosave` (crash-recovery snapshots bound to a history / dirty flag, flushed on `pagehide`; call `destroy()` when done) and `<RecoveryPrompt>` (also re-exported from the root entry) |
83
+ | `@pixerate/editor-svelte/generation` | `GenerationSettings` (controls generated from a model's capabilities, reference slots by role, plan adjustments; `onchange` / `onaddreference`), `createGenerationPlan(() => request, () => caps)`, `JobProgress`, `createJobProgress` (call `destroy()` when done) over `@pixerate/editor/generation` (also re-exported from the root entry) |
79
84
  | `@pixerate/editor-svelte/canvas` | Runes for `@xyflow/svelte`: `createCanvasGraph`, `createCanvasClipboard`, `createCanvasDocking`, `createCanvasShortcuts`, `createCanvasInteractions`, `createCanvasExplosion`, `createCanvasNodeSync`, `createCanvasMultiDrag`, `createStreamingCanvas`, `AgentPresenceLayer`, `FloatingHorizontalScrollbar`, plus layout helpers |
80
85
 
81
86
  Prop types are exported alongside each component, for example `EditableTextNodeEditorProps`, `BubbleMenuProps`, `ImageEditorProps` and `SpreadsheetEditorProps`.
@@ -0,0 +1,201 @@
1
+ <!--
2
+ Mail-merge preview: paste or pick a CSV / JSON data source, check the
3
+ automatic field mapping, step through records with the resolved prompt shown
4
+ by TemplateRenderer, and merge every record into text or JSON.
5
+ Inputs are nested in their labels, which associates them without ids.
6
+ -->
7
+ <script lang="ts">
8
+ import type {
9
+ ColorGradient,
10
+ MergeRecordsOptions,
11
+ MergeRecordsResult,
12
+ ParseDataSourceOptions,
13
+ Template,
14
+ } from "@pixerate/editor";
15
+ import TemplateRenderer from "./TemplateRenderer.svelte";
16
+ import { createMergePreview } from "./merge.svelte.js";
17
+
18
+ interface Props {
19
+ /** The prompt merged with each record. */
20
+ content: string;
21
+ templates?: Template[];
22
+ bindings?: Record<string, string>;
23
+ templateColorMap?: Map<string, ColorGradient>;
24
+ /** Initial CSV / TSV / JSON data source text. */
25
+ defaultSource?: string;
26
+ parseOptions?: ParseDataSourceOptions;
27
+ /** Drop records matching this condition (e.g. `"status=void"`) when merging. */
28
+ skipIf?: MergeRecordsOptions["skipIf"];
29
+ /** Separator between merged outputs (default a blank line; `"none"` for nothing). */
30
+ separator?: string;
31
+ maxOutputs?: number;
32
+ /** Called after "Merge all". */
33
+ onMerge?: (result: MergeRecordsResult) => void;
34
+ /** Base file name for downloads (default `merged`). */
35
+ downloadName?: string;
36
+ class?: string;
37
+ }
38
+
39
+ let {
40
+ content,
41
+ templates,
42
+ bindings,
43
+ templateColorMap,
44
+ defaultSource = "",
45
+ parseOptions,
46
+ skipIf,
47
+ separator,
48
+ maxOutputs,
49
+ onMerge,
50
+ downloadName = "merged",
51
+ class: className = "flex flex-col gap-3 text-sm",
52
+ }: Props = $props();
53
+
54
+ // svelte-ignore state_referenced_locally
55
+ const merge = createMergePreview(
56
+ () => ({ template: content, templates, bindings, parseOptions, skipIf, separator, maxOutputs }),
57
+ defaultSource,
58
+ );
59
+ let copied = $state(false);
60
+
61
+ const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
62
+
63
+ function downloadText(text: string, fileName: string, type: string) {
64
+ if (typeof document === "undefined" || typeof URL.createObjectURL !== "function") return;
65
+ const url = URL.createObjectURL(new Blob([text], { type }));
66
+ const link = document.createElement("a");
67
+ link.href = url;
68
+ link.download = fileName;
69
+ link.click();
70
+ setTimeout(() => URL.revokeObjectURL(url), 0);
71
+ }
72
+
73
+ async function copy() {
74
+ try {
75
+ await navigator.clipboard.writeText(merge.serialize("text"));
76
+ copied = true;
77
+ } catch {
78
+ copied = false;
79
+ }
80
+ }
81
+
82
+ function mergeAll() {
83
+ const result = merge.merge();
84
+ copied = false;
85
+ onMerge?.(result);
86
+ }
87
+ </script>
88
+
89
+ <div class={className} data-merge-preview="">
90
+ <label class="flex flex-col gap-1">
91
+ <span class="font-medium">Data source (CSV, TSV or JSON)</span>
92
+ <textarea
93
+ data-merge-source=""
94
+ value={merge.sourceText}
95
+ rows={5}
96
+ spellcheck={false}
97
+ oninput={(event) => merge.setSourceText(event.currentTarget.value)}
98
+ class="px-2 py-1 font-mono text-xs rounded-md border border-violet-400/40 bg-transparent"
99
+ ></textarea>
100
+ </label>
101
+ <div class="flex flex-wrap items-center gap-2">
102
+ <input
103
+ type="file"
104
+ accept=".csv,.tsv,.txt,.json,text/csv,application/json"
105
+ aria-label="Load data source file"
106
+ data-merge-file=""
107
+ onchange={(event) => {
108
+ const file = event.currentTarget.files?.[0];
109
+ if (file) void merge.loadFile(file);
110
+ }}
111
+ />
112
+ <span data-merge-summary="" class="opacity-70"
113
+ >{`${plural(merge.source.records.length, "record")} · ${plural(merge.source.fields.length, "field")}`}</span
114
+ >
115
+ </div>
116
+ {#if merge.source.warnings.length > 0 && merge.sourceText.trim() !== ""}
117
+ <ul data-merge-warnings="" class="text-amber-600 text-xs">
118
+ {#each merge.source.warnings as warning}
119
+ <li>{warning.message}</li>
120
+ {/each}
121
+ </ul>
122
+ {/if}
123
+
124
+ {#if merge.variables.length > 0}
125
+ <fieldset class="flex flex-col gap-1">
126
+ <legend class="font-medium">Field mapping</legend>
127
+ {#each merge.variables as variable (variable)}
128
+ <label class="flex items-center gap-2" data-merge-mapping={variable}>
129
+ <span class="min-w-24 font-mono">{`{${variable}}`}</span>
130
+ <select
131
+ value={merge.mapping[variable] ?? ""}
132
+ aria-label={`Field for ${variable}`}
133
+ onchange={(event) => merge.setFieldMapping(variable, event.currentTarget.value || null)}
134
+ class="px-1 py-0.5 rounded-md border border-violet-400/40 bg-transparent"
135
+ >
136
+ <option value="">(none)</option>
137
+ {#each merge.source.fields as field (field)}
138
+ <option value={field}>{field}</option>
139
+ {/each}
140
+ </select>
141
+ </label>
142
+ {/each}
143
+ </fieldset>
144
+ {/if}
145
+
146
+ <div class="flex items-center gap-2">
147
+ <button type="button" data-merge-prev="" onclick={merge.prev} disabled={merge.index <= 0}>Prev</button>
148
+ <span data-merge-position=""
149
+ >{merge.count === 0 ? "No records" : `Record ${merge.index + 1} of ${merge.count}`}</span
150
+ >
151
+ <button
152
+ type="button"
153
+ data-merge-next=""
154
+ onclick={merge.next}
155
+ disabled={merge.index >= merge.count - 1}>Next</button
156
+ >
157
+ </div>
158
+
159
+ <div data-merge-output="">
160
+ <TemplateRenderer
161
+ {content}
162
+ templates={templates ?? []}
163
+ {bindings}
164
+ {templateColorMap}
165
+ resolve={!!templates && templates.length > 0}
166
+ values={merge.values}
167
+ />
168
+ </div>
169
+
170
+ <div class="flex flex-wrap items-center gap-2">
171
+ <button type="button" data-merge-all="" disabled={merge.count === 0} onclick={mergeAll}>Merge all</button>
172
+ {#if merge.result}
173
+ <span data-merge-result=""
174
+ >{`${merge.result.outputs.length} merged${merge.result.skipped.length ? `, ${merge.result.skipped.length} skipped` : ""}${merge.result.truncated ? " (stopped early)" : ""}`}</span
175
+ >
176
+ <button type="button" data-merge-copy="" onclick={copy}>{copied ? "Copied" : "Copy"}</button>
177
+ <button
178
+ type="button"
179
+ data-merge-download="text"
180
+ onclick={() => downloadText(merge.serialize("text"), `${downloadName}.txt`, "text/plain")}
181
+ >Download .txt</button
182
+ >
183
+ <button
184
+ type="button"
185
+ data-merge-download="json"
186
+ onclick={() => downloadText(merge.serialize("json"), `${downloadName}.json`, "application/json")}
187
+ >Download .json</button
188
+ >
189
+ {/if}
190
+ </div>
191
+ {#if merge.result}
192
+ <textarea
193
+ readonly
194
+ aria-label="Merged output"
195
+ data-merge-text=""
196
+ rows={6}
197
+ value={merge.serialize("text")}
198
+ class="px-2 py-1 font-mono text-xs rounded-md border border-violet-400/40 bg-transparent"
199
+ ></textarea>
200
+ {/if}
201
+ </div>
@@ -0,0 +1,25 @@
1
+ import type { ColorGradient, MergeRecordsOptions, MergeRecordsResult, ParseDataSourceOptions, Template } from "@pixerate/editor";
2
+ interface Props {
3
+ /** The prompt merged with each record. */
4
+ content: string;
5
+ templates?: Template[];
6
+ bindings?: Record<string, string>;
7
+ templateColorMap?: Map<string, ColorGradient>;
8
+ /** Initial CSV / TSV / JSON data source text. */
9
+ defaultSource?: string;
10
+ parseOptions?: ParseDataSourceOptions;
11
+ /** Drop records matching this condition (e.g. `"status=void"`) when merging. */
12
+ skipIf?: MergeRecordsOptions["skipIf"];
13
+ /** Separator between merged outputs (default a blank line; `"none"` for nothing). */
14
+ separator?: string;
15
+ maxOutputs?: number;
16
+ /** Called after "Merge all". */
17
+ onMerge?: (result: MergeRecordsResult) => void;
18
+ /** Base file name for downloads (default `merged`). */
19
+ downloadName?: string;
20
+ class?: string;
21
+ }
22
+ declare const MergePreview: import("svelte").Component<Props, {}, "">;
23
+ type MergePreview = ReturnType<typeof MergePreview>;
24
+ export default MergePreview;
25
+ //# sourceMappingURL=MergePreview.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MergePreview.svelte.d.ts","sourceRoot":"","sources":["../src/MergePreview.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACR,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAK1B,UAAU,KAAK;IACb,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,gFAAgF;IAChF,MAAM,CAAC,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvC,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyHH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import {
3
3
  evaluatePromptTokens,
4
+ describeConditionalTag,
5
+ formatConditionalTag,
4
6
  resolveTemplates,
5
7
  type Template,
6
8
  type ColorGradient,
@@ -36,8 +38,6 @@
36
38
  onVariableClick,
37
39
  }: Props = $props();
38
40
 
39
- const CONDITIONAL_PREFIX = { if: "?", unless: "!", end: "/" } as const;
40
-
41
41
  let displayText = $derived(
42
42
  resolve && templates.length > 0
43
43
  ? resolveTemplates(content, templates, { bindings })
@@ -58,12 +58,6 @@
58
58
  .join("\n");
59
59
  }
60
60
 
61
- function conditionalTitle(token: (typeof tokens)[number]): string {
62
- if (token.conditional === "end") return `End of section: ${token.value}`;
63
- return token.conditional === "if"
64
- ? `Shown when "${token.value}" has a value`
65
- : `Shown when "${token.value}" is empty`;
66
- }
67
61
  </script>
68
62
 
69
63
  <div class={className}>
@@ -102,8 +96,8 @@
102
96
  <span
103
97
  data-conditional={token.conditional}
104
98
  class="inline-flex items-center px-1 py-0.5 mx-0.5 text-xs font-medium rounded-md border border-amber-400/40 bg-amber-400/15 text-amber-700"
105
- title={conditionalTitle(token)}
106
- >{`{${CONDITIONAL_PREFIX[token.conditional]}${token.value}}`}</span>
99
+ title={describeConditionalTag(token)}
100
+ >{formatConditionalTag(token)}</span>
107
101
  {:else if token.type === "instruction"}
108
102
  <span
109
103
  class="inline-flex items-center px-1.5 py-0.5 mx-0.5 text-xs italic font-medium rounded-md border border-rose-400/40 bg-rose-400/15 text-rose-600"
@@ -1 +1 @@
1
- {"version":3,"file":"TemplateRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/TemplateRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAGH,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAG1B,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,eAAe,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AA6EH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"TemplateRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/TemplateRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAKH,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAG1B,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,eAAe,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAqEH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -10,6 +10,8 @@
10
10
  color?: string;
11
11
  /** Colour of equal-spacing guides. */
12
12
  spacingColor?: string;
13
+ /** Colour of measuring lines, their labels and extension lines. */
14
+ measureColor?: string;
13
15
  class?: string;
14
16
  }
15
17
 
@@ -18,6 +20,7 @@
18
20
  viewport = { x: 0, y: 0, zoom: 1 },
19
21
  color = '#f59e0b',
20
22
  spacingColor = '#ec4899',
23
+ measureColor = '#ef4444',
21
24
  class: className = ''
22
25
  }: Props = $props();
23
26
 
@@ -42,11 +45,38 @@
42
45
  y1={a.y}
43
46
  x2={b.x}
44
47
  y2={b.y}
45
- stroke={guide.kind === 'spacing' ? spacingColor : color}
48
+ stroke={guide.kind === 'measure' || guide.kind === 'extension'
49
+ ? measureColor
50
+ : guide.kind === 'spacing'
51
+ ? spacingColor
52
+ : color}
46
53
  stroke-width="1"
47
- stroke-dasharray={guide.kind === 'centre' ? '4 3' : undefined}
54
+ stroke-dasharray={guide.kind === 'centre' ? '4 3' : guide.kind === 'extension' ? '3 3' : undefined}
48
55
  shape-rendering="crispEdges"
49
56
  />
50
57
  {/each}
58
+ {#each guides as guide, i (i)}
59
+ {#if guide.label}
60
+ {@const mid = toScreen((guide.x1 + guide.x2) / 2, (guide.y1 + guide.y2) / 2)}
61
+ {@const vertical = guide.orientation === 'vertical'}
62
+ {@const width = guide.label.length * 6.5 + 8}
63
+ <!-- Labels sit beside vertical lines and below horizontal ones. -->
64
+ <g
65
+ class="pixerate-canvas-guides__label"
66
+ data-label={guide.label}
67
+ transform="translate({vertical ? mid.x + 4 : mid.x - width / 2} {vertical ? mid.y - 8 : mid.y + 4})"
68
+ >
69
+ <rect {width} height="16" rx="3" fill={measureColor} />
70
+ <text
71
+ x={width / 2}
72
+ y="11.5"
73
+ text-anchor="middle"
74
+ font-size="11"
75
+ font-family="system-ui, sans-serif"
76
+ fill="#fff">{guide.label}</text
77
+ >
78
+ </g>
79
+ {/if}
80
+ {/each}
51
81
  </svg>
52
82
  {/if}
@@ -8,6 +8,8 @@ interface Props {
8
8
  color?: string;
9
9
  /** Colour of equal-spacing guides. */
10
10
  spacingColor?: string;
11
+ /** Colour of measuring lines, their labels and extension lines. */
12
+ measureColor?: string;
11
13
  class?: string;
12
14
  }
13
15
  declare const CanvasGuidesLayer: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"CanvasGuidesLayer.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/CanvasGuidesLayer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGxE,UAAU,KAAK;IACd,kEAAkE;IAClE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AA+BF,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"CanvasGuidesLayer.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/CanvasGuidesLayer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGxE,UAAU,KAAK;IACd,kEAAkE;IAClE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAgDF,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -16,10 +16,21 @@ export interface CreateCanvasArrangeOptions<TNode extends CanvasNode = CanvasNod
16
16
  * Wire snapping to SvelteFlow with `onnodedragstart={() => arrange.beginDrag()}`,
17
17
  * `onnodedrag={({ nodes }) => arrange.drag(nodes.map((n) => n.id))}` and
18
18
  * `onnodedragstop={() => arrange.endDrag()}`, and render `arrange.guides` with
19
- * `<CanvasGuidesLayer>`.
19
+ * `<CanvasGuidesLayer>`. Snap resizes with `<NodeResizer {...arrange.resizerHandlers(id)} />`.
20
20
  */
21
21
  export declare function createCanvasArrange<TNode extends CanvasNode = CanvasNode>(options: CreateCanvasArrangeOptions<TNode>): {
22
22
  guides: GuideLine[];
23
+ /**
24
+ * Resize snapping for SvelteFlow's `<NodeResizer>` / `<NodeResizeControl>`:
25
+ * `<NodeResizer {...arrange.resizerHandlers(id)} />`. The resizer writes the raw size
26
+ * to the nodes after `onResize`, so snapping runs a microtask later and writes the
27
+ * snapped box back. Locked nodes snap back to their starting box.
28
+ */
29
+ resizerHandlers(id: string): {
30
+ onResizeStart: () => void;
31
+ onResize: () => void;
32
+ onResizeEnd: () => void;
33
+ };
23
34
  apply: (name: string, updates: import("@pixerate/editor/canvas").CanvasNodeUpdate[]) => import("@pixerate/editor/canvas").CanvasNodeUpdate[];
24
35
  align(edge: import("@pixerate/editor/canvas").AlignEdge, ids?: Iterable<string>, alignOptions?: import("@pixerate/editor/canvas").AlignNodesOptions): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
25
36
  distribute(axis: import("@pixerate/editor/canvas").DistributeAxis, mode?: import("@pixerate/editor/canvas").DistributeMode, ids?: Iterable<string>, distributeOptions?: import("@pixerate/editor/canvas").DistributeNodesOptions): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
@@ -44,6 +55,10 @@ export declare function createCanvasArrange<TNode extends CanvasNode = CanvasNod
44
55
  beginDrag(ids?: Iterable<string>): void;
45
56
  drag(ids?: Iterable<string>): import("@pixerate/editor/canvas").SnapNodeDragResult | null;
46
57
  snapPositionChanges<TChange extends import("@pixerate/editor/canvas").PositionChangeLike>(changes: TChange[]): TChange[];
58
+ beginResize(id: string): void;
59
+ resize(id: string, handle?: import("@pixerate/editor/canvas").ResizeHandle): import("@pixerate/editor/canvas").SnapNodeResizeResult | null;
60
+ endResize(): void;
61
+ snapResizeChanges<TChange extends import("@pixerate/editor/canvas").PositionChangeLike>(changes: TChange[]): TChange[];
47
62
  endDrag(): void;
48
63
  };
49
64
  export type CanvasArrange<TNode extends CanvasNode = CanvasNode> = ReturnType<typeof createCanvasArrange<TNode>>;
@@ -1 +1 @@
1
- {"version":3,"file":"createCanvasArrange.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/createCanvasArrange.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,SAAS,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,KAAK,WAAW,GAAG,8BAA8B,CAAC,GAAG,CAAC,CAAC;AAEvD,MAAM,WAAW,0BAA0B,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,CAChF,SAAQ,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,UAAU,GAAG,gBAAgB,CAAC;IACrE,QAAQ,EAAE,MAAM,KAAK,EAAE,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACnC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,EACxE,OAAO,EAAE,0BAA0B,CAAC,KAAK,CAAC;;;;;;;sBA2D2rlD,CAAC;sBAAiE,CAAC;sBAAiE,CAAC;oBAA+D,CAAC;cAAyD,CAAC;gBAAiD,CAAC;;;;;;;;;0BAA8tC,CAAC;;;;;;EAxCrvoD;AAED,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,IAAI,UAAU,CAAC,OAAO,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;AAEjH,MAAM,WAAW,6BAA6B;IAC7C,OAAO,EAAE;QAAE,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,CAAA;KAAE,CAAC;IACjE,uEAAuE;IACvE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC;IAC1D,aAAa,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAqB/F"}
1
+ {"version":3,"file":"createCanvasArrange.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/createCanvasArrange.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,SAAS,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,KAAK,WAAW,GAAG,8BAA8B,CAAC,GAAG,CAAC,CAAC;AAEvD,MAAM,WAAW,0BAA0B,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,CAChF,SAAQ,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,UAAU,GAAG,gBAAgB,CAAC;IACrE,QAAQ,EAAE,MAAM,KAAK,EAAE,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACnC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,EACxE,OAAO,EAAE,0BAA0B,CAAC,KAAK,CAAC;;IAkBzC;;;;;OAKG;wBACiB,MAAM;;;;;;;;;;sBAoDwv0D,CAAC;sBAAiE,CAAC;sBAAiE,CAAC;oBAA+D,CAAC;cAAyD,CAAC;gBAAiD,CAAC;;;;;;;;;0BAA8tC,CAAC;;;;;;;;;;EAxCny3D;AAED,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,IAAI,UAAU,CAAC,OAAO,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;AAEjH,MAAM,WAAW,6BAA6B;IAC7C,OAAO,EAAE;QAAE,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,CAAA;KAAE,CAAC;IACjE,uEAAuE;IACvE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC;IAC1D,aAAa,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAqB/F"}
@@ -9,7 +9,7 @@ import { createCanvasArrangeController, matchArrangeShortcut } from '@pixerate/e
9
9
  * Wire snapping to SvelteFlow with `onnodedragstart={() => arrange.beginDrag()}`,
10
10
  * `onnodedrag={({ nodes }) => arrange.drag(nodes.map((n) => n.id))}` and
11
11
  * `onnodedragstop={() => arrange.endDrag()}`, and render `arrange.guides` with
12
- * `<CanvasGuidesLayer>`.
12
+ * `<CanvasGuidesLayer>`. Snap resizes with `<NodeResizer {...arrange.resizerHandlers(id)} />`.
13
13
  */
14
14
  export function createCanvasArrange(options) {
15
15
  let guides = $state.raw([]);
@@ -22,9 +22,26 @@ export function createCanvasArrange(options) {
22
22
  });
23
23
  return {
24
24
  ...controller,
25
- /** Guide segments (flow coordinates) for the active drag; empty otherwise. */
25
+ /** Guide segments (flow coordinates) for the active drag or resize; empty otherwise. */
26
26
  get guides() {
27
27
  return guides;
28
+ },
29
+ /**
30
+ * Resize snapping for SvelteFlow's `<NodeResizer>` / `<NodeResizeControl>`:
31
+ * `<NodeResizer {...arrange.resizerHandlers(id)} />`. The resizer writes the raw size
32
+ * to the nodes after `onResize`, so snapping runs a microtask later and writes the
33
+ * snapped box back. Locked nodes snap back to their starting box.
34
+ */
35
+ resizerHandlers(id) {
36
+ return {
37
+ onResizeStart: () => controller.beginResize(id),
38
+ onResize: () => {
39
+ queueMicrotask(() => controller.resize(id));
40
+ },
41
+ onResizeEnd: () => {
42
+ queueMicrotask(() => controller.endResize());
43
+ }
44
+ };
28
45
  }
29
46
  };
30
47
  }
@@ -0,0 +1,68 @@
1
+ import { type CanvasAutoLayoutControllerOptions, type GuideLine, type PositionChangeLike } from '@pixerate/editor/canvas';
2
+ import type { CanvasNode } from './types.js';
3
+ type CoreOptions = CanvasAutoLayoutControllerOptions<any>;
4
+ export interface CreateCanvasAutoLayoutOptions<TNode extends CanvasNode = CanvasNode> extends Omit<CoreOptions, 'getNodes' | 'setNodes' | 'onGuidesChange'> {
5
+ getNodes: () => TNode[];
6
+ setNodes: (nodes: TNode[]) => void;
7
+ onGuidesChange?: (guides: GuideLine[]) => void;
8
+ }
9
+ /**
10
+ * Headless Svelte 5 rune for Figma-style auto layout on SvelteFlow sub-flows. Frames are
11
+ * nodes with `data.layout`; their children (`parentId`) are stacked, wrapped or placed on a
12
+ * grid, with hug / fill / fixed sizing, min / max, padding, alignment and constraints.
13
+ *
14
+ * Its actions (`addNode`, `removeNodes`, `resizeNode`, `moveChild`, `applyAutoLayout`) relayout
15
+ * in the same undoable step when a `HistoryManager` is passed. Wire the flow's own edits with
16
+ * `onnodedrag={({ targetNode }) => autoLayout.dragReorder(targetNode.id)}`,
17
+ * `onnodedragstop={({ targetNode }) => autoLayout.endReorderDrag(targetNode.id)}`,
18
+ * `ondelete={({ nodes }) => autoLayout.nodesDeleted(nodes)}`,
19
+ * `<NodeResizer {...autoLayout.resizerHandlers(id)} />` and call `autoLayout.measured(id)` when
20
+ * a node's content size changes. Render `guides` (the drop marker) with `<CanvasGuidesLayer>`.
21
+ */
22
+ export declare function createCanvasAutoLayout<TNode extends CanvasNode = CanvasNode>(options: CreateCanvasAutoLayoutOptions<TNode>): {
23
+ guides: GuideLine[];
24
+ hasPending: boolean;
25
+ /** Notes node changes (see the core controller) and flushes them on the next microtask. */
26
+ handleChanges<TChange extends PositionChangeLike>(changes: TChange[]): TChange[];
27
+ /** For SvelteFlow's `ondelete`: relayouts the frames the deleted nodes left. */
28
+ nodesDeleted(removed: Iterable<{
29
+ id: string;
30
+ parentId?: string;
31
+ }>): void;
32
+ /** Relayouts around node `id` after its measured (content) size changed. */
33
+ measured(id: string): void;
34
+ /**
35
+ * Handlers for SvelteFlow's `<NodeResizer>`: children follow their constraints and the
36
+ * frames around the node relayout when the resize ends.
37
+ */
38
+ resizerHandlers(id: string): {
39
+ onResizeStart: () => void;
40
+ onResizeEnd: () => void;
41
+ };
42
+ applyAutoLayout(frameId?: string): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
43
+ layoutUpdates(ids: Iterable<string>): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
44
+ relayout(ids: Iterable<string>): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
45
+ addNode(node: any, addOptions?: {
46
+ index?: number;
47
+ }): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
48
+ removeNodes(ids: Iterable<string>): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
49
+ resizeNode(id: string, size: {
50
+ width?: number;
51
+ height?: number;
52
+ }): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
53
+ moveChild(childId: string, index: number): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
54
+ getInsertTarget(id: string, point?: import("@pixerate/editor/canvas").XYPosition): import("@pixerate/editor/canvas").AutoLayoutInsertTarget | undefined;
55
+ beginReorderDrag(id: string): void;
56
+ dragReorder(id: string, point?: import("@pixerate/editor/canvas").XYPosition): import("@pixerate/editor/canvas").AutoLayoutInsertTarget | undefined;
57
+ endReorderDrag(id: string, point?: import("@pixerate/editor/canvas").XYPosition): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
58
+ noteChanges<TChange extends PositionChangeLike>(changes: TChange[]): TChange[];
59
+ noteRemoved(removed: Iterable<{
60
+ id: string;
61
+ parentId?: string;
62
+ }>): void;
63
+ flush(): import("@pixerate/editor/canvas").CanvasNodeUpdate[];
64
+ isFrame: (id: string) => boolean;
65
+ };
66
+ export type CanvasAutoLayout<TNode extends CanvasNode = CanvasNode> = ReturnType<typeof createCanvasAutoLayout<TNode>>;
67
+ export {};
68
+ //# sourceMappingURL=createCanvasAutoLayout.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createCanvasAutoLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/createCanvasAutoLayout.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,iCAAiC,EACtC,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,KAAK,WAAW,GAAG,iCAAiC,CAAC,GAAG,CAAC,CAAC;AAE1D,MAAM,WAAW,6BAA6B,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,CACnF,SAAQ,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,UAAU,GAAG,gBAAgB,CAAC;IACrE,QAAQ,EAAE,MAAM,KAAK,EAAE,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACnC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,EAAE,OAAO,EAAE,6BAA6B,CAAC,KAAK,CAAC;;;IA+BzH,2FAA2F;kBAC7E,OAAO,SAAS,kBAAkB,WAAW,OAAO,EAAE,GAAG,OAAO,EAAE;IAKhF,gFAAgF;0BAC1D,QAAQ,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAIjE,4EAA4E;iBAC/D,MAAM;IAInB;;;OAGG;wBACiB,MAAM;;;;;;;;aAiBw1vE,CAAC;;;;aAAiX,CAAC;cAAwB,CAAC;;;;;;;;;;gBAAqrD,CAAC;;;;EAHr7zE;AAED,MAAM,MAAM,gBAAgB,CAAC,KAAK,SAAS,UAAU,GAAG,UAAU,IAAI,UAAU,CAAC,OAAO,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { createCanvasAutoLayoutController } from '@pixerate/editor/canvas';
2
+ /**
3
+ * Headless Svelte 5 rune for Figma-style auto layout on SvelteFlow sub-flows. Frames are
4
+ * nodes with `data.layout`; their children (`parentId`) are stacked, wrapped or placed on a
5
+ * grid, with hug / fill / fixed sizing, min / max, padding, alignment and constraints.
6
+ *
7
+ * Its actions (`addNode`, `removeNodes`, `resizeNode`, `moveChild`, `applyAutoLayout`) relayout
8
+ * in the same undoable step when a `HistoryManager` is passed. Wire the flow's own edits with
9
+ * `onnodedrag={({ targetNode }) => autoLayout.dragReorder(targetNode.id)}`,
10
+ * `onnodedragstop={({ targetNode }) => autoLayout.endReorderDrag(targetNode.id)}`,
11
+ * `ondelete={({ nodes }) => autoLayout.nodesDeleted(nodes)}`,
12
+ * `<NodeResizer {...autoLayout.resizerHandlers(id)} />` and call `autoLayout.measured(id)` when
13
+ * a node's content size changes. Render `guides` (the drop marker) with `<CanvasGuidesLayer>`.
14
+ */
15
+ export function createCanvasAutoLayout(options) {
16
+ let guides = $state.raw([]);
17
+ let scheduled = false;
18
+ const controller = createCanvasAutoLayoutController({
19
+ ...options,
20
+ onGuidesChange: (next) => {
21
+ guides = next;
22
+ options.onGuidesChange?.(next);
23
+ }
24
+ });
25
+ // SvelteFlow writes its own changes after its callbacks run, so the flush waits a microtask.
26
+ const schedule = () => {
27
+ if (scheduled)
28
+ return;
29
+ scheduled = true;
30
+ queueMicrotask(() => {
31
+ scheduled = false;
32
+ controller.flush();
33
+ });
34
+ };
35
+ return {
36
+ ...controller,
37
+ /** Drop marker of the active reorder drag (flow coordinates); empty otherwise. */
38
+ get guides() {
39
+ return guides;
40
+ },
41
+ get hasPending() {
42
+ return controller.hasPending;
43
+ },
44
+ /** Notes node changes (see the core controller) and flushes them on the next microtask. */
45
+ handleChanges(changes) {
46
+ controller.noteChanges(changes);
47
+ schedule();
48
+ return changes;
49
+ },
50
+ /** For SvelteFlow's `ondelete`: relayouts the frames the deleted nodes left. */
51
+ nodesDeleted(removed) {
52
+ controller.noteRemoved(removed);
53
+ schedule();
54
+ },
55
+ /** Relayouts around node `id` after its measured (content) size changed. */
56
+ measured(id) {
57
+ controller.noteChanges([{ type: 'dimensions', id }]);
58
+ schedule();
59
+ },
60
+ /**
61
+ * Handlers for SvelteFlow's `<NodeResizer>`: children follow their constraints and the
62
+ * frames around the node relayout when the resize ends.
63
+ */
64
+ resizerHandlers(id) {
65
+ return {
66
+ onResizeStart: () => {
67
+ controller.noteChanges([{ type: 'dimensions', id, resizing: true }]);
68
+ },
69
+ onResizeEnd: () => {
70
+ queueMicrotask(() => {
71
+ controller.noteChanges([{ type: 'dimensions', id, resizing: false }]);
72
+ controller.flush();
73
+ });
74
+ }
75
+ };
76
+ }
77
+ };
78
+ }