@pixerate/editor-svelte 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +6 -2
  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/StylesGallery.svelte +169 -0
  6. package/dist/StylesGallery.svelte.d.ts +27 -0
  7. package/dist/StylesGallery.svelte.d.ts.map +1 -0
  8. package/dist/TemplateRenderer.svelte +4 -10
  9. package/dist/TemplateRenderer.svelte.d.ts.map +1 -1
  10. package/dist/canvas/TokenBindingPicker.svelte +93 -0
  11. package/dist/canvas/TokenBindingPicker.svelte.d.ts +20 -0
  12. package/dist/canvas/TokenBindingPicker.svelte.d.ts.map +1 -0
  13. package/dist/canvas/TokenModeSwitcher.svelte +55 -0
  14. package/dist/canvas/TokenModeSwitcher.svelte.d.ts +18 -0
  15. package/dist/canvas/TokenModeSwitcher.svelte.d.ts.map +1 -0
  16. package/dist/canvas/createCanvasArrange.svelte.d.ts +4 -0
  17. package/dist/canvas/createCanvasArrange.svelte.d.ts.map +1 -1
  18. package/dist/canvas/createCanvasAutoLayout.svelte.d.ts +68 -0
  19. package/dist/canvas/createCanvasAutoLayout.svelte.d.ts.map +1 -0
  20. package/dist/canvas/createCanvasAutoLayout.svelte.js +78 -0
  21. package/dist/canvas/createCanvasTokens.svelte.d.ts +31 -0
  22. package/dist/canvas/createCanvasTokens.svelte.d.ts.map +1 -0
  23. package/dist/canvas/createCanvasTokens.svelte.js +50 -0
  24. package/dist/canvas/index.d.ts +7 -0
  25. package/dist/canvas/index.d.ts.map +1 -1
  26. package/dist/canvas/index.js +10 -0
  27. package/dist/fileImport.svelte.d.ts +8 -1
  28. package/dist/fileImport.svelte.d.ts.map +1 -1
  29. package/dist/fileImport.svelte.js +11 -1
  30. package/dist/find-replace/FindReplaceBar.svelte +203 -0
  31. package/dist/find-replace/FindReplaceBar.svelte.d.ts +15 -0
  32. package/dist/find-replace/FindReplaceBar.svelte.d.ts.map +1 -0
  33. package/dist/find-replace/findReplace.svelte.d.ts +27 -0
  34. package/dist/find-replace/findReplace.svelte.d.ts.map +1 -0
  35. package/dist/find-replace/findReplace.svelte.js +79 -0
  36. package/dist/find-replace/index.d.ts +6 -0
  37. package/dist/find-replace/index.d.ts.map +1 -0
  38. package/dist/find-replace/index.js +2 -0
  39. package/dist/generation/AssetMentionChips.svelte +130 -0
  40. package/dist/generation/AssetMentionChips.svelte.d.ts +21 -0
  41. package/dist/generation/AssetMentionChips.svelte.d.ts.map +1 -0
  42. package/dist/generation/GenerationHistory.svelte +161 -0
  43. package/dist/generation/GenerationHistory.svelte.d.ts +28 -0
  44. package/dist/generation/GenerationHistory.svelte.d.ts.map +1 -0
  45. package/dist/generation/GenerationSettings.svelte +174 -0
  46. package/dist/generation/GenerationSettings.svelte.d.ts +23 -0
  47. package/dist/generation/GenerationSettings.svelte.d.ts.map +1 -0
  48. package/dist/generation/JobProgress.svelte +53 -0
  49. package/dist/generation/JobProgress.svelte.d.ts +17 -0
  50. package/dist/generation/JobProgress.svelte.d.ts.map +1 -0
  51. package/dist/generation/index.d.ts +17 -0
  52. package/dist/generation/index.d.ts.map +1 -0
  53. package/dist/generation/index.js +7 -0
  54. package/dist/generation/plan.svelte.d.ts +16 -0
  55. package/dist/generation/plan.svelte.d.ts.map +1 -0
  56. package/dist/generation/plan.svelte.js +26 -0
  57. package/dist/generation/progress.svelte.d.ts +25 -0
  58. package/dist/generation/progress.svelte.d.ts.map +1 -0
  59. package/dist/generation/progress.svelte.js +36 -0
  60. package/dist/generation/records.svelte.d.ts +18 -0
  61. package/dist/generation/records.svelte.d.ts.map +1 -0
  62. package/dist/generation/records.svelte.js +66 -0
  63. package/dist/image-editor/AdjustmentLayersPanel.svelte +309 -0
  64. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +11 -0
  65. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts.map +1 -0
  66. package/dist/image-editor/ControlSlider.svelte +39 -0
  67. package/dist/image-editor/ControlSlider.svelte.d.ts +13 -0
  68. package/dist/image-editor/ControlSlider.svelte.d.ts.map +1 -0
  69. package/dist/image-editor/CurvesEditor.svelte +105 -0
  70. package/dist/image-editor/CurvesEditor.svelte.d.ts +13 -0
  71. package/dist/image-editor/CurvesEditor.svelte.d.ts.map +1 -0
  72. package/dist/image-editor/Histogram.svelte +49 -0
  73. package/dist/image-editor/Histogram.svelte.d.ts +12 -0
  74. package/dist/image-editor/Histogram.svelte.d.ts.map +1 -0
  75. package/dist/image-editor/ImageEditor.svelte +104 -141
  76. package/dist/image-editor/ImageEditor.svelte.d.ts.map +1 -1
  77. package/dist/image-editor/RefineEdgePanel.svelte +180 -0
  78. package/dist/image-editor/RefineEdgePanel.svelte.d.ts +13 -0
  79. package/dist/image-editor/RefineEdgePanel.svelte.d.ts.map +1 -0
  80. package/dist/image-editor/SelectionOptionsBar.svelte +135 -0
  81. package/dist/image-editor/SelectionOptionsBar.svelte.d.ts +15 -0
  82. package/dist/image-editor/SelectionOptionsBar.svelte.d.ts.map +1 -0
  83. package/dist/image-editor/imageEditorState.svelte.d.ts +14 -1
  84. package/dist/image-editor/imageEditorState.svelte.d.ts.map +1 -1
  85. package/dist/image-editor/imageEditorState.svelte.js +40 -0
  86. package/dist/image-editor/index.d.ts +18 -0
  87. package/dist/image-editor/index.d.ts.map +1 -1
  88. package/dist/image-editor/index.js +6 -0
  89. package/dist/index.d.ts +12 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +7 -0
  92. package/dist/merge.svelte.d.ts +51 -0
  93. package/dist/merge.svelte.d.ts.map +1 -0
  94. package/dist/merge.svelte.js +117 -0
  95. package/dist/paragraphStyles.svelte.d.ts +30 -0
  96. package/dist/paragraphStyles.svelte.d.ts.map +1 -0
  97. package/dist/paragraphStyles.svelte.js +44 -0
  98. package/dist/persistence/RecoveryPrompt.svelte +57 -0
  99. package/dist/persistence/RecoveryPrompt.svelte.d.ts +18 -0
  100. package/dist/persistence/RecoveryPrompt.svelte.d.ts.map +1 -0
  101. package/dist/persistence/autosave.svelte.d.ts +63 -0
  102. package/dist/persistence/autosave.svelte.d.ts.map +1 -0
  103. package/dist/persistence/autosave.svelte.js +110 -0
  104. package/dist/persistence/index.d.ts +6 -0
  105. package/dist/persistence/index.d.ts.map +1 -0
  106. package/dist/persistence/index.js +2 -0
  107. package/dist/review/TrackedChangesList.svelte +67 -0
  108. package/dist/review/TrackedChangesList.svelte.d.ts +13 -0
  109. package/dist/review/TrackedChangesList.svelte.d.ts.map +1 -0
  110. package/dist/review/documentCompare.svelte.d.ts +21 -0
  111. package/dist/review/documentCompare.svelte.d.ts.map +1 -0
  112. package/dist/review/documentCompare.svelte.js +23 -0
  113. package/dist/review/index.d.ts +4 -0
  114. package/dist/review/index.d.ts.map +1 -1
  115. package/dist/review/index.js +2 -0
  116. package/dist/review/review.svelte.d.ts +6 -0
  117. package/dist/review/review.svelte.d.ts.map +1 -1
  118. package/dist/review/review.svelte.js +3 -0
  119. package/dist/spreadsheet/FormulaBar.svelte +13 -4
  120. package/dist/spreadsheet/FormulaBar.svelte.d.ts.map +1 -1
  121. package/dist/spreadsheet/FormulaHints.svelte +7 -5
  122. package/dist/spreadsheet/FormulaHints.svelte.d.ts +3 -0
  123. package/dist/spreadsheet/FormulaHints.svelte.d.ts.map +1 -1
  124. package/dist/spreadsheet/NameManager.svelte +165 -0
  125. package/dist/spreadsheet/NameManager.svelte.d.ts +11 -0
  126. package/dist/spreadsheet/NameManager.svelte.d.ts.map +1 -0
  127. package/dist/spreadsheet/SheetTabs.svelte +161 -0
  128. package/dist/spreadsheet/SheetTabs.svelte.d.ts +10 -0
  129. package/dist/spreadsheet/SheetTabs.svelte.d.ts.map +1 -0
  130. package/dist/spreadsheet/SpreadsheetConditionalFormatPanel.svelte +184 -0
  131. package/dist/spreadsheet/SpreadsheetConditionalFormatPanel.svelte.d.ts +9 -0
  132. package/dist/spreadsheet/SpreadsheetConditionalFormatPanel.svelte.d.ts.map +1 -0
  133. package/dist/spreadsheet/SpreadsheetContextMenu.svelte +13 -5
  134. package/dist/spreadsheet/SpreadsheetContextMenu.svelte.d.ts +5 -1
  135. package/dist/spreadsheet/SpreadsheetContextMenu.svelte.d.ts.map +1 -1
  136. package/dist/spreadsheet/SpreadsheetDataValidationPanel.svelte +141 -0
  137. package/dist/spreadsheet/SpreadsheetDataValidationPanel.svelte.d.ts +9 -0
  138. package/dist/spreadsheet/SpreadsheetDataValidationPanel.svelte.d.ts.map +1 -0
  139. package/dist/spreadsheet/SpreadsheetEditor.svelte +199 -11
  140. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +2 -0
  141. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts.map +1 -1
  142. package/dist/spreadsheet/SpreadsheetValidationDialog.svelte +92 -0
  143. package/dist/spreadsheet/SpreadsheetValidationDialog.svelte.d.ts +9 -0
  144. package/dist/spreadsheet/SpreadsheetValidationDialog.svelte.d.ts.map +1 -0
  145. package/dist/spreadsheet/index.d.ts +9 -0
  146. package/dist/spreadsheet/index.d.ts.map +1 -1
  147. package/dist/spreadsheet/index.js +5 -0
  148. package/dist/spreadsheet/spreadsheet.svelte.d.ts +57 -2
  149. package/dist/spreadsheet/spreadsheet.svelte.d.ts.map +1 -1
  150. package/dist/spreadsheet/spreadsheet.svelte.js +105 -6
  151. package/dist/tokens/TokenSwatch.svelte +14 -0
  152. package/dist/tokens/TokenSwatch.svelte.d.ts +8 -0
  153. package/dist/tokens/TokenSwatch.svelte.d.ts.map +1 -0
  154. package/dist/tokens/TokensPanel.svelte +323 -0
  155. package/dist/tokens/TokensPanel.svelte.d.ts +14 -0
  156. package/dist/tokens/TokensPanel.svelte.d.ts.map +1 -0
  157. package/dist/tokens/createTokens.svelte.d.ts +31 -0
  158. package/dist/tokens/createTokens.svelte.d.ts.map +1 -0
  159. package/dist/tokens/createTokens.svelte.js +43 -0
  160. package/dist/tokens/index.d.ts +7 -0
  161. package/dist/tokens/index.d.ts.map +1 -0
  162. package/dist/tokens/index.js +3 -0
  163. package/package.json +26 -2
