@svgrid/enterprise 3.0.0 → 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 (390) hide show
  1. package/README.md +95 -95
  2. package/dist/ExpressionEditorHarness.svelte.d.ts +2 -2
  3. package/dist/SvAdvancedFilter.svelte.d.ts +2 -2
  4. package/dist/SvAlertRuleEditor.svelte.d.ts +2 -2
  5. package/dist/SvAlertsManager.svelte.d.ts +3 -3
  6. package/dist/SvAlertsPanel.svelte.d.ts +1 -1
  7. package/dist/SvAuthGate.svelte +115 -115
  8. package/dist/SvAuthGate.svelte.d.ts +1 -1
  9. package/dist/SvBoard.svelte.d.ts +1 -1
  10. package/dist/SvExportMenu.svelte +553 -553
  11. package/dist/SvExportMenu.svelte.d.ts +2 -2
  12. package/dist/SvExpressionEditor.svelte.d.ts +2 -2
  13. package/dist/SvFileInput.svelte +113 -113
  14. package/dist/SvFormulaBar.svelte +606 -0
  15. package/dist/SvFormulaBar.svelte.d.ts +62 -0
  16. package/dist/SvGridAlerts.svelte.d.ts +2 -2
  17. package/dist/SvGridBoard.svelte +2358 -2358
  18. package/dist/SvGridBulkEditDrawer.svelte.d.ts +1 -1
  19. package/dist/SvGridEditPanel.svelte +1157 -1157
  20. package/dist/SvGridEditPanel.svelte.d.ts +3 -3
  21. package/dist/SvGridMasterDetail.svelte +116 -116
  22. package/dist/SvGridMasterDetail.svelte.d.ts +1 -1
  23. package/dist/SvGridScheduler.svelte +5358 -5358
  24. package/dist/SvImportDialog.svelte +1020 -1020
  25. package/dist/SvImportDialog.svelte.d.ts +2 -2
  26. package/dist/SvLookupInput.svelte +180 -180
  27. package/dist/SvLookupInput.svelte.d.ts +1 -1
  28. package/dist/SvPivotDesigner.svelte +2041 -1990
  29. package/dist/SvPivotDesigner.svelte.d.ts +2 -2
  30. package/dist/SvRecordDetail.svelte.d.ts +1 -1
  31. package/dist/SvRibbonIcon.svelte +46 -0
  32. package/dist/SvRibbonIcon.svelte.d.ts +17 -0
  33. package/dist/SvSchedule.svelte.d.ts +1 -1
  34. package/dist/SvSchemaChart.svelte +236 -234
  35. package/dist/SvSchemaChart.svelte.d.ts +2 -2
  36. package/dist/SvSchemaDashboard.svelte +130 -130
  37. package/dist/SvSchemaDashboard.svelte.d.ts +3 -3
  38. package/dist/SvSheet.svelte +4114 -0
  39. package/dist/SvSheet.svelte.d.ts +142 -0
  40. package/dist/SvSheetComment.svelte +98 -0
  41. package/dist/SvSheetComment.svelte.d.ts +22 -0
  42. package/dist/SvSheetConditionalFormat.svelte +162 -0
  43. package/dist/SvSheetConditionalFormat.svelte.d.ts +14 -0
  44. package/dist/SvSheetDataValidation.svelte +249 -0
  45. package/dist/SvSheetDataValidation.svelte.d.ts +14 -0
  46. package/dist/SvSheetFilterMenu.svelte +217 -0
  47. package/dist/SvSheetFilterMenu.svelte.d.ts +17 -0
  48. package/dist/SvSheetFindReplace.svelte +132 -0
  49. package/dist/SvSheetFindReplace.svelte.d.ts +10 -0
  50. package/dist/SvSheetFormatCells.svelte +421 -0
  51. package/dist/SvSheetFormatCells.svelte.d.ts +17 -0
  52. package/dist/SvSheetGoalSeek.svelte +144 -0
  53. package/dist/SvSheetGoalSeek.svelte.d.ts +12 -0
  54. package/dist/SvSheetInsertFunction.svelte +147 -0
  55. package/dist/SvSheetInsertFunction.svelte.d.ts +8 -0
  56. package/dist/SvSheetListPicker.svelte +89 -0
  57. package/dist/SvSheetListPicker.svelte.d.ts +16 -0
  58. package/dist/SvSheetManageRules.svelte +185 -0
  59. package/dist/SvSheetManageRules.svelte.d.ts +16 -0
  60. package/dist/SvSheetNameManager.svelte +225 -0
  61. package/dist/SvSheetNameManager.svelte.d.ts +11 -0
  62. package/dist/SvSheetPasteSpecial.svelte +105 -0
  63. package/dist/SvSheetPasteSpecial.svelte.d.ts +12 -0
  64. package/dist/SvSheetRemoveDuplicates.svelte +149 -0
  65. package/dist/SvSheetRemoveDuplicates.svelte.d.ts +12 -0
  66. package/dist/SvSheetRibbon.svelte +1201 -0
  67. package/dist/SvSheetRibbon.svelte.d.ts +34 -0
  68. package/dist/SvSheetSizeDialog.svelte +58 -0
  69. package/dist/SvSheetSizeDialog.svelte.d.ts +11 -0
  70. package/dist/SvSheetTabs.svelte +441 -0
  71. package/dist/SvSheetTabs.svelte.d.ts +36 -0
  72. package/dist/SvSheetTabs.test-harness.svelte.d.ts +17 -0
  73. package/dist/SvSheetTabs.test-harness.svelte.js +22 -0
  74. package/dist/SvSheetTextToColumns.svelte +172 -0
  75. package/dist/SvSheetTextToColumns.svelte.d.ts +13 -0
  76. package/dist/SvSheetValidationAlert.svelte +83 -0
  77. package/dist/SvSheetValidationAlert.svelte.d.ts +15 -0
  78. package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -1
  79. package/dist/advanced-filter-enable.js +3 -3
  80. package/dist/alerts/alert-engine-attach.d.ts +2 -2
  81. package/dist/alerts/alert-engine-attach.js +3 -3
  82. package/dist/alerts/alert-engine.d.ts +1 -1
  83. package/dist/alerts/alert-engine.js +2 -2
  84. package/dist/alerts/alert-formats.d.ts +1 -1
  85. package/dist/alerts/alert-formats.js +1 -1
  86. package/dist/alerts/alert-observer.d.ts +3 -3
  87. package/dist/alerts/alert-observer.js +1 -1
  88. package/dist/alerts/alert-storage.d.ts +1 -1
  89. package/dist/alerts/alert-store.svelte.d.ts +1 -1
  90. package/dist/alerts/alert-types.d.ts +2 -2
  91. package/dist/alerts.js +2 -2
  92. package/dist/board.js +2 -2
  93. package/dist/cdn/svgrid-enterprise.svelte-external.js +19046 -5428
  94. package/dist/designer/assets/GridMenus-BlsvL_aU.js +8 -0
  95. package/dist/designer/assets/SvChartMenu-BYO9cYdy.css +1 -0
  96. package/dist/designer/assets/SvChartMenu-D9hEeHar.js +1 -0
  97. package/dist/designer/assets/SvDateRangeInput-BBRzrQeg.js +1 -0
  98. package/dist/designer/assets/SvDateRangeInput-C0iCCZ8o.css +1 -0
  99. package/dist/designer/assets/SvDateTimePicker-DARvKUL2.js +1 -0
  100. package/dist/designer/assets/SvDateTimePicker-DPv09DIT.css +1 -0
  101. package/dist/designer/assets/{preload-helper-CG0n22O7.js → SvGrid.helpers-BgT-gnCo.js} +1 -1
  102. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +4 -0
  103. package/dist/designer/assets/SvGridChart-B8j1SSy3.js +3 -0
  104. package/dist/designer/assets/SvGridChart-BgLuPO7I.css +1 -0
  105. package/dist/designer/assets/SvGridChartBuilder-08YWWeqY.css +1 -0
  106. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +1 -0
  107. package/dist/designer/assets/SvGridChartPanel-4Pc8y5yO.css +1 -0
  108. package/dist/designer/assets/SvGridChartPanel-pT6ITc2P.js +4 -0
  109. package/dist/designer/assets/SvGridChartView-7msfnWZT.css +1 -0
  110. package/dist/designer/assets/SvGridChartView-bg281UA1.js +1 -0
  111. package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +1 -0
  112. package/dist/designer/assets/SvGridDropdown-CcidB3rm.css +1 -0
  113. package/dist/designer/assets/SvListBox-BDS66lmZ.js +110 -0
  114. package/dist/designer/assets/SvListBox-BXfyqy4a.css +1 -0
  115. package/dist/designer/assets/SvMenuList-DJ47ba1h.css +1 -0
  116. package/dist/designer/assets/SvMenuList-Dgq2D5VN.js +1 -0
  117. package/dist/designer/assets/SvTabs-DKmwb5p1.css +1 -0
  118. package/dist/designer/assets/SvTabs-UFCMTHXS.js +1 -0
  119. package/dist/designer/assets/cell-formatting-wr5wm7US.js +1 -0
  120. package/dist/designer/assets/chart-cIPUsbM5.js +1 -0
  121. package/dist/designer/assets/chart-export-B6bkQ3X8.js +11 -0
  122. package/dist/designer/assets/chart-export-pdf-BbA28-gt.js +11 -0
  123. package/dist/designer/assets/chart-panel-messages-DtTxpBUU.js +1 -0
  124. package/dist/designer/assets/chart-summary-Czu_5epb.js +1 -0
  125. package/dist/designer/assets/chart-trend-D5I5VEe8.js +1 -0
  126. package/dist/designer/assets/chart-validate-BMqM-qkF.js +1 -0
  127. package/dist/designer/assets/column-resize-BYDzR1Ve.js +1 -0
  128. package/dist/designer/assets/date-format-B8JbEtIG.js +1 -0
  129. package/dist/designer/assets/date-format-DKS-z0xS.css +1 -0
  130. package/dist/designer/assets/editor-contract-Bpoa7A5g.js +1 -0
  131. package/dist/designer/assets/export-format-D-eP4b2l.js +6 -0
  132. package/dist/designer/assets/focus-trap-CAreAhlU.js +1 -0
  133. package/dist/designer/assets/index-CMFgAoVV.css +1 -0
  134. package/dist/designer/assets/{index-zUBix-JQ.js → index-DwU7CAMm.js} +2985 -141
  135. package/dist/designer/assets/list-option-BP_dWS66.js +1 -0
  136. package/dist/designer/assets/preload-helper-HclGiUj8.js +1 -0
  137. package/dist/designer/assets/row-resize-DLktcqUK.js +1 -0
  138. package/dist/designer/assets/src-CITpcm6b.js +1 -0
  139. package/dist/designer/assets/src-EdI3Xqds.js +58 -0
  140. package/dist/designer/index.html +32 -23
  141. package/dist/export-conditional.d.ts +1 -1
  142. package/dist/export-pdf.d.ts +89 -0
  143. package/dist/export-pdf.js +130 -0
  144. package/dist/export-print.js +19 -19
  145. package/dist/export-xls.d.ts +2 -2
  146. package/dist/export.d.ts +2 -2
  147. package/dist/export.js +23 -16
  148. package/dist/expressions/builder-tree.d.ts +2 -2
  149. package/dist/expressions/builder-tree.js +1 -1
  150. package/dist/expressions/compile.d.ts +1 -1
  151. package/dist/import.js +1 -1
  152. package/dist/index.d.ts +116 -53
  153. package/dist/index.js +117 -50
  154. package/dist/install.d.ts +4 -4
  155. package/dist/install.js +16 -11
  156. package/dist/license.d.ts +3 -2
  157. package/dist/license.js +9 -4
  158. package/dist/node/studio.js +187 -116
  159. package/dist/pivot-chart.d.ts +26 -2
  160. package/dist/pivot-chart.js +25 -69
  161. package/dist/pivot-designer.d.ts +1 -1
  162. package/dist/pivot-enable.js +3 -3
  163. package/dist/print.d.ts +1 -1
  164. package/dist/print.js +4 -4
  165. package/dist/scheduler-config.d.ts +5 -5
  166. package/dist/scheduler-freebusy.d.ts +1 -1
  167. package/dist/scheduler-freebusy.js +1 -1
  168. package/dist/scheduler.js +2 -2
  169. package/dist/schema-designer.d.ts +1 -1
  170. package/dist/schema-designer.js +2 -2
  171. package/dist/selection-bar.js +2 -2
  172. package/dist/sheet/address.d.ts +43 -0
  173. package/dist/sheet/address.js +81 -0
  174. package/dist/sheet/ast.d.ts +94 -0
  175. package/dist/sheet/ast.js +56 -0
  176. package/dist/sheet/auto-filter.d.ts +66 -0
  177. package/dist/sheet/auto-filter.js +147 -0
  178. package/dist/sheet/autocomplete.d.ts +42 -0
  179. package/dist/sheet/autocomplete.js +162 -0
  180. package/dist/sheet/coerce.d.ts +31 -0
  181. package/dist/sheet/coerce.js +122 -0
  182. package/dist/sheet/commands.d.ts +89 -0
  183. package/dist/sheet/commands.js +230 -0
  184. package/dist/sheet/comments.d.ts +29 -0
  185. package/dist/sheet/comments.js +73 -0
  186. package/dist/sheet/conditional-formats.d.ts +144 -0
  187. package/dist/sheet/conditional-formats.js +316 -0
  188. package/dist/sheet/deps.d.ts +60 -0
  189. package/dist/sheet/deps.js +199 -0
  190. package/dist/sheet/document.d.ts +150 -0
  191. package/dist/sheet/document.js +264 -0
  192. package/dist/sheet/edit-keys.d.ts +14 -0
  193. package/dist/sheet/edit-keys.js +40 -0
  194. package/dist/sheet/entry.d.ts +31 -0
  195. package/dist/sheet/entry.js +76 -0
  196. package/dist/sheet/evaluate.d.ts +61 -0
  197. package/dist/sheet/evaluate.js +337 -0
  198. package/dist/sheet/find-replace.d.ts +62 -0
  199. package/dist/sheet/find-replace.js +152 -0
  200. package/dist/sheet/format-store.d.ts +99 -0
  201. package/dist/sheet/format-store.js +233 -0
  202. package/dist/sheet/freeze.d.ts +54 -0
  203. package/dist/sheet/freeze.js +58 -0
  204. package/dist/sheet/function-catalog.d.ts +10 -0
  205. package/dist/sheet/function-catalog.js +94 -0
  206. package/dist/sheet/functions.d.ts +28 -0
  207. package/dist/sheet/functions.js +362 -0
  208. package/dist/sheet/goal-seek.d.ts +73 -0
  209. package/dist/sheet/goal-seek.js +153 -0
  210. package/dist/sheet/index.d.ts +53 -0
  211. package/dist/sheet/index.js +53 -0
  212. package/dist/sheet/merges.d.ts +71 -0
  213. package/dist/sheet/merges.js +137 -0
  214. package/dist/sheet/names.d.ts +31 -0
  215. package/dist/sheet/names.js +87 -0
  216. package/dist/sheet/navigate.d.ts +52 -0
  217. package/dist/sheet/navigate.js +135 -0
  218. package/dist/sheet/number-format.d.ts +63 -0
  219. package/dist/sheet/number-format.js +508 -0
  220. package/dist/sheet/palette.d.ts +19 -0
  221. package/dist/sheet/palette.js +84 -0
  222. package/dist/sheet/parse.d.ts +13 -0
  223. package/dist/sheet/parse.js +108 -0
  224. package/dist/sheet/paste-special.d.ts +164 -0
  225. package/dist/sheet/paste-special.js +592 -0
  226. package/dist/sheet/protection.d.ts +21 -0
  227. package/dist/sheet/protection.js +48 -0
  228. package/dist/sheet/rects.d.ts +45 -0
  229. package/dist/sheet/rects.js +112 -0
  230. package/dist/sheet/refs.d.ts +82 -0
  231. package/dist/sheet/refs.js +266 -0
  232. package/dist/sheet/ribbon-icons.d.ts +31 -0
  233. package/dist/sheet/ribbon-icons.js +350 -0
  234. package/dist/sheet/ribbon.d.ts +173 -0
  235. package/dist/sheet/ribbon.js +753 -0
  236. package/dist/sheet/shortcuts.d.ts +164 -0
  237. package/dist/sheet/shortcuts.js +623 -0
  238. package/dist/sheet/structure.d.ts +87 -0
  239. package/dist/sheet/structure.js +166 -0
  240. package/dist/sheet/tables.d.ts +70 -0
  241. package/dist/sheet/tables.js +147 -0
  242. package/dist/sheet/tokenize.d.ts +54 -0
  243. package/dist/sheet/tokenize.js +304 -0
  244. package/dist/sheet/transforms.d.ts +76 -0
  245. package/dist/sheet/transforms.js +154 -0
  246. package/dist/sheet/validation.d.ts +92 -0
  247. package/dist/sheet/validation.js +241 -0
  248. package/dist/sheet/workbook.d.ts +72 -0
  249. package/dist/sheet/workbook.js +409 -0
  250. package/dist/sheet-enable.d.ts +6 -0
  251. package/dist/sheet-enable.js +46 -0
  252. package/dist/sources/aggregate.d.ts +1 -1
  253. package/dist/sources/auth-supabase.js +1 -1
  254. package/dist/sources/dashboard.d.ts +2 -2
  255. package/dist/sources/dashboard.js +1 -1
  256. package/dist/sources/index.d.ts +13 -13
  257. package/dist/sources/index.js +13 -13
  258. package/dist/sources/realtime-supabase.js +1 -1
  259. package/dist/sources/relation-lookup.d.ts +1 -1
  260. package/dist/sources/relation-lookup.js +1 -1
  261. package/dist/sources/rest-adapters.d.ts +1 -1
  262. package/dist/sources/rest-adapters.js +1 -1
  263. package/dist/sources/rest.d.ts +2 -2
  264. package/dist/sources/rest.js +3 -3
  265. package/dist/sources/supabase.d.ts +2 -2
  266. package/dist/sources/supabase.js +3 -3
  267. package/dist/sources/with-entity-rules.d.ts +1 -1
  268. package/dist/sources/with-entity-rules.js +1 -1
  269. package/dist/sources/with-relation-labels.d.ts +1 -1
  270. package/dist/studio/emit-project.js +2161 -2127
  271. package/dist/studio/emit-schema.js +369 -369
  272. package/dist/studio/index.d.ts +1 -1
  273. package/dist/studio/index.js +1 -1
  274. package/dist/studio/introspect-db.js +45 -45
  275. package/dist/studio/project.d.ts +10 -0
  276. package/dist/studio/samples/crm.js +6 -6
  277. package/dist/studio/samples/fleet.js +7 -7
  278. package/dist/studio/samples/insurance.js +9 -9
  279. package/dist/studio/samples/inventory.js +12 -12
  280. package/dist/studio/samples/projects.js +5 -5
  281. package/dist/studio/samples/support.js +8 -8
  282. package/dist/studio/scaffold-app.js +104 -104
  283. package/dist/studio/scaffold.js +151 -151
  284. package/dist/studio/ui-components.d.ts +7 -0
  285. package/dist/studio/ui-components.generated.js +63 -1
  286. package/dist/studio/ui-components.js +35 -0
  287. package/dist/sveltekit/in-memory.d.ts +3 -3
  288. package/dist/sveltekit/in-memory.js +4 -4
  289. package/dist/sveltekit/index.d.ts +6 -6
  290. package/dist/sveltekit/index.js +5 -5
  291. package/dist/sveltekit/query-plan.d.ts +1 -1
  292. package/dist/sveltekit/sql-source.d.ts +4 -4
  293. package/dist/sveltekit/sql-source.js +4 -4
  294. package/dist/sveltekit/sql.d.ts +1 -1
  295. package/dist/sveltekit/transport.d.ts +2 -2
  296. package/dist/sveltekit/transport.js +1 -1
  297. package/dist/upgrade-prompt.js +25 -25
  298. package/dist/version.d.ts +1 -1
  299. package/dist/version.js +1 -1
  300. package/package.json +12 -4
  301. package/dist/designer/assets/GridMenus-Cgilexpk.js +0 -8
  302. package/dist/designer/assets/SvDateRangeInput-BV5OXYQ2.css +0 -1
  303. package/dist/designer/assets/SvDateRangeInput-CVn-yqrQ.js +0 -1
  304. package/dist/designer/assets/SvDateTimePicker-BdioTrJQ.js +0 -1
  305. package/dist/designer/assets/SvDateTimePicker-CY8TK4BT.css +0 -1
  306. package/dist/designer/assets/SvField-BF-9TJSd.css +0 -1
  307. package/dist/designer/assets/SvField-BsqiA5me.js +0 -1
  308. package/dist/designer/assets/SvGridCellEditor-dLY7_kk9.js +0 -2
  309. package/dist/designer/assets/SvGridChart-BEDNTdc5.js +0 -2
  310. package/dist/designer/assets/SvGridChart-Vu4Nvjiv.css +0 -1
  311. package/dist/designer/assets/SvGridChartPanel-x3lzSxuf.js +0 -10
  312. package/dist/designer/assets/SvGridChartView-CAKhPELi.js +0 -1
  313. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +0 -1
  314. package/dist/designer/assets/SvGridDropdown-CAYG7Y-O.js +0 -1
  315. package/dist/designer/assets/SvGridDropdown-Crwb2P3Q.css +0 -1
  316. package/dist/designer/assets/SvListBox-CVYQcLX9.js +0 -1
  317. package/dist/designer/assets/SvListBox-DgN8kvjt.css +0 -1
  318. package/dist/designer/assets/chart-DHEgt4lN.js +0 -1
  319. package/dist/designer/assets/column-resize-C_inr5zT.js +0 -1
  320. package/dist/designer/assets/date-format-4DJvNtG2.js +0 -1
  321. package/dist/designer/assets/date-format-Dzy4mTGh.css +0 -1
  322. package/dist/designer/assets/easing-zaypKrr0.js +0 -3
  323. package/dist/designer/assets/export-format-DwjuTfrQ.js +0 -6
  324. package/dist/designer/assets/index-Cp7fDJ3b.css +0 -1
  325. package/dist/designer/assets/js-scroller.svelte-Bhxqb3-c.js +0 -110
  326. package/dist/designer/assets/row-resize-B0FWHcNr.js +0 -1
  327. package/dist/designer/assets/src-B7DNcoQK.css +0 -1
  328. package/dist/designer/assets/src-BoYyigAC.js +0 -2896
  329. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +0 -1
  330. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +0 -178
  331. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +0 -3875
  332. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +0 -1
  333. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +0 -21
  334. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +0 -1
  335. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +0 -1938
  336. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +0 -1
  337. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +0 -205
  338. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +0 -1
  339. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +0 -18
  340. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +0 -1
  341. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +0 -264
  342. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +0 -1
  343. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +0 -697
  344. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +0 -1
  345. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +0 -3
  346. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +0 -134
  347. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +0 -1
  348. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +0 -55
  349. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +0 -1
  350. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +0 -2
  351. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +0 -46843
  352. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +0 -1
  353. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +0 -237
  354. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +0 -1
  355. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +0 -2
  356. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +0 -4
  357. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +0 -1
  358. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +0 -7
  359. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +0 -4
  360. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +0 -1
  361. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +0 -5
  362. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +0 -1
  363. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +0 -5
  364. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +0 -1
  365. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +0 -5
  366. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +0 -1
  367. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +0 -6
  368. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +0 -83
  369. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +0 -1
  370. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +0 -559
  371. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +0 -1
  372. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +0 -24
  373. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +0 -1
  374. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +0 -122
  375. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +0 -1
  376. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +0 -2
  377. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +0 -89
  378. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +0 -1
  379. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +0 -241
  380. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +0 -1
  381. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +0 -4938
  382. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +0 -1
  383. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +0 -495
  384. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +0 -1
  385. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +0 -190
  386. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +0 -1
  387. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +0 -8
  388. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +0 -1
  389. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +0 -138
  390. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +0 -1
