@svgrid/enterprise 3.0.1 → 3.0.2

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 (197) hide show
  1. package/dist/SvFormulaBar.svelte +606 -0
  2. package/dist/SvFormulaBar.svelte.d.ts +62 -0
  3. package/dist/SvPivotDesigner.svelte +65 -14
  4. package/dist/SvRibbonIcon.svelte +46 -0
  5. package/dist/SvRibbonIcon.svelte.d.ts +17 -0
  6. package/dist/SvSchemaChart.svelte +4 -2
  7. package/dist/SvSheet.svelte +4114 -0
  8. package/dist/SvSheet.svelte.d.ts +142 -0
  9. package/dist/SvSheetComment.svelte +98 -0
  10. package/dist/SvSheetComment.svelte.d.ts +22 -0
  11. package/dist/SvSheetConditionalFormat.svelte +162 -0
  12. package/dist/SvSheetConditionalFormat.svelte.d.ts +14 -0
  13. package/dist/SvSheetDataValidation.svelte +249 -0
  14. package/dist/SvSheetDataValidation.svelte.d.ts +14 -0
  15. package/dist/SvSheetFilterMenu.svelte +217 -0
  16. package/dist/SvSheetFilterMenu.svelte.d.ts +17 -0
  17. package/dist/SvSheetFindReplace.svelte +132 -0
  18. package/dist/SvSheetFindReplace.svelte.d.ts +10 -0
  19. package/dist/SvSheetFormatCells.svelte +421 -0
  20. package/dist/SvSheetFormatCells.svelte.d.ts +17 -0
  21. package/dist/SvSheetGoalSeek.svelte +144 -0
  22. package/dist/SvSheetGoalSeek.svelte.d.ts +12 -0
  23. package/dist/SvSheetInsertFunction.svelte +147 -0
  24. package/dist/SvSheetInsertFunction.svelte.d.ts +8 -0
  25. package/dist/SvSheetListPicker.svelte +89 -0
  26. package/dist/SvSheetListPicker.svelte.d.ts +16 -0
  27. package/dist/SvSheetManageRules.svelte +185 -0
  28. package/dist/SvSheetManageRules.svelte.d.ts +16 -0
  29. package/dist/SvSheetNameManager.svelte +225 -0
  30. package/dist/SvSheetNameManager.svelte.d.ts +11 -0
  31. package/dist/SvSheetPasteSpecial.svelte +105 -0
  32. package/dist/SvSheetPasteSpecial.svelte.d.ts +12 -0
  33. package/dist/SvSheetRemoveDuplicates.svelte +149 -0
  34. package/dist/SvSheetRemoveDuplicates.svelte.d.ts +12 -0
  35. package/dist/SvSheetRibbon.svelte +1201 -0
  36. package/dist/SvSheetRibbon.svelte.d.ts +34 -0
  37. package/dist/SvSheetSizeDialog.svelte +58 -0
  38. package/dist/SvSheetSizeDialog.svelte.d.ts +11 -0
  39. package/dist/SvSheetTabs.svelte +441 -0
  40. package/dist/SvSheetTabs.svelte.d.ts +36 -0
  41. package/dist/SvSheetTabs.test-harness.svelte.d.ts +17 -0
  42. package/dist/SvSheetTabs.test-harness.svelte.js +22 -0
  43. package/dist/SvSheetTextToColumns.svelte +172 -0
  44. package/dist/SvSheetTextToColumns.svelte.d.ts +13 -0
  45. package/dist/SvSheetValidationAlert.svelte +83 -0
  46. package/dist/SvSheetValidationAlert.svelte.d.ts +15 -0
  47. package/dist/cdn/svgrid-enterprise.svelte-external.js +18997 -5396
  48. package/dist/designer/assets/GridMenus-BlsvL_aU.js +8 -0
  49. package/dist/designer/assets/SvChartMenu-BYO9cYdy.css +1 -0
  50. package/dist/designer/assets/SvChartMenu-D9hEeHar.js +1 -0
  51. package/dist/designer/assets/SvDateRangeInput-BBRzrQeg.js +1 -0
  52. package/dist/designer/assets/SvDateTimePicker-DARvKUL2.js +1 -0
  53. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +4 -0
  54. package/dist/designer/assets/SvGridChart-B8j1SSy3.js +3 -0
  55. package/dist/designer/assets/SvGridChart-BgLuPO7I.css +1 -0
  56. package/dist/designer/assets/SvGridChartBuilder-08YWWeqY.css +1 -0
  57. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +1 -0
  58. package/dist/designer/assets/SvGridChartPanel-4Pc8y5yO.css +1 -0
  59. package/dist/designer/assets/SvGridChartPanel-pT6ITc2P.js +4 -0
  60. package/dist/designer/assets/{SvGridChartView-DUc-1RUf.css → SvGridChartView-7msfnWZT.css} +1 -1
  61. package/dist/designer/assets/SvGridChartView-bg281UA1.js +1 -0
  62. package/dist/designer/assets/SvListBox-BDS66lmZ.js +110 -0
  63. package/dist/designer/assets/SvMenuList-DJ47ba1h.css +1 -0
  64. package/dist/designer/assets/SvMenuList-Dgq2D5VN.js +1 -0
  65. package/dist/designer/assets/SvTabs-DKmwb5p1.css +1 -0
  66. package/dist/designer/assets/SvTabs-UFCMTHXS.js +1 -0
  67. package/dist/designer/assets/chart-cIPUsbM5.js +1 -0
  68. package/dist/designer/assets/chart-export-B6bkQ3X8.js +11 -0
  69. package/dist/designer/assets/chart-export-pdf-BbA28-gt.js +11 -0
  70. package/dist/designer/assets/chart-panel-messages-DtTxpBUU.js +1 -0
  71. package/dist/designer/assets/chart-summary-Czu_5epb.js +1 -0
  72. package/dist/designer/assets/chart-trend-D5I5VEe8.js +1 -0
  73. package/dist/designer/assets/chart-validate-BMqM-qkF.js +1 -0
  74. package/dist/designer/assets/column-resize-BYDzR1Ve.js +1 -0
  75. package/dist/designer/assets/date-format-B8JbEtIG.js +1 -0
  76. package/dist/designer/assets/date-format-DKS-z0xS.css +1 -0
  77. package/dist/designer/assets/editor-contract-Bpoa7A5g.js +1 -0
  78. package/dist/designer/assets/focus-trap-CAreAhlU.js +1 -0
  79. package/dist/designer/assets/index-CMFgAoVV.css +1 -0
  80. package/dist/designer/assets/{index-C_a9Cj8Y.js → index-DwU7CAMm.js} +291 -290
  81. package/dist/designer/assets/list-option-BP_dWS66.js +1 -0
  82. package/dist/designer/assets/row-resize-DLktcqUK.js +1 -0
  83. package/dist/designer/assets/src-CITpcm6b.js +1 -0
  84. package/dist/designer/assets/src-EdI3Xqds.js +58 -0
  85. package/dist/designer/index.html +23 -14
  86. package/dist/export-pdf.d.ts +89 -0
  87. package/dist/export-pdf.js +130 -0
  88. package/dist/export.js +17 -10
  89. package/dist/index.d.ts +64 -1
  90. package/dist/index.js +68 -1
  91. package/dist/install.js +5 -0
  92. package/dist/node/studio.js +64 -2
  93. package/dist/pivot-chart.d.ts +25 -1
  94. package/dist/pivot-chart.js +25 -69
  95. package/dist/sheet/address.d.ts +43 -0
  96. package/dist/sheet/address.js +81 -0
  97. package/dist/sheet/ast.d.ts +94 -0
  98. package/dist/sheet/ast.js +56 -0
  99. package/dist/sheet/auto-filter.d.ts +66 -0
  100. package/dist/sheet/auto-filter.js +147 -0
  101. package/dist/sheet/autocomplete.d.ts +42 -0
  102. package/dist/sheet/autocomplete.js +162 -0
  103. package/dist/sheet/coerce.d.ts +31 -0
  104. package/dist/sheet/coerce.js +122 -0
  105. package/dist/sheet/commands.d.ts +89 -0
  106. package/dist/sheet/commands.js +230 -0
  107. package/dist/sheet/comments.d.ts +29 -0
  108. package/dist/sheet/comments.js +73 -0
  109. package/dist/sheet/conditional-formats.d.ts +144 -0
  110. package/dist/sheet/conditional-formats.js +316 -0
  111. package/dist/sheet/deps.d.ts +60 -0
  112. package/dist/sheet/deps.js +199 -0
  113. package/dist/sheet/document.d.ts +150 -0
  114. package/dist/sheet/document.js +264 -0
  115. package/dist/sheet/edit-keys.d.ts +14 -0
  116. package/dist/sheet/edit-keys.js +40 -0
  117. package/dist/sheet/entry.d.ts +31 -0
  118. package/dist/sheet/entry.js +76 -0
  119. package/dist/sheet/evaluate.d.ts +61 -0
  120. package/dist/sheet/evaluate.js +337 -0
  121. package/dist/sheet/find-replace.d.ts +62 -0
  122. package/dist/sheet/find-replace.js +152 -0
  123. package/dist/sheet/format-store.d.ts +99 -0
  124. package/dist/sheet/format-store.js +233 -0
  125. package/dist/sheet/freeze.d.ts +54 -0
  126. package/dist/sheet/freeze.js +58 -0
  127. package/dist/sheet/function-catalog.d.ts +10 -0
  128. package/dist/sheet/function-catalog.js +94 -0
  129. package/dist/sheet/functions.d.ts +28 -0
  130. package/dist/sheet/functions.js +362 -0
  131. package/dist/sheet/goal-seek.d.ts +73 -0
  132. package/dist/sheet/goal-seek.js +153 -0
  133. package/dist/sheet/index.d.ts +53 -0
  134. package/dist/sheet/index.js +53 -0
  135. package/dist/sheet/merges.d.ts +71 -0
  136. package/dist/sheet/merges.js +137 -0
  137. package/dist/sheet/names.d.ts +31 -0
  138. package/dist/sheet/names.js +87 -0
  139. package/dist/sheet/navigate.d.ts +52 -0
  140. package/dist/sheet/navigate.js +135 -0
  141. package/dist/sheet/number-format.d.ts +63 -0
  142. package/dist/sheet/number-format.js +508 -0
  143. package/dist/sheet/palette.d.ts +19 -0
  144. package/dist/sheet/palette.js +84 -0
  145. package/dist/sheet/parse.d.ts +13 -0
  146. package/dist/sheet/parse.js +108 -0
  147. package/dist/sheet/paste-special.d.ts +164 -0
  148. package/dist/sheet/paste-special.js +592 -0
  149. package/dist/sheet/protection.d.ts +21 -0
  150. package/dist/sheet/protection.js +48 -0
  151. package/dist/sheet/rects.d.ts +45 -0
  152. package/dist/sheet/rects.js +112 -0
  153. package/dist/sheet/refs.d.ts +82 -0
  154. package/dist/sheet/refs.js +266 -0
  155. package/dist/sheet/ribbon-icons.d.ts +31 -0
  156. package/dist/sheet/ribbon-icons.js +350 -0
  157. package/dist/sheet/ribbon.d.ts +173 -0
  158. package/dist/sheet/ribbon.js +753 -0
  159. package/dist/sheet/shortcuts.d.ts +164 -0
  160. package/dist/sheet/shortcuts.js +623 -0
  161. package/dist/sheet/structure.d.ts +87 -0
  162. package/dist/sheet/structure.js +166 -0
  163. package/dist/sheet/tables.d.ts +70 -0
  164. package/dist/sheet/tables.js +147 -0
  165. package/dist/sheet/tokenize.d.ts +54 -0
  166. package/dist/sheet/tokenize.js +304 -0
  167. package/dist/sheet/transforms.d.ts +76 -0
  168. package/dist/sheet/transforms.js +154 -0
  169. package/dist/sheet/validation.d.ts +92 -0
  170. package/dist/sheet/validation.js +241 -0
  171. package/dist/sheet/workbook.d.ts +72 -0
  172. package/dist/sheet/workbook.js +409 -0
  173. package/dist/sheet-enable.d.ts +6 -0
  174. package/dist/sheet-enable.js +46 -0
  175. package/dist/studio/ui-components.generated.js +63 -1
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/package.json +9 -4
  179. package/dist/designer/assets/GridMenus-B1JtIN21.js +0 -8
  180. package/dist/designer/assets/SvDateRangeInput-CjO7nn_x.js +0 -1
  181. package/dist/designer/assets/SvDateTimePicker-ByBD_ov6.js +0 -1
  182. package/dist/designer/assets/SvField-YDLipR6O.css +0 -1
  183. package/dist/designer/assets/SvField-cj1PFNgZ.js +0 -1
  184. package/dist/designer/assets/SvGridCellEditor-0W1L2hJ1.js +0 -2
  185. package/dist/designer/assets/SvGridChart-DBOR4xcz.js +0 -2
  186. package/dist/designer/assets/SvGridChart-fkMWeh1J.css +0 -1
  187. package/dist/designer/assets/SvGridChartPanel-symrhNnR.js +0 -11
  188. package/dist/designer/assets/SvGridChartView-BmRxq6mU.js +0 -1
  189. package/dist/designer/assets/SvListBox-1oUF6Iav.js +0 -110
  190. package/dist/designer/assets/chart-DXFganU8.js +0 -1
  191. package/dist/designer/assets/column-resize-C_inr5zT.js +0 -1
  192. package/dist/designer/assets/date-format-DGagAqRc.css +0 -1
  193. package/dist/designer/assets/date-format-onlYDsrY.js +0 -1
  194. package/dist/designer/assets/easing-zaypKrr0.js +0 -3
  195. package/dist/designer/assets/index-k2stW6L8.css +0 -1
  196. package/dist/designer/assets/row-resize-B0FWHcNr.js +0 -1
  197. package/dist/designer/assets/src-D9p27OGC.js +0 -58