@@ -0,0 +1,161 @@
1
+ <script lang="ts">
2
+ import { getSheetTabMenuItems, sheetDropIndex, type SpreadsheetMenuItem } from '@pixerate/editor/spreadsheet';
3
+ import type { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
4
+ import SpreadsheetContextMenu from './SpreadsheetContextMenu.svelte';
5
+
6
+ interface Props {
7
+ sheetState: ReturnType<typeof createReactiveSpreadsheet>;
8
+ readOnly?: boolean;
9
+ class?: string;
10
+ }
11
+
12
+ let { sheetState, readOnly = false, class: className = '' }: Props = $props();
13
+
14
+ let renaming = $state<{ id: string; draft: string } | null>(null);
15
+ let error = $state<string | null>(null);
16
+ let menu = $state<{ x: number; y: number; items: SpreadsheetMenuItem[] } | null>(null);
17
+ let dragFrom: number | null = null;
18
+ let dropTarget = $state<{ index: number; after: boolean } | null>(null);
19
+ let renameInput = $state<HTMLInputElement | null>(null);
20
+
21
+ // Focus and select the name when the rename field opens.
22
+ $effect(() => {
23
+ if (!renameInput) return;
24
+ renameInput.focus();
25
+ renameInput.select();
26
+ });
27
+
28
+ function commitRename(cancelOnError: boolean) {
29
+ if (!renaming) return;
30
+ const current = renaming;
31
+ const sheet = sheetState.sheets.find((s) => s.id === current.id);
32
+ const name = current.draft.trim();
33
+ if (!sheet || !name || name === sheet.name) {
34
+ renaming = null;
35
+ error = null;
36
+ return;
37
+ }
38
+ try {
39
+ sheetState.renameSheet(current.id, name);
40
+ renaming = null;
41
+ error = null;
42
+ } catch (err: any) {
43
+ error = err?.message ?? String(err);
44
+ if (cancelOnError) renaming = null;
45
+ }
46
+ }
47
+
48
+ function dropAfter(e: DragEvent): boolean {
49
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
50
+ return e.clientX > rect.left + rect.width / 2;
51
+ }
52
+ </script>
53
+
54
+ <!--
55
+ The workbook's sheet tabs: click to switch, double-click to rename, drag to
56
+ reorder, right-click for insert / rename / duplicate / move / delete
57
+ (delete asks to confirm), and a `+` button to add a sheet.
58
+ -->
59
+ <div class="flex flex-col {className}" data-sheet-tabs="">
60
+ <div role="tablist" aria-label="Sheets" class="flex items-center gap-0.5 overflow-x-auto border-t border-border bg-muted px-1 py-1 text-xs">
61
+ {#each sheetState.sheets as sheet, index (sheet.id)}
62
+ {#if renaming?.id === sheet.id}
63
+ <input
64
+ bind:this={renameInput}
65
+ aria-label="Sheet name"
66
+ class="w-32 rounded border border-primary bg-background px-2 py-0.5 text-xs outline-none"
67
+ value={renaming.draft}
68
+ oninput={(e) => {
69
+ if (renaming) renaming = { id: renaming.id, draft: e.currentTarget.value };
70
+ }}
71
+ onkeydown={(e) => {
72
+ e.stopPropagation();
73
+ if (e.key === 'Enter') commitRename(false);
74
+ else if (e.key === 'Escape') {
75
+ renaming = null;
76
+ error = null;
77
+ }
78
+ }}
79
+ onblur={() => commitRename(true)}
80
+ />
81
+ {:else}
82
+ <button
83
+ type="button"
84
+ role="tab"
85
+ aria-selected={sheet.active}
86
+ data-sheet-id={sheet.id}
87
+ draggable={!readOnly}
88
+ title={readOnly ? sheet.name : `${sheet.name} — double-click to rename, drag to move`}
89
+ class="whitespace-nowrap rounded px-3 py-0.5 {sheet.active
90
+ ? 'bg-background font-semibold text-foreground shadow-sm'
91
+ : 'text-muted-foreground hover:bg-background/60'} {dropTarget?.index === index
92
+ ? dropTarget.after
93
+ ? 'border-r-2 border-primary'
94
+ : 'border-l-2 border-primary'
95
+ : ''}"
96
+ onclick={() => sheetState.setActiveSheet(sheet.id)}
97
+ ondblclick={() => {
98
+ if (readOnly) return;
99
+ error = null;
100
+ renaming = { id: sheet.id, draft: sheet.name };
101
+ }}
102
+ oncontextmenu={(e) => {
103
+ if (readOnly) return;
104
+ e.preventDefault();
105
+ menu = {
106
+ x: e.clientX,
107
+ y: e.clientY,
108
+ items: getSheetTabMenuItems(sheetState.controller, sheet.id, { onError: (message) => (error = message) })
109
+ };
110
+ }}
111
+ ondragstart={(e) => {
112
+ dragFrom = index;
113
+ e.dataTransfer?.setData('text/plain', sheet.id);
114
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
115
+ }}
116
+ ondragover={(e) => {
117
+ if (dragFrom === null) return;
118
+ e.preventDefault();
119
+ const after = dropAfter(e);
120
+ if (dropTarget?.index !== index || dropTarget.after !== after) dropTarget = { index, after };
121
+ }}
122
+ ondragleave={() => (dropTarget = null)}
123
+ ondrop={(e) => {
124
+ e.preventDefault();
125
+ const from = dragFrom;
126
+ dragFrom = null;
127
+ dropTarget = null;
128
+ if (from === null) return;
129
+ sheetState.moveSheet(sheetState.sheets[from].id, sheetDropIndex(from, index, dropAfter(e)));
130
+ }}
131
+ ondragend={() => {
132
+ dragFrom = null;
133
+ dropTarget = null;
134
+ }}
135
+ >
136
+ {sheet.name}
137
+ </button>
138
+ {/if}
139
+ {/each}
140
+ {#if !readOnly}
141
+ <button
142
+ type="button"
143
+ aria-label="Add sheet"
144
+ title="Add sheet"
145
+ class="rounded px-2 py-0.5 text-muted-foreground hover:bg-background/60"
146
+ onclick={() => {
147
+ error = null;
148
+ sheetState.addSheet();
149
+ }}
150
+ >
151
+ +
152
+ </button>
153
+ {/if}
154
+ </div>
155
+ {#if error}
156
+ <div role="alert" class="px-2 py-1 text-xs text-destructive">{error}</div>
157
+ {/if}
158
+ {#if menu}
159
+ <SpreadsheetContextMenu items={menu.items} x={menu.x} y={menu.y} ariaLabel="Sheet actions" onClose={() => (menu = null)} />
160
+ {/if}
161
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
2
+ interface Props {
3
+ sheetState: ReturnType<typeof createReactiveSpreadsheet>;
4
+ readOnly?: boolean;
5
+ class?: string;
6
+ }
7
+ declare const SheetTabs: import("svelte").Component<Props, {}, "">;
8
+ type SheetTabs = ReturnType<typeof SheetTabs>;
9
+ export default SheetTabs;
10
+ //# sourceMappingURL=SheetTabs.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SheetTabs.svelte.d.ts","sourceRoot":"","sources":["../../src/spreadsheet/SheetTabs.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAIvE,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8HH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,184 @@
1
+ <script lang="ts">
2
+ import {
3
+ CONDITIONAL_FORMAT_ICON_SETS,
4
+ CONDITIONAL_FORMAT_PRESETS,
5
+ CONDITIONAL_FORMAT_STYLE_PRESETS,
6
+ CONDITIONAL_FORMAT_TIME_PERIODS,
7
+ createConditionalFormatRule,
8
+ describeConditionalFormat,
9
+ formatRuleRanges,
10
+ getConditionalFormatCss,
11
+ conditionalFormatStyleString,
12
+ parseRuleRanges,
13
+ type ConditionalFormatIconSet,
14
+ type ConditionalFormatPresetKind,
15
+ type ConditionalFormatRule,
16
+ type ConditionalFormatTimePeriod
17
+ } from '@pixerate/editor/spreadsheet';
18
+ import type { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
19
+
20
+ interface Props {
21
+ sheetState: ReturnType<typeof createReactiveSpreadsheet>;
22
+ onClose: () => void;
23
+ }
24
+
25
+ let { sheetState, onClose }: Props = $props();
26
+
27
+ const inputClass = 'w-full rounded border border-border bg-background px-1.5 py-0.5 text-xs text-foreground';
28
+ const buttonClass = 'rounded border border-border px-2 py-0.5 hover:bg-muted disabled:opacity-40';
29
+ const selectionText = () => (sheetState.selectedRange ? formatRuleRanges([sheetState.selectedRange]) : 'A1');
30
+
31
+ let rules = $derived(sheetState.document.table?.conditionalFormats ?? []);
32
+ let kind = $state<ConditionalFormatPresetKind>('greaterThan');
33
+ let appliesTo = $state(selectionText());
34
+ let value = $state('');
35
+ let value2 = $state('');
36
+ let text = $state('');
37
+ let formula = $state('=');
38
+ let rank = $state('10');
39
+ let period = $state<ConditionalFormatTimePeriod>('today');
40
+ let iconSet = $state<ConditionalFormatIconSet>('3TrafficLights1');
41
+ let color = $state('#638ec6');
42
+ let styleId = $state(CONDITIONAL_FORMAT_STYLE_PRESETS[0].id);
43
+ let preset = $derived(CONDITIONAL_FORMAT_PRESETS.find((p) => p.kind === kind)!);
44
+ let ranges = $derived(parseRuleRanges(appliesTo));
45
+
46
+ function add() {
47
+ if (!ranges) return;
48
+ sheetState.addConditionalFormat(
49
+ createConditionalFormatRule({
50
+ kind,
51
+ ranges,
52
+ value,
53
+ value2,
54
+ text,
55
+ formula,
56
+ rank: Number(rank) || 10,
57
+ period,
58
+ iconSet,
59
+ color,
60
+ style: CONDITIONAL_FORMAT_STYLE_PRESETS.find((s) => s.id === styleId)?.style
61
+ })
62
+ );
63
+ }
64
+
65
+ /** Swatch showing a rule's look (fill, font colour, bold) or its kind. */
66
+ function swatchStyle(rule: ConditionalFormatRule): string {
67
+ if (rule.type === 'colorScale') return `background: linear-gradient(90deg, ${rule.stops.map((s) => s.color).join(', ')});`;
68
+ if (rule.type === 'dataBar') return `background: ${rule.color ?? '#638ec6'};`;
69
+ if (rule.type === 'iconSet') return '';
70
+ return conditionalFormatStyleString(getConditionalFormatCss(rule.style));
71
+ }
72
+ </script>
73
+
74
+ <!-- Conditional formatting manager: rules in priority order and a form to add one. -->
75
+ <div
76
+ role="dialog"
77
+ aria-label="Conditional formatting rules"
78
+ tabindex="-1"
79
+ class="fixed right-4 top-16 z-40 flex max-h-[calc(100vh-5rem)] w-80 flex-col overflow-y-auto rounded-md border border-border bg-popover p-3 text-xs text-popover-foreground shadow-lg"
80
+ style="background: var(--popover, var(--background, #fff));"
81
+ onkeydown={(e) => {
82
+ e.stopPropagation();
83
+ if (e.key === 'Escape') onClose();
84
+ }}
85
+ oncontextmenu={(e) => e.stopPropagation()}
86
+ >
87
+ <div class="mb-2 flex items-center justify-between">
88
+ <h2 class="text-sm font-semibold">Conditional formatting</h2>
89
+ <button type="button" aria-label="Close" class="px-1 text-muted-foreground hover:text-foreground" onclick={onClose}>✕</button>
90
+ </div>
91
+ <ol class="mb-3 flex flex-col gap-1" aria-label="Rules in priority order">
92
+ {#if rules.length === 0}
93
+ <li class="text-muted-foreground">No rules on this sheet.</li>
94
+ {/if}
95
+ {#each rules as rule, i (rule.id)}
96
+ <li data-rule-id={rule.id} class="flex items-center gap-1.5 rounded border border-border px-1.5 py-1">
97
+ <span aria-hidden="true" class="inline-flex h-4 w-6 shrink-0 items-center justify-center rounded-sm border border-border" style={swatchStyle(rule)}
98
+ >{rule.type === 'iconSet' ? '●' : rule.type === 'colorScale' || rule.type === 'dataBar' ? '' : 'Aa'}</span
99
+ >
100
+ <div class="min-w-0 flex-1">
101
+ <div class="truncate" title={describeConditionalFormat(rule)}>{describeConditionalFormat(rule)}</div>
102
+ <div class="truncate font-mono text-[10px] text-muted-foreground">{formatRuleRanges(rule.ranges)}</div>
103
+ </div>
104
+ <label class="flex items-center gap-0.5 text-[10px]" title="Stop if true">
105
+ <input
106
+ type="checkbox"
107
+ aria-label="Stop if true"
108
+ checked={!!rule.stopIfTrue}
109
+ onchange={(e) => sheetState.updateConditionalFormat(rule.id, { stopIfTrue: (e.currentTarget as HTMLInputElement).checked })}
110
+ />
111
+ Stop
112
+ </label>
113
+ <button type="button" aria-label="Move up" class={buttonClass} disabled={i === 0} onclick={() => sheetState.moveConditionalFormat(rule.id, { by: -1 })}>▲</button>
114
+ <button
115
+ type="button"
116
+ aria-label="Move down"
117
+ class={buttonClass}
118
+ disabled={i === rules.length - 1}
119
+ onclick={() => sheetState.moveConditionalFormat(rule.id, { by: 1 })}>▼</button
120
+ >
121
+ <button type="button" aria-label="Delete rule" class={buttonClass} onclick={() => sheetState.deleteConditionalFormat(rule.id)}>✕</button>
122
+ </li>
123
+ {/each}
124
+ </ol>
125
+
126
+ <fieldset class="flex flex-col gap-1.5 border-t border-border pt-2">
127
+ <legend class="sr-only">New rule</legend>
128
+ <label class="flex flex-col gap-0.5">
129
+ Format cells
130
+ <select aria-label="Rule type" class={inputClass} bind:value={kind}>
131
+ {#each CONDITIONAL_FORMAT_PRESETS as p (p.kind)}
132
+ <option value={p.kind}>{p.label}</option>
133
+ {/each}
134
+ </select>
135
+ </label>
136
+ {#if preset.fields.includes('value')}
137
+ <input aria-label="Value" placeholder="Value or =formula" class={inputClass} bind:value />
138
+ {/if}
139
+ {#if preset.fields.includes('value2')}
140
+ <input aria-label="Second value" placeholder="and" class={inputClass} bind:value={value2} />
141
+ {/if}
142
+ {#if preset.fields.includes('text')}
143
+ <input aria-label="Text" placeholder="Text" class={inputClass} bind:value={text} />
144
+ {/if}
145
+ {#if preset.fields.includes('formula')}
146
+ <input aria-label="Formula" placeholder="=$A1>10" class={inputClass} bind:value={formula} />
147
+ {/if}
148
+ {#if preset.fields.includes('rank')}
149
+ <input aria-label="Rank" type="number" min="1" class={inputClass} bind:value={rank} />
150
+ {/if}
151
+ {#if preset.fields.includes('period')}
152
+ <select aria-label="Period" class={inputClass} bind:value={period}>
153
+ {#each CONDITIONAL_FORMAT_TIME_PERIODS as p (p.period)}
154
+ <option value={p.period}>{p.label}</option>
155
+ {/each}
156
+ </select>
157
+ {/if}
158
+ {#if preset.fields.includes('iconSet')}
159
+ <select aria-label="Icon set" class={inputClass} bind:value={iconSet}>
160
+ {#each CONDITIONAL_FORMAT_ICON_SETS as set (set)}
161
+ <option value={set}>{set}</option>
162
+ {/each}
163
+ </select>
164
+ {/if}
165
+ {#if preset.fields.includes('color')}
166
+ <input aria-label="Bar colour" type="color" class="h-6 w-12" bind:value={color} />
167
+ {/if}
168
+ {#if preset.usesStyle}
169
+ <select aria-label="Style" class={inputClass} bind:value={styleId}>
170
+ {#each CONDITIONAL_FORMAT_STYLE_PRESETS as s (s.id)}
171
+ <option value={s.id}>{s.label}</option>
172
+ {/each}
173
+ </select>
174
+ {/if}
175
+ <label class="flex flex-col gap-0.5">
176
+ Applies to
177
+ <input aria-label="Applies to" class="{inputClass} font-mono" bind:value={appliesTo} />
178
+ </label>
179
+ <div class="flex justify-between gap-2">
180
+ <button type="button" class={buttonClass} onclick={() => (appliesTo = selectionText())}>Use selection</button>
181
+ <button type="button" class={buttonClass} disabled={!ranges} onclick={add}>Add rule</button>
182
+ </div>
183
+ </fieldset>
184
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
2
+ interface Props {
3
+ sheetState: ReturnType<typeof createReactiveSpreadsheet>;
4
+ onClose: () => void;
5
+ }
6
+ declare const SpreadsheetConditionalFormatPanel: import("svelte").Component<Props, {}, "">;
7
+ type SpreadsheetConditionalFormatPanel = ReturnType<typeof SpreadsheetConditionalFormatPanel>;
8
+ export default SpreadsheetConditionalFormatPanel;
9
+ //# sourceMappingURL=SpreadsheetConditionalFormatPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpreadsheetConditionalFormatPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/spreadsheet/SpreadsheetConditionalFormatPanel.svelte.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAGvE,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;IACzD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAoJH,QAAA,MAAM,iCAAiC,2CAAwC,CAAC;AAChF,KAAK,iCAAiC,GAAG,UAAU,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAC9F,eAAe,iCAAiC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { clampMenuPosition, type SpreadsheetMenuItem } from '@pixerate/editor/spreadsheet';
2
+ import { clampMenuPosition, runSpreadsheetMenuItem, type SpreadsheetMenuItem, type SpreadsheetPanelId } from '@pixerate/editor/spreadsheet';
3
3
 
4
4
  // Gap kept between the menu and the viewport edges.
5
5
  const VIEWPORT_MARGIN = 4;
@@ -9,9 +9,13 @@
9
9
  x: number;
10
10
  y: number;
11
11
  onClose: () => void;
12
+ /** Accessible name of the menu (default "Table actions"). */
13
+ ariaLabel?: string;
14
+ /** Opens a panel for items with `panel` (conditional formatting, data validation). */
15
+ onOpenPanel?: (panel: SpreadsheetPanelId) => void;
12
16
  }
13
17
 
14
- let { items, x, y, onClose }: Props = $props();
18
+ let { items, x, y, onClose, onOpenPanel, ariaLabel = 'Table actions' }: Props = $props();
15
19
  let menuEl = $state<HTMLDivElement | null>(null);
16
20
  let measured = $state<{ x: number; y: number } | null>(null);
17
21
  const position = $derived(measured ?? { x, y });
@@ -51,7 +55,7 @@
51
55
  <div
52
56
  bind:this={menuEl}
53
57
  role="menu"
54
- aria-label="Table actions"
58
+ aria-label={ariaLabel}
55
59
  tabindex="-1"
56
60
  class="fixed z-50 min-w-[14rem] rounded-md border border-border bg-popover py-1 text-xs text-popover-foreground shadow-lg"
57
61
  style="left: {position.x}px; top: {position.y}px; max-height: calc(100vh - {VIEWPORT_MARGIN * 2}px); overflow-y: auto; background: var(--popover, var(--background, #fff));"
@@ -66,11 +70,15 @@
66
70
  type="button"
67
71
  role="menuitem"
68
72
  data-action={item.id}
73
+ data-checked={item.checked ? '' : undefined}
69
74
  disabled={item.disabled}
70
- class="block w-full px-3 py-1 text-left hover:bg-muted disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent"
75
+ class="block w-full px-3 py-1 text-left hover:bg-muted disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent {item.checked
76
+ ? 'font-semibold'
77
+ : ''}"
71
78
  onclick={() => {
72
79
  onClose();
73
- item.run();
80
+ if (item.panel) onOpenPanel?.(item.panel);
81
+ else runSpreadsheetMenuItem(item);
74
82
  }}
75
83
  >
76
84
  {item.label}
@@ -1,9 +1,13 @@
1
- import { type SpreadsheetMenuItem } from '@pixerate/editor/spreadsheet';
1
+ import { type SpreadsheetMenuItem, type SpreadsheetPanelId } from '@pixerate/editor/spreadsheet';
2
2
  interface Props {
3
3
  items: SpreadsheetMenuItem[];
4
4
  x: number;
5
5
  y: number;
6
6
  onClose: () => void;
7
+ /** Accessible name of the menu (default "Table actions"). */
8
+ ariaLabel?: string;
9
+ /** Opens a panel for items with `panel` (conditional formatting, data validation). */
10
+ onOpenPanel?: (panel: SpreadsheetPanelId) => void;
7
11
  }
8
12
  declare const SpreadsheetContextMenu: import("svelte").Component<Props, {}, "">;
9
13
  type SpreadsheetContextMenu = ReturnType<typeof SpreadsheetContextMenu>;
@@ -1 +1 @@
1
- {"version":3,"file":"SpreadsheetContextMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/spreadsheet/SpreadsheetContextMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqB,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGzF,UAAU,KAAK;IACb,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AA6DH,QAAA,MAAM,sBAAsB,2CAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"SpreadsheetContextMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/spreadsheet/SpreadsheetContextMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6C,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAG1I,UAAU,KAAK;IACb,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACnD;AAgEH,QAAA,MAAM,sBAAsB,2CAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import {
3
+ DATA_VALIDATION_ERROR_STYLE_OPTIONS,
4
+ DATA_VALIDATION_OPERATOR_OPTIONS,
5
+ DATA_VALIDATION_TYPE_OPTIONS,
6
+ describeDataValidation,
7
+ formatRuleRanges,
8
+ parseRuleRanges,
9
+ type DataValidationErrorStyle,
10
+ type DataValidationOperator,
11
+ type DataValidationType
12
+ } from '@pixerate/editor/spreadsheet';
13
+ import type { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
14
+
15
+ interface Props {
16
+ sheetState: ReturnType<typeof createReactiveSpreadsheet>;
17
+ onClose: () => void;
18
+ }
19
+
20
+ let { sheetState, onClose }: Props = $props();
21
+
22
+ const inputClass = 'w-full rounded border border-border bg-background px-1.5 py-0.5 text-xs text-foreground';
23
+ const buttonClass = 'rounded border border-border px-2 py-0.5 hover:bg-muted disabled:opacity-40';
24
+ const selectionText = () => (sheetState.selectedRange ? formatRuleRanges([sheetState.selectedRange]) : 'A1');
25
+
26
+ let rules = $derived(sheetState.document.table?.dataValidations ?? []);
27
+ let type = $state<DataValidationType>('list');
28
+ let operator = $state<DataValidationOperator>('between');
29
+ let formula1 = $state('');
30
+ let formula2 = $state('');
31
+ let errorStyle = $state<DataValidationErrorStyle>('stop');
32
+ let errorMessage = $state('');
33
+ let inputMessage = $state('');
34
+ let ignoreBlank = $state(true);
35
+ let appliesTo = $state(selectionText());
36
+ let ranges = $derived(parseRuleRanges(appliesTo));
37
+ let usesOperator = $derived(type === 'whole' || type === 'decimal' || type === 'date' || type === 'time' || type === 'textLength');
38
+ let needsTwo = $derived(usesOperator && (operator === 'between' || operator === 'notBetween'));
39
+ let circles = $derived.by(() => {
40
+ void sheetState.document;
41
+ return sheetState.getInvalidDataCircles();
42
+ });
43
+
44
+ function apply() {
45
+ if (!ranges) return;
46
+ sheetState.addDataValidation({
47
+ ranges,
48
+ type,
49
+ ...(usesOperator ? { operator } : {}),
50
+ ...(formula1 ? { formula1 } : {}),
51
+ ...(needsTwo && formula2 ? { formula2 } : {}),
52
+ errorStyle,
53
+ ...(errorMessage ? { errorMessage } : {}),
54
+ ...(inputMessage ? { inputMessage } : {}),
55
+ ignoreBlank
56
+ });
57
+ }
58
+ </script>
59
+
60
+ <!-- Data validation manager: rules, a form to validate a range, and "circle invalid data". -->
61
+ <div
62
+ role="dialog"
63
+ aria-label="Data validation rules"
64
+ tabindex="-1"
65
+ class="fixed right-4 top-16 z-40 flex max-h-[calc(100vh-5rem)] w-80 flex-col overflow-y-auto rounded-md border border-border bg-popover p-3 text-xs text-popover-foreground shadow-lg"
66
+ style="background: var(--popover, var(--background, #fff));"
67
+ onkeydown={(e) => {
68
+ e.stopPropagation();
69
+ if (e.key === 'Escape') onClose();
70
+ }}
71
+ oncontextmenu={(e) => e.stopPropagation()}
72
+ >
73
+ <div class="mb-2 flex items-center justify-between">
74
+ <h2 class="text-sm font-semibold">Data validation</h2>
75
+ <button type="button" aria-label="Close" class="px-1 text-muted-foreground hover:text-foreground" onclick={onClose}>✕</button>
76
+ </div>
77
+ <ul class="mb-3 flex flex-col gap-1" aria-label="Validation rules">
78
+ {#if rules.length === 0}
79
+ <li class="text-muted-foreground">No validation on this sheet.</li>
80
+ {/if}
81
+ {#each rules as rule (rule.id)}
82
+ <li data-rule-id={rule.id} class="flex items-center gap-1.5 rounded border border-border px-1.5 py-1">
83
+ <div class="min-w-0 flex-1">
84
+ <div class="truncate" title={describeDataValidation(rule)}>{describeDataValidation(rule)}</div>
85
+ <div class="truncate font-mono text-[10px] text-muted-foreground">{formatRuleRanges(rule.ranges)} · {rule.errorStyle ?? 'stop'}</div>
86
+ </div>
87
+ <button type="button" aria-label="Delete rule" class={buttonClass} onclick={() => sheetState.deleteDataValidation(rule.id)}>✕</button>
88
+ </li>
89
+ {/each}
90
+ </ul>
91
+
92
+ <fieldset class="flex flex-col gap-1.5 border-t border-border pt-2">
93
+ <legend class="sr-only">New validation</legend>
94
+ <label class="flex flex-col gap-0.5">
95
+ Allow
96
+ <select aria-label="Allow" class={inputClass} bind:value={type}>
97
+ {#each DATA_VALIDATION_TYPE_OPTIONS as o (o.type)}
98
+ <option value={o.type}>{o.label}</option>
99
+ {/each}
100
+ </select>
101
+ </label>
102
+ {#if usesOperator}
103
+ <select aria-label="Operator" class={inputClass} bind:value={operator}>
104
+ {#each DATA_VALIDATION_OPERATOR_OPTIONS as o (o.operator)}
105
+ <option value={o.operator}>{o.label}</option>
106
+ {/each}
107
+ </select>
108
+ {/if}
109
+ {#if type !== 'any'}
110
+ <input
111
+ aria-label={type === 'list' ? 'Source' : type === 'custom' ? 'Formula' : needsTwo ? 'Minimum' : 'Value'}
112
+ placeholder={type === 'list' ? 'Low,Medium,High or =$H$1:$H$5' : type === 'custom' ? '=A1>0' : 'Value or =formula'}
113
+ class={inputClass}
114
+ bind:value={formula1}
115
+ />
116
+ {/if}
117
+ {#if needsTwo}
118
+ <input aria-label="Maximum" placeholder="Maximum" class={inputClass} bind:value={formula2} />
119
+ {/if}
120
+ <label class="flex items-center gap-1"><input type="checkbox" bind:checked={ignoreBlank} /> Ignore blank</label>
121
+ <input aria-label="Input message" placeholder="Input message (optional)" class={inputClass} bind:value={inputMessage} />
122
+ <select aria-label="Error style" class={inputClass} bind:value={errorStyle}>
123
+ {#each DATA_VALIDATION_ERROR_STYLE_OPTIONS as o (o.style)}
124
+ <option value={o.style}>{o.label}</option>
125
+ {/each}
126
+ </select>
127
+ <input aria-label="Error message" placeholder="Error message (optional)" class={inputClass} bind:value={errorMessage} />
128
+ <label class="flex flex-col gap-0.5">
129
+ Applies to
130
+ <input aria-label="Applies to" class="{inputClass} font-mono" bind:value={appliesTo} />
131
+ </label>
132
+ <div class="flex flex-wrap justify-between gap-2">
133
+ <button type="button" class={buttonClass} onclick={() => (appliesTo = selectionText())}>Use selection</button>
134
+ <button type="button" class={buttonClass} disabled={!ranges} onclick={() => ranges?.forEach((r) => sheetState.clearDataValidation(r))}>Clear</button>
135
+ <button type="button" class={buttonClass} disabled={!ranges} onclick={apply}>Apply</button>
136
+ </div>
137
+ <button type="button" class={buttonClass} aria-pressed={circles} onclick={() => sheetState.setInvalidDataCircles(!circles)}>
138
+ {circles ? 'Clear validation circles' : 'Circle invalid data'}
139
+ </button>
140
+ </fieldset>
141
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
2
+ interface Props {
3
+ sheetState: ReturnType<typeof createReactiveSpreadsheet>;
4
+ onClose: () => void;
5
+ }
6
+ declare const SpreadsheetDataValidationPanel: import("svelte").Component<Props, {}, "">;
7
+ type SpreadsheetDataValidationPanel = ReturnType<typeof SpreadsheetDataValidationPanel>;
8
+ export default SpreadsheetDataValidationPanel;
9
+ //# sourceMappingURL=SpreadsheetDataValidationPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpreadsheetDataValidationPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/spreadsheet/SpreadsheetDataValidationPanel.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAGvE,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;IACzD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAsHH,QAAA,MAAM,8BAA8B,2CAAwC,CAAC;AAC7E,KAAK,8BAA8B,GAAG,UAAU,CAAC,OAAO,8BAA8B,CAAC,CAAC;AACxF,eAAe,8BAA8B,CAAC"}