@svgrid/enterprise 2.7.0 → 3.0.1

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 (238) 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/SvGridAlerts.svelte.d.ts +2 -2
  15. package/dist/SvGridBoard.svelte +2358 -2358
  16. package/dist/SvGridBulkEditDrawer.svelte.d.ts +1 -1
  17. package/dist/SvGridEditPanel.svelte +1157 -1157
  18. package/dist/SvGridEditPanel.svelte.d.ts +3 -3
  19. package/dist/SvGridMasterDetail.svelte +116 -116
  20. package/dist/SvGridMasterDetail.svelte.d.ts +1 -1
  21. package/dist/SvGridScheduler.svelte +5358 -5358
  22. package/dist/SvImportDialog.svelte +1020 -1020
  23. package/dist/SvImportDialog.svelte.d.ts +2 -2
  24. package/dist/SvLookupInput.svelte +180 -180
  25. package/dist/SvLookupInput.svelte.d.ts +1 -1
  26. package/dist/SvPivotDesigner.svelte +1990 -1990
  27. package/dist/SvPivotDesigner.svelte.d.ts +2 -2
  28. package/dist/SvRecordDetail.svelte.d.ts +1 -1
  29. package/dist/SvSchedule.svelte.d.ts +1 -1
  30. package/dist/SvSchemaChart.svelte +234 -234
  31. package/dist/SvSchemaChart.svelte.d.ts +2 -2
  32. package/dist/SvSchemaDashboard.svelte +130 -130
  33. package/dist/SvSchemaDashboard.svelte.d.ts +3 -3
  34. package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -1
  35. package/dist/advanced-filter-enable.js +3 -3
  36. package/dist/alerts/alert-engine-attach.d.ts +2 -2
  37. package/dist/alerts/alert-engine-attach.js +3 -3
  38. package/dist/alerts/alert-engine.d.ts +1 -1
  39. package/dist/alerts/alert-engine.js +2 -2
  40. package/dist/alerts/alert-formats.d.ts +1 -1
  41. package/dist/alerts/alert-formats.js +1 -1
  42. package/dist/alerts/alert-observer.d.ts +3 -3
  43. package/dist/alerts/alert-observer.js +1 -1
  44. package/dist/alerts/alert-storage.d.ts +1 -1
  45. package/dist/alerts/alert-store.svelte.d.ts +1 -1
  46. package/dist/alerts/alert-types.d.ts +2 -2
  47. package/dist/alerts.js +2 -2
  48. package/dist/board.js +2 -2
  49. package/dist/cdn/svgrid-enterprise.svelte-external.js +1162 -1145
  50. package/dist/designer/assets/GridMenus-B1JtIN21.js +8 -0
  51. package/dist/designer/assets/SvDateRangeInput-C0iCCZ8o.css +1 -0
  52. package/dist/designer/assets/{SvDateRangeInput-CVn-yqrQ.js → SvDateRangeInput-CjO7nn_x.js} +1 -1
  53. package/dist/designer/assets/SvDateTimePicker-ByBD_ov6.js +1 -0
  54. package/dist/designer/assets/SvDateTimePicker-DPv09DIT.css +1 -0
  55. package/dist/designer/assets/SvField-YDLipR6O.css +1 -0
  56. package/dist/designer/assets/{SvField-BsqiA5me.js → SvField-cj1PFNgZ.js} +1 -1
  57. package/dist/designer/assets/{preload-helper-CG0n22O7.js → SvGrid.helpers-BgT-gnCo.js} +1 -1
  58. package/dist/designer/assets/SvGridCellEditor-0W1L2hJ1.js +2 -0
  59. package/dist/designer/assets/SvGridChart-DBOR4xcz.js +2 -0
  60. package/dist/designer/assets/SvGridChart-fkMWeh1J.css +1 -0
  61. package/dist/designer/assets/SvGridChartPanel-symrhNnR.js +11 -0
  62. package/dist/designer/assets/SvGridChartView-BmRxq6mU.js +1 -0
  63. package/dist/designer/assets/SvGridChartView-DUc-1RUf.css +1 -0
  64. package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +1 -0
  65. package/dist/designer/assets/SvGridDropdown-CcidB3rm.css +1 -0
  66. package/dist/designer/assets/SvListBox-1oUF6Iav.js +110 -0
  67. package/dist/designer/assets/SvListBox-BXfyqy4a.css +1 -0
  68. package/dist/designer/assets/cell-formatting-wr5wm7US.js +1 -0
  69. package/dist/designer/assets/chart-DXFganU8.js +1 -0
  70. package/dist/designer/assets/date-format-DGagAqRc.css +1 -0
  71. package/dist/designer/assets/{date-format-4DJvNtG2.js → date-format-onlYDsrY.js} +1 -1
  72. package/dist/designer/assets/export-format-D-eP4b2l.js +6 -0
  73. package/dist/designer/assets/{index-BvaLmQSd.js → index-C_a9Cj8Y.js} +2984 -141
  74. package/dist/designer/assets/index-k2stW6L8.css +1 -0
  75. package/dist/designer/assets/preload-helper-HclGiUj8.js +1 -0
  76. package/dist/designer/assets/{row-drag-touch-IuJPEt0z.js → row-drag-touch-BdknxBNL.js} +1 -1
  77. package/dist/designer/assets/src-D9p27OGC.js +58 -0
  78. package/dist/designer/index.html +22 -22
  79. package/dist/export-conditional.d.ts +1 -1
  80. package/dist/export-print.js +19 -19
  81. package/dist/export-xls.d.ts +2 -2
  82. package/dist/export.d.ts +2 -2
  83. package/dist/export.js +7 -7
  84. package/dist/expressions/builder-tree.d.ts +2 -2
  85. package/dist/expressions/builder-tree.js +1 -1
  86. package/dist/expressions/compile.d.ts +1 -1
  87. package/dist/import.js +1 -1
  88. package/dist/index.d.ts +53 -53
  89. package/dist/index.js +50 -50
  90. package/dist/install.d.ts +4 -4
  91. package/dist/install.js +11 -11
  92. package/dist/license.d.ts +3 -2
  93. package/dist/license.js +9 -4
  94. package/dist/node/studio.js +124 -115
  95. package/dist/pivot-chart.d.ts +1 -1
  96. package/dist/pivot-designer.d.ts +1 -1
  97. package/dist/pivot-enable.js +3 -3
  98. package/dist/print.d.ts +1 -1
  99. package/dist/print.js +4 -4
  100. package/dist/scheduler-config.d.ts +5 -5
  101. package/dist/scheduler-freebusy.d.ts +1 -1
  102. package/dist/scheduler-freebusy.js +1 -1
  103. package/dist/scheduler.js +2 -2
  104. package/dist/schema-designer.d.ts +1 -1
  105. package/dist/schema-designer.js +2 -2
  106. package/dist/selection-bar.js +2 -2
  107. package/dist/sources/aggregate.d.ts +1 -1
  108. package/dist/sources/auth-supabase.js +1 -1
  109. package/dist/sources/dashboard.d.ts +2 -2
  110. package/dist/sources/dashboard.js +1 -1
  111. package/dist/sources/index.d.ts +13 -13
  112. package/dist/sources/index.js +13 -13
  113. package/dist/sources/realtime-supabase.js +1 -1
  114. package/dist/sources/relation-lookup.d.ts +1 -1
  115. package/dist/sources/relation-lookup.js +1 -1
  116. package/dist/sources/rest-adapters.d.ts +1 -1
  117. package/dist/sources/rest-adapters.js +1 -1
  118. package/dist/sources/rest.d.ts +2 -2
  119. package/dist/sources/rest.js +3 -3
  120. package/dist/sources/supabase.d.ts +2 -2
  121. package/dist/sources/supabase.js +3 -3
  122. package/dist/sources/with-entity-rules.d.ts +1 -1
  123. package/dist/sources/with-entity-rules.js +1 -1
  124. package/dist/sources/with-relation-labels.d.ts +1 -1
  125. package/dist/studio/emit-project.js +2161 -2127
  126. package/dist/studio/emit-schema.js +369 -369
  127. package/dist/studio/index.d.ts +1 -1
  128. package/dist/studio/index.js +1 -1
  129. package/dist/studio/introspect-db.js +45 -45
  130. package/dist/studio/project.d.ts +10 -0
  131. package/dist/studio/samples/crm.js +6 -6
  132. package/dist/studio/samples/fleet.js +7 -7
  133. package/dist/studio/samples/insurance.js +9 -9
  134. package/dist/studio/samples/inventory.js +12 -12
  135. package/dist/studio/samples/projects.js +5 -5
  136. package/dist/studio/samples/support.js +8 -8
  137. package/dist/studio/scaffold-app.js +104 -104
  138. package/dist/studio/scaffold.js +151 -151
  139. package/dist/studio/ui-components.d.ts +7 -0
  140. package/dist/studio/ui-components.js +35 -0
  141. package/dist/sveltekit/in-memory.d.ts +3 -3
  142. package/dist/sveltekit/in-memory.js +4 -4
  143. package/dist/sveltekit/index.d.ts +6 -6
  144. package/dist/sveltekit/index.js +5 -5
  145. package/dist/sveltekit/query-plan.d.ts +1 -1
  146. package/dist/sveltekit/sql-source.d.ts +4 -4
  147. package/dist/sveltekit/sql-source.js +4 -4
  148. package/dist/sveltekit/sql.d.ts +1 -1
  149. package/dist/sveltekit/transport.d.ts +2 -2
  150. package/dist/sveltekit/transport.js +1 -1
  151. package/dist/upgrade-prompt.js +25 -25
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/package.json +6 -3
  155. package/dist/designer/assets/GridMenus-Cgilexpk.js +0 -8
  156. package/dist/designer/assets/SvDateRangeInput-BV5OXYQ2.css +0 -1
  157. package/dist/designer/assets/SvDateTimePicker-BdioTrJQ.js +0 -1
  158. package/dist/designer/assets/SvDateTimePicker-CY8TK4BT.css +0 -1
  159. package/dist/designer/assets/SvField-BF-9TJSd.css +0 -1
  160. package/dist/designer/assets/SvGridCellEditor-dLY7_kk9.js +0 -2
  161. package/dist/designer/assets/SvGridChart-BEDNTdc5.js +0 -2
  162. package/dist/designer/assets/SvGridChart-Vu4Nvjiv.css +0 -1
  163. package/dist/designer/assets/SvGridChartPanel-x3lzSxuf.js +0 -10
  164. package/dist/designer/assets/SvGridChartView-CAKhPELi.js +0 -1
  165. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +0 -1
  166. package/dist/designer/assets/SvGridDropdown-CAYG7Y-O.js +0 -1
  167. package/dist/designer/assets/SvGridDropdown-Crwb2P3Q.css +0 -1
  168. package/dist/designer/assets/SvListBox-CVYQcLX9.js +0 -1
  169. package/dist/designer/assets/SvListBox-DgN8kvjt.css +0 -1
  170. package/dist/designer/assets/chart-DHEgt4lN.js +0 -1
  171. package/dist/designer/assets/date-format-Dzy4mTGh.css +0 -1
  172. package/dist/designer/assets/export-format-DwjuTfrQ.js +0 -6
  173. package/dist/designer/assets/index-Cp7fDJ3b.css +0 -1
  174. package/dist/designer/assets/js-scroller.svelte-Bhxqb3-c.js +0 -110
  175. package/dist/designer/assets/src-7qOYsBug.js +0 -2896
  176. package/dist/designer/assets/src-B7DNcoQK.css +0 -1
  177. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +0 -1
  178. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +0 -178
  179. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +0 -3875
  180. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +0 -1
  181. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +0 -21
  182. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +0 -1
  183. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +0 -1938
  184. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +0 -1
  185. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +0 -205
  186. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +0 -1
  187. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +0 -18
  188. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +0 -1
  189. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +0 -264
  190. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +0 -1
  191. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +0 -697
  192. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +0 -1
  193. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +0 -3
  194. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +0 -134
  195. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +0 -1
  196. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +0 -55
  197. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +0 -1
  198. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +0 -2
  199. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +0 -46843
  200. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +0 -1
  201. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +0 -237
  202. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +0 -1
  203. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +0 -2
  204. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +0 -4
  205. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +0 -1
  206. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +0 -7
  207. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +0 -4
  208. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +0 -1
  209. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +0 -5
  210. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +0 -1
  211. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +0 -5
  212. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +0 -1
  213. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +0 -5
  214. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +0 -1
  215. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +0 -6
  216. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +0 -83
  217. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +0 -1
  218. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +0 -559
  219. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +0 -1
  220. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +0 -24
  221. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +0 -1
  222. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +0 -122
  223. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +0 -1
  224. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +0 -2
  225. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +0 -89
  226. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +0 -1
  227. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +0 -241
  228. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +0 -1
  229. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +0 -4938
  230. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +0 -1
  231. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +0 -495
  232. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +0 -1
  233. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +0 -190
  234. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +0 -1
  235. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +0 -8
  236. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +0 -1
  237. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +0 -138
  238. package/dist/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +0 -1
