@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,34 @@
1
+ import type { GridCommandContext } from '@svgrid/grid/shortcuts';
2
+ import { type RibbonTab, type RibbonActionId } from './sheet/ribbon.js';
3
+ type Props = {
4
+ /**
5
+ * How to reach the grid. A getter rather than the context itself because
6
+ * the context is rebuilt per call and the ribbon needs a live one at the
7
+ * moment a button is pressed, not the one that existed at mount.
8
+ */
9
+ cmd: () => GridCommandContext | null;
10
+ /** Called after any action that changed something, so the host re-renders. */
11
+ onChange?: () => void;
12
+ /** Actions the ribbon does not run itself, because they need chrome. */
13
+ onAction?: (action: RibbonActionId, cmd: GridCommandContext) => void;
14
+ /** Which of `onAction`'s toggles are currently on. The host owns that
15
+ * state (there is no store here to read it from), so it reports it. */
16
+ activeActions?: ReadonlyArray<RibbonActionId>;
17
+ tabs?: ReadonlyArray<RibbonTab>;
18
+ /** Start on a tab other than the first. */
19
+ tab?: string;
20
+ /**
21
+ * Actions to leave off the ribbon: a button nobody answers is worse
22
+ * than no button. A group with nothing left in it goes too.
23
+ */
24
+ without?: ReadonlyArray<RibbonActionId>;
25
+ /**
26
+ * Excel's collapsed ribbon: the tabs alone. A click on a tab shows the
27
+ * band over the sheet until a command runs or the pointer leaves;
28
+ * double-clicking a tab, the chevron and Ctrl+F1 toggle it.
29
+ */
30
+ collapsed?: boolean;
31
+ };
32
+ declare const SvSheetRibbon: import("svelte").Component<Props, {}, "tab" | "collapsed">;
33
+ type SvSheetRibbon = ReturnType<typeof SvSheetRibbon>;
34
+ export default SvSheetRibbon;
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Excel's Column Width and Row Height boxes: one number, OK, Cancel. The
4
+ * shell opens it from the column letter's and the row number's menus and
5
+ * applies the size to every column or row the selection spans.
6
+ */
7
+ import { SvModal } from '@svgrid/grid'
8
+
9
+ type Props = {
10
+ open?: boolean
11
+ title: string
12
+ label: string
13
+ value: number
14
+ onApply: (px: number) => void
15
+ onClose?: () => void
16
+ }
17
+
18
+ let { open = $bindable(false), title, label, value, onApply, onClose }: Props = $props()
19
+
20
+ let draft = $state('')
21
+ let input = $state<HTMLInputElement | null>(null)
22
+
23
+ $effect(() => {
24
+ if (!open) return
25
+ draft = String(Math.round(value))
26
+ queueMicrotask(() => { input?.focus(); input?.select() })
27
+ })
28
+
29
+ const px = $derived(Number(draft))
30
+ const valid = $derived(Number.isFinite(px) && px >= 4 && px <= 2000)
31
+
32
+ function ok() {
33
+ if (!valid) return
34
+ onApply(Math.round(px))
35
+ open = false
36
+ onClose?.()
37
+ }
38
+
39
+ function close() {
40
+ open = false
41
+ onClose?.()
42
+ }
43
+ </script>
44
+
45
+ <SvModal bind:open onClose={onClose} {title} size="sm">
46
+ <form class="sv-sheet-dialog" onsubmit={(e) => { e.preventDefault(); ok() }}>
47
+ <label class="field auto">
48
+ <span>{label}</span>
49
+ <input bind:this={input} type="number" min="4" max="2000" bind:value={draft} />
50
+ </label>
51
+ </form>
52
+ {#snippet footer()}
53
+ <div class="sv-sheet-dialog-buttons">
54
+ <button type="button" class="btn primary" onclick={ok} disabled={!valid}>OK</button>
55
+ <button type="button" class="btn" onclick={close}>Cancel</button>
56
+ </div>
57
+ {/snippet}
58
+ </SvModal>
@@ -0,0 +1,11 @@
1
+ type Props = {
2
+ open?: boolean;
3
+ title: string;
4
+ label: string;
5
+ value: number;
6
+ onApply: (px: number) => void;
7
+ onClose?: () => void;
8
+ };
9
+ declare const SvSheetSizeDialog: import("svelte").Component<Props, {}, "open">;
10
+ type SvSheetSizeDialog = ReturnType<typeof SvSheetSizeDialog>;
11
+ export default SvSheetSizeDialog;
@@ -0,0 +1,441 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The sheet tab strip along the bottom of a workbook.
4
+ *
5
+ * Owns no workbook state of its own: it renders what the workbook reports
6
+ * and calls back, so the keyboard shortcuts and the tabs cannot disagree
7
+ * about which sheet is active.
8
+ */
9
+ import type { Workbook } from './sheet/workbook'
10
+ import { isValidSheetName } from './sheet/workbook'
11
+
12
+ type Props = {
13
+ workbook: Workbook
14
+ /** Called after any change, so the consumer can re-render. */
15
+ onChange?: () => void
16
+ /**
17
+ * Called after a sheet is renamed or removed, before `onChange`. Anything
18
+ * the consumer keeps per sheet and keys by name - the shell keeps a
19
+ * format store per sheet - has to follow the rename or drop the entry,
20
+ * and the workbook itself has no event for either.
21
+ */
22
+ onRename?: (from: string, to: string) => void
23
+ onRemove?: (name: string) => void
24
+ /** Off hides the add button and the context actions. */
25
+ editable?: boolean
26
+ /**
27
+ * Bump this whenever the workbook is mutated from OUTSIDE the strip - the
28
+ * Ctrl+PageUp/PageDown shortcuts, or app code calling `addSheet`.
29
+ *
30
+ * A `Workbook` is a plain object, not `$state`, so reading `workbook.sheets`
31
+ * creates no reactive dependency and the strip would otherwise render once
32
+ * and then show whatever the sheet list was at mount. Pass the same counter
33
+ * `onChange` increments.
34
+ */
35
+ version?: number
36
+ }
37
+
38
+ let { workbook, onChange, onRename, onRemove, editable = true, version = 0 }: Props = $props()
39
+
40
+ let renaming = $state<string | null>(null)
41
+ let draft = $state('')
42
+ let error = $state<string | null>(null)
43
+ let dragging = $state<string | null>(null)
44
+
45
+ // The strip's own mutations do not go through the consumer's counter, so it
46
+ // keeps one of its own and reads both.
47
+ let localVersion = $state(0)
48
+
49
+ const sheets = $derived.by(() => {
50
+ void version
51
+ void localVersion
52
+ return workbook.sheets
53
+ })
54
+ const activeSheet = $derived.by(() => {
55
+ void version
56
+ void localVersion
57
+ return workbook.active
58
+ })
59
+
60
+ function changed() {
61
+ localVersion += 1
62
+ onChange?.()
63
+ }
64
+
65
+ function select(name: string) {
66
+ workbook.setActive(name)
67
+ changed()
68
+ }
69
+
70
+ function startRename(name: string) {
71
+ if (!editable) return
72
+ renaming = name
73
+ draft = name
74
+ error = null
75
+ }
76
+
77
+ function commitRename() {
78
+ const from = renaming
79
+ if (from === null) return
80
+ const to = draft.trim()
81
+ renaming = null
82
+ if (to === '' || to === from) return
83
+ if (!isValidSheetName(to)) {
84
+ error = `"${to}" is not a valid sheet name`
85
+ return
86
+ }
87
+ if (!workbook.renameSheet(from, to)) {
88
+ error = `a sheet named "${to}" already exists`
89
+ return
90
+ }
91
+ error = null
92
+ onRename?.(from, to)
93
+ changed()
94
+ }
95
+
96
+ function add() {
97
+ workbook.addSheet()
98
+ changed()
99
+ }
100
+
101
+ function remove(name: string) {
102
+ // The workbook refuses to remove the last sheet; reflect that rather than
103
+ // showing a button that does nothing.
104
+ if (!workbook.removeSheet(name)) return
105
+ onRemove?.(name)
106
+ changed()
107
+ }
108
+
109
+ // ---- Excel's right-click menu on a tab ------------------------------------
110
+ let menu = $state<{ name: string; x: number; y: number } | null>(null)
111
+
112
+ function openMenu(event: MouseEvent, name: string) {
113
+ if (!editable) return
114
+ event.preventDefault()
115
+ select(name)
116
+ menu = { name, x: event.clientX, y: event.clientY }
117
+ }
118
+ /**
119
+ * The tabs sit at the foot of the sheet, so a menu dropped from the click
120
+ * runs off the bottom of the window; it opens upward from the click, as
121
+ * Excel's does, and is pulled back inside at the right edge.
122
+ */
123
+ function keepMenuInView(node: HTMLElement, _at: { x: number; y: number } | null) {
124
+ const fit = () => {
125
+ const box = node.getBoundingClientRect()
126
+ if (!menu) return
127
+ const x = Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8))
128
+ const y = menu.y + box.height > window.innerHeight - 8 ? Math.max(8, menu.y - box.height) : menu.y
129
+ if (x !== menu.x || y !== menu.y) menu = { ...menu, x, y }
130
+ }
131
+ fit()
132
+ return { update: fit }
133
+ }
134
+
135
+ function moveBy(name: string, delta: -1 | 1) {
136
+ const at = workbook.sheets.indexOf(name)
137
+ const to = at + delta
138
+ if (at < 0 || to < 0 || to >= workbook.sheets.length) return
139
+ workbook.moveSheet(name, to)
140
+ changed()
141
+ }
142
+
143
+ function insertBefore(name: string) {
144
+ const at = workbook.sheets.indexOf(name)
145
+ const added = workbook.addSheet(undefined, Math.max(at, 0))
146
+ workbook.setActive(added)
147
+ changed()
148
+ }
149
+
150
+ // ---- the scroll arrows, for more tabs than fit --------------------------
151
+ // Excel greys each arrow until it can move something: with every tab in
152
+ // view both are grey, and at either end of a long strip the arrow that
153
+ // points off it is. Read from the strip's scroll box after every scroll,
154
+ // resize and tab change.
155
+ let strip = $state<HTMLDivElement | null>(null)
156
+ let canLeft = $state(false)
157
+ let canRight = $state(false)
158
+ function measureStrip() {
159
+ const el = strip
160
+ if (!el) { canLeft = false; canRight = false; return }
161
+ canLeft = el.scrollLeft > 1
162
+ canRight = el.scrollLeft + el.clientWidth < el.scrollWidth - 1
163
+ }
164
+ $effect(() => {
165
+ const el = strip
166
+ if (!el) return
167
+ void version
168
+ void sheets
169
+ measureStrip()
170
+ const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measureStrip)
171
+ ro?.observe(el)
172
+ el.addEventListener('scroll', measureStrip)
173
+ return () => { ro?.disconnect(); el.removeEventListener('scroll', measureStrip) }
174
+ })
175
+ function scrollTabs(delta: number) {
176
+ strip?.scrollBy({ left: delta, behavior: 'smooth' })
177
+ }
178
+
179
+ function onDrop(target: string) {
180
+ const moved = dragging
181
+ dragging = null
182
+ if (!moved || moved === target) return
183
+ const to = workbook.sheets.indexOf(target)
184
+ if (to < 0) return
185
+ workbook.moveSheet(moved, to)
186
+ changed()
187
+ }
188
+
189
+ /** Left and right arrows move between tabs, which is what a tablist owes a
190
+ * keyboard user; the shortcut layer's Ctrl+PageUp/Down does the same from
191
+ * anywhere in the grid. */
192
+ function onTabKey(event: KeyboardEvent, name: string) {
193
+ const at = sheets.indexOf(name)
194
+ if (event.key === 'ArrowRight' && at < sheets.length - 1) {
195
+ event.preventDefault()
196
+ select(sheets[at + 1]!)
197
+ } else if (event.key === 'ArrowLeft' && at > 0) {
198
+ event.preventDefault()
199
+ select(sheets[at - 1]!)
200
+ } else if (event.key === 'F2') {
201
+ event.preventDefault()
202
+ startRename(name)
203
+ }
204
+ }
205
+ </script>
206
+
207
+ <svelte:window
208
+ onpointerdown={(e) => { if (menu && !(e.target as HTMLElement).closest('.sheet-menu')) menu = null }}
209
+ onkeydown={(e) => { if (e.key === 'Escape') menu = null }}
210
+ />
211
+
212
+ <div class="sv-sheet-tabs">
213
+ <!-- Excel's tab-scrolling arrows: always drawn, greyed until they can move something. -->
214
+ <div class="nav">
215
+ <button type="button" class="nav-btn" tabindex="-1" aria-label="Scroll tabs left" disabled={!canLeft} onclick={() => scrollTabs(-120)}>
216
+ <svg viewBox="0 0 8 8" width="8" height="8"><path d="M5.5 1L2.5 4l3 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" /></svg>
217
+ </button>
218
+ <button type="button" class="nav-btn" tabindex="-1" aria-label="Scroll tabs right" disabled={!canRight} onclick={() => scrollTabs(120)}>
219
+ <svg viewBox="0 0 8 8" width="8" height="8"><path d="M2.5 1l3 3-3 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" /></svg>
220
+ </button>
221
+ </div>
222
+
223
+ <div role="tablist" aria-label="Sheets" class="tabs" bind:this={strip}>
224
+ {#each sheets as name (name)}
225
+ {@const isActive = name === activeSheet}
226
+ <!-- role="presentation" so the tablist still OWNS the role="tab"
227
+ button: an unmarked wrapper div between them breaks the ARIA
228
+ tablist / tab relationship. -->
229
+ <div
230
+ role="presentation"
231
+ class="tab"
232
+ class:active={isActive}
233
+ class:dragging={dragging === name}
234
+ draggable={editable}
235
+ ondragstart={() => (dragging = name)}
236
+ ondragover={(e) => e.preventDefault()}
237
+ ondrop={() => onDrop(name)}
238
+ ondragend={() => (dragging = null)}
239
+ oncontextmenu={(e) => openMenu(e, name)}
240
+ >
241
+ {#if renaming === name}
242
+ <!-- svelte-ignore a11y_autofocus -->
243
+ <input
244
+ class="rename"
245
+ aria-label="Sheet name"
246
+ autofocus
247
+ value={draft}
248
+ oninput={(e) => (draft = e.currentTarget.value)}
249
+ onblur={commitRename}
250
+ onkeydown={(e) => {
251
+ if (e.key === 'Enter') { e.preventDefault(); commitRename() }
252
+ if (e.key === 'Escape') { e.preventDefault(); renaming = null }
253
+ }}
254
+ />
255
+ {:else}
256
+ <button
257
+ type="button"
258
+ role="tab"
259
+ aria-selected={isActive}
260
+ tabindex={isActive ? 0 : -1}
261
+ onclick={() => select(name)}
262
+ ondblclick={() => startRename(name)}
263
+ onkeydown={(e) => onTabKey(e, name)}
264
+ >{name}</button>
265
+ {/if}
266
+ </div>
267
+ {/each}
268
+ </div>
269
+
270
+ {#if editable}
271
+ <button type="button" class="add" aria-label="New sheet" title="New sheet (Shift+F11)" onclick={add}>
272
+ <svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><path d="M5 1.5v7M1.5 5h7" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" /></svg>
273
+ </button>
274
+ {/if}
275
+
276
+ {#if error}
277
+ <span class="error" role="alert">{error}</span>
278
+ {/if}
279
+ </div>
280
+
281
+ {#if menu}
282
+ <!--
283
+ The sheet menu Excel opens on right-click. Delete lives here and nowhere
284
+ on the tab face, so a stray click cannot remove a sheet.
285
+ -->
286
+ <div class="sheet-menu" role="menu" aria-label={`${menu.name} sheet`} style:left={`${menu.x}px`} style:top={`${menu.y}px`} use:keepMenuInView={menu}>
287
+ <button type="button" role="menuitem" onclick={() => { insertBefore(menu!.name); menu = null }}>Insert...</button>
288
+ <button type="button" role="menuitem" disabled={sheets.length < 2} onclick={() => { remove(menu!.name); menu = null }}>Delete</button>
289
+ <button type="button" role="menuitem" onclick={() => { startRename(menu!.name); menu = null }}>Rename</button>
290
+ <div class="sep" role="separator"></div>
291
+ <button type="button" role="menuitem" disabled={sheets.indexOf(menu.name) === 0} onclick={() => { moveBy(menu!.name, -1); menu = null }}>Move Left</button>
292
+ <button type="button" role="menuitem" disabled={sheets.indexOf(menu.name) === sheets.length - 1} onclick={() => { moveBy(menu!.name, 1); menu = null }}>Move Right</button>
293
+ </div>
294
+ {/if}
295
+
296
+ <style>
297
+ .sv-sheet-tabs {
298
+ display: flex;
299
+ align-items: stretch;
300
+ height: 26px;
301
+ font-family: var(--sg-font, "Segoe UI", system-ui, sans-serif);
302
+ font-size: 12px;
303
+ background: var(--sg-header-bg, #f3f3f3);
304
+ border-top: 1px solid var(--sg-border, #d1d1d1);
305
+ color: var(--sg-fg, #242424);
306
+ }
307
+
308
+ .nav {
309
+ display: flex;
310
+ align-items: center;
311
+ padding: 0 4px 0 6px;
312
+ gap: 2px;
313
+ color: var(--sg-muted, #616161);
314
+ }
315
+ .nav-btn {
316
+ display: inline-flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ width: 14px;
320
+ height: 18px;
321
+ padding: 0;
322
+ border: 0;
323
+ border-radius: 2px;
324
+ background: transparent;
325
+ color: inherit;
326
+ cursor: pointer;
327
+ }
328
+ .nav-btn:hover:not(:disabled) { background: var(--sg-row-hover-bg, #e6e6e6); color: var(--sg-fg, #242424); }
329
+ .nav-btn:disabled { opacity: 0.35; cursor: default; }
330
+
331
+ .tabs {
332
+ display: flex;
333
+ align-items: stretch;
334
+ overflow-x: auto;
335
+ scrollbar-width: none;
336
+ }
337
+ .tabs::-webkit-scrollbar { display: none; }
338
+
339
+ /* Excel's tabs: grey strip, the active one lifted to white with the accent
340
+ drawn as a line along its bottom edge and its name in the accent. */
341
+ .tab {
342
+ position: relative;
343
+ display: flex;
344
+ align-items: stretch;
345
+ min-width: 64px;
346
+ border-right: 1px solid var(--sg-border, #d1d1d1);
347
+ }
348
+ .tab:first-child { border-left: 1px solid var(--sg-border, #d1d1d1); }
349
+ .tab.active {
350
+ background: var(--sg-bg, #fff);
351
+ box-shadow: inset 0 -2px 0 0 var(--sg-accent, #107c41);
352
+ }
353
+ .tab.active button {
354
+ color: var(--sg-accent, #107c41);
355
+ font-weight: 600;
356
+ }
357
+ .tab:not(.active):hover { background: var(--sg-row-hover-bg, #e9e9e9); }
358
+ .tab.dragging { opacity: 0.5; }
359
+ button[role='tab'] {
360
+ flex: 1 1 auto;
361
+ font: inherit;
362
+ border: 0;
363
+ background: transparent;
364
+ color: var(--sg-muted, #444);
365
+ padding: 0 12px;
366
+ cursor: pointer;
367
+ white-space: nowrap;
368
+ text-align: center;
369
+ }
370
+ button[role='tab']:focus-visible {
371
+ outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #107c41));
372
+ outline-offset: -2px;
373
+ }
374
+
375
+ .add {
376
+ align-self: center;
377
+ display: inline-flex;
378
+ align-items: center;
379
+ justify-content: center;
380
+ width: 18px;
381
+ height: 18px;
382
+ margin-left: 6px;
383
+ padding: 0;
384
+ border: 1px solid var(--sg-border, #c8c6c4);
385
+ border-radius: 50%;
386
+ background: var(--sg-bg, #fff);
387
+ color: var(--sg-muted, #616161);
388
+ cursor: pointer;
389
+ }
390
+ .add:hover { color: var(--sg-fg, #242424); border-color: var(--sg-muted, #8a8886); }
391
+ .add:focus-visible {
392
+ outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #107c41));
393
+ outline-offset: 1px;
394
+ }
395
+
396
+ .rename {
397
+ font: inherit;
398
+ width: 92px;
399
+ margin: 2px 4px;
400
+ border: 1px solid var(--sg-accent, #107c41);
401
+ border-radius: 2px;
402
+ padding: 1px 4px;
403
+ background: var(--sg-input-bg, var(--sg-bg, #fff));
404
+ color: var(--sg-fg, #242424);
405
+ }
406
+ .error {
407
+ align-self: center;
408
+ margin-left: 8px;
409
+ color: var(--sg-danger, #dc2626);
410
+ font-size: 12px;
411
+ }
412
+
413
+ .sheet-menu {
414
+ position: fixed;
415
+ z-index: 1000;
416
+ min-width: 150px;
417
+ padding: 4px;
418
+ font-family: var(--sg-font, "Segoe UI", system-ui, sans-serif);
419
+ font-size: 12px;
420
+ background: var(--sg-bg, #fff);
421
+ color: var(--sg-fg, #242424);
422
+ border: 1px solid var(--sg-border, #d1d1d1);
423
+ border-radius: 4px;
424
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.14);
425
+ }
426
+ .sheet-menu button {
427
+ display: block;
428
+ width: 100%;
429
+ padding: 5px 10px;
430
+ font: inherit;
431
+ text-align: left;
432
+ border: 0;
433
+ border-radius: 3px;
434
+ background: transparent;
435
+ color: inherit;
436
+ cursor: pointer;
437
+ }
438
+ .sheet-menu button:hover:not(:disabled) { background: var(--sg-row-hover-bg, #f0f0f0); }
439
+ .sheet-menu button:disabled { opacity: 0.4; cursor: default; }
440
+ .sheet-menu .sep { height: 1px; margin: 4px 6px; background: var(--sg-border, #e0e0e0); }
441
+ </style>
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The sheet tab strip along the bottom of a workbook.
3
+ *
4
+ * Owns no workbook state of its own: it renders what the workbook reports
5
+ * and calls back, so the keyboard shortcuts and the tabs cannot disagree
6
+ * about which sheet is active.
7
+ */
8
+ import type { Workbook } from './sheet/workbook.js';
9
+ type Props = {
10
+ workbook: Workbook;
11
+ /** Called after any change, so the consumer can re-render. */
12
+ onChange?: () => void;
13
+ /**
14
+ * Called after a sheet is renamed or removed, before `onChange`. Anything
15
+ * the consumer keeps per sheet and keys by name - the shell keeps a
16
+ * format store per sheet - has to follow the rename or drop the entry,
17
+ * and the workbook itself has no event for either.
18
+ */
19
+ onRename?: (from: string, to: string) => void;
20
+ onRemove?: (name: string) => void;
21
+ /** Off hides the add button and the context actions. */
22
+ editable?: boolean;
23
+ /**
24
+ * Bump this whenever the workbook is mutated from OUTSIDE the strip - the
25
+ * Ctrl+PageUp/PageDown shortcuts, or app code calling `addSheet`.
26
+ *
27
+ * A `Workbook` is a plain object, not `$state`, so reading `workbook.sheets`
28
+ * creates no reactive dependency and the strip would otherwise render once
29
+ * and then show whatever the sheet list was at mount. Pass the same counter
30
+ * `onChange` increments.
31
+ */
32
+ version?: number;
33
+ };
34
+ declare const SvSheetTabs: import("svelte").Component<Props, {}, "">;
35
+ type SvSheetTabs = ReturnType<typeof SvSheetTabs>;
36
+ export default SvSheetTabs;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Test-only harness for SvSheetTabs.
3
+ *
4
+ * `mount()` takes a plain props object, and a plain object's fields are not
5
+ * reactive, so a test cannot change a prop after mounting and see the
6
+ * component respond. This box holds the props in `$state` so it can - which is
7
+ * what proves the strip follows an OUTSIDE workbook mutation (Ctrl+PageUp and
8
+ * friends) rather than only its own button clicks.
9
+ *
10
+ * Runes only compile in `.svelte.ts`, hence the separate file. The `.test.`
11
+ * segment keeps it out of the published tarball (tools/strip-dist-tests.mjs)
12
+ * and out of vitest's `*.dom.test.ts` include.
13
+ */
14
+ export declare function reactiveProps<T extends Record<string, unknown>>(initial: T): {
15
+ props: T;
16
+ set(patch: Partial<T>): void;
17
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Test-only harness for SvSheetTabs.
3
+ *
4
+ * `mount()` takes a plain props object, and a plain object's fields are not
5
+ * reactive, so a test cannot change a prop after mounting and see the
6
+ * component respond. This box holds the props in `$state` so it can - which is
7
+ * what proves the strip follows an OUTSIDE workbook mutation (Ctrl+PageUp and
8
+ * friends) rather than only its own button clicks.
9
+ *
10
+ * Runes only compile in `.svelte.ts`, hence the separate file. The `.test.`
11
+ * segment keeps it out of the published tarball (tools/strip-dist-tests.mjs)
12
+ * and out of vitest's `*.dom.test.ts` include.
13
+ */
14
+ export function reactiveProps(initial) {
15
+ const box = $state({ ...initial });
16
+ return {
17
+ props: box,
18
+ set(patch) {
19
+ Object.assign(box, patch);
20
+ },
21
+ };
22
+ }