@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.svelte.d.ts","sourceRoot":"","sources":["../../src/generation/plan.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,4BAA4B;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,cAAc,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,iBAAiB,EAChC,YAAY,EAAE,MAAM,iBAAiB,EACrC,OAAO,GAAE,MAAM,WAAwB,GACtC,oBAAoB,CAmBtB"}
@@ -0,0 +1,26 @@
1
+ import { planGenerationRequest, } from "@pixerate/editor/generation";
2
+ /**
3
+ * Reactive `planGenerationRequest` (Svelte 5 runes). Pass getters so the plan
4
+ * follows `$state` request/capabilities/options:
5
+ * `createGenerationPlan(() => request, () => caps, () => ({ strategy }))`.
6
+ */
7
+ export function createGenerationPlan(request, capabilities, options = () => ({})) {
8
+ const plan = $derived(planGenerationRequest(request(), capabilities(), options()));
9
+ return {
10
+ get plan() {
11
+ return plan;
12
+ },
13
+ get ok() {
14
+ return plan.ok;
15
+ },
16
+ get request() {
17
+ return plan.request;
18
+ },
19
+ get adjustments() {
20
+ return plan.adjustments;
21
+ },
22
+ get errors() {
23
+ return plan.errors;
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,25 @@
1
+ import { type JobProgressEstimate } from "@pixerate/editor/generation";
2
+ export interface JobProgressOptions {
3
+ startedAt: number | Date | string;
4
+ expectedMs?: number;
5
+ done?: boolean;
6
+ /** Fixed current time (disables ticking; useful for SSR and tests). */
7
+ now?: number | Date | string;
8
+ /** Update interval while running in ms (default 1000). */
9
+ intervalMs?: number;
10
+ }
11
+ export interface SvelteJobProgress {
12
+ readonly progress: JobProgressEstimate;
13
+ readonly percent: number;
14
+ readonly label: string;
15
+ /** Stops ticking. */
16
+ destroy(): void;
17
+ }
18
+ /**
19
+ * Ticking `estimateJobProgress` (Svelte 5 runes). Pass a getter so it follows
20
+ * `$state` options; it ticks every `intervalMs` while not `done` and no fixed
21
+ * `now` is given (browser only; nothing ticks during SSR). Call `destroy()`
22
+ * when done (e.g. in `onDestroy`).
23
+ */
24
+ export declare function createJobProgress(options: () => JobProgressOptions): SvelteJobProgress;
25
+ //# sourceMappingURL=progress.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.svelte.d.ts","sourceRoot":"","sources":["../../src/generation/progress.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAE5F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IAC7B,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qBAAqB;IACrB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,kBAAkB,GAAG,iBAAiB,CA2BtF"}
@@ -0,0 +1,36 @@
1
+ import { estimateJobProgress } from "@pixerate/editor/generation";
2
+ /**
3
+ * Ticking `estimateJobProgress` (Svelte 5 runes). Pass a getter so it follows
4
+ * `$state` options; it ticks every `intervalMs` while not `done` and no fixed
5
+ * `now` is given (browser only; nothing ticks during SSR). Call `destroy()`
6
+ * when done (e.g. in `onDestroy`).
7
+ */
8
+ export function createJobProgress(options) {
9
+ let tick = $state(Date.now());
10
+ const timer = typeof window !== "undefined"
11
+ ? setInterval(() => {
12
+ const current = options();
13
+ if (!current.done && current.now === undefined)
14
+ tick = Date.now();
15
+ }, Math.max(16, options().intervalMs ?? 1000))
16
+ : null;
17
+ const progress = $derived.by(() => {
18
+ const { startedAt, expectedMs, done, now } = options();
19
+ return estimateJobProgress({ startedAt, expectedMs, done, now: now ?? tick });
20
+ });
21
+ return {
22
+ get progress() {
23
+ return progress;
24
+ },
25
+ get percent() {
26
+ return progress.percent;
27
+ },
28
+ get label() {
29
+ return progress.label;
30
+ },
31
+ destroy() {
32
+ if (timer !== null)
33
+ clearInterval(timer);
34
+ },
35
+ };
36
+ }
@@ -1,4 +1,4 @@
1
- import { HistoryManager } from "@pixerate/editor/history";
1
+ import type { HistoryManager } from "@pixerate/editor/history";
2
2
  export interface HistoryShortcutsOptions {
3
3
  /**
4
4
  * Whether keyboard shortcuts are enabled. Defaults to true.
@@ -1 +1 @@
1
- {"version":3,"file":"createHistoryShortcuts.d.ts","sourceRoot":"","sources":["../../src/history/createHistoryShortcuts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IACrC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,uBAA4B,GACpC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAqEzB"}
1
+ {"version":3,"file":"createHistoryShortcuts.d.ts","sourceRoot":"","sources":["../../src/history/createHistoryShortcuts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IACrC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,uBAA4B,GACpC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAqCzB"}
@@ -1,3 +1,4 @@
1
+ import { matchHistoryShortcut } from "@pixerate/editor/commands";
1
2
  /**
2
3
  * Attaches keyboard listeners for Undo (Mod+Z) and Redo (Mod+Shift+Z / Mod+Y).
3
4
  * Returns an object with a `destroy()` function for lifecycle cleanup.
@@ -13,39 +14,16 @@ export function createHistoryShortcuts(manager, options = {}) {
13
14
  }
14
15
  const handleKeyDown = (event) => {
15
16
  const e = event;
16
- const isMac = typeof navigator !== "undefined" &&
17
- /Mac|iPod|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
18
- const modifier = isMac ? e.metaKey : e.ctrlKey;
19
- if (!modifier)
20
- return;
21
- if (ignoreEditableElements && typeof document !== "undefined") {
22
- const targetEl = (e.target || document.activeElement);
23
- if (targetEl && targetEl.tagName) {
24
- const tagName = targetEl.tagName.toLowerCase();
25
- const isInput = tagName === "input" || tagName === "textarea";
26
- const isContentEditable = targetEl.isContentEditable ||
27
- targetEl.getAttribute?.("contenteditable") === "true";
28
- if (isInput || isContentEditable) {
29
- return;
30
- }
31
- }
17
+ // Default bindings from the command system: Mod+Z, Mod+Shift+Z and (off macOS) Mod+Y,
18
+ // matched by `event.code` with `Mod` = ⌘ on macOS and Ctrl elsewhere.
19
+ const action = matchHistoryShortcut(e, { allowInEditable: !ignoreEditableElements });
20
+ if (action === "undo" && manager.getState().canUndo) {
21
+ e.preventDefault();
22
+ manager.undo();
32
23
  }
33
- const key = e.key?.toLowerCase();
34
- // Undo: Mod+Z (without Shift)
35
- if (key === "z" && !e.shiftKey) {
36
- if (manager.getState().canUndo) {
37
- e.preventDefault();
38
- manager.undo();
39
- }
40
- return;
41
- }
42
- // Redo: Mod+Shift+Z or (on non-Mac) Mod+Y
43
- const isRedoKey = (key === "z" && e.shiftKey) || (!isMac && key === "y" && !e.shiftKey);
44
- if (isRedoKey) {
45
- if (manager.getState().canRedo) {
46
- e.preventDefault();
47
- manager.redo();
48
- }
24
+ else if (action === "redo" && manager.getState().canRedo) {
25
+ e.preventDefault();
26
+ manager.redo();
49
27
  }
50
28
  };
51
29
  eventTarget.addEventListener("keydown", handleKeyDown);
@@ -1,14 +1,23 @@
1
- import { HistoryManager, HistoryState, Command, HistoryManagerOptions } from "@pixerate/editor/history";
1
+ import { HistoryManager, HistoryState, HistoryEntry, HistoryMark, MergedExecutionOptions, Command, HistoryManagerOptions } from "@pixerate/editor/history";
2
2
  export interface SvelteHistory extends HistoryState {
3
3
  readonly manager: HistoryManager;
4
+ /** History panel rows (undo steps, then redo steps). */
5
+ readonly entries: HistoryEntry[];
4
6
  undo(): boolean;
5
7
  redo(): boolean;
6
8
  clear(): void;
7
9
  execute(command: Command): void;
8
- executeMerged(command: Command, options?: {
9
- windowMs?: number;
10
- }): void;
10
+ executeMerged(command: Command, options?: MergedExecutionOptions): void;
11
11
  batch(name: string, fn: () => void): void;
12
+ /** Undoes / redoes until the entry at `index` is current (`-1` = before all). */
13
+ goTo(index: number): number;
14
+ /** Captures the current point, e.g. when a drag starts. */
15
+ mark(): HistoryMark;
16
+ /** Takes back every step since `mark` without leaving redo (Escape-cancel). */
17
+ revertTo(mark: HistoryMark): number;
18
+ /** Joins every merged execute into one step until `endGesture`. */
19
+ beginGesture(key: string): void;
20
+ endGesture(): void;
12
21
  subscribe(run: (value: HistoryState) => void): () => void;
13
22
  }
14
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"history.svelte.d.ts","sourceRoot":"","sources":["../../src/history/history.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,OAAO,EAEP,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,IAAI,IAAI,OAAO,CAAC;IAChB,IAAI,IAAI,OAAO,CAAC;IAChB,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC3D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,gBAAgB,CAAC,EAAE,cAAc,GAAG,qBAAqB,GACxD,aAAa,CA2Cf"}
1
+ {"version":3,"file":"history.svelte.d.ts","sourceRoot":"","sources":["../../src/history/history.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,sBAAsB,EACtB,OAAO,EAEP,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;IACjC,IAAI,IAAI,OAAO,CAAC;IAChB,IAAI,IAAI,OAAO,CAAC;IAChB,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACxE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,iFAAiF;IACjF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,IAAI,IAAI,WAAW,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAAC;IACpC,mEAAmE;IACnE,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,IAAI,IAAI,CAAC;IACnB,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC3D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,gBAAgB,CAAC,EAAE,cAAc,GAAG,qBAAqB,GACxD,aAAa,CAqDf"}
@@ -32,12 +32,22 @@ export function createHistory(managerOrOptions) {
32
32
  get manager() {
33
33
  return manager;
34
34
  },
35
+ get entries() {
36
+ // Reading `state` makes the getter reactive; getEntries() is cached per change.
37
+ void state;
38
+ return manager.getEntries();
39
+ },
35
40
  undo: () => manager.undo(),
36
41
  redo: () => manager.redo(),
37
42
  clear: () => manager.clear(),
38
43
  execute: (cmd) => manager.execute(cmd),
39
44
  executeMerged: (cmd, opts) => manager.executeMerged(cmd, opts),
40
45
  batch: (name, fn) => manager.batch(name, fn),
46
+ goTo: (index) => manager.goTo(index),
47
+ mark: () => manager.mark(),
48
+ revertTo: (m) => manager.revertTo(m),
49
+ beginGesture: (key) => manager.beginGesture(key),
50
+ endGesture: () => manager.endGesture(),
41
51
  subscribe: (run) => manager.subscribe(run),
42
52
  };
43
53
  }
@@ -0,0 +1,31 @@
1
+ import { DirtyTracker, type DirtyTrackerOptions, type HistoryManager } from "@pixerate/editor/history";
2
+ export interface CreateHistoryDirtyOptions<T> extends DirtyTrackerOptions<T> {
3
+ /** The history whose changes re-check the document (a `createHistory().manager`). */
4
+ history: HistoryManager;
5
+ /** Reads the current document value. */
6
+ getValue: () => T;
7
+ /** Existing tracker to drive. */
8
+ tracker?: DirtyTracker<T>;
9
+ }
10
+ export interface SvelteHistoryDirty<T> {
11
+ /** Whether the document differs from the saved state (reactive). */
12
+ readonly isDirty: boolean;
13
+ /** Marks the current document as saved. */
14
+ markClean(): void;
15
+ /** Runs a load / restore without marking the document dirty (re-entrant). */
16
+ suppress<R>(fn: () => R): R;
17
+ /** Re-checks after a change made outside the history. */
18
+ refresh(): boolean;
19
+ readonly tracker: DirtyTracker<T>;
20
+ /** Stops following the history. */
21
+ destroy(): void;
22
+ }
23
+ /**
24
+ * Svelte 5 rune: dirty-vs-saved tracking bound to a HistoryManager. Undoing
25
+ * back to the saved state makes the document clean again, so a navigation
26
+ * guard fed with `isDirty` stops prompting.
27
+ *
28
+ * Usage: `createNavigationGuard({ isDirty: () => dirty.isDirty })`.
29
+ */
30
+ export declare function createHistoryDirty<T>(options: CreateHistoryDirtyOptions<T>): SvelteHistoryDirty<T>;
31
+ //# sourceMappingURL=historyDirty.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"historyDirty.svelte.d.ts","sourceRoot":"","sources":["../../src/history/historyDirty.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAEZ,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,yBAAyB,CAAC,CAAC,CAAE,SAAQ,mBAAmB,CAAC,CAAC,CAAC;IAC1E,qFAAqF;IACrF,OAAO,EAAE,cAAc,CAAC;IACxB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClB,iCAAiC;IACjC,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,2CAA2C;IAC3C,SAAS,IAAI,IAAI,CAAC;IAClB,6EAA6E;IAC7E,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,yDAAyD;IACzD,OAAO,IAAI,OAAO,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,mCAAmC;IACnC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GACpC,kBAAkB,CAAC,CAAC,CAAC,CAsBvB"}
@@ -0,0 +1,30 @@
1
+ import { bindDirtyToHistory, } from "@pixerate/editor/history";
2
+ /**
3
+ * Svelte 5 rune: dirty-vs-saved tracking bound to a HistoryManager. Undoing
4
+ * back to the saved state makes the document clean again, so a navigation
5
+ * guard fed with `isDirty` stops prompting.
6
+ *
7
+ * Usage: `createNavigationGuard({ isDirty: () => dirty.isDirty })`.
8
+ */
9
+ export function createHistoryDirty(options) {
10
+ const binding = bindDirtyToHistory(options);
11
+ let isDirty = $state(binding.dirty);
12
+ const unsubscribe = binding.subscribe((dirty) => {
13
+ isDirty = dirty;
14
+ });
15
+ return {
16
+ get isDirty() {
17
+ return isDirty;
18
+ },
19
+ markClean: () => binding.markClean(),
20
+ suppress: (fn) => binding.suppress(fn),
21
+ refresh: () => binding.refresh(),
22
+ get tracker() {
23
+ return binding.tracker;
24
+ },
25
+ destroy() {
26
+ unsubscribe();
27
+ binding.destroy();
28
+ },
29
+ };
30
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./history.svelte.js";
2
2
  export * from "./createHistoryShortcuts.js";
3
+ export * from "./historyDirty.svelte.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export * from "./history.svelte.js";
2
2
  export * from "./createHistoryShortcuts.js";
3
+ export * from "./historyDirty.svelte.js";
@@ -0,0 +1,309 @@
1
+ <script lang="ts">
2
+ import type {
3
+ AdjustmentKind,
4
+ AdjustmentLayer,
5
+ BlendMode,
6
+ ControlSpec,
7
+ CurvePoint,
8
+ ImageEditorController,
9
+ ImageHistogram,
10
+ } from '@pixerate/editor/image-editor';
11
+ import {
12
+ ADJUSTMENT_KINDS,
13
+ ADJUSTMENT_LAYER_CONTROLS,
14
+ ADJUSTMENT_LAYER_OPACITY_CONTROL,
15
+ BLEND_MODES,
16
+ HUE_RANGE_LABELS,
17
+ adjustmentKindLabel,
18
+ getControlPath,
19
+ hueRangeWeight,
20
+ setControlPath,
21
+ } from '@pixerate/editor/image-editor';
22
+ import { untrack } from 'svelte';
23
+ import ControlSlider from './ControlSlider.svelte';
24
+ import CurvesEditor from './CurvesEditor.svelte';
25
+
26
+ interface Props {
27
+ controller: ImageEditorController;
28
+ /** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
29
+ layers?: AdjustmentLayer[];
30
+ histogram?: ImageHistogram | null;
31
+ }
32
+
33
+ let { controller, layers: layersProp, histogram = null }: Props = $props();
34
+
35
+ // Follows the controller when no `layers` prop is given.
36
+ let tick = $state(0);
37
+ $effect(() => {
38
+ const c = controller;
39
+ // `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
40
+ return untrack(() => c.subscribe(() => tick++));
41
+ });
42
+ const layers = $derived.by(() => {
43
+ void tick;
44
+ return layersProp ?? controller.getAdjustmentLayers();
45
+ });
46
+
47
+ type Channel = 'master' | 'red' | 'green' | 'blue';
48
+ const CHANNELS: { id: Channel; label: string }[] = [
49
+ { id: 'master', label: 'RGB' },
50
+ { id: 'red', label: 'Red' },
51
+ { id: 'green', label: 'Green' },
52
+ { id: 'blue', label: 'Blue' },
53
+ ];
54
+ const TONES = [
55
+ { id: 'shadows', label: 'Shadows' },
56
+ { id: 'midtones', label: 'Midtones' },
57
+ { id: 'highlights', label: 'Highlights' },
58
+ ] as const;
59
+ type Tone = (typeof TONES)[number]['id'];
60
+
61
+ let newKind: AdjustmentKind = $state('curves');
62
+ let selectedId: string | null = $state(null);
63
+ let channel: Channel = $state('master');
64
+ let range = $state(-1);
65
+ let tone: Tone = $state('midtones');
66
+
67
+ const selected = $derived(layers.find((l) => l.id === selectedId) ?? null);
68
+ const topFirst = $derived([...layers].reverse());
69
+
70
+ const selectClass = 'bg-slate-800 border border-slate-700 rounded px-1.5 py-1 text-slate-200';
71
+ const chipClass = (on: boolean) =>
72
+ `px-2 py-0.5 rounded text-[10px] ${on ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-400 hover:text-slate-200'}`;
73
+
74
+ function setParam(layer: AdjustmentLayer, path: string, value: unknown, commit = true) {
75
+ controller.updateAdjustmentLayer(layer.id, { params: setControlPath(layer.params, path, value) as never }, commit);
76
+ }
77
+ const paramValue = (layer: AdjustmentLayer, spec: ControlSpec) => Number(getControlPath(layer.params, spec.id) ?? spec.default);
78
+
79
+ function visibleControls(layer: AdjustmentLayer): ControlSpec[] {
80
+ const all = ADJUSTMENT_LAYER_CONTROLS[layer.kind];
81
+ switch (layer.kind) {
82
+ case 'levels':
83
+ return all.filter((s) => s.id.startsWith(`${channel}.`));
84
+ case 'hueSaturation':
85
+ return all.filter((s) =>
86
+ range === -1 || layer.params.colorize ? !s.id.startsWith('ranges.') : s.id.startsWith(`ranges.${range}.`)
87
+ );
88
+ case 'colorBalance':
89
+ return all.filter((s) => s.id.startsWith(`${tone}.`));
90
+ default:
91
+ return [...all];
92
+ }
93
+ }
94
+
95
+ function stripPoints(bounds: [number, number, number, number]) {
96
+ return Array.from({ length: 73 }, (_, i) => `${(i / 72) * 360},${20 - hueRangeWeight({ bounds }, i * 5) * 18}`).join(' ');
97
+ }
98
+
99
+ function addLayer() {
100
+ selectedId = controller.addAdjustmentLayer(newKind);
101
+ channel = 'master';
102
+ range = -1;
103
+ }
104
+ </script>
105
+
106
+ <div class="flex flex-col gap-2 text-xs" data-testid="adjustment-layers-panel">
107
+ <div class="flex items-center justify-between">
108
+ <span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Adjustment Layers</span>
109
+ </div>
110
+ <div class="flex gap-1.5">
111
+ <select aria-label="New adjustment layer kind" class="{selectClass} flex-1" bind:value={newKind}>
112
+ {#each ADJUSTMENT_KINDS as k (k.kind)}
113
+ <option value={k.kind}>{k.label}</option>
114
+ {/each}
115
+ </select>
116
+ <button type="button" class="px-2 py-1 rounded bg-indigo-600 text-white hover:bg-indigo-500" onclick={addLayer}>
117
+ Add Layer
118
+ </button>
119
+ </div>
120
+
121
+ {#if topFirst.length > 0}
122
+ <ul class="flex flex-col gap-1" aria-label="Adjustment layers">
123
+ {#each topFirst as layer (layer.id)}
124
+ {@const index = layers.indexOf(layer)}
125
+ {@const label = layer.name || adjustmentKindLabel(layer.kind)}
126
+ <li
127
+ class="flex items-center gap-1.5 rounded px-1.5 py-1 {layer.id === selectedId
128
+ ? 'bg-indigo-600/20 ring-1 ring-indigo-500/50'
129
+ : 'bg-slate-800/60'}"
130
+ >
131
+ <input
132
+ type="checkbox"
133
+ aria-label="Show {label}"
134
+ checked={layer.enabled}
135
+ onchange={() => controller.toggleAdjustmentLayer(layer.id)}
136
+ class="accent-indigo-500"
137
+ />
138
+ <button
139
+ type="button"
140
+ class="flex-1 text-left truncate {layer.enabled ? 'text-slate-200' : 'text-slate-500 line-through'}"
141
+ aria-pressed={layer.id === selectedId}
142
+ onclick={() => (selectedId = layer.id === selectedId ? null : layer.id)}
143
+ >
144
+ {label}
145
+ </button>
146
+ <button
147
+ type="button"
148
+ aria-label="Move {label} up"
149
+ disabled={index === layers.length - 1}
150
+ onclick={() => controller.moveAdjustmentLayer(layer.id, index + 1)}
151
+ class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
152
+ >
153
+ ↑
154
+ </button>
155
+ <button
156
+ type="button"
157
+ aria-label="Move {label} down"
158
+ disabled={index === 0}
159
+ onclick={() => controller.moveAdjustmentLayer(layer.id, index - 1)}
160
+ class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
161
+ >
162
+ ↓
163
+ </button>
164
+ <button
165
+ type="button"
166
+ aria-label="Remove {label}"
167
+ onclick={() => {
168
+ controller.removeAdjustmentLayer(layer.id);
169
+ if (selectedId === layer.id) selectedId = null;
170
+ }}
171
+ class="px-1 text-rose-400 hover:text-rose-300"
172
+ >
173
+ ✕
174
+ </button>
175
+ </li>
176
+ {/each}
177
+ </ul>
178
+ {/if}
179
+
180
+ {#if selected}
181
+ {@const layer = selected}
182
+ <div class="flex flex-col gap-2 rounded border border-slate-700/60 p-2" data-testid="adjustment-layer-editor">
183
+ <div class="flex items-center gap-2">
184
+ <span class="text-slate-400">Blend</span>
185
+ <select
186
+ aria-label="Blend mode"
187
+ class="{selectClass} flex-1"
188
+ value={layer.blend}
189
+ onchange={(e) => controller.updateAdjustmentLayer(layer.id, { blend: (e.target as HTMLSelectElement).value as BlendMode })}
190
+ >
191
+ {#each BLEND_MODES as m (m.id)}
192
+ <option value={m.id}>{m.label}</option>
193
+ {/each}
194
+ </select>
195
+ </div>
196
+ <ControlSlider
197
+ spec={ADJUSTMENT_LAYER_OPACITY_CONTROL}
198
+ label="Layer opacity"
199
+ value={layer.opacity}
200
+ onchange={(v) => controller.updateAdjustmentLayer(layer.id, { opacity: v })}
201
+ />
202
+
203
+ {#if layer.kind === 'curves' || layer.kind === 'levels'}
204
+ <div class="flex gap-1" role="group" aria-label={layer.kind === 'curves' ? 'Curve channel' : 'Levels channel'}>
205
+ {#each CHANNELS as c (c.id)}
206
+ <button type="button" class={chipClass(channel === c.id)} onclick={() => (channel = c.id)}>{c.label}</button>
207
+ {/each}
208
+ </div>
209
+ {/if}
210
+
211
+ {#if layer.kind === 'curves'}
212
+ <CurvesEditor
213
+ points={layer.params[channel]}
214
+ {channel}
215
+ {histogram}
216
+ onchange={(points: CurvePoint[], commit: boolean) => setParam(layer, channel, points, commit)}
217
+ />
218
+ {:else if layer.kind === 'hueSaturation'}
219
+ <div class="flex flex-wrap gap-1" role="group" aria-label="Hue range">
220
+ <button type="button" class={chipClass(range === -1)} onclick={() => (range = -1)}>Master</button>
221
+ {#each HUE_RANGE_LABELS as name, i (name)}
222
+ <button type="button" class={chipClass(range === i)} disabled={layer.params.colorize} onclick={() => (range = i)}>
223
+ {name}
224
+ </button>
225
+ {/each}
226
+ </div>
227
+ {#if range >= 0 && !layer.params.colorize}
228
+ <svg viewBox="0 0 360 20" preserveAspectRatio="none" class="w-full h-4 rounded" data-testid="hue-range-strip">
229
+ <defs>
230
+ <linearGradient id="pixerate-hue-strip-svelte">
231
+ {#each [0, 60, 120, 180, 240, 300, 360] as h (h)}
232
+ <stop offset="{(h / 360) * 100}%" stop-color="hsl({h}, 90%, 50%)" />
233
+ {/each}
234
+ </linearGradient>
235
+ </defs>
236
+ <rect width="360" height="20" fill="url(#pixerate-hue-strip-svelte)" opacity="0.6" />
237
+ <polyline points={stripPoints(layer.params.ranges[range].bounds)} fill="none" stroke="#fff" stroke-width="1.5" />
238
+ </svg>
239
+ {/if}
240
+ {:else if layer.kind === 'colorBalance'}
241
+ <div class="flex gap-1" role="group" aria-label="Tone">
242
+ {#each TONES as t (t.id)}
243
+ <button type="button" class={chipClass(tone === t.id)} onclick={() => (tone = t.id)}>{t.label}</button>
244
+ {/each}
245
+ </div>
246
+ {:else if layer.kind === 'photoFilter'}
247
+ <label class="flex items-center gap-2 text-slate-300">
248
+ Filter colour
249
+ <input
250
+ type="color"
251
+ aria-label="Filter colour"
252
+ value={layer.params.color}
253
+ oninput={(e) => setParam(layer, 'color', (e.target as HTMLInputElement).value)}
254
+ class="h-5 w-8 bg-transparent"
255
+ />
256
+ </label>
257
+ {:else if layer.kind === 'invert'}
258
+ <p class="text-slate-500 text-[11px]">Invert has no settings.</p>
259
+ {/if}
260
+
261
+ {#each visibleControls(layer) as spec (spec.id)}
262
+ <ControlSlider {spec} value={paramValue(layer, spec)} onchange={(v) => setParam(layer, spec.id, v)} />
263
+ {/each}
264
+
265
+ {#if layer.kind === 'hueSaturation' && range === -1}
266
+ <label class="flex items-center gap-2 text-slate-300">
267
+ <input
268
+ type="checkbox"
269
+ checked={layer.params.colorize}
270
+ onchange={(e) => setParam(layer, 'colorize', (e.target as HTMLInputElement).checked)}
271
+ class="accent-indigo-500"
272
+ />
273
+ Colorize
274
+ </label>
275
+ {/if}
276
+ {#if layer.kind === 'colorBalance' || layer.kind === 'photoFilter'}
277
+ <label class="flex items-center gap-2 text-slate-300">
278
+ <input
279
+ type="checkbox"
280
+ checked={layer.params.preserveLuminosity}
281
+ onchange={(e) => setParam(layer, 'preserveLuminosity', (e.target as HTMLInputElement).checked)}
282
+ class="accent-indigo-500"
283
+ />
284
+ Preserve luminosity
285
+ </label>
286
+ {/if}
287
+ {#if layer.kind === 'blackWhite'}
288
+ <label class="flex items-center gap-2 text-slate-300">
289
+ <input
290
+ type="checkbox"
291
+ checked={layer.params.tint !== null}
292
+ onchange={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).checked ? '#e1d3b3' : null)}
293
+ class="accent-indigo-500"
294
+ />
295
+ Tint
296
+ {#if layer.params.tint}
297
+ <input
298
+ type="color"
299
+ aria-label="Tint colour"
300
+ value={layer.params.tint}
301
+ oninput={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).value)}
302
+ class="h-5 w-8 bg-transparent"
303
+ />
304
+ {/if}
305
+ </label>
306
+ {/if}
307
+ </div>
308
+ {/if}
309
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { AdjustmentLayer, ImageEditorController, ImageHistogram } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ controller: ImageEditorController;
4
+ /** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
5
+ layers?: AdjustmentLayer[];
6
+ histogram?: ImageHistogram | null;
7
+ }
8
+ declare const AdjustmentLayersPanel: import("svelte").Component<Props, {}, "">;
9
+ type AdjustmentLayersPanel = ReturnType<typeof AdjustmentLayersPanel>;
10
+ export default AdjustmentLayersPanel;
11
+ //# sourceMappingURL=AdjustmentLayersPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdjustmentLayersPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/AdjustmentLayersPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAER,eAAe,EAIf,qBAAqB,EACrB,cAAc,EACf,MAAM,+BAA+B,CAAC;AAiBvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,qGAAqG;IACrG,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AA0NH,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}