@@ -0,0 +1,421 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Excel's Format Cells, the six tabs that have something behind them in
4
+ * the sheet: Number, Alignment, Font, Border, Fill, Protection. It opens on the active
5
+ * cell's format and, on OK, applies only what was changed to the whole
6
+ * selection, so setting a fill on a block with mixed fonts leaves the
7
+ * fonts as they were. One undo, through `applyFormat`.
8
+ *
9
+ * The shell opens it for Ctrl+1, the group launchers on the ribbon, the
10
+ * cell menu's Format Cells... and the `format-cells` action.
11
+ */
12
+ import { SvModal } from '@svgrid/grid'
13
+ import type { CellFormatEntry } from './sheet/format-store'
14
+ import { compileNumberFormat, FORMAT_PRESETS, formatCategory } from './sheet/number-format'
15
+ import { FONT_FAMILIES, FONT_SIZES, type BorderPreset } from './sheet/ribbon'
16
+ import { ALL_COLOURS } from './sheet/palette'
17
+
18
+ type Props = {
19
+ open?: boolean
20
+ /** The active cell's format, what the dialog opens on. */
21
+ entry: CellFormatEntry | undefined
22
+ /** The active cell's value, for the Number tab's sample. */
23
+ sample: unknown
24
+ /** The selection holds locked and unlocked cells both: the Protection
25
+ * tab's box opens indeterminate and, left alone, changes nothing. */
26
+ mixedLocked?: boolean
27
+ onApply: (patch: CellFormatEntry, border: BorderPreset | null) => void
28
+ onClose?: () => void
29
+ }
30
+
31
+ let { open = $bindable(false), entry, sample, mixedLocked = false, onApply, onClose }: Props = $props()
32
+
33
+ type Tab = 'number' | 'alignment' | 'font' | 'border' | 'fill' | 'protection'
34
+ const TABS: ReadonlyArray<{ id: Tab; label: string }> = [
35
+ { id: 'number', label: 'Number' },
36
+ { id: 'alignment', label: 'Alignment' },
37
+ { id: 'font', label: 'Font' },
38
+ { id: 'border', label: 'Border' },
39
+ { id: 'fill', label: 'Fill' },
40
+ { id: 'protection', label: 'Protection' },
41
+ ]
42
+ let tab = $state<Tab>('number')
43
+
44
+ type Category = 'general' | 'number' | 'currency' | 'percent' | 'date' | 'time' | 'scientific' | 'custom'
45
+ const CATEGORIES: ReadonlyArray<{ id: Category; label: string }> = [
46
+ { id: 'general', label: 'General' },
47
+ { id: 'number', label: 'Number' },
48
+ { id: 'currency', label: 'Currency' },
49
+ { id: 'percent', label: 'Percentage' },
50
+ { id: 'date', label: 'Date' },
51
+ { id: 'time', label: 'Time' },
52
+ { id: 'scientific', label: 'Scientific' },
53
+ { id: 'custom', label: 'Custom' },
54
+ ]
55
+
56
+ // Number
57
+ let category = $state<Category>('general')
58
+ let decimals = $state(2)
59
+ let thousands = $state(true)
60
+ let custom = $state('')
61
+ // Alignment
62
+ let align = $state<'' | 'left' | 'center' | 'right'>('')
63
+ let wrap = $state(false)
64
+ let indent = $state(0)
65
+ // Font
66
+ let fontFamily = $state('')
67
+ let fontSize = $state('')
68
+ let bold = $state(false)
69
+ let italic = $state(false)
70
+ let underline = $state(false)
71
+ let strike = $state(false)
72
+ let colorAuto = $state(true)
73
+ let color = $state('#000000')
74
+ // Border
75
+ let border = $state<BorderPreset | ''>('')
76
+ // Fill
77
+ let fill = $state<string | null>(null)
78
+ // Protection: null is the indeterminate box of a mixed selection.
79
+ let locked = $state<boolean | null>(true)
80
+
81
+ let initial: CellFormatEntry = {}
82
+ /** What the colour input opened on, so an untouched picker keeps a colour
83
+ * the input cannot show (a CSS name, an rgb()) rather than replacing it. */
84
+ let initialColor = '#000000'
85
+
86
+ /** Which category a pattern belongs to, so the dialog opens on it. */
87
+ const categoryOf = (fmt: string | undefined): { category: Category; decimals: number; thousands: boolean } =>
88
+ formatCategory(fmt)
89
+
90
+ const zeros = (n: number) => (n > 0 ? '.' + '0'.repeat(n) : '')
91
+
92
+ /** The pattern the Number tab's choices spell. */
93
+ const pattern = $derived.by<string | undefined>(() => {
94
+ switch (category) {
95
+ case 'general': return undefined
96
+ case 'number': return `${thousands ? '#,##' : ''}0${zeros(decimals)}`
97
+ case 'currency': return `$#,##0${zeros(decimals)};($#,##0${zeros(decimals)})`
98
+ case 'percent': return `0${zeros(decimals)}%`
99
+ case 'date': return FORMAT_PRESETS.date
100
+ case 'time': return FORMAT_PRESETS.time
101
+ case 'scientific': return `0${zeros(decimals)}E+00`
102
+ case 'custom': return custom || undefined
103
+ }
104
+ })
105
+
106
+ const preview = $derived.by(() => {
107
+ if (sample === '' || sample == null) return ''
108
+ if (!pattern) return String(sample)
109
+ try { return compileNumberFormat(pattern).format(sample).text } catch { return String(sample) }
110
+ })
111
+
112
+ // Open on the active cell's format.
113
+ $effect(() => {
114
+ if (!open) return
115
+ initial = { ...(entry ?? {}) }
116
+ const num = categoryOf(initial.numFmt)
117
+ category = num.category
118
+ decimals = num.decimals
119
+ thousands = num.thousands
120
+ custom = initial.numFmt ?? ''
121
+ align = initial.align ?? ''
122
+ wrap = initial.wrap ?? false
123
+ indent = initial.indent ?? 0
124
+ fontFamily = initial.fontFamily ?? ''
125
+ fontSize = initial.fontSize ? String(initial.fontSize) : ''
126
+ bold = !!initial.bold
127
+ italic = !!initial.italic
128
+ underline = !!initial.underline
129
+ strike = !!initial.strike
130
+ colorAuto = !initial.color
131
+ color = /^#[0-9a-f]{6}$/i.test(initial.color ?? '') ? initial.color! : '#000000'
132
+ initialColor = color
133
+ border = ''
134
+ fill = initial.fill ?? null
135
+ locked = mixedLocked ? null : initial.locked !== false
136
+ })
137
+
138
+ function ok() {
139
+ // Only what changed goes into the patch; `undefined` clears a field.
140
+ const patch: CellFormatEntry = {}
141
+ const set = <K extends keyof CellFormatEntry>(key: K, next: CellFormatEntry[K]) => {
142
+ if (initial[key] !== next) patch[key] = next
143
+ }
144
+ set('numFmt', pattern)
145
+ set('align', align || undefined)
146
+ set('wrap', wrap || undefined)
147
+ set('indent', indent > 0 ? indent : undefined)
148
+ set('fontFamily', fontFamily || undefined)
149
+ set('fontSize', fontSize ? Number(fontSize) : undefined)
150
+ set('bold', bold || undefined)
151
+ set('italic', italic || undefined)
152
+ set('underline', underline || undefined)
153
+ set('strike', strike || undefined)
154
+ set('color', colorAuto ? undefined : color === initialColor ? initial.color : color)
155
+ set('fill', fill ?? undefined)
156
+ // Locked is the default, so only "unlocked" is ever stored; a mixed
157
+ // selection left indeterminate stays as it was.
158
+ if (locked !== null) {
159
+ const next = locked ? undefined : false
160
+ if (mixedLocked || initial.locked !== next) patch.locked = next
161
+ }
162
+ onApply(patch, border || null)
163
+ open = false
164
+ onClose?.()
165
+ }
166
+
167
+ function close() {
168
+ open = false
169
+ onClose?.()
170
+ }
171
+
172
+ const BORDERS: ReadonlyArray<{ value: BorderPreset; label: string }> = [
173
+ { value: 'none', label: 'None' },
174
+ { value: 'outside', label: 'Outline' },
175
+ { value: 'all', label: 'All borders' },
176
+ { value: 'top', label: 'Top' },
177
+ { value: 'bottom', label: 'Bottom' },
178
+ { value: 'left', label: 'Left' },
179
+ { value: 'right', label: 'Right' },
180
+ { value: 'thick-bottom', label: 'Thick bottom' },
181
+ ]
182
+ </script>
183
+
184
+ <SvModal bind:open onClose={onClose} title="Format Cells" size="md">
185
+ <div class="sv-sheet-dialog format" role="group" aria-label="Format Cells">
186
+ <div class="tabs" role="tablist" aria-label="Format Cells tabs">
187
+ {#each TABS as t (t.id)}
188
+ <button
189
+ type="button"
190
+ role="tab"
191
+ class="tab"
192
+ class:on={tab === t.id}
193
+ aria-selected={tab === t.id}
194
+ onclick={() => (tab = t.id)}
195
+ >{t.label}</button>
196
+ {/each}
197
+ </div>
198
+
199
+ <div class="panel" role="tabpanel">
200
+ {#if tab === 'number'}
201
+ <div class="number">
202
+ <div class="categories">
203
+ <div class="label">Category:</div>
204
+ <ul role="listbox" aria-label="Category">
205
+ {#each CATEGORIES as c (c.id)}
206
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
207
+ <li role="option" aria-selected={category === c.id} class:selected={category === c.id} onclick={() => (category = c.id)}>{c.label}</li>
208
+ {/each}
209
+ </ul>
210
+ </div>
211
+ <div class="settings">
212
+ <div class="sample">
213
+ <div class="label">Sample</div>
214
+ <div class="sample-text">{preview}</div>
215
+ </div>
216
+ {#if category === 'number' || category === 'currency' || category === 'percent' || category === 'scientific'}
217
+ <label class="field auto">
218
+ <span>Decimal places:</span>
219
+ <input type="number" min="0" max="10" bind:value={decimals} />
220
+ </label>
221
+ {/if}
222
+ {#if category === 'number'}
223
+ <label class="check"><input type="checkbox" bind:checked={thousands} /> Use 1000 separator (,)</label>
224
+ {/if}
225
+ {#if category === 'custom'}
226
+ <label class="field auto">
227
+ <span>Type:</span>
228
+ <input type="text" bind:value={custom} spellcheck="false" placeholder="#,##0.00" />
229
+ </label>
230
+ {/if}
231
+ <p class="hint">
232
+ {#if category === 'general'}General format cells have no specific number format.
233
+ {:else if category === 'number'}Number is used for general display of numbers.
234
+ {:else if category === 'currency'}Currency formats are used for general monetary values.
235
+ {:else if category === 'percent'}Percentage formats multiply the cell value by 100 and display the result with a percent symbol.
236
+ {:else if category === 'date'}Date formats display date serial numbers as dates.
237
+ {:else if category === 'time'}Time formats display date serial numbers as times.
238
+ {:else if category === 'scientific'}Scientific formats display numbers in exponential notation.
239
+ {:else}Type the number format code, using one of the existing codes as a starting point.{/if}
240
+ </p>
241
+ </div>
242
+ </div>
243
+ {:else if tab === 'alignment'}
244
+ <label class="field">
245
+ <span>Horizontal:</span>
246
+ <select bind:value={align}>
247
+ <option value="">General</option>
248
+ <option value="left">Left (Indent)</option>
249
+ <option value="center">Center</option>
250
+ <option value="right">Right (Indent)</option>
251
+ </select>
252
+ </label>
253
+ <label class="field">
254
+ <span>Indent:</span>
255
+ <input type="number" min="0" max="15" bind:value={indent} />
256
+ </label>
257
+ <div class="label">Text control</div>
258
+ <label class="check"><input type="checkbox" bind:checked={wrap} /> Wrap text</label>
259
+ {:else if tab === 'font'}
260
+ <label class="field">
261
+ <span>Font:</span>
262
+ <select bind:value={fontFamily}>
263
+ {#each FONT_FAMILIES as f (f.value)}
264
+ <option value={f.value}>{f.label}</option>
265
+ {/each}
266
+ </select>
267
+ </label>
268
+ <label class="field">
269
+ <span>Size:</span>
270
+ <select bind:value={fontSize}>
271
+ <option value="">Default</option>
272
+ {#each FONT_SIZES as s (s)}
273
+ <option value={String(s)}>{s}</option>
274
+ {/each}
275
+ </select>
276
+ </label>
277
+ <div class="label">Font style</div>
278
+ <div class="checks">
279
+ <label class="check"><input type="checkbox" bind:checked={bold} /> Bold</label>
280
+ <label class="check"><input type="checkbox" bind:checked={italic} /> Italic</label>
281
+ <label class="check"><input type="checkbox" bind:checked={underline} /> Underline</label>
282
+ <label class="check"><input type="checkbox" bind:checked={strike} /> Strikethrough</label>
283
+ </div>
284
+ <div class="label">Color</div>
285
+ <div class="checks">
286
+ <label class="check"><input type="checkbox" bind:checked={colorAuto} /> Automatic</label>
287
+ <input type="color" bind:value={color} disabled={colorAuto} aria-label="Font colour" />
288
+ </div>
289
+ <div class="font-preview" style:font-family={fontFamily || undefined} style:font-size={fontSize ? `${fontSize}px` : undefined} style:font-weight={bold ? 700 : 400} style:font-style={italic ? 'italic' : 'normal'} style:text-decoration={[underline ? 'underline' : '', strike ? 'line-through' : ''].join(' ').trim() || 'none'} style:color={colorAuto ? undefined : color}>AaBbCcYyZz</div>
290
+ {:else if tab === 'border'}
291
+ <div class="label">Presets</div>
292
+ <div class="borders" role="radiogroup" aria-label="Border">
293
+ <label class="check"><input type="radio" name="sheet-border" value="" bind:group={border} /> Keep as is</label>
294
+ {#each BORDERS as b (b.value)}
295
+ <label class="check"><input type="radio" name="sheet-border" value={b.value} bind:group={border} /> {b.label}</label>
296
+ {/each}
297
+ </div>
298
+ <p class="hint">Bottom, top, left and right go on the edge of the selection; All borders lines every cell; Outline frames the block.</p>
299
+ {:else if tab === 'fill'}
300
+ <div class="label">Background Color</div>
301
+ <div class="no-fill">
302
+ <button type="button" class="swatch none" class:on={fill === null} aria-label="No Color" title="No Color" onclick={() => (fill = null)}></button>
303
+ <span>No Color</span>
304
+ </div>
305
+ <div class="swatches" role="radiogroup" aria-label="Fill colour">
306
+ {#each ALL_COLOURS as c, i (i)}
307
+ <button type="button" class="swatch" class:on={fill === c.value} style:background={c.value} aria-label={c.label} title={c.label} onclick={() => (fill = c.value)}></button>
308
+ {/each}
309
+ </div>
310
+ <div class="sample">
311
+ <div class="label">Sample</div>
312
+ <div class="fill-sample" style:background={fill ?? 'transparent'}></div>
313
+ </div>
314
+ {:else if tab === 'protection'}
315
+ <label class="check">
316
+ <input
317
+ type="checkbox"
318
+ checked={locked === true}
319
+ indeterminate={locked === null}
320
+ onchange={(e) => (locked = (e.currentTarget as HTMLInputElement).checked)}
321
+ />
322
+ Locked
323
+ </label>
324
+ <p class="hint">Locking cells has no effect until you protect the sheet (Review tab, Protect Sheet). Every cell is locked to begin with; unlock the ones that may change, then protect the sheet.</p>
325
+ {/if}
326
+ </div>
327
+ </div>
328
+ {#snippet footer()}
329
+ <div class="sv-sheet-dialog-buttons">
330
+ <button type="button" class="btn primary" onclick={ok}>OK</button>
331
+ <button type="button" class="btn" onclick={close}>Cancel</button>
332
+ </div>
333
+ {/snippet}
334
+ </SvModal>
335
+
336
+ <style>
337
+ .tabs {
338
+ display: flex;
339
+ gap: 2px;
340
+ border-bottom: 1px solid var(--sg-border, #d1d1d1);
341
+ }
342
+ .tab {
343
+ padding: 5px 12px;
344
+ font: inherit;
345
+ font-size: 13px;
346
+ color: var(--sg-muted, #616161);
347
+ background: transparent;
348
+ border: 1px solid transparent;
349
+ border-bottom: 0;
350
+ border-radius: var(--sg-radius, 3px) var(--sg-radius, 3px) 0 0;
351
+ cursor: pointer;
352
+ }
353
+ .tab:hover { color: var(--sg-fg, #242424); }
354
+ .tab.on {
355
+ color: var(--sg-fg, #242424);
356
+ background: var(--sg-bg, #fff);
357
+ border-color: var(--sg-border, #d1d1d1);
358
+ margin-bottom: -1px;
359
+ }
360
+ .panel {
361
+ display: flex;
362
+ flex-direction: column;
363
+ gap: 10px;
364
+ min-height: 220px;
365
+ padding-top: 4px;
366
+ }
367
+ .label { font-weight: 600; }
368
+ .number {
369
+ display: grid;
370
+ grid-template-columns: 140px 1fr;
371
+ gap: 14px;
372
+ }
373
+ .categories ul {
374
+ height: 200px;
375
+ margin: 4px 0 0;
376
+ padding: 2px 0;
377
+ overflow-y: auto;
378
+ list-style: none;
379
+ background: var(--sg-bg, #fff);
380
+ border: 1px solid var(--sg-border, #d1d1d1);
381
+ border-radius: var(--sg-radius, 3px);
382
+ }
383
+ .categories li { padding: 2px 8px; cursor: default; }
384
+ .categories li:hover { background: var(--sg-row-hover-bg, #f5f5f5); }
385
+ .categories li.selected { color: var(--sg-on-accent, #fff); background: var(--sg-accent, #217346); }
386
+ .settings { display: flex; flex-direction: column; gap: 10px; }
387
+ .sample { display: flex; flex-direction: column; gap: 4px; }
388
+ .sample-text,
389
+ .font-preview,
390
+ .fill-sample {
391
+ min-height: 28px;
392
+ padding: 4px 8px;
393
+ background: var(--sg-bg, #fff);
394
+ border: 1px solid var(--sg-border, #d1d1d1);
395
+ border-radius: var(--sg-radius, 3px);
396
+ }
397
+ .font-preview { min-height: 44px; display: flex; align-items: center; }
398
+ .fill-sample { min-height: 44px; }
399
+ .hint { margin: 0; color: var(--sg-muted, #616161); }
400
+ .borders { display: grid; grid-template-columns: repeat(3, auto); gap: 6px 16px; justify-content: start; }
401
+ .no-fill { display: flex; align-items: center; gap: 8px; }
402
+ .swatches {
403
+ display: grid;
404
+ grid-template-columns: repeat(10, 22px);
405
+ gap: 3px;
406
+ }
407
+ .swatch {
408
+ width: 22px;
409
+ height: 22px;
410
+ padding: 0;
411
+ border: 1px solid var(--sg-border, #d1d1d1);
412
+ border-radius: 2px;
413
+ cursor: pointer;
414
+ }
415
+ .swatch.none {
416
+ background:
417
+ linear-gradient(to top left, transparent calc(50% - 1px), var(--sg-danger, #c00) calc(50% - 1px), var(--sg-danger, #c00) calc(50% + 1px), transparent calc(50% + 1px)),
418
+ var(--sg-bg, #fff);
419
+ }
420
+ .swatch.on { outline: 2px solid var(--sg-accent, #217346); outline-offset: 1px; }
421
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { CellFormatEntry } from './sheet/format-store.js';
2
+ import { type BorderPreset } from './sheet/ribbon.js';
3
+ type Props = {
4
+ open?: boolean;
5
+ /** The active cell's format, what the dialog opens on. */
6
+ entry: CellFormatEntry | undefined;
7
+ /** The active cell's value, for the Number tab's sample. */
8
+ sample: unknown;
9
+ /** The selection holds locked and unlocked cells both: the Protection
10
+ * tab's box opens indeterminate and, left alone, changes nothing. */
11
+ mixedLocked?: boolean;
12
+ onApply: (patch: CellFormatEntry, border: BorderPreset | null) => void;
13
+ onClose?: () => void;
14
+ };
15
+ declare const SvSheetFormatCells: import("svelte").Component<Props, {}, "open">;
16
+ type SvSheetFormatCells = ReturnType<typeof SvSheetFormatCells>;
17
+ export default SvSheetFormatCells;
@@ -0,0 +1,144 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Excel's Goal Seek: set a formula cell to a value by changing one input
4
+ * cell. The solver restores the input while it searches and the status
5
+ * page shows what it found; OK keeps the answer through the command
6
+ * context, so it lands in the grid's history like a typed number.
7
+ */
8
+ import { untrack } from 'svelte'
9
+ import { SvModal } from '@svgrid/grid'
10
+ import type { GridCommandContext } from '@svgrid/grid/shortcuts'
11
+ import type { Workbook } from './sheet/workbook'
12
+ import { goalSeekCell, type GoalSeekResult } from './sheet/goal-seek'
13
+ import { parseA1, colToLetters } from './sheet/address'
14
+
15
+ type Props = {
16
+ open?: boolean
17
+ workbook: Workbook
18
+ /** The command context the dialog was opened with. */
19
+ cmd: () => GridCommandContext | null
20
+ onClose?: () => void
21
+ }
22
+
23
+ let { open = $bindable(false), workbook, cmd, onClose }: Props = $props()
24
+
25
+ let step = $state<'ask' | 'done'>('ask')
26
+ let setCell = $state('')
27
+ let toValue = $state('')
28
+ let byCell = $state('')
29
+ let problem = $state<string | null>(null)
30
+ let found = $state<GoalSeekResult | null>(null)
31
+ let firstInput = $state<HTMLInputElement | null>(null)
32
+
33
+ // The active cell is the natural "Set cell", as it is in Excel.
34
+ $effect(() => {
35
+ if (!open) return
36
+ untrack(prepare)
37
+ })
38
+
39
+ function prepare() {
40
+ const at = cmd()?.activeCell
41
+ setCell = at ? `${colToLetters(at.colIndex)}${at.rowIndex + 1}` : ''
42
+ toValue = ''
43
+ byCell = ''
44
+ step = 'ask'
45
+ problem = null
46
+ found = null
47
+ queueMicrotask(() => firstInput?.focus())
48
+ }
49
+
50
+ function address(text: string): { row: number; col: number } | null {
51
+ const ref = parseA1(text.trim().toUpperCase())
52
+ return ref && ref.row !== null ? { row: ref.row, col: ref.col } : null
53
+ }
54
+
55
+ function solve() {
56
+ const formula = address(setCell)
57
+ const input = address(byCell)
58
+ const target = Number(toValue)
59
+ if (!formula) { problem = `"${setCell}" is not a cell address`; return }
60
+ if (!input) { problem = `"${byCell}" is not a cell address`; return }
61
+ if (toValue.trim() === '' || !Number.isFinite(target)) { problem = 'To value must be a number'; return }
62
+ const sheet = workbook.active
63
+ if (!workbook.getRaw(sheet, formula.row, formula.col).startsWith('=')) {
64
+ problem = `${setCell.toUpperCase()} must contain a formula`
65
+ return
66
+ }
67
+ if (workbook.getRaw(sheet, input.row, input.col).startsWith('=')) {
68
+ problem = `${byCell.toUpperCase()} must contain a value, not a formula`
69
+ return
70
+ }
71
+ found = goalSeekCell(workbook, { sheet, ...formula }, { sheet, ...input }, target, { maxIterations: 100 })
72
+ problem = null
73
+ step = 'done'
74
+ }
75
+
76
+ function keep() {
77
+ const input = address(byCell)
78
+ const c = cmd()
79
+ if (!input || !found || !c) return
80
+ c.setCellValue(input.row, input.col, String(round(found.value)))
81
+ c.setActiveCell(input.row, input.col)
82
+ close()
83
+ }
84
+
85
+ function close() {
86
+ open = false
87
+ onClose?.()
88
+ }
89
+
90
+ const round = (n: number) => Number(n.toFixed(4))
91
+ const money = (n: number) => n.toLocaleString('en-US', { maximumFractionDigits: 2 })
92
+ </script>
93
+
94
+ <SvModal bind:open onClose={onClose} title={step === 'ask' ? 'Goal Seek' : 'Goal Seek Status'} size="sm">
95
+ {#if step === 'ask'}
96
+ <form class="sv-sheet-dialog" onsubmit={(e) => { e.preventDefault(); solve() }}>
97
+ <label class="field">
98
+ <span>Set cell:</span>
99
+ <input bind:this={firstInput} type="text" bind:value={setCell} spellcheck="false" />
100
+ </label>
101
+ <label class="field">
102
+ <span>To value:</span>
103
+ <input type="text" bind:value={toValue} inputmode="decimal" />
104
+ </label>
105
+ <label class="field">
106
+ <span>By changing cell:</span>
107
+ <input type="text" bind:value={byCell} spellcheck="false" />
108
+ </label>
109
+ {#if problem}<p class="status problem" role="alert">{problem}</p>{/if}
110
+ </form>
111
+ {:else if found}
112
+ <div class="sv-sheet-dialog">
113
+ <p class="lead">
114
+ Goal Seeking with Cell <strong>{setCell.toUpperCase()}</strong>
115
+ {found.converged ? 'found a solution.' : 'may not have found a solution.'}
116
+ </p>
117
+ <dl class="facts">
118
+ <dt>Target value:</dt><dd>{money(Number(toValue))}</dd>
119
+ <dt>Current value:</dt><dd>{money(found.result)}</dd>
120
+ <dt>{byCell.toUpperCase()} becomes:</dt><dd>{money(round(found.value))}</dd>
121
+ </dl>
122
+ <p class="status">{found.iterations} iterations.</p>
123
+ </div>
124
+ {/if}
125
+ {#snippet footer()}
126
+ <div class="sv-sheet-dialog-buttons">
127
+ <button type="button" class="btn primary" onclick={step === 'ask' ? solve : keep}>OK</button>
128
+ <button type="button" class="btn" onclick={close}>Cancel</button>
129
+ </div>
130
+ {/snippet}
131
+ </SvModal>
132
+
133
+ <style>
134
+ .lead { margin: 0; }
135
+ .facts {
136
+ display: grid;
137
+ grid-template-columns: auto 1fr;
138
+ gap: 4px 12px;
139
+ margin: 0;
140
+ }
141
+ .facts dt { color: var(--sg-muted, #616161); }
142
+ .facts dd { margin: 0; font-variant-numeric: tabular-nums; }
143
+ .problem { color: var(--sg-danger, #dc2626); }
144
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { GridCommandContext } from '@svgrid/grid/shortcuts';
2
+ import type { Workbook } from './sheet/workbook.js';
3
+ type Props = {
4
+ open?: boolean;
5
+ workbook: Workbook;
6
+ /** The command context the dialog was opened with. */
7
+ cmd: () => GridCommandContext | null;
8
+ onClose?: () => void;
9
+ };
10
+ declare const SvSheetGoalSeek: import("svelte").Component<Props, {}, "open">;
11
+ type SvSheetGoalSeek = ReturnType<typeof SvSheetGoalSeek>;
12
+ export default SvSheetGoalSeek;