@svgrid/enterprise 3.0.2 → 3.0.3

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 (364) hide show
  1. package/LICENSE +94 -24
  2. package/README.md +36 -4
  3. package/dist/SvAdvancedFilter.svelte +1 -1
  4. package/dist/SvAlertsManager.svelte +2 -1
  5. package/dist/SvBoard.svelte +2 -1
  6. package/dist/SvExportMenu.svelte +4 -1
  7. package/dist/SvExportMenu.svelte.d.ts +0 -13
  8. package/dist/SvExpressionEditor.svelte +3 -3
  9. package/dist/SvFormulaBar.svelte +94 -20
  10. package/dist/SvFormulaBar.svelte.d.ts +7 -0
  11. package/dist/SvGridAlerts.svelte +49 -9
  12. package/dist/SvGridBoard.svelte +13 -8
  13. package/dist/SvGridBulkEditDrawer.svelte +26 -2
  14. package/dist/SvGridEditPanel.svelte +3 -0
  15. package/dist/SvGridScheduler.svelte +69 -40
  16. package/dist/SvGridSelectionBar.svelte +10 -3
  17. package/dist/SvPivotDesigner.svelte +198 -40
  18. package/dist/SvPivotDesigner.svelte.d.ts +27 -20
  19. package/dist/SvSheet.svelte +4043 -243
  20. package/dist/SvSheet.svelte.d.ts +76 -3
  21. package/dist/SvSheetChartSetup.svelte +151 -0
  22. package/dist/SvSheetChartSetup.svelte.d.ts +18 -0
  23. package/dist/SvSheetComment.svelte +216 -31
  24. package/dist/SvSheetComment.svelte.d.ts +11 -11
  25. package/dist/SvSheetConditionalFormat.svelte +28 -31
  26. package/dist/SvSheetDataValidation.svelte +66 -42
  27. package/dist/SvSheetEditRanges.svelte +217 -0
  28. package/dist/SvSheetEditRanges.svelte.d.ts +15 -0
  29. package/dist/SvSheetErrors.svelte +125 -0
  30. package/dist/SvSheetErrors.svelte.d.ts +20 -0
  31. package/dist/SvSheetEvaluate.svelte +133 -0
  32. package/dist/SvSheetEvaluate.svelte.d.ts +15 -0
  33. package/dist/SvSheetFilterMenu.svelte +137 -34
  34. package/dist/SvSheetFilterMenu.svelte.d.ts +5 -0
  35. package/dist/SvSheetFindReplace.svelte +19 -19
  36. package/dist/SvSheetFormatCells.svelte +125 -81
  37. package/dist/SvSheetGoalSeek.svelte +17 -16
  38. package/dist/SvSheetInsertFunction.svelte +15 -13
  39. package/dist/SvSheetIteration.svelte +81 -0
  40. package/dist/SvSheetIteration.svelte.d.ts +10 -0
  41. package/dist/SvSheetLink.svelte +99 -0
  42. package/dist/SvSheetLink.svelte.d.ts +17 -0
  43. package/dist/SvSheetListPicker.svelte +4 -2
  44. package/dist/SvSheetListPicker.svelte.d.ts +0 -6
  45. package/dist/SvSheetManageRules.svelte +20 -18
  46. package/dist/SvSheetNameManager.svelte +17 -15
  47. package/dist/SvSheetPageSetup.svelte +159 -0
  48. package/dist/SvSheetPageSetup.svelte.d.ts +13 -0
  49. package/dist/SvSheetPasteSpecial.svelte +22 -20
  50. package/dist/SvSheetPivot.svelte +220 -0
  51. package/dist/SvSheetPivot.svelte.d.ts +21 -0
  52. package/dist/SvSheetProtectSheet.svelte +81 -0
  53. package/dist/SvSheetProtectSheet.svelte.d.ts +11 -0
  54. package/dist/SvSheetRemoveDuplicates.svelte +13 -10
  55. package/dist/SvSheetRibbon.svelte +104 -51
  56. package/dist/SvSheetSizeDialog.svelte +4 -2
  57. package/dist/SvSheetSort.svelte +218 -0
  58. package/dist/SvSheetSort.svelte.d.ts +28 -0
  59. package/dist/SvSheetSparklines.svelte +135 -0
  60. package/dist/SvSheetSparklines.svelte.d.ts +14 -0
  61. package/dist/SvSheetTable.svelte +160 -0
  62. package/dist/SvSheetTable.svelte.d.ts +24 -0
  63. package/dist/SvSheetTabs.svelte +161 -31
  64. package/dist/SvSheetTabs.svelte.d.ts +22 -8
  65. package/dist/SvSheetTextToColumns.svelte +20 -20
  66. package/dist/SvSheetValidationAlert.svelte +8 -6
  67. package/dist/advanced-filter-enable.js +1 -1
  68. package/dist/alerts/alert-engine.d.ts +6 -0
  69. package/dist/alerts/alert-engine.js +21 -4
  70. package/dist/alerts/alert-formats.js +2 -0
  71. package/dist/alerts/alert-types.d.ts +1 -1
  72. package/dist/alerts.d.ts +1 -1
  73. package/dist/alerts.js +2 -2
  74. package/dist/board.js +2 -2
  75. package/dist/bulk-edit.d.ts +3 -1
  76. package/dist/bulk-edit.js +1 -1
  77. package/dist/cdn/svgrid-enterprise.svelte-external.js +39827 -16429
  78. package/dist/designer/assets/GridMenus-8gTNssgV.js +8 -0
  79. package/dist/designer/assets/{SvChartMenu-D9hEeHar.js → SvChartMenu-BDoeqGz6.js} +1 -1
  80. package/dist/designer/assets/{SvDateTimePicker-DARvKUL2.js → SvDateTimePicker-DCPg7WGN.js} +1 -1
  81. package/dist/designer/assets/SvGridCellEditor-DMoWJjEI.js +4 -0
  82. package/dist/designer/assets/{SvGridChart-B8j1SSy3.js → SvGridChart-Cy1ATrWh.js} +2 -2
  83. package/dist/designer/assets/SvGridChartBuilder-eQ3mn0L9.js +1 -0
  84. package/dist/designer/assets/{SvGridChartPanel-pT6ITc2P.js → SvGridChartPanel-CATdVc9X.js} +3 -3
  85. package/dist/designer/assets/{SvGridChartView-bg281UA1.js → SvGridChartView-CmsZtCED.js} +1 -1
  86. package/dist/designer/assets/SvGridDropdown-Cjz0H-fI.js +1 -0
  87. package/dist/designer/assets/{SvTabs-UFCMTHXS.js → SvTabs-2mHC6oaz.js} +1 -1
  88. package/dist/designer/assets/cell-formatting-BbubJTVz.js +1 -0
  89. package/dist/designer/assets/{chart-cIPUsbM5.js → chart-Cn7MnNqA.js} +1 -1
  90. package/dist/designer/assets/{chart-panel-messages-DtTxpBUU.js → chart-panel-messages-Dz7PTIqT.js} +1 -1
  91. package/dist/designer/assets/{chart-summary-Czu_5epb.js → chart-summary-BD1qYB2R.js} +1 -1
  92. package/dist/designer/assets/{chart-trend-D5I5VEe8.js → chart-trend-B8xr5LuD.js} +1 -1
  93. package/dist/designer/assets/{dismissable-BO547MH9.js → dismissable-ChWRPkoa.js} +1 -1
  94. package/dist/designer/assets/{export-format-D-eP4b2l.js → export-format-DGckmgKr.js} +1 -1
  95. package/dist/designer/assets/{focus-trap-CAreAhlU.js → focus-trap-V2-6Fy9I.js} +1 -1
  96. package/dist/designer/assets/{index-DwU7CAMm.js → index-D5bVbuzI.js} +317 -315
  97. package/dist/designer/assets/index-_JVFxt02.css +1 -0
  98. package/dist/designer/assets/row-resize-BInFWCpr.js +1 -0
  99. package/dist/designer/assets/server-block-cache-CXJKkJpy.js +1 -0
  100. package/dist/designer/assets/src-DUQ4xE3x.js +58 -0
  101. package/dist/designer/assets/validate-C3B8QKsM.js +1 -0
  102. package/dist/designer/index.html +17 -16
  103. package/dist/export-pdf.d.ts +75 -0
  104. package/dist/export-pdf.js +216 -15
  105. package/dist/export.js +10 -2
  106. package/dist/gantt/SvGridGantt.svelte +2890 -0
  107. package/dist/gantt/SvGridGantt.svelte.d.ts +30 -0
  108. package/dist/gantt/gantt-config.d.ts +73 -0
  109. package/dist/gantt/gantt-config.js +1 -0
  110. package/dist/gantt/gantt-critical-path.d.ts +67 -0
  111. package/dist/gantt/gantt-critical-path.js +207 -0
  112. package/dist/gantt/gantt-model.d.ts +257 -0
  113. package/dist/gantt/gantt-model.js +651 -0
  114. package/dist/gantt/gantt-resources.d.ts +59 -0
  115. package/dist/gantt/gantt-resources.js +71 -0
  116. package/dist/gantt/gantt.d.ts +7 -0
  117. package/dist/gantt/gantt.js +36 -0
  118. package/dist/gantt/index.d.ts +15 -0
  119. package/dist/gantt/index.js +22 -0
  120. package/dist/gantt/timeline-arrows.d.ts +102 -0
  121. package/dist/gantt/timeline-arrows.js +181 -0
  122. package/dist/import.d.ts +2 -2
  123. package/dist/import.js +1 -1
  124. package/dist/index.d.ts +34 -7
  125. package/dist/index.js +44 -7
  126. package/dist/install.js +8 -1
  127. package/dist/license-core.d.ts +17 -1
  128. package/dist/license-core.js +16 -5
  129. package/dist/license.d.ts +18 -2
  130. package/dist/license.js +71 -6
  131. package/dist/node/studio.js +140 -8
  132. package/dist/pivot-enable.js +2 -2
  133. package/dist/scheduler-dependencies.d.ts +35 -0
  134. package/dist/scheduler-dependencies.js +36 -1
  135. package/dist/scheduler.js +2 -2
  136. package/dist/selection-bar.js +2 -2
  137. package/dist/server/SvGroupCell.svelte +213 -0
  138. package/dist/server/SvGroupCell.svelte.d.ts +62 -0
  139. package/dist/server/SvRowGroupPanel.svelte +322 -0
  140. package/dist/server/SvRowGroupPanel.svelte.d.ts +37 -0
  141. package/dist/server/enable.d.ts +17 -0
  142. package/dist/server/enable.js +49 -0
  143. package/dist/server/index.d.ts +22 -0
  144. package/dist/server/index.js +24 -0
  145. package/dist/server/messages.d.ts +45 -0
  146. package/dist/server/messages.js +35 -0
  147. package/dist/server/server-group-model.d.ts +104 -0
  148. package/dist/server/server-group-model.js +272 -0
  149. package/dist/server/server-pivot.d.ts +46 -0
  150. package/dist/server/server-pivot.js +57 -0
  151. package/dist/server/server-row-model.d.ts +547 -0
  152. package/dist/server/server-row-model.js +1551 -0
  153. package/dist/server/server-selection.d.ts +99 -0
  154. package/dist/server/server-selection.js +215 -0
  155. package/dist/server/svgrid-adapter.d.ts +155 -0
  156. package/dist/server/svgrid-adapter.js +202 -0
  157. package/dist/sheet/ast.d.ts +45 -1
  158. package/dist/sheet/ast.js +14 -0
  159. package/dist/sheet/auto-filter.d.ts +58 -4
  160. package/dist/sheet/auto-filter.js +159 -11
  161. package/dist/sheet/autocomplete.d.ts +9 -0
  162. package/dist/sheet/autocomplete.js +90 -4
  163. package/dist/sheet/cell-images.d.ts +39 -0
  164. package/dist/sheet/cell-images.js +90 -0
  165. package/dist/sheet/cell-types.d.ts +58 -0
  166. package/dist/sheet/cell-types.js +96 -0
  167. package/dist/sheet/coerce.d.ts +40 -0
  168. package/dist/sheet/coerce.js +170 -3
  169. package/dist/sheet/comments.d.ts +43 -9
  170. package/dist/sheet/comments.js +85 -27
  171. package/dist/sheet/conditional-formats.d.ts +29 -6
  172. package/dist/sheet/conditional-formats.js +37 -12
  173. package/dist/sheet/csv.d.ts +32 -0
  174. package/dist/sheet/csv.js +158 -0
  175. package/dist/sheet/culture.d.ts +87 -0
  176. package/dist/sheet/culture.js +247 -0
  177. package/dist/sheet/delta.d.ts +102 -0
  178. package/dist/sheet/delta.js +203 -0
  179. package/dist/sheet/deps.d.ts +45 -1
  180. package/dist/sheet/deps.js +77 -2
  181. package/dist/sheet/document.d.ts +106 -4
  182. package/dist/sheet/document.js +204 -3
  183. package/dist/sheet/engine.d.ts +67 -0
  184. package/dist/sheet/engine.js +19 -0
  185. package/dist/sheet/entry.d.ts +8 -4
  186. package/dist/sheet/entry.js +171 -21
  187. package/dist/sheet/error-check.d.ts +49 -0
  188. package/dist/sheet/error-check.js +83 -0
  189. package/dist/sheet/evaluate-steps.d.ts +48 -0
  190. package/dist/sheet/evaluate-steps.js +196 -0
  191. package/dist/sheet/evaluate.d.ts +67 -0
  192. package/dist/sheet/evaluate.js +725 -13
  193. package/dist/sheet/filter-files.d.ts +37 -0
  194. package/dist/sheet/filter-files.js +465 -0
  195. package/dist/sheet/format-store.d.ts +15 -0
  196. package/dist/sheet/format-store.js +11 -1
  197. package/dist/sheet/from-rows.d.ts +14 -0
  198. package/dist/sheet/from-rows.js +50 -0
  199. package/dist/sheet/function-catalog.d.ts +1 -1
  200. package/dist/sheet/function-catalog.js +296 -5
  201. package/dist/sheet/functions.js +363 -91
  202. package/dist/sheet/index.d.ts +35 -8
  203. package/dist/sheet/index.js +35 -8
  204. package/dist/sheet/links.d.ts +57 -0
  205. package/dist/sheet/links.js +174 -0
  206. package/dist/sheet/messages.d.ts +778 -0
  207. package/dist/sheet/messages.js +806 -0
  208. package/dist/sheet/number-format.d.ts +52 -5
  209. package/dist/sheet/number-format.js +465 -68
  210. package/dist/sheet/objects.d.ts +106 -0
  211. package/dist/sheet/objects.js +149 -0
  212. package/dist/sheet/ods-document.d.ts +44 -0
  213. package/dist/sheet/ods-document.js +1275 -0
  214. package/dist/sheet/open-file.d.ts +26 -0
  215. package/dist/sheet/open-file.js +56 -0
  216. package/dist/sheet/outline.d.ts +82 -0
  217. package/dist/sheet/outline.js +228 -0
  218. package/dist/sheet/packs/array.d.ts +16 -0
  219. package/dist/sheet/packs/array.js +172 -0
  220. package/dist/sheet/packs/database.d.ts +42 -0
  221. package/dist/sheet/packs/database.js +142 -0
  222. package/dist/sheet/packs/engineering.d.ts +2 -0
  223. package/dist/sheet/packs/engineering.js +296 -0
  224. package/dist/sheet/packs/financial.d.ts +5 -0
  225. package/dist/sheet/packs/financial.js +220 -0
  226. package/dist/sheet/packs/math-stats.d.ts +2 -0
  227. package/dist/sheet/packs/math-stats.js +372 -0
  228. package/dist/sheet/packs/special.d.ts +44 -0
  229. package/dist/sheet/packs/special.js +187 -0
  230. package/dist/sheet/packs/stats-dist.d.ts +2 -0
  231. package/dist/sheet/packs/stats-dist.js +785 -0
  232. package/dist/sheet/packs/text-date.d.ts +2 -0
  233. package/dist/sheet/packs/text-date.js +332 -0
  234. package/dist/sheet/packs/trig.d.ts +2 -0
  235. package/dist/sheet/packs/trig.js +73 -0
  236. package/dist/sheet/page-setup.d.ts +73 -0
  237. package/dist/sheet/page-setup.js +73 -0
  238. package/dist/sheet/parse.js +42 -10
  239. package/dist/sheet/paste-special.d.ts +22 -0
  240. package/dist/sheet/paste-special.js +14 -5
  241. package/dist/sheet/pivot-range.d.ts +160 -0
  242. package/dist/sheet/pivot-range.js +333 -0
  243. package/dist/sheet/presence.d.ts +73 -0
  244. package/dist/sheet/presence.js +79 -0
  245. package/dist/sheet/print.d.ts +104 -0
  246. package/dist/sheet/print.js +169 -0
  247. package/dist/sheet/protection.d.ts +55 -1
  248. package/dist/sheet/protection.js +55 -2
  249. package/dist/sheet/rects.d.ts +1 -1
  250. package/dist/sheet/refs.d.ts +48 -0
  251. package/dist/sheet/refs.js +155 -8
  252. package/dist/sheet/ribbon-icons.d.ts +1 -1
  253. package/dist/sheet/ribbon-icons.js +186 -0
  254. package/dist/sheet/ribbon.d.ts +1 -1
  255. package/dist/sheet/ribbon.js +324 -23
  256. package/dist/sheet/sheet-kinds.d.ts +74 -0
  257. package/dist/sheet/sheet-kinds.js +91 -0
  258. package/dist/sheet/shortcuts.d.ts +13 -3
  259. package/dist/sheet/shortcuts.js +63 -12
  260. package/dist/sheet/sort.d.ts +51 -0
  261. package/dist/sheet/sort.js +107 -0
  262. package/dist/sheet/sparklines.d.ts +111 -0
  263. package/dist/sheet/sparklines.js +204 -0
  264. package/dist/sheet/table-styles.d.ts +74 -0
  265. package/dist/sheet/table-styles.js +152 -0
  266. package/dist/sheet/tables.d.ts +19 -0
  267. package/dist/sheet/tables.js +31 -0
  268. package/dist/sheet/tokenize.d.ts +18 -1
  269. package/dist/sheet/tokenize.js +60 -9
  270. package/dist/sheet/validation.d.ts +16 -0
  271. package/dist/sheet/validation.js +37 -0
  272. package/dist/sheet/workbook.d.ts +148 -3
  273. package/dist/sheet/workbook.js +742 -31
  274. package/dist/sheet/xls-cfb.d.ts +32 -0
  275. package/dist/sheet/xls-cfb.js +290 -0
  276. package/dist/sheet/xls-document.d.ts +10 -0
  277. package/dist/sheet/xls-document.js +1243 -0
  278. package/dist/sheet/xls-formula.d.ts +40 -0
  279. package/dist/sheet/xls-formula.js +569 -0
  280. package/dist/sheet/xlsx-document.d.ts +108 -0
  281. package/dist/sheet/xlsx-document.js +1849 -0
  282. package/dist/sheet/xlsx-drawing.d.ts +99 -0
  283. package/dist/sheet/xlsx-drawing.js +420 -0
  284. package/dist/sheet-enable.js +6 -7
  285. package/dist/sheet-text.d.ts +7 -0
  286. package/dist/sheet-text.js +29 -0
  287. package/dist/sources/auth-supabase.js +2 -2
  288. package/dist/sources/realtime-supabase.js +2 -2
  289. package/dist/sources/rest.d.ts +6 -2
  290. package/dist/sources/rest.js +42 -7
  291. package/dist/sources/supabase.d.ts +10 -1
  292. package/dist/sources/supabase.js +73 -40
  293. package/dist/studio/emit-project.js +14 -1
  294. package/dist/studio/index.js +1 -1
  295. package/dist/studio/project.d.ts +11 -2
  296. package/dist/studio/project.js +5 -2
  297. package/dist/studio/ui-components.generated.js +107 -2
  298. package/dist/sveltekit/in-memory.d.ts +2 -2
  299. package/dist/sveltekit/in-memory.js +125 -4
  300. package/dist/sveltekit/index.d.ts +1 -1
  301. package/dist/sveltekit/index.js +1 -1
  302. package/dist/sveltekit/query-plan.d.ts +50 -4
  303. package/dist/sveltekit/query-plan.js +25 -3
  304. package/dist/sveltekit/sql-source.d.ts +6 -0
  305. package/dist/sveltekit/sql-source.js +34 -6
  306. package/dist/sveltekit/sql.d.ts +31 -0
  307. package/dist/sveltekit/sql.js +135 -25
  308. package/dist/sveltekit/transport.d.ts +10 -2
  309. package/dist/sveltekit/transport.js +30 -6
  310. package/dist/upgrade-prompt.d.ts +1 -1
  311. package/dist/version.d.ts +1 -1
  312. package/dist/version.js +1 -1
  313. package/dist/watermark.js +35 -2
  314. package/dist/wc/GridMenus-By9vPuq0.js +1088 -0
  315. package/dist/wc/SvChartMenu-CQNrQGT2.js +308 -0
  316. package/dist/wc/SvDateRangeInput-DR6m7bSM.js +199 -0
  317. package/dist/wc/SvDateTimePicker-D-vRnaMo.js +771 -0
  318. package/dist/wc/SvField-B_kurx_P.js +149 -0
  319. package/dist/wc/SvGrid.helpers-BdVDtjUm.js +135 -0
  320. package/dist/wc/SvGridCellEditor-Bnf1Kr5C.js +560 -0
  321. package/dist/wc/SvGridChart-BqUJh2pG.js +3572 -0
  322. package/dist/wc/SvGridChartBuilder-CGycC5cn.js +988 -0
  323. package/dist/wc/SvGridChartPanel-CRnizsHD.js +1746 -0
  324. package/dist/wc/SvGridChartView-D1pa_xMy.js +47 -0
  325. package/dist/wc/SvGridDropdown-45WsObLL.js +259 -0
  326. package/dist/wc/SvModal-DrfOYYyX.js +189 -0
  327. package/dist/wc/cell-formatting-CBfgahv1.js +86 -0
  328. package/dist/wc/chart-6OXlybVT.js +4341 -0
  329. package/dist/wc/chart-export-CeSADGfi.js +135 -0
  330. package/dist/wc/chart-export-pdf-BGko7MER.js +104 -0
  331. package/dist/wc/chart-summary-2m3ia1-n.js +137 -0
  332. package/dist/wc/chart-trend-CQOQtTMl.js +50 -0
  333. package/dist/wc/chart-validate-CD6ptiwg.js +180 -0
  334. package/dist/wc/client-DQIzjDBG.js +3064 -0
  335. package/dist/wc/column-resize-DUPgynvc.js +101 -0
  336. package/dist/wc/date-format-DS-z_h9v.js +1130 -0
  337. package/dist/wc/dismissable-CQ6XFXsk.js +45 -0
  338. package/dist/wc/editor-contract-peBDxir_.js +22 -0
  339. package/dist/wc/editor-registry-DxGKqcvx.js +24 -0
  340. package/dist/wc/focus-trap-Dbb2tJxb.js +65 -0
  341. package/dist/wc/list-option-DrRyivWp.js +82 -0
  342. package/dist/wc/popover-WsGlvtQv.js +69 -0
  343. package/dist/wc/positioning-BiKLfu1E.js +112 -0
  344. package/dist/wc/react.d.ts +41 -0
  345. package/dist/wc/react.js +90 -0
  346. package/dist/wc/rolldown-runtime-Dy4uBu1J.js +11 -0
  347. package/dist/wc/row-drag-touch-CycRR65n.js +93 -0
  348. package/dist/wc/row-resize-limG_N1z.js +111 -0
  349. package/dist/wc/sv-sheet-element.d.ts +97 -0
  350. package/dist/wc/sv-sheet-element.js +44213 -0
  351. package/dist/wc/svelte-virtualizer.svelte-CXtQy2TQ.js +530 -0
  352. package/dist/wc/validate-BqDthkNr.js +88 -0
  353. package/dist/wc/vue.d.ts +31 -0
  354. package/dist/wc/vue.js +75 -0
  355. package/package.json +30 -5
  356. package/dist/designer/assets/GridMenus-BlsvL_aU.js +0 -8
  357. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +0 -4
  358. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +0 -1
  359. package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +0 -1
  360. package/dist/designer/assets/cell-formatting-wr5wm7US.js +0 -1
  361. package/dist/designer/assets/index-CMFgAoVV.css +0 -1
  362. package/dist/designer/assets/row-resize-DLktcqUK.js +0 -1
  363. package/dist/designer/assets/src-EdI3Xqds.js +0 -58
  364. package/dist/designer/assets/validate-h-aslRV9.js +0 -1