@@ -0,0 +1,1201 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The ribbon.
4
+ *
5
+ * Renders `RIBBON_TABS` and nothing else: the tab strip, then a band of
6
+ * groups, each a block of controls over a small centred group label.
7
+ * That label is the thing that makes a ribbon read as a ribbon rather than
8
+ * as a toolbar, so it is not optional.
9
+ *
10
+ * The geometry is Excel's. A group is three rows of 24px; a large button
11
+ * (icon over label) takes a whole column of that height and a small one
12
+ * takes a row, so Cut / Copy / Paste Special stack beside a tall Paste. A
13
+ * split button pairs a face that applies the last pick with an arrow that
14
+ * opens the menu, and the colour menus are Excel's picker: the theme row,
15
+ * five tints and shades under each colour, then the standard colours.
16
+ *
17
+ * It is responsive the way Excel's is, in Excel's two steps: when the band
18
+ * is narrower than its groups, groups go compact from the right (small
19
+ * buttons keep the icon and drop the label), and only when every group is
20
+ * compact do groups fold, again from the right, into one large button each
21
+ * that opens the group in a dropdown. Nothing is hidden, it is only folded.
22
+ * The three widths of every group are measured off invisible copies of the
23
+ * band, so a group that starts folded still knows how wide it would like
24
+ * to be.
25
+ *
26
+ * It collapses the way Excel's does: a double-click on a tab, the chevron
27
+ * at the band's end or Ctrl+F1 leaves the tab row alone, a click on a tab
28
+ * then shows the band over the sheet until a command runs, and the pin at
29
+ * its end brings it back for good (`collapsed`, bindable).
30
+ *
31
+ * Painted with the grid's own --sg-* tokens rather than Excel's palette, so
32
+ * it matches whatever theme the app runs. Pick the `excel` theme preset and
33
+ * it reads as Excel; pick a dark one and it stays legible.
34
+ *
35
+ * The ribbon owns no sheet state. Every button calls an action from
36
+ * `sheet/ribbon.ts`, which calls the same function the matching keystroke
37
+ * does, so a button and its shortcut cannot drift apart.
38
+ */
39
+ import { SvPopover } from '@svgrid/grid'
40
+ import type { GridCommandContext } from '@svgrid/grid/shortcuts'
41
+ import SvRibbonIcon from './SvRibbonIcon.svelte'
42
+ import {
43
+ RIBBON_TABS,
44
+ type RibbonTab, type RibbonGroup, type RibbonItem, type RibbonOption, type RibbonActionId,
45
+ } from './sheet/ribbon'
46
+ import { THEME_COLOURS, STANDARD_COLOURS } from './sheet/palette'
47
+
48
+ type Props = {
49
+ /**
50
+ * How to reach the grid. A getter rather than the context itself because
51
+ * the context is rebuilt per call and the ribbon needs a live one at the
52
+ * moment a button is pressed, not the one that existed at mount.
53
+ */
54
+ cmd: () => GridCommandContext | null
55
+ /** Called after any action that changed something, so the host re-renders. */
56
+ onChange?: () => void
57
+ /** Actions the ribbon does not run itself, because they need chrome. */
58
+ onAction?: (action: RibbonActionId, cmd: GridCommandContext) => void
59
+ /** Which of `onAction`'s toggles are currently on. The host owns that
60
+ * state (there is no store here to read it from), so it reports it. */
61
+ activeActions?: ReadonlyArray<RibbonActionId>
62
+ tabs?: ReadonlyArray<RibbonTab>
63
+ /** Start on a tab other than the first. */
64
+ tab?: string
65
+ /**
66
+ * Actions to leave off the ribbon: a button nobody answers is worse
67
+ * than no button. A group with nothing left in it goes too.
68
+ */
69
+ without?: ReadonlyArray<RibbonActionId>
70
+ /**
71
+ * Excel's collapsed ribbon: the tabs alone. A click on a tab shows the
72
+ * band over the sheet until a command runs or the pointer leaves;
73
+ * double-clicking a tab, the chevron and Ctrl+F1 toggle it.
74
+ */
75
+ collapsed?: boolean
76
+ }
77
+
78
+ let {
79
+ cmd,
80
+ onChange,
81
+ onAction,
82
+ activeActions = [],
83
+ tabs = RIBBON_TABS,
84
+ tab = $bindable(tabs[0]?.id ?? 'home'),
85
+ without = [],
86
+ collapsed = $bindable(false),
87
+ }: Props = $props()
88
+
89
+ /** The band shown over the sheet while the ribbon is collapsed. */
90
+ let peek = $state(false)
91
+ function onTabClick(id: string) {
92
+ if (collapsed) { peek = tab === id ? !peek : true }
93
+ tab = id
94
+ }
95
+ function toggleCollapsed() {
96
+ collapsed = !collapsed
97
+ peek = false
98
+ }
99
+ $effect(() => {
100
+ if (!collapsed) peek = false
101
+ })
102
+ $effect(() => {
103
+ if (!peek) return
104
+ // A click outside the ribbon, or Escape, puts the peeked band away.
105
+ const away = (e: PointerEvent) => { if (!rootEl?.contains(e.target as Node) && !(e.target as HTMLElement)?.closest?.('.sv-popover, .sv-modal')) peek = false }
106
+ const esc = (e: KeyboardEvent) => { if (e.key === 'Escape') peek = false }
107
+ window.addEventListener('pointerdown', away, true)
108
+ window.addEventListener('keydown', esc, true)
109
+ return () => { window.removeEventListener('pointerdown', away, true); window.removeEventListener('keydown', esc, true) }
110
+ })
111
+
112
+ /**
113
+ * Bumped after every action so the `isOn` / `isEnabled` / `current` reads
114
+ * below re-run.
115
+ *
116
+ * They read through the command context into the grid and the format store,
117
+ * neither of which is `$state`, so nothing would otherwise tell Svelte that
118
+ * pressing Bold should light the Bold button up.
119
+ */
120
+ let version = $state(0)
121
+
122
+ const current = $derived.by(() => {
123
+ const found = tabs.find((t) => t.id === tab) ?? tabs[0]
124
+ if (!found || without.length === 0) return found
125
+ const groups = found.groups
126
+ .map((g) => ({ ...g, items: g.items.filter((i) => !i.emits || !without.includes(i.emits)) }))
127
+ .filter((g) => g.items.length > 0)
128
+ return { ...found, groups }
129
+ })
130
+
131
+ function stateOf(item: RibbonItem): { on: boolean; enabled: boolean } {
132
+ void version
133
+ const context = cmd()
134
+ if (!context) return { on: false, enabled: false }
135
+ if (item.emits) {
136
+ return { on: activeActions.includes(item.emits), enabled: true }
137
+ }
138
+ return {
139
+ on: item.lit ? activeActions.includes(item.lit) : (item.isOn?.(context) ?? false),
140
+ enabled: item.isEnabled?.(context) ?? true,
141
+ }
142
+ }
143
+
144
+ function currentOf(item: RibbonItem): string {
145
+ void version
146
+ const context = cmd()
147
+ return context && item.current ? item.current(context) : ''
148
+ }
149
+
150
+ function run(item: RibbonItem, value?: string) {
151
+ const context = cmd()
152
+ if (!context) return
153
+ if (item.emits) {
154
+ onAction?.(item.emits, context)
155
+ version += 1
156
+ // Focus goes back to the sheet here too. A dialog the action opens
157
+ // takes it from there and, being what had focus when the dialog
158
+ // opened, the sheet is where the dialog returns it; an action with no
159
+ // dialog (Sort A to Z) leaves the next Ctrl+Z on the sheet rather
160
+ // than on the button.
161
+ context.focus()
162
+ peek = false
163
+ return
164
+ }
165
+ // Only report a change when the action says it did something. A declined
166
+ // action (nothing selected, no store attached) should not make the host
167
+ // re-render and should not look like it worked.
168
+ if (item.run?.(context, value)) onChange?.()
169
+ version += 1
170
+ // The click left focus on the button. Excel puts it back on the sheet, so
171
+ // the next keystroke - Ctrl+Z, or just typing - goes where the user is
172
+ // looking rather than re-pressing Bold.
173
+ context.focus()
174
+ // A command from the peeked band is the band's last act; Excel's goes
175
+ // away the same moment.
176
+ peek = false
177
+ }
178
+
179
+ function launch(action: RibbonActionId) {
180
+ const context = cmd()
181
+ if (context) onAction?.(action, context)
182
+ peek = false
183
+ }
184
+
185
+ /** Excel puts the shortcut after the name in the tooltip. */
186
+ function tip(item: RibbonItem): string {
187
+ return item.keys ? `${item.title} (${item.keys})` : item.title
188
+ }
189
+
190
+ /**
191
+ * Re-read the toggles when the selection moves, so Bold lights up when you
192
+ * click a bold cell rather than only after you press a ribbon button.
193
+ * Cheap: it only bumps a counter, and the reads are lazy.
194
+ */
195
+ function refresh() {
196
+ version += 1
197
+ }
198
+
199
+ // ---- split buttons -------------------------------------------------------
200
+ /** The last value picked from each menu, which its face re-applies. */
201
+ let picked = $state<Record<string, string>>({})
202
+ // Every key exists from the start: a popover's `open` is bound to one of
203
+ // these, and binding an undefined value to a prop with a fallback is an
204
+ // error in Svelte rather than a false.
205
+ // svelte-ignore state_referenced_locally
206
+ let menuOpen = $state<Record<string, boolean>>(
207
+ Object.fromEntries(tabs.flatMap((t) => t.groups.flatMap((g) => g.items.map((i) => [i.id, false])))),
208
+ )
209
+
210
+ function faceValue(item: RibbonItem): string {
211
+ return picked[item.id] ?? item.initial ?? item.options?.[0]?.value ?? ''
212
+ }
213
+
214
+ function pick(item: RibbonItem, value: string) {
215
+ picked = { ...picked, [item.id]: value }
216
+ menuOpen = { ...menuOpen, [item.id]: false }
217
+ run(item, value)
218
+ }
219
+
220
+ /** A dropdown's entry: its own action, or the item's run with its value. */
221
+ function pickOption(item: RibbonItem, option: RibbonOption) {
222
+ menuOpen = { ...menuOpen, [item.id]: false }
223
+ if (option.emits) {
224
+ const context = cmd()
225
+ if (!context) return
226
+ onAction?.(option.emits, context)
227
+ version += 1
228
+ context.focus()
229
+ peek = false
230
+ return
231
+ }
232
+ run(item, option.value)
233
+ }
234
+
235
+ /** Whether a face's colour bar has a colour to show, or means "none". */
236
+ function barColour(item: RibbonItem): string | null {
237
+ const value = faceValue(item)
238
+ return value === item.none?.value ? null : value
239
+ }
240
+
241
+ // ---- responsive collapse -------------------------------------------------
242
+ /** Natural width of each group, expanded, measured off the hidden band. */
243
+ const natural = new Map<string, number>()
244
+ /** The same with the small buttons' labels off: Excel's first step down. */
245
+ const compactWidth = new Map<string, number>()
246
+ /** And folded to one large button, whose width its label sets. */
247
+ const foldedWidth = new Map<string, number>()
248
+ let measured = $state(0)
249
+ let bandWidth = $state(0)
250
+ // svelte-ignore state_referenced_locally
251
+ let groupOpen = $state<Record<string, boolean>>(
252
+ Object.fromEntries(tabs.flatMap((t) => t.groups.map((g) => [g.id, false]))),
253
+ )
254
+
255
+ /** A folded group before its button has been measured. */
256
+ const COLLAPSED_WIDTH = 66
257
+ /** The band's own padding (4px a side; `clientWidth` already leaves the
258
+ * border out) and the collapse chevron at its end (20px and 4px of
259
+ * margins), so the fit is judged against the space the groups really
260
+ * get. Without the chevron in the sum the last group sat 18px past the
261
+ * edge and the band scrolled by that much. */
262
+ const BAND_PADDING = 8 + 24
263
+
264
+ function measure(node: HTMLElement, key: string) {
265
+ // "id" measures the group as drawn, "id:compact" with its labels off,
266
+ // "id:folded" as the one large button it folds into.
267
+ const colon = key.indexOf(':')
268
+ const id = colon < 0 ? key : key.slice(0, colon)
269
+ const into = key.endsWith(':compact') ? compactWidth : key.endsWith(':folded') ? foldedWidth : natural
270
+ const record = () => {
271
+ const width = node.getBoundingClientRect().width
272
+ if (width > 0 && into.get(id) !== width) {
273
+ into.set(id, width)
274
+ measured += 1
275
+ }
276
+ }
277
+ record()
278
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(record)
279
+ observer?.observe(node)
280
+ return { destroy: () => observer?.disconnect() }
281
+ }
282
+
283
+ /**
284
+ * How the band fits its width, Excel's way and in Excel's order: first
285
+ * the groups go compact from the right (their small buttons keep the
286
+ * icon and lose the label, so Fill Down is an arrow and Wrap Text a
287
+ * glyph), and only when every group is compact and the band is still too
288
+ * narrow do groups fold, again from the right, into one large button
289
+ * each. Recomputed from scratch on every width change rather than
290
+ * nudged, so growing the window undoes exactly what shrinking it did.
291
+ */
292
+ const fit = $derived.by(() => {
293
+ void measured
294
+ const groups = current?.groups ?? []
295
+ const compact = new Set<string>()
296
+ const folded = new Set<string>()
297
+ if (!bandWidth) return { compact, folded }
298
+ const wide = (g: RibbonGroup) => natural.get(g.id) ?? 120
299
+ const narrow = (g: RibbonGroup) => compactWidth.get(g.id) ?? wide(g)
300
+ const oneButton = (g: RibbonGroup) => foldedWidth.get(g.id) ?? COLLAPSED_WIDTH
301
+ let total = BAND_PADDING
302
+ for (const group of groups) total += wide(group)
303
+ for (let i = groups.length - 1; i >= 0 && total > bandWidth; i -= 1) {
304
+ const group = groups[i]!
305
+ if (narrow(group) < wide(group)) { total += narrow(group) - wide(group); compact.add(group.id) }
306
+ }
307
+ for (let i = groups.length - 1; i >= 0 && total > bandWidth; i -= 1) {
308
+ const group = groups[i]!
309
+ total += oneButton(group) - (compact.has(group.id) ? narrow(group) : wide(group))
310
+ folded.add(group.id)
311
+ }
312
+ return { compact, folded }
313
+ })
314
+ const collapsedGroups = $derived(fit.folded)
315
+
316
+ /**
317
+ * Excel's column model, made explicit. Items run down a column while
318
+ * their rows go up (1, 2, 3) and a new column starts when they do not,
319
+ * or at a large button, which is a column by itself. The grid's own
320
+ * auto-placement keeps a cursor per row, so a two-item column (Undo over
321
+ * Redo) had the next column's third item slide back under it.
322
+ */
323
+ function placed(items: ReadonlyArray<RibbonItem>): Array<{ item: RibbonItem; col: number }> {
324
+ let col = 0
325
+ let lastRow = 99
326
+ return items.map((item) => {
327
+ const row = item.size === 'large' ? 1 : (item.row ?? 1)
328
+ if (item.size === 'large' || row <= lastRow) col += 1
329
+ lastRow = item.size === 'large' ? 99 : row
330
+ return { item, col }
331
+ })
332
+ }
333
+
334
+ /** The icon a folded group shows: its own, else its large command's, else its first. */
335
+ function groupIcon(group: RibbonGroup) {
336
+ return group.icon ?? (group.items.find((i) => i.size === 'large') ?? group.items.find((i) => i.icon))?.icon ?? 'format-cells'
337
+ }
338
+
339
+ /**
340
+ * What the empty font option is called. A cell with no font of its own
341
+ * renders in the theme's font, and Excel's combo names the font a cell
342
+ * has ("Aptos Narrow"), so the option reads the theme's first family
343
+ * rather than "Default".
344
+ */
345
+ let rootEl = $state<HTMLDivElement | null>(null)
346
+ let themeFont = $state('Default')
347
+ $effect(() => {
348
+ if (!rootEl) return
349
+ const first = getComputedStyle(rootEl).fontFamily.split(',')[0]?.replace(/["']/g, '').trim()
350
+ if (first) themeFont = first
351
+ })
352
+
353
+ function onTabKey(event: KeyboardEvent) {
354
+ const ids = tabs.map((t) => t.id)
355
+ 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]! }
358
+ }
359
+ </script>
360
+
361
+ <svelte:window onpointerup={refresh} onkeyup={refresh} />
362
+
363
+ <!--
364
+ One button, either size. `live` is off inside the hidden measuring band:
365
+ the copy there exists for its width alone, so it reads no state.
366
+ -->
367
+ {#snippet plainButton(item: RibbonItem, live: boolean)}
368
+ {@const state = live ? stateOf(item) : { on: false, enabled: true }}
369
+ <button
370
+ type="button"
371
+ class="btn"
372
+ class:large={item.size === 'large'}
373
+ class:wide={item.wide && item.size !== 'large'}
374
+ class:on={state.on}
375
+ disabled={!state.enabled}
376
+ title={tip(item)}
377
+ aria-label={item.title}
378
+ aria-pressed={item.kind === 'toggle' ? state.on : undefined}
379
+ onclick={() => run(item)}
380
+ >
381
+ {#if item.icon}
382
+ <SvRibbonIcon name={item.icon} size={item.size === 'large' ? 32 : 16} />
383
+ {:else}
384
+ <span class="glyph" data-glyph={item.id}>{item.label}</span>
385
+ {/if}
386
+ {#if item.size === 'large' || item.wide}
387
+ <span class="label">{item.label}</span>
388
+ {/if}
389
+ </button>
390
+ {/snippet}
391
+
392
+ <!-- Excel's split button: a face that re-applies the last pick, an arrow that opens the menu. -->
393
+ {#snippet split(item: RibbonItem, live: boolean)}
394
+ {@const state = live ? stateOf(item) : { on: false, enabled: true }}
395
+ {@const colour = barColour(item)}
396
+ <div class="split" class:on={state.on} class:disabled={!state.enabled}>
397
+ <button
398
+ type="button"
399
+ class="btn face"
400
+ class:has-bar={item.palette}
401
+ disabled={!state.enabled}
402
+ title={tip(item)}
403
+ aria-label={item.title}
404
+ onclick={() => run(item, faceValue(item))}
405
+ >
406
+ {#if item.icon && !(item.palette && item.id === 'text-colour')}
407
+ <SvRibbonIcon name={item.icon} />
408
+ {:else}
409
+ <span class="glyph" data-glyph={item.id}>{item.label}</span>
410
+ {/if}
411
+ {#if item.palette}
412
+ <span class="bar" class:none={colour === null} style:background={colour ?? 'transparent'}></span>
413
+ {/if}
414
+ </button>
415
+ {#if live}
416
+ <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={item.title}>
417
+ {#snippet anchor()}
418
+ <button
419
+ type="button"
420
+ class="btn arrow"
421
+ disabled={!state.enabled}
422
+ aria-label={`${item.title} options`}
423
+ aria-haspopup="menu"
424
+ aria-expanded={menuOpen[item.id] ?? false}
425
+ ><SvRibbonIcon name="chevron-down" size={10} /></button>
426
+ {/snippet}
427
+ {#if item.palette}
428
+ <div class="palette" role="menu" aria-label={item.title}>
429
+ {#if item.none}
430
+ <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}
432
+ </button>
433
+ {/if}
434
+ <div class="palette-title">Theme Colors</div>
435
+ <div class="swatches">
436
+ {#each THEME_COLOURS as row, r (r)}
437
+ {#each row as c (c.value + r)}
438
+ <button type="button" class="swatch" class:shade={r > 0} role="menuitem" title={c.label} aria-label={c.label} style:background={c.value} onclick={() => pick(item, c.value)}></button>
439
+ {/each}
440
+ {/each}
441
+ </div>
442
+ <div class="palette-title">Standard Colors</div>
443
+ <div class="swatches">
444
+ {#each STANDARD_COLOURS as c (c.value)}
445
+ <button type="button" class="swatch" role="menuitem" title={c.label} aria-label={c.label} style:background={c.value} onclick={() => pick(item, c.value)}></button>
446
+ {/each}
447
+ </div>
448
+ </div>
449
+ {:else}
450
+ <div class="menu" role="menu" aria-label={item.title}>
451
+ {#each item.options ?? [] as option (option.value)}
452
+ <button type="button" class="menu-item" role="menuitem" onclick={() => pick(item, option.value)}>
453
+ {#if option.icon}<SvRibbonIcon name={option.icon} />{/if}
454
+ <span>{option.label}</span>
455
+ </button>
456
+ {/each}
457
+ </div>
458
+ {/if}
459
+ </SvPopover>
460
+ {:else}
461
+ <span class="btn arrow" aria-hidden="true"><SvRibbonIcon name="chevron-down" size={10} /></span>
462
+ {/if}
463
+ </div>
464
+ {/snippet}
465
+
466
+ <!-- Excel's plain menu button: the face opens the list, every entry acts on its own. -->
467
+ {#snippet dropdown(item: RibbonItem, live: boolean)}
468
+ {@const state = live ? stateOf(item) : { on: false, enabled: true }}
469
+ {#snippet face()}
470
+ <button
471
+ type="button"
472
+ class="btn dropdown"
473
+ class:wide={item.wide}
474
+ class:large={item.size === 'large'}
475
+ disabled={!state.enabled}
476
+ title={tip(item)}
477
+ aria-label={item.title}
478
+ aria-haspopup="menu"
479
+ aria-expanded={live ? (menuOpen[item.id] ?? false) : false}
480
+ >
481
+ {#if item.icon}
482
+ <SvRibbonIcon name={item.icon} size={item.size === 'large' ? 32 : 16} />
483
+ {:else}
484
+ <span class="glyph" data-glyph={item.id}>{item.label}</span>
485
+ {/if}
486
+ {#if item.size === 'large' || item.wide}
487
+ <span class="label">{item.label}</span>
488
+ {/if}
489
+ <SvRibbonIcon name="chevron-down" size={8} />
490
+ </button>
491
+ {/snippet}
492
+ {#snippet entries()}
493
+ <div class="menu" role="menu" aria-label={item.title}>
494
+ {#each item.options ?? [] as option (option.value)}
495
+ {#if option.heading}
496
+ <div class="menu-heading" role="presentation">{option.label}</div>
497
+ {:else}
498
+ {#if option.toggle && option.emits}
499
+ {@const on = live && activeActions.includes(option.emits)}
500
+ <button type="button" class="menu-item" class:on role="menuitemcheckbox" aria-checked={on} onclick={() => pickOption(item, option)}>
501
+ {#if option.icon}<SvRibbonIcon name={option.icon} />{/if}
502
+ <span>{option.label}</span>
503
+ {#if option.keys}<span class="keys">{option.keys}</span>{/if}
504
+ </button>
505
+ {:else}
506
+ <button type="button" class="menu-item" role="menuitem" onclick={() => pickOption(item, option)}>
507
+ {#if option.icon}<SvRibbonIcon name={option.icon} />{/if}
508
+ <span>{option.label}</span>
509
+ {#if option.keys}<span class="keys">{option.keys}</span>{/if}
510
+ </button>
511
+ {/if}
512
+ {/if}
513
+ {/each}
514
+ </div>
515
+ {/snippet}
516
+ {#if item.split}
517
+ <!-- Excel's split button. Large: the icon runs the item and the label
518
+ with its chevron opens the entries. Small: face beside arrow, as
519
+ the colour menus. -->
520
+ <div class="split" class:split-large={item.size === 'large'} class:on={state.on} class:disabled={!state.enabled}>
521
+ <button
522
+ type="button"
523
+ class="btn face"
524
+ class:large={item.size === 'large'}
525
+ disabled={!state.enabled}
526
+ title={tip(item)}
527
+ aria-label={item.title}
528
+ onclick={() => run(item)}
529
+ >
530
+ {#if item.icon}
531
+ <SvRibbonIcon name={item.icon} size={item.size === 'large' ? 32 : 16} />
532
+ {:else}
533
+ <span class="glyph" data-glyph={item.id}>{item.label}</span>
534
+ {/if}
535
+ <!-- A small wide split (Merge & Center) prints its label on the face, as
536
+ Excel does; the colour splits stay icon-only. -->
537
+ {#if item.wide && item.size !== 'large'}<span class="label">{item.label}</span>{/if}
538
+ </button>
539
+ {#if live}
540
+ <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={item.title}>
541
+ {#snippet anchor()}
542
+ <button
543
+ type="button"
544
+ class="btn arrow"
545
+ disabled={!state.enabled}
546
+ aria-label={`${item.title} options`}
547
+ aria-haspopup="menu"
548
+ 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>
550
+ {/snippet}
551
+ {@render entries()}
552
+ </SvPopover>
553
+ {: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>
555
+ {/if}
556
+ </div>
557
+ {:else if live}
558
+ <SvPopover bind:open={menuOpen[item.id]} placement="bottom-start" arrow={false} offset={2} ariaLabel={item.title}>
559
+ {#snippet anchor()}
560
+ {@render face()}
561
+ {/snippet}
562
+ {@render entries()}
563
+ </SvPopover>
564
+ {:else}
565
+ {@render face()}
566
+ {/if}
567
+ {/snippet}
568
+
569
+ {#snippet control(item: RibbonItem, live: boolean)}
570
+ {#if item.kind === 'select'}
571
+ {@const state = live ? stateOf(item) : { on: false, enabled: true }}
572
+ <select
573
+ class="select"
574
+ data-select={item.id}
575
+ title={tip(item)}
576
+ aria-label={item.title}
577
+ disabled={!state.enabled}
578
+ value={live ? currentOf(item) : ''}
579
+ onchange={(e) => run(item, e.currentTarget.value)}
580
+ >
581
+ {#if live && currentOf(item) && !item.options?.some((o) => o.value === currentOf(item))}
582
+ <!-- A value the list does not carry (a 17px title) still reads on the
583
+ face, the way Excel's combo shows whatever the cell has. -->
584
+ <option value={currentOf(item)}>{currentOf(item)}</option>
585
+ {/if}
586
+ {#each item.options ?? [] as option (option.value)}
587
+ <option value={option.value}>{option.value === '' && item.id === 'font-family' ? themeFont : option.label}</option>
588
+ {/each}
589
+ </select>
590
+ {:else if item.kind === 'menu'}
591
+ {@render split(item, live)}
592
+ {:else if item.kind === 'dropdown'}
593
+ {@render dropdown(item, live)}
594
+ {:else}
595
+ {@render plainButton(item, live)}
596
+ {/if}
597
+ {/snippet}
598
+
599
+ <!-- The controls of one group, in Excel's three-row block. -->
600
+ {#snippet body(group: RibbonGroup, live: boolean)}
601
+ {#if group.layout === 'flow'}
602
+ <div class="controls flow">
603
+ {#each [1, 2, 3] as row (row)}
604
+ {@const items = group.items.filter((i) => (i.row ?? 1) === row)}
605
+ {#if items.length}
606
+ <div class="row">
607
+ {#each items as item (item.id)}{@render control(item, live)}{/each}
608
+ </div>
609
+ {/if}
610
+ {/each}
611
+ </div>
612
+ {:else}
613
+ <div class="controls grid">
614
+ {#each placed(group.items) as { item, col } (item.id)}
615
+ <div class="cell" style:grid-row={item.size === 'large' ? '1 / span 3' : String(item.row ?? 1)} style:grid-column={col}>
616
+ {@render control(item, live)}
617
+ </div>
618
+ {/each}
619
+ </div>
620
+ {/if}
621
+ {/snippet}
622
+
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">
625
+ {#each tabs as entry (entry.id)}
626
+ <button
627
+ type="button"
628
+ role="tab"
629
+ class="tab"
630
+ class:active={entry.id === current?.id}
631
+ aria-selected={entry.id === current?.id}
632
+ tabindex={entry.id === current?.id ? 0 : -1}
633
+ onclick={() => onTabClick(entry.id)}
634
+ ondblclick={toggleCollapsed}
635
+ onkeydown={onTabKey}
636
+ >{entry.label}</button>
637
+ {/each}
638
+ <span class="tabs-grow"></span>
639
+ {#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}>
641
+ <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
+ </button>
643
+ {/if}
644
+ </div>
645
+
646
+ <div class="band" class:away={collapsed && !peek} class:peek={collapsed && peek} bind:clientWidth={bandWidth}>
647
+ {#each current?.groups ?? [] as group (group.id)}
648
+ {#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}>
651
+ {#snippet anchor()}
652
+ <button type="button" class="btn large" aria-haspopup="true" aria-expanded={groupOpen[group.id] ?? false} title={group.label}>
653
+ <span class="fold-icon"><SvRibbonIcon name={groupIcon(group)} size={32} /></span>
654
+ <span class="label">{group.label} <SvRibbonIcon name="chevron-down" size={10} /></span>
655
+ </button>
656
+ {/snippet}
657
+ <div class="popup-group">
658
+ {@render body(group, true)}
659
+ <div class="label-row"><span class="group-label">{group.label}</span></div>
660
+ </div>
661
+ </SvPopover>
662
+ </section>
663
+ {:else}
664
+ <section class="group" class:compact={fit.compact.has(group.id)} aria-label={group.label}>
665
+ {@render body(group, true)}
666
+ <div class="label-row">
667
+ <span class="group-label">{group.label}</span>
668
+ {#if group.launcher}
669
+ <button type="button" class="launcher" title={`${group.label} settings`} aria-label={`${group.label} settings`} onclick={() => launch(group.launcher!)}>
670
+ <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
+ </button>
672
+ {/if}
673
+ </div>
674
+ </section>
675
+ {/if}
676
+ {/each}
677
+ <!-- Excel's chevron at the band's end: collapse, or pin the peeked band. -->
678
+ <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}>
680
+ {#if collapsed}
681
+ <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
+ {:else}
683
+ <svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><path d="M2 6.5l3-3 3 3" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
684
+ {/if}
685
+ </button>
686
+ </div>
687
+
688
+ <!--
689
+ The measuring bands: every group of the current tab expanded, then
690
+ every group compact, invisible and out of the tab order, so the fit
691
+ above has both widths for each group even when it never got to draw
692
+ itself either way.
693
+ -->
694
+ <div class="measure-well" aria-hidden="true" inert>
695
+ <div class="band measure">
696
+ {#each current?.groups ?? [] as group (group.id)}
697
+ <section class="group" use:measure={group.id}>
698
+ {@render body(group, false)}
699
+ <div class="label-row"><span class="group-label">{group.label}</span></div>
700
+ </section>
701
+ {/each}
702
+ </div>
703
+ <div class="band measure">
704
+ {#each current?.groups ?? [] as group (group.id)}
705
+ <section class="group compact" use:measure={`${group.id}:compact`}>
706
+ {@render body(group, false)}
707
+ <div class="label-row"><span class="group-label">{group.label}</span></div>
708
+ </section>
709
+ {/each}
710
+ </div>
711
+ <div class="band measure">
712
+ {#each current?.groups ?? [] as group (group.id)}
713
+ <section class="group folded" use:measure={`${group.id}:folded`}>
714
+ <button type="button" class="btn large" tabindex="-1">
715
+ <span class="fold-icon"><SvRibbonIcon name={groupIcon(group)} size={32} /></span>
716
+ <span class="label">{group.label} <SvRibbonIcon name="chevron-down" size={10} /></span>
717
+ </button>
718
+ </section>
719
+ {/each}
720
+ </div>
721
+ </div>
722
+ </div>
723
+
724
+ <style>
725
+ .sv-ribbon {
726
+ position: relative;
727
+ display: flex;
728
+ flex-direction: column;
729
+ font-family: var(--sg-font, "Segoe UI", system-ui, sans-serif);
730
+ font-size: 12px;
731
+ line-height: 1.2;
732
+ background: var(--sg-bg-subtle, var(--sg-header-bg, #f3f3f3));
733
+ color: var(--sg-fg, #242424);
734
+ border: 1px solid var(--sg-border, #e0e0e0);
735
+ border-radius: var(--sg-radius, 4px);
736
+ }
737
+ /* Collapsed, the peeked band hangs below the tabs over the sheet: the
738
+ ribbon has to sit above the formula bar and the grid's sticky header
739
+ (z-index 30) for it to show. */
740
+ .sv-ribbon.collapsed { z-index: 70; }
741
+
742
+ /* ---- tabs ---------------------------------------------------------- */
743
+ .tabs {
744
+ display: flex;
745
+ align-items: stretch;
746
+ gap: 0;
747
+ height: 30px;
748
+ padding: 0 8px;
749
+ }
750
+ .tab {
751
+ position: relative;
752
+ font: inherit;
753
+ border: 0;
754
+ background: transparent;
755
+ color: var(--sg-fg, #242424);
756
+ padding: 0 11px;
757
+ cursor: pointer;
758
+ white-space: nowrap;
759
+ border-radius: 4px 4px 0 0;
760
+ }
761
+ .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; }
763
+ /* Excel's selected-tab mark: a short rounded underline, not a full-width
764
+ border, inset from the label's edges. */
765
+ .tab.active::after {
766
+ content: "";
767
+ position: absolute;
768
+ left: 9px;
769
+ right: 9px;
770
+ bottom: 0;
771
+ height: 3px;
772
+ border-radius: 3px 3px 0 0;
773
+ background: var(--sg-accent, #107c41);
774
+ }
775
+ .tab:focus-visible {
776
+ outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #107c41));
777
+ outline-offset: -2px;
778
+ }
779
+
780
+ /* ---- band ---------------------------------------------------------- */
781
+ /* Excel's band is a white card on the ribbon's grey ground, inset a few
782
+ pixels and rounded, with the groups divided by hairlines inside it. */
783
+ .band {
784
+ display: flex;
785
+ align-items: stretch;
786
+ margin: 0 6px 6px;
787
+ padding: 4px 4px 2px;
788
+ background: var(--sg-bg, #fff);
789
+ border: 1px solid var(--sg-border, #e0e0e0);
790
+ border-radius: var(--sg-radius-lg, 6px);
791
+ /* Seven folded groups are 472px; on a phone the band is narrower than
792
+ that, and a hidden overflow put Cells and Editing out of reach. The
793
+ band scrolls sideways instead, as Excel's does, without a scrollbar
794
+ stealing a row. */
795
+ overflow-x: auto;
796
+ overflow-y: hidden;
797
+ scrollbar-width: none;
798
+ }
799
+ .band::-webkit-scrollbar {
800
+ display: none;
801
+ }
802
+ /* The measuring bands live in a well of no size that clips them, so
803
+ their max-content widths never reach the page's scroll width. */
804
+ .measure-well {
805
+ position: absolute;
806
+ left: 0;
807
+ top: 0;
808
+ width: 0;
809
+ height: 0;
810
+ overflow: hidden;
811
+ visibility: hidden;
812
+ pointer-events: none;
813
+ }
814
+ .band.measure {
815
+ position: absolute;
816
+ left: 0;
817
+ top: 0;
818
+ width: max-content;
819
+ height: 0;
820
+ margin: 0;
821
+ padding-block: 0;
822
+ border: 0;
823
+ }
824
+
825
+ .group {
826
+ display: flex;
827
+ flex-direction: column;
828
+ flex: 0 0 auto;
829
+ /* Three rows of controls and the label row: the band keeps this
830
+ height when every group has folded to a button. */
831
+ min-height: 92px;
832
+ padding: 0 4px;
833
+ border-right: 1px solid var(--sg-border, #e0e0e0);
834
+ }
835
+ .group:last-of-type { border-right: 0; }
836
+ /* Folded: one button the height of the group, its icon centred where the
837
+ controls sit and its label down on the group-label row, so the row of
838
+ labels reads straight across folded and open groups alike. */
839
+ .group.folded { padding: 0 2px; }
840
+ /* The popover's anchor wrapper sits between the group and its button;
841
+ it has to stretch for the button's 100% height to mean the group's. */
842
+ .group.folded :global(.sv-pop__anchor) { flex: 1 1 auto; display: flex; align-items: stretch; }
843
+ .group.folded :global(.btn.large) { height: 100%; padding-top: 0; padding-bottom: 0; gap: 0; }
844
+ .group.folded :global(.btn.large .fold-icon) { flex: 1 1 auto; display: flex; align-items: center; }
845
+ .group.folded :global(.btn.large .label) { height: 16px; padding-bottom: 1px; color: var(--sg-muted, #616161); }
846
+ .group.folded :global(.btn.large:hover .label) { color: inherit; }
847
+ /* Compact: the small buttons keep their icon and drop their label. */
848
+ .group.compact :global(.btn.wide:not(.large) .label) { display: none; }
849
+ .group.compact :global(.btn.wide:not(.large)) { justify-content: center; padding-right: 3px; }
850
+ .group.compact :global(.split .btn.face .label) { display: none; }
851
+
852
+ /* Collapsed: the tabs alone; peeked: the band floats over the sheet. */
853
+ /* Not "hidden": a host reset owns that name too. */
854
+ .band.away { display: none; }
855
+ .band.peek {
856
+ position: absolute;
857
+ left: 0;
858
+ right: 0;
859
+ top: 30px;
860
+ z-index: 60;
861
+ margin: 0;
862
+ border-radius: 0 0 var(--sg-radius-lg, 6px) var(--sg-radius-lg, 6px);
863
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
864
+ }
865
+ .tabs-grow, .band-grow { flex: 1 1 auto; }
866
+ /* Not "collapse": that is a visibility utility in a host's reset. */
867
+ .ribbon-toggle {
868
+ align-self: center;
869
+ width: 20px;
870
+ height: 20px;
871
+ padding: 0;
872
+ border: 1px solid transparent;
873
+ border-radius: 3px;
874
+ background: transparent;
875
+ color: var(--sg-muted, #616161);
876
+ cursor: pointer;
877
+ display: inline-flex;
878
+ align-items: center;
879
+ justify-content: center;
880
+ }
881
+ .ribbon-toggle.in-band { align-self: flex-end; margin: 0 2px 2px 2px; }
882
+ .ribbon-toggle:hover { background: var(--sg-row-hover-bg, #f0f0f0); border-color: var(--sg-border, #e0e0e0); color: var(--sg-fg, #242424); }
883
+
884
+ /* Three rows of 24px: what a large button fills and a small one takes one of. */
885
+ .controls {
886
+ flex: 1 0 auto;
887
+ min-height: 76px;
888
+ }
889
+ .controls.grid {
890
+ display: grid;
891
+ grid-template-rows: repeat(3, 24px);
892
+ grid-auto-columns: max-content;
893
+ grid-auto-flow: row;
894
+ align-content: start;
895
+ gap: 2px 3px;
896
+ }
897
+ .cell { display: flex; align-items: stretch; }
898
+ .cell > :global(*) { flex: 1 1 auto; }
899
+ .controls.flow {
900
+ display: flex;
901
+ flex-direction: column;
902
+ justify-content: space-evenly;
903
+ gap: 2px;
904
+ }
905
+ .controls.flow .row {
906
+ display: flex;
907
+ align-items: center;
908
+ gap: 2px;
909
+ }
910
+
911
+ .label-row {
912
+ position: relative;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ height: 16px;
917
+ margin-top: 2px;
918
+ }
919
+ .group-label {
920
+ font-size: 10.5px;
921
+ color: var(--sg-muted, #616161);
922
+ white-space: nowrap;
923
+ padding: 0 8px;
924
+ }
925
+ /* The dialog box launcher, in the corner where Excel keeps it. */
926
+ .launcher {
927
+ position: absolute;
928
+ right: -4px;
929
+ bottom: 1px;
930
+ width: 14px;
931
+ height: 14px;
932
+ padding: 0;
933
+ border: 1px solid transparent;
934
+ border-radius: 3px;
935
+ background: transparent;
936
+ color: var(--sg-muted, #616161);
937
+ cursor: pointer;
938
+ display: inline-flex;
939
+ align-items: center;
940
+ justify-content: center;
941
+ }
942
+ .launcher:hover { background: var(--sg-row-hover-bg, #f0f0f0); border-color: var(--sg-border, #e0e0e0); color: var(--sg-fg, #242424); }
943
+
944
+ /* ---- buttons ------------------------------------------------------- */
945
+ .btn {
946
+ font: inherit;
947
+ display: inline-flex;
948
+ align-items: center;
949
+ justify-content: center;
950
+ gap: 5px;
951
+ min-width: 22px;
952
+ height: 24px;
953
+ padding: 0 2px;
954
+ border: 1px solid transparent;
955
+ border-radius: 4px;
956
+ background: transparent;
957
+ color: inherit;
958
+ cursor: pointer;
959
+ white-space: nowrap;
960
+ position: relative;
961
+ }
962
+ .btn.wide { justify-content: flex-start; padding-right: 6px; }
963
+ .btn:hover:not(:disabled) {
964
+ background: var(--sg-row-hover-bg, #f0f0f0);
965
+ border-color: var(--sg-border, #d1d1d1);
966
+ }
967
+ .btn:active:not(:disabled) { background: var(--sg-muted-bg, #e6e6e6); }
968
+ /* A pressed toggle: Excel tints it, it does not invert it. */
969
+ .btn.on,
970
+ .split.on .btn {
971
+ background: var(--sg-selection-bg, rgba(16, 124, 65, 0.12));
972
+ border-color: var(--sg-border, #d1d1d1);
973
+ }
974
+ .btn:disabled { opacity: 0.38; cursor: default; }
975
+ .btn:focus-visible {
976
+ outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #107c41));
977
+ outline-offset: -2px;
978
+ }
979
+ .btn.large {
980
+ flex-direction: column;
981
+ justify-content: flex-start;
982
+ gap: 3px;
983
+ height: 100%;
984
+ min-width: 40px;
985
+ padding: 5px 3px 2px;
986
+ }
987
+ .btn.large .label {
988
+ white-space: normal;
989
+ text-align: center;
990
+ line-height: 1.15;
991
+ max-width: 68px;
992
+ display: inline-flex;
993
+ flex-wrap: wrap;
994
+ justify-content: center;
995
+ align-items: center;
996
+ gap: 2px;
997
+ }
998
+
999
+ /* Typographic faces: B I U S read as their own styling, as Excel's do. */
1000
+ .glyph {
1001
+ display: inline-flex;
1002
+ align-items: center;
1003
+ justify-content: center;
1004
+ min-width: 16px;
1005
+ font-size: 13px;
1006
+ line-height: 1;
1007
+ }
1008
+ .glyph[data-glyph='bold'] { font-weight: 800; }
1009
+ .glyph[data-glyph='italic'] { font-style: italic; font-family: Georgia, "Times New Roman", serif; }
1010
+ .glyph[data-glyph='underline'] { text-decoration: underline; }
1011
+ .glyph[data-glyph='strike'] { text-decoration: line-through; }
1012
+ .glyph[data-glyph='text-colour'] { font-weight: 700; font-size: 14px; transform: translateY(-2px); }
1013
+ .glyph[data-glyph='fmt-currency'],
1014
+ .glyph[data-glyph='fmt-percent'],
1015
+ .glyph[data-glyph='fmt-number'] { font-weight: 600; }
1016
+
1017
+ /* ---- split buttons ------------------------------------------------- */
1018
+ .split {
1019
+ display: inline-flex;
1020
+ align-items: stretch;
1021
+ height: 24px;
1022
+ border-radius: 4px;
1023
+ }
1024
+ .split .face { border-radius: 4px 0 0 4px; padding: 0 3px; }
1025
+ .split .arrow {
1026
+ border-radius: 0 4px 4px 0;
1027
+ min-width: 12px;
1028
+ width: 12px;
1029
+ padding: 0;
1030
+ color: var(--sg-muted, #616161);
1031
+ }
1032
+ .split:hover:not(.disabled) .btn { border-color: var(--sg-border, #d1d1d1); }
1033
+ .split:hover:not(.disabled) .face { border-right-color: transparent; }
1034
+ .split .face.has-bar { padding-bottom: 4px; }
1035
+ /* Excel's colour bar under the icon: the colour the face will apply. */
1036
+ .bar {
1037
+ position: absolute;
1038
+ left: 3px;
1039
+ right: 3px;
1040
+ bottom: 2px;
1041
+ height: 3px;
1042
+ border-radius: 1px;
1043
+ }
1044
+ .bar.none {
1045
+ background: repeating-linear-gradient(90deg, var(--sg-muted, #8a8886) 0 3px, transparent 3px 5px) !important;
1046
+ }
1047
+
1048
+ /* ---- selects ------------------------------------------------------- */
1049
+ .select {
1050
+ font: inherit;
1051
+ height: 22px;
1052
+ padding: 0 2px 0 5px;
1053
+ border: 1px solid var(--sg-input-border, var(--sg-border, #d1d1d1));
1054
+ border-radius: 3px;
1055
+ /* Not transparent: a native select paints its option list with this
1056
+ background, and a see-through one is unreadable on a dark theme. */
1057
+ background: var(--sg-input-bg, var(--sg-bg, #fff));
1058
+ color: var(--sg-fg, #242424);
1059
+ }
1060
+ .select[data-select='font-family'] { width: 112px; }
1061
+ .select[data-select='font-size'] { width: 46px; }
1062
+ .select[data-select='num-format'] { width: 108px; }
1063
+ .select:disabled { opacity: 0.38; }
1064
+ .select:focus-visible {
1065
+ outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #107c41));
1066
+ outline-offset: -1px;
1067
+ }
1068
+
1069
+ /* ---- menus --------------------------------------------------------- */
1070
+ .menu {
1071
+ min-width: 200px;
1072
+ padding: 4px;
1073
+ }
1074
+ .menu-item {
1075
+ display: flex;
1076
+ align-items: center;
1077
+ gap: 10px;
1078
+ width: 100%;
1079
+ padding: 5px 10px;
1080
+ font: inherit;
1081
+ font-size: 12px;
1082
+ border: 0;
1083
+ border-radius: 3px;
1084
+ background: transparent;
1085
+ color: inherit;
1086
+ text-align: left;
1087
+ cursor: pointer;
1088
+ white-space: nowrap;
1089
+ }
1090
+ .menu-item:hover { background: var(--sg-row-hover-bg, #f0f0f0); }
1091
+ /* A toggle entry that is on (Lock Cell on a locked cell): the same tint
1092
+ as a pressed button, so the menu and the ribbon agree. */
1093
+ .menu-item.on { background: var(--sg-selection-bg, rgba(16, 124, 65, 0.12)); }
1094
+ .menu-item.on:hover { background: var(--sg-muted-bg, #e6e6e6); }
1095
+ .menu-item .keys {
1096
+ margin-left: auto;
1097
+ padding-left: 18px;
1098
+ color: var(--sg-muted, #616161);
1099
+ font-size: 11px;
1100
+ }
1101
+ /* Excel's section labels between a menu's groups: small caps, a rule above. */
1102
+ .menu-heading {
1103
+ margin: 4px 0 2px;
1104
+ padding: 6px 10px 2px;
1105
+ border-top: 1px solid var(--sg-border, #e1e1e1);
1106
+ color: var(--sg-muted, #616161);
1107
+ font-size: 11px;
1108
+ font-weight: 600;
1109
+ }
1110
+ .menu-heading:first-child { border-top: 0; margin-top: 0; padding-top: 2px; }
1111
+ /* The chevron sits close to the label, as Excel's does, so the face is a
1112
+ few pixels wider than the plain button and the band still fits 1050px
1113
+ with every group open. */
1114
+ .btn.dropdown { padding-right: 3px; }
1115
+ /* The large split: icon on top pastes, "Paste v" under it opens the menu. */
1116
+ .split.split-large {
1117
+ flex-direction: column;
1118
+ align-items: stretch;
1119
+ height: 100%;
1120
+ min-width: 46px;
1121
+ }
1122
+ .split.split-large .face {
1123
+ flex: 1 1 auto;
1124
+ border-radius: 4px 4px 0 0;
1125
+ justify-content: flex-start;
1126
+ padding: 5px 4px 0;
1127
+ }
1128
+ .split.split-large .arrow {
1129
+ width: auto;
1130
+ min-width: 0;
1131
+ height: auto;
1132
+ padding: 0 2px 3px;
1133
+ gap: 2px;
1134
+ border-radius: 0 0 4px 4px;
1135
+ color: inherit;
1136
+ font-size: 12px;
1137
+ line-height: 1.15;
1138
+ }
1139
+ .btn.dropdown .label { margin-right: -3px; }
1140
+
1141
+ .palette {
1142
+ width: 232px;
1143
+ padding: 8px;
1144
+ font-size: 12px;
1145
+ }
1146
+ .palette-none {
1147
+ display: flex;
1148
+ align-items: center;
1149
+ gap: 8px;
1150
+ width: 100%;
1151
+ padding: 4px 6px;
1152
+ margin-bottom: 4px;
1153
+ font: inherit;
1154
+ border: 0;
1155
+ border-radius: 3px;
1156
+ background: transparent;
1157
+ color: inherit;
1158
+ text-align: left;
1159
+ cursor: pointer;
1160
+ }
1161
+ .palette-none:hover { background: var(--sg-row-hover-bg, #f0f0f0); }
1162
+ .palette-title {
1163
+ font-size: 11px;
1164
+ font-weight: 600;
1165
+ color: var(--sg-muted, #616161);
1166
+ margin: 6px 0 4px;
1167
+ }
1168
+ .swatches {
1169
+ display: grid;
1170
+ grid-template-columns: repeat(10, 18px);
1171
+ gap: 0 4px;
1172
+ }
1173
+ /* The five shades under each theme colour stack touching, one column per
1174
+ colour; the base row stands a little apart above them, as in Excel. */
1175
+ .swatches .swatch:nth-child(-n + 10) { margin-bottom: 4px; }
1176
+ .swatches .swatch.shade { border-radius: 0; border-top-width: 0; }
1177
+ .swatches .swatch.shade:nth-child(n + 11):nth-child(-n + 20) { border-top-width: 1px; }
1178
+ .swatch {
1179
+ width: 18px;
1180
+ height: 18px;
1181
+ padding: 0;
1182
+ border: 1px solid rgba(0, 0, 0, 0.18);
1183
+ border-radius: 2px;
1184
+ cursor: pointer;
1185
+ }
1186
+ .swatch:hover { outline: 2px solid var(--sg-accent, #107c41); outline-offset: 1px; position: relative; z-index: 1; }
1187
+ .swatch.no-colour {
1188
+ cursor: default;
1189
+ background:
1190
+ linear-gradient(to top right,
1191
+ transparent calc(50% - 1px), var(--sg-danger, #dc2626) 50%,
1192
+ transparent calc(50% + 1px)),
1193
+ var(--sg-bg, #fff);
1194
+ }
1195
+
1196
+ .popup-group {
1197
+ display: flex;
1198
+ flex-direction: column;
1199
+ padding: 6px 8px 2px;
1200
+ }
1201
+ </style>