@@ -1,553 +1,553 @@
1
- <script lang="ts" generics="TFeatures extends TableFeatures, TData extends RowData">
2
- /**
3
- * SvExportMenu - a drop-in export button + menu for a licensed grid. Wraps
4
- * `api.exportData` / `api.copyExport` / `api.print` so consumers don't
5
- * hand-roll a toolbar in every app:
6
- * - a sectioned format picker with per-format icons + descriptions,
7
- * - a row-scope toggle (current view / selected / all rows),
8
- * - a per-column include picker,
9
- * - copy-to-clipboard + optional print actions,
10
- * - a progress bar + Cancel for large exports (via AbortController),
11
- * - inline error surface when a peer dep is missing or the grid is empty.
12
- *
13
- * Minimal, unopinionated styling via a few overridable classes / CSS vars.
14
- */
15
- import type { RowData, TableFeatures } from '@svgrid/grid'
16
- import type { ConditionalFormat } from '@svgrid/grid/format'
17
- import type { EnterpriseGridApi } from './install'
18
- import type {
19
- ClipboardFormat,
20
- ExportColumn,
21
- ExportFormat,
22
- ExportProgress,
23
- ExportRowSource,
24
- } from './export'
25
-
26
- type Props = {
27
- api: EnterpriseGridApi<TFeatures, TData> | null | undefined
28
- /** Base filename (no extension). Default 'grid'. */
29
- filename?: string
30
- /** Which formats to offer, in order. Default xlsx/xls/pdf/csv/tsv/html. */
31
- formats?: ReadonlyArray<ExportFormat>
32
- /** Show the "current view / selected / all" scope toggle. Default true. */
33
- allowScope?: boolean
34
- /** Show the per-column include checkboxes. Default true. */
35
- allowColumns?: boolean
36
- /** Show the "Copy to clipboard" actions. Default true. */
37
- allowCopy?: boolean
38
- /** Show a "Print / Save as PDF" action. Default false. */
39
- allowPrint?: boolean
40
- /** Initial scope. Default 'displayed'. */
41
- scope?: Extract<ExportRowSource<TData>, string>
42
- /** Button label. Default 'Export'. */
43
- label?: string
44
- /** Conditional formatting rules to carry into styled exports (pdf/xlsx/html). */
45
- conditionalFormats?: ReadonlyArray<ConditionalFormat<TData>>
46
- }
47
-
48
- let {
49
- api,
50
- filename = 'grid',
51
- formats = ['xlsx', 'xls', 'pdf', 'csv', 'tsv', 'html'],
52
- allowScope = true,
53
- allowColumns = true,
54
- allowCopy = true,
55
- allowPrint = false,
56
- scope = 'displayed',
57
- label = 'Export',
58
- conditionalFormats,
59
- }: Props = $props()
60
-
61
- type IconKind = 'sheet' | 'doc' | 'text' | 'code'
62
- const FORMAT_META: Record<ExportFormat, { label: string; desc: string; icon: IconKind }> = {
63
- xlsx: { label: 'Excel', desc: '.xlsx, formatted', icon: 'sheet' },
64
- xls: { label: 'Excel 97-2003', desc: 'Legacy .xls', icon: 'sheet' },
65
- pdf: { label: 'PDF', desc: 'Paginated document', icon: 'doc' },
66
- csv: { label: 'CSV', desc: 'Comma-separated', icon: 'text' },
67
- tsv: { label: 'TSV', desc: 'Tab-separated', icon: 'text' },
68
- html: { label: 'HTML', desc: 'Styled table', icon: 'code' },
69
- json: { label: 'JSON', desc: 'Data array', icon: 'code' },
70
- xml: { label: 'XML', desc: 'Rows + fields', icon: 'code' },
71
- md: { label: 'Markdown', desc: 'GFM table', icon: 'text' },
72
- }
73
- const SCOPES: Array<{ value: Extract<ExportRowSource<TData>, string>; label: string }> = [
74
- { value: 'displayed', label: 'Current view' },
75
- { value: 'selected', label: 'Selected rows' },
76
- { value: 'all', label: 'All rows' },
77
- ]
78
-
79
- let open = $state(false)
80
- let currentScope = $state<Extract<ExportRowSource<TData>, string>>(scope)
81
- let busy = $state(false)
82
- let progress = $state(0)
83
- let error = $state<string | null>(null)
84
- let copied = $state<string | null>(null)
85
- let controller: AbortController | null = null
86
- let rootEl: HTMLDivElement | undefined = $state()
87
- let menuEl: HTMLDivElement | undefined = $state()
88
- let triggerEl: HTMLButtonElement | undefined = $state()
89
-
90
- // --- Keyboard accessibility -------------------------------------------
91
- /** Focusable controls inside the menu, in DOM order. */
92
- function menuItems(): HTMLElement[] {
93
- if (!menuEl) return []
94
- return Array.from(
95
- menuEl.querySelectorAll<HTMLElement>(
96
- 'button:not([disabled]), input:not([disabled]), [tabindex="0"]',
97
- ),
98
- )
99
- }
100
-
101
- function focusFirst() {
102
- // Wait a tick so the menu is rendered, then focus its first control.
103
- requestAnimationFrame(() => menuItems()[0]?.focus())
104
- }
105
-
106
- function closeMenu(returnFocus = true) {
107
- open = false
108
- if (returnFocus) triggerEl?.focus()
109
- }
110
-
111
- function onMenuKeydown(e: KeyboardEvent) {
112
- const items = menuItems()
113
- if (items.length === 0) return
114
- const idx = items.indexOf(document.activeElement as HTMLElement)
115
- if (e.key === 'Escape') {
116
- e.preventDefault()
117
- closeMenu()
118
- } else if (e.key === 'ArrowDown') {
119
- e.preventDefault()
120
- items[(idx + 1 + items.length) % items.length]?.focus()
121
- } else if (e.key === 'ArrowUp') {
122
- e.preventDefault()
123
- items[(idx - 1 + items.length) % items.length]?.focus()
124
- } else if (e.key === 'Home') {
125
- e.preventDefault()
126
- items[0]?.focus()
127
- } else if (e.key === 'End') {
128
- e.preventDefault()
129
- items[items.length - 1]?.focus()
130
- } else if (e.key === 'Tab') {
131
- // Trap focus within the menu (wrap at the ends).
132
- if (e.shiftKey && (idx <= 0)) {
133
- e.preventDefault()
134
- items[items.length - 1]?.focus()
135
- } else if (!e.shiftKey && idx === items.length - 1) {
136
- e.preventDefault()
137
- items[0]?.focus()
138
- }
139
- }
140
- }
141
-
142
- let picks = $state<Array<{ field: string; header: string; checked: boolean }>>([])
143
- let showColumns = $state(false)
144
-
145
- function refreshColumns() {
146
- if (!api) return
147
- picks = api
148
- .getColumns()
149
- .filter((c) => c.visible && c.field)
150
- .map((c) => ({ field: c.field!, header: c.header, checked: true }))
151
- }
152
-
153
- function toggleOpen() {
154
- open = !open
155
- copied = null
156
- error = null
157
- if (open) {
158
- refreshColumns()
159
- focusFirst()
160
- }
161
- }
162
-
163
- function selectedColumns(): ExportColumn<TData>[] | undefined {
164
- if (!allowColumns || picks.length === 0) return undefined
165
- const chosen = picks.filter((p) => p.checked)
166
- if (chosen.length === picks.length) return undefined
167
- return chosen.map((p) => ({ field: p.field, header: p.header }))
168
- }
169
-
170
- function onDocClick(e: MouseEvent) {
171
- if (rootEl && !rootEl.contains(e.target as Node)) open = false
172
- }
173
-
174
- async function withBusy(fn: () => Promise<unknown>, closeOnDone = true) {
175
- if (!api || busy) return
176
- error = null
177
- copied = null
178
- busy = true
179
- progress = 0
180
- controller = new AbortController()
181
- try {
182
- await fn()
183
- if (closeOnDone) closeMenu()
184
- } catch (err) {
185
- if ((err as { name?: string })?.name === 'AbortError') error = 'Export cancelled.'
186
- else error = err instanceof Error ? err.message : String(err)
187
- } finally {
188
- busy = false
189
- controller = null
190
- }
191
- }
192
-
193
- async function run(format: ExportFormat) {
194
- await withBusy(() =>
195
- api!.exportData({
196
- format,
197
- filename,
198
- rows: currentScope,
199
- columns: selectedColumns(),
200
- conditionalFormats,
201
- signal: controller!.signal,
202
- onProgress: (p: ExportProgress) => {
203
- progress = p.ratio
204
- },
205
- }),
206
- )
207
- }
208
-
209
- async function copy(format: ClipboardFormat) {
210
- await withBusy(
211
- () => api!.copyExport({ format, rows: currentScope, columns: selectedColumns() }),
212
- false,
213
- )
214
- if (!error) copied = format === 'tsv' ? 'for Excel' : format
215
- }
216
-
217
- async function print() {
218
- await withBusy(() =>
219
- api!.print({ title: filename, rows: currentScope, columns: selectedColumns() }),
220
- )
221
- }
222
-
223
- function cancel() {
224
- controller?.abort()
225
- }
226
- </script>
227
-
228
- <svelte:document onclick={onDocClick} />
229
-
230
- {#snippet formatIcon(kind: IconKind)}
231
- <svg class="sv-export-icon" width="16" height="16" viewBox="0 0 24 24" fill="none"
232
- stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
233
- {#if kind === 'sheet'}
234
- <rect x="3" y="3" width="18" height="18" rx="1.5" /><path d="M3 9h18M3 15h18M9 3v18M15 3v18" />
235
- {:else if kind === 'doc'}
236
- <path d="M6 2.5h8l4 4v15H6z" /><path d="M14 2.5v4h4" />
237
- {:else if kind === 'text'}
238
- <path d="M5 6h14M5 12h14M5 18h9" />
239
- {:else}
240
- <path d="M8 7l-4 5 4 5M16 7l4 5-4 5" />
241
- {/if}
242
- </svg>
243
- {/snippet}
244
-
245
- <div class="sv-export" bind:this={rootEl}>
246
- <button
247
- type="button"
248
- class="sv-export-btn"
249
- aria-haspopup="menu"
250
- aria-expanded={open}
251
- disabled={!api}
252
- bind:this={triggerEl}
253
- onclick={toggleOpen}
254
- >
255
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
256
- stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
257
- <path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
258
- </svg>
259
- {label}
260
- <span class="sv-export-caret" aria-hidden="true">▾</span>
261
- </button>
262
-
263
- {#if open}
264
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
265
- <div
266
- class="sv-export-menu"
267
- role="menu"
268
- aria-label="Export options"
269
- bind:this={menuEl}
270
- onkeydown={onMenuKeydown}
271
- >
272
- {#if allowScope}
273
- <div class="sv-export-section">
274
- <span class="sv-export-section-label">Rows</span>
275
- <div class="sv-export-scope">
276
- {#each SCOPES as s (s.value)}
277
- <label class="sv-export-opt">
278
- <input
279
- type="radio"
280
- name="sv-export-scope"
281
- value={s.value}
282
- checked={currentScope === s.value}
283
- onchange={() => (currentScope = s.value)}
284
- />
285
- {s.label}
286
- </label>
287
- {/each}
288
- </div>
289
- </div>
290
- {/if}
291
-
292
- {#if allowColumns && picks.length > 0}
293
- <div class="sv-export-section">
294
- <button type="button" class="sv-export-cols-toggle" onclick={() => (showColumns = !showColumns)}>
295
- Columns <span class="sv-export-count">{picks.filter((p) => p.checked).length}/{picks.length}</span>
296
- <span aria-hidden="true">{showColumns ? '▴' : '▾'}</span>
297
- </button>
298
- {#if showColumns}
299
- <div class="sv-export-cols-list">
300
- {#each picks as p (p.field)}
301
- <label class="sv-export-opt">
302
- <input type="checkbox" bind:checked={p.checked} />
303
- {p.header}
304
- </label>
305
- {/each}
306
- </div>
307
- {/if}
308
- </div>
309
- {/if}
310
-
311
- <div class="sv-export-section">
312
- <span class="sv-export-section-label">Download</span>
313
- {#each formats as f (f)}
314
- <button type="button" class="sv-export-item" role="menuitem" disabled={busy} onclick={() => run(f)}>
315
- {@render formatIcon(FORMAT_META[f].icon)}
316
- <span class="sv-export-item-text">
317
- <span class="sv-export-item-label">{FORMAT_META[f].label}</span>
318
- <span class="sv-export-item-desc">{FORMAT_META[f].desc}</span>
319
- </span>
320
- </button>
321
- {/each}
322
- </div>
323
-
324
- {#if allowCopy || allowPrint}
325
- <div class="sv-export-actions">
326
- {#if allowCopy}
327
- <button type="button" class="sv-export-chip" disabled={busy} onclick={() => copy('tsv')}>Copy for Excel</button>
328
- <button type="button" class="sv-export-chip" disabled={busy} onclick={() => copy('md')}>Copy Markdown</button>
329
- {/if}
330
- {#if allowPrint}
331
- <button type="button" class="sv-export-chip" disabled={busy} onclick={print}>Print…</button>
332
- {/if}
333
- {#if copied}<span class="sv-export-copied">Copied {copied} ✓</span>{/if}
334
- </div>
335
- {/if}
336
-
337
- {#if busy}
338
- <div class="sv-export-progress">
339
- <div class="sv-export-track"><div class="sv-export-bar" style:width={`${Math.round(progress * 100)}%`}></div></div>
340
- <button type="button" class="sv-export-cancel" onclick={cancel}>Cancel</button>
341
- </div>
342
- {/if}
343
-
344
- {#if error}
345
- <p class="sv-export-error" role="alert">{error}</p>
346
- {/if}
347
- </div>
348
- {/if}
349
- </div>
350
-
351
- <style>
352
- .sv-export {
353
- position: relative;
354
- display: inline-block;
355
- }
356
- .sv-export-btn {
357
- display: inline-flex;
358
- align-items: center;
359
- gap: 6px;
360
- padding: 6px 12px;
361
- font: inherit;
362
- font-size: 13px;
363
- font-weight: 500;
364
- border: 1px solid var(--sg-border, #d0d7de);
365
- border-radius: 8px;
366
- background: var(--sg-header-bg, #f6f8fa);
367
- color: var(--sg-fg, #1f2328);
368
- cursor: pointer;
369
- }
370
- .sv-export-btn:disabled {
371
- opacity: 0.5;
372
- cursor: not-allowed;
373
- }
374
- .sv-export-caret {
375
- font-size: 10px;
376
- opacity: 0.7;
377
- }
378
- .sv-export-menu {
379
- position: absolute;
380
- z-index: 50;
381
- top: calc(100% + 6px);
382
- left: 0;
383
- min-width: 232px;
384
- padding: 6px;
385
- border: 1px solid var(--sg-border, #d0d7de);
386
- border-radius: 12px;
387
- background: var(--sg-bg, #fff);
388
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14);
389
- }
390
- .sv-export-section {
391
- padding: 6px 6px 8px;
392
- border-bottom: 1px solid var(--sg-border, #eaecef);
393
- }
394
- .sv-export-section:last-of-type {
395
- border-bottom: none;
396
- }
397
- .sv-export-section-label {
398
- display: block;
399
- padding: 2px 6px 6px;
400
- font-size: 10px;
401
- font-weight: 700;
402
- text-transform: uppercase;
403
- letter-spacing: 0.07em;
404
- color: var(--sg-muted, #8b949e);
405
- }
406
- .sv-export-scope {
407
- display: flex;
408
- flex-direction: column;
409
- gap: 1px;
410
- }
411
- .sv-export-opt {
412
- display: flex;
413
- align-items: center;
414
- gap: 7px;
415
- padding: 3px 6px;
416
- font-size: 12.5px;
417
- color: var(--sg-fg, #1f2328);
418
- cursor: pointer;
419
- border-radius: 6px;
420
- }
421
- .sv-export-opt:hover {
422
- background: var(--sg-header-bg, #f6f8fa);
423
- }
424
- .sv-export-item {
425
- display: flex;
426
- align-items: center;
427
- gap: 10px;
428
- width: 100%;
429
- text-align: left;
430
- padding: 6px 8px;
431
- font: inherit;
432
- border: none;
433
- border-radius: 8px;
434
- background: transparent;
435
- color: var(--sg-fg, #1f2328);
436
- cursor: pointer;
437
- }
438
- .sv-export-item:hover:not(:disabled) {
439
- background: var(--sg-header-bg, #f6f8fa);
440
- }
441
- .sv-export-item:disabled {
442
- opacity: 0.5;
443
- cursor: default;
444
- }
445
- .sv-export-icon {
446
- flex-shrink: 0;
447
- color: var(--site-accent-2, #6366f1);
448
- }
449
- .sv-export-item-text {
450
- display: flex;
451
- flex-direction: column;
452
- line-height: 1.25;
453
- }
454
- .sv-export-item-label {
455
- font-size: 13px;
456
- font-weight: 500;
457
- }
458
- .sv-export-item-desc {
459
- font-size: 11px;
460
- color: var(--sg-muted, #8b949e);
461
- }
462
- .sv-export-cols-toggle {
463
- display: flex;
464
- align-items: center;
465
- gap: 6px;
466
- width: 100%;
467
- font: inherit;
468
- font-size: 11px;
469
- font-weight: 700;
470
- text-transform: uppercase;
471
- letter-spacing: 0.07em;
472
- color: var(--sg-muted, #8b949e);
473
- background: transparent;
474
- border: none;
475
- padding: 2px 6px;
476
- cursor: pointer;
477
- }
478
- .sv-export-count {
479
- font-weight: 600;
480
- text-transform: none;
481
- letter-spacing: 0;
482
- color: var(--sg-fg, #1f2328);
483
- }
484
- .sv-export-cols-toggle > span:last-child {
485
- margin-left: auto;
486
- }
487
- .sv-export-cols-list {
488
- max-height: 168px;
489
- overflow-y: auto;
490
- margin-top: 4px;
491
- }
492
- .sv-export-actions {
493
- display: flex;
494
- align-items: center;
495
- flex-wrap: wrap;
496
- gap: 6px;
497
- padding: 8px 6px 4px;
498
- }
499
- .sv-export-chip {
500
- font: inherit;
501
- font-size: 12px;
502
- padding: 4px 10px;
503
- border-radius: 999px;
504
- border: 1px solid var(--sg-border, #d0d7de);
505
- background: var(--sg-header-bg, #f6f8fa);
506
- color: var(--sg-fg, #1f2328);
507
- cursor: pointer;
508
- }
509
- .sv-export-chip:hover:not(:disabled) {
510
- border-color: var(--site-accent, #3b82f6);
511
- }
512
- .sv-export-chip:disabled {
513
- opacity: 0.5;
514
- cursor: default;
515
- }
516
- .sv-export-copied {
517
- font-size: 11px;
518
- color: #16a34a;
519
- }
520
- .sv-export-progress {
521
- display: flex;
522
- align-items: center;
523
- gap: 8px;
524
- padding: 8px 6px 4px;
525
- }
526
- .sv-export-track {
527
- flex: 1;
528
- min-width: 0;
529
- height: 6px;
530
- border-radius: 999px;
531
- background: var(--sg-border, #eaecef);
532
- overflow: hidden;
533
- }
534
- .sv-export-bar {
535
- height: 100%;
536
- border-radius: 999px;
537
- background: var(--site-accent, #3b82f6);
538
- transition: width 120ms ease;
539
- }
540
- .sv-export-cancel {
541
- font: inherit;
542
- font-size: 12px;
543
- border: none;
544
- background: transparent;
545
- color: var(--site-accent-2, #6366f1);
546
- cursor: pointer;
547
- }
548
- .sv-export-error {
549
- margin: 6px 6px 2px;
550
- font-size: 12px;
551
- color: #dc2626;
552
- }
553
- </style>
1
+ <script lang="ts" generics="TFeatures extends TableFeatures, TData extends RowData">
2
+ /**
3
+ * SvExportMenu - a drop-in export button + menu for a licensed grid. Wraps
4
+ * `api.exportData` / `api.copyExport` / `api.print` so consumers don't
5
+ * hand-roll a toolbar in every app:
6
+ * - a sectioned format picker with per-format icons + descriptions,
7
+ * - a row-scope toggle (current view / selected / all rows),
8
+ * - a per-column include picker,
9
+ * - copy-to-clipboard + optional print actions,
10
+ * - a progress bar + Cancel for large exports (via AbortController),
11
+ * - inline error surface when a peer dep is missing or the grid is empty.
12
+ *
13
+ * Minimal, unopinionated styling via a few overridable classes / CSS vars.
14
+ */
15
+ import type { RowData, TableFeatures } from '@svgrid/grid'
16
+ import type { ConditionalFormat } from '@svgrid/grid/format'
17
+ import type { EnterpriseGridApi } from './install'
18
+ import type {
19
+ ClipboardFormat,
20
+ ExportColumn,
21
+ ExportFormat,
22
+ ExportProgress,
23
+ ExportRowSource,
24
+ } from './export'
25
+
26
+ type Props = {
27
+ api: EnterpriseGridApi<TFeatures, TData> | null | undefined
28
+ /** Base filename (no extension). Default 'grid'. */
29
+ filename?: string
30
+ /** Which formats to offer, in order. Default xlsx/xls/pdf/csv/tsv/html. */
31
+ formats?: ReadonlyArray<ExportFormat>
32
+ /** Show the "current view / selected / all" scope toggle. Default true. */
33
+ allowScope?: boolean
34
+ /** Show the per-column include checkboxes. Default true. */
35
+ allowColumns?: boolean
36
+ /** Show the "Copy to clipboard" actions. Default true. */
37
+ allowCopy?: boolean
38
+ /** Show a "Print / Save as PDF" action. Default false. */
39
+ allowPrint?: boolean
40
+ /** Initial scope. Default 'displayed'. */
41
+ scope?: Extract<ExportRowSource<TData>, string>
42
+ /** Button label. Default 'Export'. */
43
+ label?: string
44
+ /** Conditional formatting rules to carry into styled exports (pdf/xlsx/html). */
45
+ conditionalFormats?: ReadonlyArray<ConditionalFormat<TData>>
46
+ }
47
+
48
+ let {
49
+ api,
50
+ filename = 'grid',
51
+ formats = ['xlsx', 'xls', 'pdf', 'csv', 'tsv', 'html'],
52
+ allowScope = true,
53
+ allowColumns = true,
54
+ allowCopy = true,
55
+ allowPrint = false,
56
+ scope = 'displayed',
57
+ label = 'Export',
58
+ conditionalFormats,
59
+ }: Props = $props()
60
+
61
+ type IconKind = 'sheet' | 'doc' | 'text' | 'code'
62
+ const FORMAT_META: Record<ExportFormat, { label: string; desc: string; icon: IconKind }> = {
63
+ xlsx: { label: 'Excel', desc: '.xlsx, formatted', icon: 'sheet' },
64
+ xls: { label: 'Excel 97-2003', desc: 'Legacy .xls', icon: 'sheet' },
65
+ pdf: { label: 'PDF', desc: 'Paginated document', icon: 'doc' },
66
+ csv: { label: 'CSV', desc: 'Comma-separated', icon: 'text' },
67
+ tsv: { label: 'TSV', desc: 'Tab-separated', icon: 'text' },
68
+ html: { label: 'HTML', desc: 'Styled table', icon: 'code' },
69
+ json: { label: 'JSON', desc: 'Data array', icon: 'code' },
70
+ xml: { label: 'XML', desc: 'Rows + fields', icon: 'code' },
71
+ md: { label: 'Markdown', desc: 'GFM table', icon: 'text' },
72
+ }
73
+ const SCOPES: Array<{ value: Extract<ExportRowSource<TData>, string>; label: string }> = [
74
+ { value: 'displayed', label: 'Current view' },
75
+ { value: 'selected', label: 'Selected rows' },
76
+ { value: 'all', label: 'All rows' },
77
+ ]
78
+
79
+ let open = $state(false)
80
+ let currentScope = $state<Extract<ExportRowSource<TData>, string>>(scope)
81
+ let busy = $state(false)
82
+ let progress = $state(0)
83
+ let error = $state<string | null>(null)
84
+ let copied = $state<string | null>(null)
85
+ let controller: AbortController | null = null
86
+ let rootEl: HTMLDivElement | undefined = $state()
87
+ let menuEl: HTMLDivElement | undefined = $state()
88
+ let triggerEl: HTMLButtonElement | undefined = $state()
89
+
90
+ // --- Keyboard accessibility -------------------------------------------
91
+ /** Focusable controls inside the menu, in DOM order. */
92
+ function menuItems(): HTMLElement[] {
93
+ if (!menuEl) return []
94
+ return Array.from(
95
+ menuEl.querySelectorAll<HTMLElement>(
96
+ 'button:not([disabled]), input:not([disabled]), [tabindex="0"]',
97
+ ),
98
+ )
99
+ }
100
+
101
+ function focusFirst() {
102
+ // Wait a tick so the menu is rendered, then focus its first control.
103
+ requestAnimationFrame(() => menuItems()[0]?.focus())
104
+ }
105
+
106
+ function closeMenu(returnFocus = true) {
107
+ open = false
108
+ if (returnFocus) triggerEl?.focus()
109
+ }
110
+
111
+ function onMenuKeydown(e: KeyboardEvent) {
112
+ const items = menuItems()
113
+ if (items.length === 0) return
114
+ const idx = items.indexOf(document.activeElement as HTMLElement)
115
+ if (e.key === 'Escape') {
116
+ e.preventDefault()
117
+ closeMenu()
118
+ } else if (e.key === 'ArrowDown') {
119
+ e.preventDefault()
120
+ items[(idx + 1 + items.length) % items.length]?.focus()
121
+ } else if (e.key === 'ArrowUp') {
122
+ e.preventDefault()
123
+ items[(idx - 1 + items.length) % items.length]?.focus()
124
+ } else if (e.key === 'Home') {
125
+ e.preventDefault()
126
+ items[0]?.focus()
127
+ } else if (e.key === 'End') {
128
+ e.preventDefault()
129
+ items[items.length - 1]?.focus()
130
+ } else if (e.key === 'Tab') {
131
+ // Trap focus within the menu (wrap at the ends).
132
+ if (e.shiftKey && (idx <= 0)) {
133
+ e.preventDefault()
134
+ items[items.length - 1]?.focus()
135
+ } else if (!e.shiftKey && idx === items.length - 1) {
136
+ e.preventDefault()
137
+ items[0]?.focus()
138
+ }
139
+ }
140
+ }
141
+
142
+ let picks = $state<Array<{ field: string; header: string; checked: boolean }>>([])
143
+ let showColumns = $state(false)
144
+
145
+ function refreshColumns() {
146
+ if (!api) return
147
+ picks = api
148
+ .getColumns()
149
+ .filter((c) => c.visible && c.field)
150
+ .map((c) => ({ field: c.field!, header: c.header, checked: true }))
151
+ }
152
+
153
+ function toggleOpen() {
154
+ open = !open
155
+ copied = null
156
+ error = null
157
+ if (open) {
158
+ refreshColumns()
159
+ focusFirst()
160
+ }
161
+ }
162
+
163
+ function selectedColumns(): ExportColumn<TData>[] | undefined {
164
+ if (!allowColumns || picks.length === 0) return undefined
165
+ const chosen = picks.filter((p) => p.checked)
166
+ if (chosen.length === picks.length) return undefined
167
+ return chosen.map((p) => ({ field: p.field, header: p.header }))
168
+ }
169
+
170
+ function onDocClick(e: MouseEvent) {
171
+ if (rootEl && !rootEl.contains(e.target as Node)) open = false
172
+ }
173
+
174
+ async function withBusy(fn: () => Promise<unknown>, closeOnDone = true) {
175
+ if (!api || busy) return
176
+ error = null
177
+ copied = null
178
+ busy = true
179
+ progress = 0
180
+ controller = new AbortController()
181
+ try {
182
+ await fn()
183
+ if (closeOnDone) closeMenu()
184
+ } catch (err) {
185
+ if ((err as { name?: string })?.name === 'AbortError') error = 'Export cancelled.'
186
+ else error = err instanceof Error ? err.message : String(err)
187
+ } finally {
188
+ busy = false
189
+ controller = null
190
+ }
191
+ }
192
+
193
+ async function run(format: ExportFormat) {
194
+ await withBusy(() =>
195
+ api!.exportData({
196
+ format,
197
+ filename,
198
+ rows: currentScope,
199
+ columns: selectedColumns(),
200
+ conditionalFormats,
201
+ signal: controller!.signal,
202
+ onProgress: (p: ExportProgress) => {
203
+ progress = p.ratio
204
+ },
205
+ }),
206
+ )
207
+ }
208
+
209
+ async function copy(format: ClipboardFormat) {
210
+ await withBusy(
211
+ () => api!.copyExport({ format, rows: currentScope, columns: selectedColumns() }),
212
+ false,
213
+ )
214
+ if (!error) copied = format === 'tsv' ? 'for Excel' : format
215
+ }
216
+
217
+ async function print() {
218
+ await withBusy(() =>
219
+ api!.print({ title: filename, rows: currentScope, columns: selectedColumns() }),
220
+ )
221
+ }
222
+
223
+ function cancel() {
224
+ controller?.abort()
225
+ }
226
+ </script>
227
+
228
+ <svelte:document onclick={onDocClick} />
229
+
230
+ {#snippet formatIcon(kind: IconKind)}
231
+ <svg class="sv-export-icon" width="16" height="16" viewBox="0 0 24 24" fill="none"
232
+ stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
233
+ {#if kind === 'sheet'}
234
+ <rect x="3" y="3" width="18" height="18" rx="1.5" /><path d="M3 9h18M3 15h18M9 3v18M15 3v18" />
235
+ {:else if kind === 'doc'}
236
+ <path d="M6 2.5h8l4 4v15H6z" /><path d="M14 2.5v4h4" />
237
+ {:else if kind === 'text'}
238
+ <path d="M5 6h14M5 12h14M5 18h9" />
239
+ {:else}
240
+ <path d="M8 7l-4 5 4 5M16 7l4 5-4 5" />
241
+ {/if}
242
+ </svg>
243
+ {/snippet}
244
+
245
+ <div class="sv-export" bind:this={rootEl}>
246
+ <button
247
+ type="button"
248
+ class="sv-export-btn"
249
+ aria-haspopup="menu"
250
+ aria-expanded={open}
251
+ disabled={!api}
252
+ bind:this={triggerEl}
253
+ onclick={toggleOpen}
254
+ >
255
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
256
+ stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
257
+ <path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
258
+ </svg>
259
+ {label}
260
+ <span class="sv-export-caret" aria-hidden="true">▾</span>
261
+ </button>
262
+
263
+ {#if open}
264
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
265
+ <div
266
+ class="sv-export-menu"
267
+ role="menu"
268
+ aria-label="Export options"
269
+ bind:this={menuEl}
270
+ onkeydown={onMenuKeydown}
271
+ >
272
+ {#if allowScope}
273
+ <div class="sv-export-section">
274
+ <span class="sv-export-section-label">Rows</span>
275
+ <div class="sv-export-scope">
276
+ {#each SCOPES as s (s.value)}
277
+ <label class="sv-export-opt">
278
+ <input
279
+ type="radio"
280
+ name="sv-export-scope"
281
+ value={s.value}
282
+ checked={currentScope === s.value}
283
+ onchange={() => (currentScope = s.value)}
284
+ />
285
+ {s.label}
286
+ </label>
287
+ {/each}
288
+ </div>
289
+ </div>
290
+ {/if}
291
+
292
+ {#if allowColumns && picks.length > 0}
293
+ <div class="sv-export-section">
294
+ <button type="button" class="sv-export-cols-toggle" onclick={() => (showColumns = !showColumns)}>
295
+ Columns <span class="sv-export-count">{picks.filter((p) => p.checked).length}/{picks.length}</span>
296
+ <span aria-hidden="true">{showColumns ? '▴' : '▾'}</span>
297
+ </button>
298
+ {#if showColumns}
299
+ <div class="sv-export-cols-list">
300
+ {#each picks as p (p.field)}
301
+ <label class="sv-export-opt">
302
+ <input type="checkbox" bind:checked={p.checked} />
303
+ {p.header}
304
+ </label>
305
+ {/each}
306
+ </div>
307
+ {/if}
308
+ </div>
309
+ {/if}
310
+
311
+ <div class="sv-export-section">
312
+ <span class="sv-export-section-label">Download</span>
313
+ {#each formats as f (f)}
314
+ <button type="button" class="sv-export-item" role="menuitem" disabled={busy} onclick={() => run(f)}>
315
+ {@render formatIcon(FORMAT_META[f].icon)}
316
+ <span class="sv-export-item-text">
317
+ <span class="sv-export-item-label">{FORMAT_META[f].label}</span>
318
+ <span class="sv-export-item-desc">{FORMAT_META[f].desc}</span>
319
+ </span>
320
+ </button>
321
+ {/each}
322
+ </div>
323
+
324
+ {#if allowCopy || allowPrint}
325
+ <div class="sv-export-actions">
326
+ {#if allowCopy}
327
+ <button type="button" class="sv-export-chip" disabled={busy} onclick={() => copy('tsv')}>Copy for Excel</button>
328
+ <button type="button" class="sv-export-chip" disabled={busy} onclick={() => copy('md')}>Copy Markdown</button>
329
+ {/if}
330
+ {#if allowPrint}
331
+ <button type="button" class="sv-export-chip" disabled={busy} onclick={print}>Print…</button>
332
+ {/if}
333
+ {#if copied}<span class="sv-export-copied">Copied {copied} ✓</span>{/if}
334
+ </div>
335
+ {/if}
336
+
337
+ {#if busy}
338
+ <div class="sv-export-progress">
339
+ <div class="sv-export-track"><div class="sv-export-bar" style:width={`${Math.round(progress * 100)}%`}></div></div>
340
+ <button type="button" class="sv-export-cancel" onclick={cancel}>Cancel</button>
341
+ </div>
342
+ {/if}
343
+
344
+ {#if error}
345
+ <p class="sv-export-error" role="alert">{error}</p>
346
+ {/if}
347
+ </div>
348
+ {/if}
349
+ </div>
350
+
351
+ <style>
352
+ .sv-export {
353
+ position: relative;
354
+ display: inline-block;
355
+ }
356
+ .sv-export-btn {
357
+ display: inline-flex;
358
+ align-items: center;
359
+ gap: 6px;
360
+ padding: 6px 12px;
361
+ font: inherit;
362
+ font-size: 13px;
363
+ font-weight: 500;
364
+ border: 1px solid var(--sg-border, #d0d7de);
365
+ border-radius: 8px;
366
+ background: var(--sg-header-bg, #f6f8fa);
367
+ color: var(--sg-fg, #1f2328);
368
+ cursor: pointer;
369
+ }
370
+ .sv-export-btn:disabled {
371
+ opacity: 0.5;
372
+ cursor: not-allowed;
373
+ }
374
+ .sv-export-caret {
375
+ font-size: 10px;
376
+ opacity: 0.7;
377
+ }
378
+ .sv-export-menu {
379
+ position: absolute;
380
+ z-index: 50;
381
+ top: calc(100% + 6px);
382
+ left: 0;
383
+ min-width: 232px;
384
+ padding: 6px;
385
+ border: 1px solid var(--sg-border, #d0d7de);
386
+ border-radius: 12px;
387
+ background: var(--sg-bg, #fff);
388
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14);
389
+ }
390
+ .sv-export-section {
391
+ padding: 6px 6px 8px;
392
+ border-bottom: 1px solid var(--sg-border, #eaecef);
393
+ }
394
+ .sv-export-section:last-of-type {
395
+ border-bottom: none;
396
+ }
397
+ .sv-export-section-label {
398
+ display: block;
399
+ padding: 2px 6px 6px;
400
+ font-size: 10px;
401
+ font-weight: 700;
402
+ text-transform: uppercase;
403
+ letter-spacing: 0.07em;
404
+ color: var(--sg-muted, #8b949e);
405
+ }
406
+ .sv-export-scope {
407
+ display: flex;
408
+ flex-direction: column;
409
+ gap: 1px;
410
+ }
411
+ .sv-export-opt {
412
+ display: flex;
413
+ align-items: center;
414
+ gap: 7px;
415
+ padding: 3px 6px;
416
+ font-size: 12.5px;
417
+ color: var(--sg-fg, #1f2328);
418
+ cursor: pointer;
419
+ border-radius: 6px;
420
+ }
421
+ .sv-export-opt:hover {
422
+ background: var(--sg-header-bg, #f6f8fa);
423
+ }
424
+ .sv-export-item {
425
+ display: flex;
426
+ align-items: center;
427
+ gap: 10px;
428
+ width: 100%;
429
+ text-align: left;
430
+ padding: 6px 8px;
431
+ font: inherit;
432
+ border: none;
433
+ border-radius: 8px;
434
+ background: transparent;
435
+ color: var(--sg-fg, #1f2328);
436
+ cursor: pointer;
437
+ }
438
+ .sv-export-item:hover:not(:disabled) {
439
+ background: var(--sg-header-bg, #f6f8fa);
440
+ }
441
+ .sv-export-item:disabled {
442
+ opacity: 0.5;
443
+ cursor: default;
444
+ }
445
+ .sv-export-icon {
446
+ flex-shrink: 0;
447
+ color: var(--site-accent-2, #6366f1);
448
+ }
449
+ .sv-export-item-text {
450
+ display: flex;
451
+ flex-direction: column;
452
+ line-height: 1.25;
453
+ }
454
+ .sv-export-item-label {
455
+ font-size: 13px;
456
+ font-weight: 500;
457
+ }
458
+ .sv-export-item-desc {
459
+ font-size: 11px;
460
+ color: var(--sg-muted, #8b949e);
461
+ }
462
+ .sv-export-cols-toggle {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 6px;
466
+ width: 100%;
467
+ font: inherit;
468
+ font-size: 11px;
469
+ font-weight: 700;
470
+ text-transform: uppercase;
471
+ letter-spacing: 0.07em;
472
+ color: var(--sg-muted, #8b949e);
473
+ background: transparent;
474
+ border: none;
475
+ padding: 2px 6px;
476
+ cursor: pointer;
477
+ }
478
+ .sv-export-count {
479
+ font-weight: 600;
480
+ text-transform: none;
481
+ letter-spacing: 0;
482
+ color: var(--sg-fg, #1f2328);
483
+ }
484
+ .sv-export-cols-toggle > span:last-child {
485
+ margin-left: auto;
486
+ }
487
+ .sv-export-cols-list {
488
+ max-height: 168px;
489
+ overflow-y: auto;
490
+ margin-top: 4px;
491
+ }
492
+ .sv-export-actions {
493
+ display: flex;
494
+ align-items: center;
495
+ flex-wrap: wrap;
496
+ gap: 6px;
497
+ padding: 8px 6px 4px;
498
+ }
499
+ .sv-export-chip {
500
+ font: inherit;
501
+ font-size: 12px;
502
+ padding: 4px 10px;
503
+ border-radius: 999px;
504
+ border: 1px solid var(--sg-border, #d0d7de);
505
+ background: var(--sg-header-bg, #f6f8fa);
506
+ color: var(--sg-fg, #1f2328);
507
+ cursor: pointer;
508
+ }
509
+ .sv-export-chip:hover:not(:disabled) {
510
+ border-color: var(--site-accent, #3b82f6);
511
+ }
512
+ .sv-export-chip:disabled {
513
+ opacity: 0.5;
514
+ cursor: default;
515
+ }
516
+ .sv-export-copied {
517
+ font-size: 11px;
518
+ color: #16a34a;
519
+ }
520
+ .sv-export-progress {
521
+ display: flex;
522
+ align-items: center;
523
+ gap: 8px;
524
+ padding: 8px 6px 4px;
525
+ }
526
+ .sv-export-track {
527
+ flex: 1;
528
+ min-width: 0;
529
+ height: 6px;
530
+ border-radius: 999px;
531
+ background: var(--sg-border, #eaecef);
532
+ overflow: hidden;
533
+ }
534
+ .sv-export-bar {
535
+ height: 100%;
536
+ border-radius: 999px;
537
+ background: var(--site-accent, #3b82f6);
538
+ transition: width 120ms ease;
539
+ }
540
+ .sv-export-cancel {
541
+ font: inherit;
542
+ font-size: 12px;
543
+ border: none;
544
+ background: transparent;
545
+ color: var(--site-accent-2, #6366f1);
546
+ cursor: pointer;
547
+ }
548
+ .sv-export-error {
549
+ margin: 6px 6px 2px;
550
+ font-size: 12px;
551
+ color: #dc2626;
552
+ }
553
+ </style>