@@ -36,6 +36,7 @@
36
36
  * `sheet/ribbon.ts`, which calls the same function the matching keystroke
37
37
  * does, so a button and its shortcut cannot drift apart.
38
38
  */
39
+ import { tick } from 'svelte'
39
40
  import { SvPopover } from '@svgrid/grid'
40
41
  import type { GridCommandContext } from '@svgrid/grid/shortcuts'
41
42
  import SvRibbonIcon from './SvRibbonIcon.svelte'
@@ -44,6 +45,8 @@
44
45
  type RibbonTab, type RibbonGroup, type RibbonItem, type RibbonOption, type RibbonActionId,
45
46
  } from './sheet/ribbon'
46
47
  import { THEME_COLOURS, STANDARD_COLOURS } from './sheet/palette'
48
+ import { useSheetText } from './sheet-text'
49
+ import { defaultSheetMessages } from './sheet/messages'
47
50
 
48
51
  type Props = {
49
52
  /**
@@ -81,7 +84,8 @@
81
84
  onAction,
82
85
  activeActions = [],
83
86
  tabs = RIBBON_TABS,
84
- tab = $bindable(tabs[0]?.id ?? 'home'),
87
+ // Excel opens on Home, not on File, whatever tab comes first.
88
+ tab = $bindable(tabs.some((t) => t.id === 'home') ? 'home' : tabs[0]?.id ?? 'home'),
85
89
  without = [],
86
90
  collapsed = $bindable(false),
87
91
  }: Props = $props()
@@ -183,8 +187,20 @@
183
187
  }
184
188
 
185
189
  /** Excel puts the shortcut after the name in the tooltip. */
190
+ // Every string the ribbon prints goes through the shell's map, keyed
191
+ // off the model, so a translation covers a button the model gains later.
192
+ const t = useSheetText()
193
+ const label = (item: RibbonItem): string => t(`ribbon.${item.id}.label`)
194
+ const title = (item: RibbonItem): string => t(`ribbon.${item.id}.title`)
195
+ /** An option's text, unless its key belongs to an earlier option with the
196
+ * same value (two palette colours on one hex): that one keeps its own label. */
197
+ const optionLabel = (item: RibbonItem, option: RibbonOption): string => {
198
+ const key = `ribbon.${item.id}.option.${option.value}`
199
+ return defaultSheetMessages[key] === option.label ? t(key) : option.label
200
+ }
201
+ const groupLabel = (group: RibbonGroup): string => t(`ribbon.group.${group.id}`)
186
202
  function tip(item: RibbonItem): string {
187
- return item.keys ? `${item.title} (${item.keys})` : item.title
203
+ return item.keys ? `${title(item)} (${item.keys})` : title(item)
188
204
  }
189
205
 
190
206
  /**
@@ -350,11 +366,31 @@
350
366
  if (first) themeFont = first
351
367
  })
352
368
 
369
+ /**
370
+ * The tab strip's keyboard model, as WAI-ARIA asks for a tablist: the arrows
371
+ * move between tabs, Home and End jump to the ends, and FOCUS FOLLOWS the
372
+ * selection - without that last part a screen reader announces the old tab
373
+ * while the band under it has changed. The arrows are mirrored on a
374
+ * right-to-left ribbon, where the next tab is to the left.
375
+ */
353
376
  function onTabKey(event: KeyboardEvent) {
354
377
  const ids = tabs.map((t) => t.id)
355
378
  const at = ids.indexOf(tab)
356
- if (event.key === 'ArrowRight' && at < ids.length - 1) { event.preventDefault(); tab = ids[at + 1]! }
357
- if (event.key === 'ArrowLeft' && at > 0) { event.preventDefault(); tab = ids[at - 1]! }
379
+ const strip = (event.currentTarget as HTMLElement).parentElement
380
+ const rtl = strip ? getComputedStyle(strip).direction === 'rtl' : false
381
+ const forward = rtl ? 'ArrowLeft' : 'ArrowRight'
382
+ const back = rtl ? 'ArrowRight' : 'ArrowLeft'
383
+ let next = -1
384
+ if (event.key === forward && at < ids.length - 1) next = at + 1
385
+ else if (event.key === back && at > 0) next = at - 1
386
+ else if (event.key === 'Home') next = 0
387
+ else if (event.key === 'End') next = ids.length - 1
388
+ if (next < 0 || next === at) return
389
+ event.preventDefault()
390
+ tab = ids[next]!
391
+ // After the strip re-renders, the selected tab is the one Tab reaches, so
392
+ // it is the one that should hold focus.
393
+ void tick().then(() => strip?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus())
358
394
  }
359
395
  </script>
360
396
 
@@ -374,17 +410,17 @@
374
410
  class:on={state.on}
375
411
  disabled={!state.enabled}
376
412
  title={tip(item)}
377
- aria-label={item.title}
413
+ aria-label={title(item)}
378
414
  aria-pressed={item.kind === 'toggle' ? state.on : undefined}
379
415
  onclick={() => run(item)}
380
416
  >
381
417
  {#if item.icon}
382
418
  <SvRibbonIcon name={item.icon} size={item.size === 'large' ? 32 : 16} />
383
419
  {:else}
384
- <span class="glyph" data-glyph={item.id}>{item.label}</span>
420
+ <span class="glyph" data-glyph={item.id}>{label(item)}</span>
385
421
  {/if}
386
422
  {#if item.size === 'large' || item.wide}
387
- <span class="label">{item.label}</span>
423
+ <span class="label">{label(item)}</span>
388
424
  {/if}
389
425
  </button>
390
426
  {/snippet}
@@ -400,38 +436,38 @@
400
436
  class:has-bar={item.palette}
401
437
  disabled={!state.enabled}
402
438
  title={tip(item)}
403
- aria-label={item.title}
439
+ aria-label={title(item)}
404
440
  onclick={() => run(item, faceValue(item))}
405
441
  >
406
442
  {#if item.icon && !(item.palette && item.id === 'text-colour')}
407
443
  <SvRibbonIcon name={item.icon} />
408
444
  {:else}
409
- <span class="glyph" data-glyph={item.id}>{item.label}</span>
445
+ <span class="glyph" data-glyph={item.id}>{label(item)}</span>
410
446
  {/if}
411
447
  {#if item.palette}
412
448
  <span class="bar" class:none={colour === null} style:background={colour ?? 'transparent'}></span>
413
449
  {/if}
414
450
  </button>
415
451
  {#if live}
416
- <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={item.title}>
452
+ <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={title(item)}>
417
453
  {#snippet anchor()}
418
454
  <button
419
455
  type="button"
420
456
  class="btn arrow"
421
457
  disabled={!state.enabled}
422
- aria-label={`${item.title} options`}
458
+ aria-label={t('menuOptions', { title: title(item) })}
423
459
  aria-haspopup="menu"
424
460
  aria-expanded={menuOpen[item.id] ?? false}
425
461
  ><SvRibbonIcon name="chevron-down" size={10} /></button>
426
462
  {/snippet}
427
463
  {#if item.palette}
428
- <div class="palette" role="menu" aria-label={item.title}>
464
+ <div class="palette" role="menu" aria-label={title(item)}>
429
465
  {#if item.none}
430
466
  <button type="button" class="palette-none" role="menuitem" onclick={() => pick(item, item.none!.value)}>
431
- <span class="swatch no-colour" aria-hidden="true"></span>{item.none.label}
467
+ <span class="swatch no-colour" aria-hidden="true"></span>{t(`ribbon.${item.id}.none`)}
432
468
  </button>
433
469
  {/if}
434
- <div class="palette-title">Theme Colors</div>
470
+ <div class="palette-title">{t('paletteThemeColors')}</div>
435
471
  <div class="swatches">
436
472
  {#each THEME_COLOURS as row, r (r)}
437
473
  {#each row as c (c.value + r)}
@@ -439,7 +475,7 @@
439
475
  {/each}
440
476
  {/each}
441
477
  </div>
442
- <div class="palette-title">Standard Colors</div>
478
+ <div class="palette-title">{t('paletteStandardColors')}</div>
443
479
  <div class="swatches">
444
480
  {#each STANDARD_COLOURS as c (c.value)}
445
481
  <button type="button" class="swatch" role="menuitem" title={c.label} aria-label={c.label} style:background={c.value} onclick={() => pick(item, c.value)}></button>
@@ -447,11 +483,11 @@
447
483
  </div>
448
484
  </div>
449
485
  {:else}
450
- <div class="menu" role="menu" aria-label={item.title}>
486
+ <div class="menu" role="menu" aria-label={title(item)}>
451
487
  {#each item.options ?? [] as option (option.value)}
452
488
  <button type="button" class="menu-item" role="menuitem" onclick={() => pick(item, option.value)}>
453
489
  {#if option.icon}<SvRibbonIcon name={option.icon} />{/if}
454
- <span>{option.label}</span>
490
+ <span>{optionLabel(item, option)}</span>
455
491
  </button>
456
492
  {/each}
457
493
  </div>
@@ -474,38 +510,38 @@
474
510
  class:large={item.size === 'large'}
475
511
  disabled={!state.enabled}
476
512
  title={tip(item)}
477
- aria-label={item.title}
513
+ aria-label={title(item)}
478
514
  aria-haspopup="menu"
479
515
  aria-expanded={live ? (menuOpen[item.id] ?? false) : false}
480
516
  >
481
517
  {#if item.icon}
482
518
  <SvRibbonIcon name={item.icon} size={item.size === 'large' ? 32 : 16} />
483
519
  {:else}
484
- <span class="glyph" data-glyph={item.id}>{item.label}</span>
520
+ <span class="glyph" data-glyph={item.id}>{label(item)}</span>
485
521
  {/if}
486
522
  {#if item.size === 'large' || item.wide}
487
- <span class="label">{item.label}</span>
523
+ <span class="label">{label(item)}</span>
488
524
  {/if}
489
525
  <SvRibbonIcon name="chevron-down" size={8} />
490
526
  </button>
491
527
  {/snippet}
492
528
  {#snippet entries()}
493
- <div class="menu" role="menu" aria-label={item.title}>
529
+ <div class="menu" role="menu" aria-label={title(item)}>
494
530
  {#each item.options ?? [] as option (option.value)}
495
531
  {#if option.heading}
496
- <div class="menu-heading" role="presentation">{option.label}</div>
532
+ <div class="menu-heading" role="presentation">{optionLabel(item, option)}</div>
497
533
  {:else}
498
534
  {#if option.toggle && option.emits}
499
535
  {@const on = live && activeActions.includes(option.emits)}
500
536
  <button type="button" class="menu-item" class:on role="menuitemcheckbox" aria-checked={on} onclick={() => pickOption(item, option)}>
501
537
  {#if option.icon}<SvRibbonIcon name={option.icon} />{/if}
502
- <span>{option.label}</span>
538
+ <span>{optionLabel(item, option)}</span>
503
539
  {#if option.keys}<span class="keys">{option.keys}</span>{/if}
504
540
  </button>
505
541
  {:else}
506
542
  <button type="button" class="menu-item" role="menuitem" onclick={() => pickOption(item, option)}>
507
543
  {#if option.icon}<SvRibbonIcon name={option.icon} />{/if}
508
- <span>{option.label}</span>
544
+ <span>{optionLabel(item, option)}</span>
509
545
  {#if option.keys}<span class="keys">{option.keys}</span>{/if}
510
546
  </button>
511
547
  {/if}
@@ -524,38 +560,38 @@
524
560
  class:large={item.size === 'large'}
525
561
  disabled={!state.enabled}
526
562
  title={tip(item)}
527
- aria-label={item.title}
563
+ aria-label={title(item)}
528
564
  onclick={() => run(item)}
529
565
  >
530
566
  {#if item.icon}
531
567
  <SvRibbonIcon name={item.icon} size={item.size === 'large' ? 32 : 16} />
532
568
  {:else}
533
- <span class="glyph" data-glyph={item.id}>{item.label}</span>
569
+ <span class="glyph" data-glyph={item.id}>{label(item)}</span>
534
570
  {/if}
535
571
  <!-- A small wide split (Merge & Center) prints its label on the face, as
536
572
  Excel does; the colour splits stay icon-only. -->
537
- {#if item.wide && item.size !== 'large'}<span class="label">{item.label}</span>{/if}
573
+ {#if item.wide && item.size !== 'large'}<span class="label">{label(item)}</span>{/if}
538
574
  </button>
539
575
  {#if live}
540
- <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={item.title}>
576
+ <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={title(item)}>
541
577
  {#snippet anchor()}
542
578
  <button
543
579
  type="button"
544
580
  class="btn arrow"
545
581
  disabled={!state.enabled}
546
- aria-label={`${item.title} options`}
582
+ aria-label={t('menuOptions', { title: title(item) })}
547
583
  aria-haspopup="menu"
548
584
  aria-expanded={menuOpen[item.id] ?? false}
549
- >{#if item.size === 'large'}<span class="label">{item.label}</span>{/if}<SvRibbonIcon name="chevron-down" size={10} /></button>
585
+ >{#if item.size === 'large'}<span class="label">{label(item)}</span>{/if}<SvRibbonIcon name="chevron-down" size={10} /></button>
550
586
  {/snippet}
551
587
  {@render entries()}
552
588
  </SvPopover>
553
589
  {:else}
554
- <span class="btn arrow" aria-hidden="true">{#if item.size === 'large'}<span class="label">{item.label}</span>{/if}<SvRibbonIcon name="chevron-down" size={10} /></span>
590
+ <span class="btn arrow" aria-hidden="true">{#if item.size === 'large'}<span class="label">{label(item)}</span>{/if}<SvRibbonIcon name="chevron-down" size={10} /></span>
555
591
  {/if}
556
592
  </div>
557
593
  {:else if live}
558
- <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={item.title}>
594
+ <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={title(item)}>
559
595
  {#snippet anchor()}
560
596
  {@render face()}
561
597
  {/snippet}
@@ -573,7 +609,7 @@
573
609
  class="select"
574
610
  data-select={item.id}
575
611
  title={tip(item)}
576
- aria-label={item.title}
612
+ aria-label={title(item)}
577
613
  disabled={!state.enabled}
578
614
  value={live ? currentOf(item) : ''}
579
615
  onchange={(e) => run(item, e.currentTarget.value)}
@@ -584,7 +620,7 @@
584
620
  <option value={currentOf(item)}>{currentOf(item)}</option>
585
621
  {/if}
586
622
  {#each item.options ?? [] as option (option.value)}
587
- <option value={option.value}>{option.value === '' && item.id === 'font-family' ? themeFont : option.label}</option>
623
+ <option value={option.value}>{option.value === '' && item.id === 'font-family' ? themeFont : optionLabel(item, option)}</option>
588
624
  {/each}
589
625
  </select>
590
626
  {:else if item.kind === 'menu'}
@@ -620,8 +656,8 @@
620
656
  {/if}
621
657
  {/snippet}
622
658
 
623
- <div class="sv-ribbon" class:collapsed role="toolbar" aria-label="Spreadsheet ribbon" aria-orientation="horizontal" bind:this={rootEl}>
624
- <div class="tabs" role="tablist" aria-label="Ribbon tabs">
659
+ <div class="sv-ribbon" class:collapsed role="toolbar" aria-label={t('ribbonLabel')} aria-orientation="horizontal" bind:this={rootEl}>
660
+ <div class="tabs" role="tablist" aria-label={t('ribbonTabs')}>
625
661
  {#each tabs as entry (entry.id)}
626
662
  <button
627
663
  type="button"
@@ -633,11 +669,11 @@
633
669
  onclick={() => onTabClick(entry.id)}
634
670
  ondblclick={toggleCollapsed}
635
671
  onkeydown={onTabKey}
636
- >{entry.label}</button>
672
+ >{t(`ribbon.tab.${entry.id}`)}</button>
637
673
  {/each}
638
674
  <span class="tabs-grow"></span>
639
675
  {#if collapsed && !peek}
640
- <button type="button" class="ribbon-toggle" title="Expand the Ribbon (Ctrl+F1)" aria-label="Expand the Ribbon" aria-expanded="false" onclick={toggleCollapsed}>
676
+ <button type="button" class="ribbon-toggle" title={`${t('ribbonExpand')} (Ctrl+F1)`} aria-label={t('ribbonExpand')} aria-expanded="false" onclick={toggleCollapsed}>
641
677
  <svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><path d="M2 3.5l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
642
678
  </button>
643
679
  {/if}
@@ -646,27 +682,30 @@
646
682
  <div class="band" class:away={collapsed && !peek} class:peek={collapsed && peek} bind:clientWidth={bandWidth}>
647
683
  {#each current?.groups ?? [] as group (group.id)}
648
684
  {#if collapsedGroups.has(group.id)}
649
- <section class="group folded" aria-label={group.label}>
650
- <SvPopover bind:open={groupOpen[group.id]} placement="bottom-start" arrow={false} offset={4} ariaLabel={group.label}>
685
+ <section class="group folded" role="group" aria-label={groupLabel(group)}>
686
+ <SvPopover bind:open={groupOpen[group.id]} placement="bottom-start" arrow={false} offset={4} ariaLabel={groupLabel(group)}>
651
687
  {#snippet anchor()}
652
- <button type="button" class="btn large" aria-haspopup="true" aria-expanded={groupOpen[group.id] ?? false} title={group.label}>
688
+ <button type="button" class="btn large" aria-haspopup="true" aria-expanded={groupOpen[group.id] ?? false} title={groupLabel(group)}>
653
689
  <span class="fold-icon"><SvRibbonIcon name={groupIcon(group)} size={32} /></span>
654
- <span class="label">{group.label} <SvRibbonIcon name="chevron-down" size={10} /></span>
690
+ <span class="label">{groupLabel(group)} <SvRibbonIcon name="chevron-down" size={10} /></span>
655
691
  </button>
656
692
  {/snippet}
657
693
  <div class="popup-group">
658
694
  {@render body(group, true)}
659
- <div class="label-row"><span class="group-label">{group.label}</span></div>
695
+ <div class="label-row"><span class="group-label">{groupLabel(group)}</span></div>
660
696
  </div>
661
697
  </SvPopover>
662
698
  </section>
663
699
  {:else}
664
- <section class="group" class:compact={fit.compact.has(group.id)} aria-label={group.label}>
700
+ <!-- A group of the toolbar, not a landmark: a named <section> is a
701
+ region, and a ribbon of twelve regions (or two ribbons on one
702
+ page) buries whatever landmarks the page really has. -->
703
+ <section class="group" class:compact={fit.compact.has(group.id)} role="group" aria-label={groupLabel(group)}>
665
704
  {@render body(group, true)}
666
705
  <div class="label-row">
667
- <span class="group-label">{group.label}</span>
706
+ <span class="group-label">{groupLabel(group)}</span>
668
707
  {#if group.launcher}
669
- <button type="button" class="launcher" title={`${group.label} settings`} aria-label={`${group.label} settings`} onclick={() => launch(group.launcher!)}>
708
+ <button type="button" class="launcher" title={t('groupSettings', { group: groupLabel(group) })} aria-label={t('groupSettings', { group: groupLabel(group) })} onclick={() => launch(group.launcher!)}>
670
709
  <svg viewBox="0 0 8 8" width="8" height="8" aria-hidden="true"><path d="M1 1h3.5M1 1v3.5M1.5 1.5L6.5 6.5M6.5 3.5V6.5H3.5" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round" /></svg>
671
710
  </button>
672
711
  {/if}
@@ -676,7 +715,7 @@
676
715
  {/each}
677
716
  <!-- Excel's chevron at the band's end: collapse, or pin the peeked band. -->
678
717
  <span class="band-grow"></span>
679
- <button type="button" class="ribbon-toggle in-band" title={collapsed ? 'Pin the Ribbon (Ctrl+F1)' : 'Collapse the Ribbon (Ctrl+F1)'} aria-label={collapsed ? 'Pin the Ribbon' : 'Collapse the Ribbon'} onclick={toggleCollapsed}>
718
+ <button type="button" class="ribbon-toggle in-band" title={`${collapsed ? t('ribbonPin') : t('ribbonCollapse')} (Ctrl+F1)`} aria-label={collapsed ? t('ribbonPin') : t('ribbonCollapse')} onclick={toggleCollapsed}>
680
719
  {#if collapsed}
681
720
  <svg viewBox="0 0 12 12" width="11" height="11" aria-hidden="true"><path d="M4 1.5h4M6 1.5v3.5M3.5 5l2.5 2.5L8.5 5M6 7.5V11" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
682
721
  {:else}
@@ -696,7 +735,7 @@
696
735
  {#each current?.groups ?? [] as group (group.id)}
697
736
  <section class="group" use:measure={group.id}>
698
737
  {@render body(group, false)}
699
- <div class="label-row"><span class="group-label">{group.label}</span></div>
738
+ <div class="label-row"><span class="group-label">{groupLabel(group)}</span></div>
700
739
  </section>
701
740
  {/each}
702
741
  </div>
@@ -704,7 +743,7 @@
704
743
  {#each current?.groups ?? [] as group (group.id)}
705
744
  <section class="group compact" use:measure={`${group.id}:compact`}>
706
745
  {@render body(group, false)}
707
- <div class="label-row"><span class="group-label">{group.label}</span></div>
746
+ <div class="label-row"><span class="group-label">{groupLabel(group)}</span></div>
708
747
  </section>
709
748
  {/each}
710
749
  </div>
@@ -713,7 +752,7 @@
713
752
  <section class="group folded" use:measure={`${group.id}:folded`}>
714
753
  <button type="button" class="btn large" tabindex="-1">
715
754
  <span class="fold-icon"><SvRibbonIcon name={groupIcon(group)} size={32} /></span>
716
- <span class="label">{group.label} <SvRibbonIcon name="chevron-down" size={10} /></span>
755
+ <span class="label">{groupLabel(group)} <SvRibbonIcon name="chevron-down" size={10} /></span>
717
756
  </button>
718
757
  </section>
719
758
  {/each}
@@ -746,8 +785,14 @@
746
785
  gap: 0;
747
786
  height: 30px;
748
787
  padding: 0 8px;
788
+ /* On a phone the eight labels are wider than the screen. They scroll
789
+ sideways rather than shrink into each other, with no bar showing. */
790
+ overflow-x: auto;
791
+ scrollbar-width: none;
749
792
  }
793
+ .tabs::-webkit-scrollbar { display: none; }
750
794
  .tab {
795
+ flex: 0 0 auto;
751
796
  position: relative;
752
797
  font: inherit;
753
798
  border: 0;
@@ -759,7 +804,15 @@
759
804
  border-radius: 4px 4px 0 0;
760
805
  }
761
806
  .tab:hover { background: var(--sg-row-hover-bg, rgba(0, 0, 0, 0.04)); }
762
- .tab.active { color: var(--sg-accent, #107c41); font-weight: 600; }
807
+ /* The label is the accent mixed toward the text colour, not the raw
808
+ accent: a 12px label in a host theme's accent (an orange, a cyan) lands
809
+ under 4.5:1 on the ribbon's near-white band, which axe fails and a
810
+ low-vision reader feels. The underline below keeps the accent pure, so
811
+ the tab still reads as Excel's. */
812
+ .tab.active {
813
+ color: color-mix(in srgb, var(--sg-accent, #107c41) 72%, var(--sg-fg, #242424));
814
+ font-weight: 600;
815
+ }
763
816
  /* Excel's selected-tab mark: a short rounded underline, not a full-width
764
817
  border, inset from the label's edges. */
765
818
  .tab.active::after {
@@ -4,6 +4,7 @@
4
4
  * shell opens it from the column letter's and the row number's menus and
5
5
  * applies the size to every column or row the selection spans.
6
6
  */
7
+ import { useSheetText } from './sheet-text'
7
8
  import { SvModal } from '@svgrid/grid'
8
9
 
9
10
  type Props = {
@@ -16,6 +17,7 @@
16
17
  }
17
18
 
18
19
  let { open = $bindable(false), title, label, value, onApply, onClose }: Props = $props()
20
+ const t = useSheetText()
19
21
 
20
22
  let draft = $state('')
21
23
  let input = $state<HTMLInputElement | null>(null)
@@ -51,8 +53,8 @@
51
53
  </form>
52
54
  {#snippet footer()}
53
55
  <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
+ <button type="button" class="btn primary" onclick={ok} disabled={!valid}>{t('ok')}</button>
57
+ <button type="button" class="btn" onclick={close}>{t('cancel')}</button>
56
58
  </div>
57
59
  {/snippet}
58
60
  </SvModal>
@@ -0,0 +1,218 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Excel's Sort dialog: a level per key, each a column and an order, over
4
+ * the block the shell worked out, with "My data has headers" deciding
5
+ * whether the first row is named in the column lists or sorted with the
6
+ * rest. Add Level, Delete Level, OK; the shell does the sorting, in one
7
+ * undo, from the keys handed back.
8
+ */
9
+ import { useSheetText } from './sheet-text'
10
+ import { untrack } from 'svelte'
11
+ import { SvModal } from '@svgrid/grid'
12
+ import type { Workbook } from './sheet/workbook'
13
+ import type { SortKey } from './sheet/sort'
14
+ import { colToLetters } from './sheet/address'
15
+
16
+ type Block = { top: number; left: number; bottom: number; right: number }
17
+
18
+ type Props = {
19
+ open?: boolean
20
+ workbook: Workbook
21
+ /** The block to sort, and whether its first row looks like headers. */
22
+ block: Block
23
+ headerGuess: boolean
24
+ /** The column the dialog opens on: the active cell's. */
25
+ activeCol: number
26
+ /**
27
+ * A cell's fill or font colour, for a level that sorts on one. Without
28
+ * it the Sort On list offers values only, since there would be no
29
+ * colours to offer.
30
+ */
31
+ colourAt?: (row: number, col: number, on: 'fill' | 'color') => string | null
32
+ onApply: (keys: SortKey[], hasHeaders: boolean) => void
33
+ onClose?: () => void
34
+ }
35
+
36
+ let { open = $bindable(false), workbook, block, headerGuess, activeCol, colourAt, onApply, onClose }: Props = $props()
37
+ const t = useSheetText()
38
+
39
+ let levels = $state<SortKey[]>([])
40
+ let hasHeaders = $state(true)
41
+
42
+ const raw = (r: number, c: number) => workbook.getRaw(workbook.active, r, c)
43
+
44
+ /** The column list: the header text when the block has headers, else the letter. */
45
+ const columns = $derived.by(() =>
46
+ Array.from({ length: block.right - block.left + 1 }, (_, i) => {
47
+ const col = block.left + i
48
+ const letter = t('sort.columnLetter', { letter: colToLetters(col) })
49
+ return { col, label: hasHeaders ? raw(block.top, col) || letter : letter }
50
+ }),
51
+ )
52
+
53
+ $effect(() => {
54
+ if (!open) return
55
+ untrack(() => {
56
+ hasHeaders = headerGuess
57
+ levels = [{ col: Math.min(Math.max(activeCol, block.left), block.right), direction: 'asc', on: 'value' }]
58
+ })
59
+ })
60
+
61
+ /**
62
+ * The colours a level's column carries, each once. Excel offers exactly
63
+ * these, because a colour that is nowhere in the column would sort
64
+ * nothing.
65
+ */
66
+ function coloursIn(col: number, on: 'fill' | 'color'): string[] {
67
+ if (!colourAt) return []
68
+ const seen: string[] = []
69
+ for (let r = block.top + (hasHeaders ? 1 : 0); r <= block.bottom; r += 1) {
70
+ const colour = colourAt(r, col, on)?.trim().toLowerCase()
71
+ if (colour && !seen.includes(colour)) seen.push(colour)
72
+ }
73
+ return seen
74
+ }
75
+
76
+ /** Keep a level's colour one the column actually carries. */
77
+ function onSortOnChange(level: SortKey) {
78
+ if (level.on === 'value') { level.colour = undefined; return }
79
+ const colours = coloursIn(level.col, level.on === 'color' ? 'color' : 'fill')
80
+ if (!level.colour || !colours.includes(level.colour)) level.colour = colours[0]
81
+ }
82
+
83
+ function addLevel() {
84
+ // The next column not yet used, or the first one when all are.
85
+ const used = new Set(levels.map((l) => l.col))
86
+ const free = columns.find((c) => !used.has(c.col)) ?? columns[0]
87
+ if (!free) return
88
+ levels = [...levels, { col: free.col, direction: 'asc', on: 'value' }]
89
+ }
90
+
91
+ function deleteLevel(index: number) {
92
+ if (levels.length <= 1) return
93
+ levels = levels.filter((_, i) => i !== index)
94
+ }
95
+
96
+ function ok() {
97
+ if (!levels.length) return
98
+ onApply(levels.map((l) => ({ ...l })), hasHeaders)
99
+ open = false
100
+ onClose?.()
101
+ }
102
+
103
+ function close() {
104
+ open = false
105
+ onClose?.()
106
+ }
107
+ </script>
108
+
109
+ <SvModal bind:open onClose={onClose} title={t('sort.title')} size="sm" width={460}>
110
+ <form class="sv-sheet-dialog sort" onsubmit={(e) => { e.preventDefault(); ok() }}>
111
+ <div class="toolbar">
112
+ <button type="button" class="btn" onclick={addLevel} disabled={levels.length >= columns.length}>{t('sort.addLevel')}</button>
113
+ <button type="button" class="btn" onclick={() => deleteLevel(levels.length - 1)} disabled={levels.length <= 1}>{t('sort.deleteLevel')}</button>
114
+ <span class="spacer"></span>
115
+ <label class="check"><input type="checkbox" bind:checked={hasHeaders} /> {t('sort.hasHeaders')}</label>
116
+ </div>
117
+ <div class="levels" role="list" aria-label={t('sort.levels')}>
118
+ {#each levels as level, index (index)}
119
+ {@const by = t(index === 0 ? 'sort.sortBy' : 'sort.thenBy')}
120
+ <div class="level" role="listitem">
121
+ <span class="by">{by}</span>
122
+ <select aria-label={t('sort.column', { by })} bind:value={level.col}>
123
+ {#each columns as c (c.col)}<option value={c.col}>{c.label}</option>{/each}
124
+ </select>
125
+ <select
126
+ aria-label={t('sort.on', { by })}
127
+ bind:value={level.on}
128
+ onchange={() => onSortOnChange(level)}
129
+ disabled={!colourAt}
130
+ >
131
+ <option value="value">{t('sort.onValue')}</option>
132
+ <option value="fill">{t('sort.onFill')}</option>
133
+ <option value="color">{t('sort.onFont')}</option>
134
+ </select>
135
+ <select aria-label={t('sort.order', { by })} bind:value={level.direction}>
136
+ <option value="asc">{level.on === 'value' ? t('sort.asc') : t('sort.onTop')}</option>
137
+ <option value="desc">{level.on === 'value' ? t('sort.desc') : t('sort.onBottom')}</option>
138
+ </select>
139
+ {#if level.on !== 'value'}
140
+ {@const colours = coloursIn(level.col, level.on === 'color' ? 'color' : 'fill')}
141
+ <span class="colour-row">
142
+ <span class="by">{t('sort.colour')}</span>
143
+ {#if colours.length}
144
+ <select aria-label={t('sort.colour')} bind:value={level.colour}>
145
+ {#each colours as colour (colour)}<option value={colour}>{colour}</option>{/each}
146
+ </select>
147
+ <span class="swatch" style:background={level.colour} aria-hidden="true"></span>
148
+ {:else}
149
+ <span class="none">{t('sort.noColours')}</span>
150
+ {/if}
151
+ </span>
152
+ {/if}
153
+ </div>
154
+ {/each}
155
+ </div>
156
+ <p class="hint">
157
+ {t('sort.hint', { range: `${colToLetters(block.left)}${block.top + 1}:${colToLetters(block.right)}${block.bottom + 1}` })}
158
+ </p>
159
+ </form>
160
+ {#snippet footer()}
161
+ <div class="sv-sheet-dialog-buttons">
162
+ <button type="button" class="btn primary" onclick={ok} disabled={!levels.length}>{t('ok')}</button>
163
+ <button type="button" class="btn" onclick={close}>{t('cancel')}</button>
164
+ </div>
165
+ {/snippet}
166
+ </SvModal>
167
+
168
+ <style>
169
+ .toolbar {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 6px;
173
+ margin-bottom: 8px;
174
+ }
175
+ .toolbar .spacer { flex: 1; }
176
+ .levels {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: 6px;
180
+ padding: 6px;
181
+ border: 1px solid var(--sg-border, #d1d1d1);
182
+ border-radius: 3px;
183
+ background: var(--sg-bg, #fff);
184
+ }
185
+ .level {
186
+ display: grid;
187
+ grid-template-columns: 64px 1fr 104px 112px;
188
+ align-items: center;
189
+ gap: 8px;
190
+ }
191
+ /* The colour a colour level sorts on, under the level it belongs to. */
192
+ .colour-row {
193
+ grid-column: 1 / -1;
194
+ display: grid;
195
+ grid-template-columns: 64px 1fr auto;
196
+ align-items: center;
197
+ gap: 8px;
198
+ }
199
+ .colour-row .swatch {
200
+ width: 18px;
201
+ height: 18px;
202
+ border: 1px solid var(--sg-border, #d1d1d1);
203
+ border-radius: 3px;
204
+ }
205
+ .colour-row .none { color: var(--sg-muted, #616161); font-size: 12px; }
206
+ .level .by { color: var(--sg-muted, #616161); }
207
+ .level select {
208
+ min-width: 0;
209
+ font: inherit;
210
+ color: inherit;
211
+ background: var(--sg-bg, #fff);
212
+ border: 1px solid var(--sg-border, #d1d1d1);
213
+ border-radius: 2px;
214
+ padding: 2px 4px;
215
+ }
216
+ .check { display: inline-flex; align-items: center; gap: 6px; }
217
+ .hint { margin: 8px 0 0; color: var(--sg-muted, #616161); font-size: 12px; }
218
+ </style>