@svgrid/enterprise 3.1.7 → 3.1.9

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 (270) hide show
  1. package/README.md +1 -0
  2. package/dist/SvAlertRuleEditor.svelte +16 -5
  3. package/dist/SvGridAlerts.svelte +8 -0
  4. package/dist/SvGridAlerts.svelte.d.ts +4 -0
  5. package/dist/SvGridBoard.svelte +69 -10
  6. package/dist/SvGridBulkEditDrawer.svelte +13 -5
  7. package/dist/SvGridGallery.svelte +286 -0
  8. package/dist/SvGridGallery.svelte.d.ts +30 -0
  9. package/dist/SvGridScheduler.svelte +33 -11
  10. package/dist/SvGridSelectionBar.svelte +47 -0
  11. package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
  12. package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
  13. package/dist/advanced-filter-enable.d.ts +6 -0
  14. package/dist/advanced-filter-enable.js +8 -0
  15. package/dist/alerts/alert-engine.d.ts +7 -0
  16. package/dist/alerts/alert-engine.js +5 -2
  17. package/dist/alerts/condition-editor.d.ts +26 -0
  18. package/dist/alerts/condition-editor.js +17 -0
  19. package/dist/base/index.d.ts +20 -0
  20. package/dist/base/index.js +20 -0
  21. package/dist/bulk-edit.d.ts +11 -0
  22. package/dist/bulk-edit.js +18 -1
  23. package/dist/cdn/svgrid-enterprise.svelte-external.js +11375 -10805
  24. package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
  25. package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
  26. package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
  27. package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
  28. package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
  29. package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
  30. package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
  31. package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
  32. package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
  33. package/dist/designer/assets/{index-DtBIaQno.js → index-BPU2KNc5.js} +396 -395
  34. package/dist/designer/assets/index-BmvG99kR.css +1 -0
  35. package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
  36. package/dist/designer/assets/src-C_U1NKOY.js +58 -0
  37. package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
  38. package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
  39. package/dist/designer/index.html +11 -7
  40. package/dist/expressions/compile.js +10 -0
  41. package/dist/expressions/evaluate.js +18 -0
  42. package/dist/expressions/expression-types.d.ts +10 -0
  43. package/dist/expressions/kinds.d.ts +28 -0
  44. package/dist/expressions/kinds.js +19 -0
  45. package/dist/expressions/parse.js +9 -1
  46. package/dist/fields/AttachmentCell.svelte +242 -0
  47. package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
  48. package/dist/fields/AttachmentDialog.svelte +357 -0
  49. package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
  50. package/dist/fields/AttachmentEditor.svelte +41 -0
  51. package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
  52. package/dist/fields/AttachmentPreview.svelte +72 -0
  53. package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
  54. package/dist/fields/BarcodeCell.svelte +20 -0
  55. package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
  56. package/dist/fields/BaseAutomations.svelte +558 -0
  57. package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
  58. package/dist/fields/BaseChartView.svelte +47 -0
  59. package/dist/fields/BaseChartView.svelte.d.ts +8 -0
  60. package/dist/fields/BaseTableView.svelte +185 -0
  61. package/dist/fields/BaseTableView.svelte.d.ts +37 -0
  62. package/dist/fields/BaseTabs.svelte +308 -0
  63. package/dist/fields/BaseTabs.svelte.d.ts +21 -0
  64. package/dist/fields/ButtonCell.svelte +47 -0
  65. package/dist/fields/ButtonCell.svelte.d.ts +20 -0
  66. package/dist/fields/CheckboxCell.svelte +46 -0
  67. package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
  68. package/dist/fields/DeleteFieldConfirm.svelte +80 -0
  69. package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
  70. package/dist/fields/DeleteRecordConfirm.svelte +48 -0
  71. package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
  72. package/dist/fields/FieldCell.svelte +80 -0
  73. package/dist/fields/FieldCell.svelte.d.ts +9 -0
  74. package/dist/fields/FieldEditor.svelte +321 -0
  75. package/dist/fields/FieldEditor.svelte.d.ts +10 -0
  76. package/dist/fields/FieldInput.svelte +188 -0
  77. package/dist/fields/FieldInput.svelte.d.ts +29 -0
  78. package/dist/fields/FieldsHeaderEnd.svelte +146 -0
  79. package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
  80. package/dist/fields/FootRowAdder.svelte +8 -0
  81. package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
  82. package/dist/fields/GroupHeader.svelte +127 -0
  83. package/dist/fields/GroupHeader.svelte.d.ts +24 -0
  84. package/dist/fields/LinkCell.svelte +46 -0
  85. package/dist/fields/LinkCell.svelte.d.ts +9 -0
  86. package/dist/fields/LinkPickerEditor.svelte +172 -0
  87. package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
  88. package/dist/fields/LinkPreview.svelte +60 -0
  89. package/dist/fields/LinkPreview.svelte.d.ts +7 -0
  90. package/dist/fields/LinkedRecordView.svelte +94 -0
  91. package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
  92. package/dist/fields/LinkedRecordsCell.svelte +59 -0
  93. package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
  94. package/dist/fields/LongTextEditor.svelte +157 -0
  95. package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
  96. package/dist/fields/NumberEditor.svelte +309 -0
  97. package/dist/fields/NumberEditor.svelte.d.ts +12 -0
  98. package/dist/fields/OptionsEditor.svelte +117 -0
  99. package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
  100. package/dist/fields/PercentCell.svelte +21 -0
  101. package/dist/fields/PercentCell.svelte.d.ts +8 -0
  102. package/dist/fields/PropertyInput.svelte +204 -0
  103. package/dist/fields/PropertyInput.svelte.d.ts +11 -0
  104. package/dist/fields/RatingCell.svelte +75 -0
  105. package/dist/fields/RatingCell.svelte.d.ts +16 -0
  106. package/dist/fields/RatingEditor.svelte +82 -0
  107. package/dist/fields/RatingEditor.svelte.d.ts +9 -0
  108. package/dist/fields/RecordView.svelte +310 -0
  109. package/dist/fields/RecordView.svelte.d.ts +11 -0
  110. package/dist/fields/RichTextCell.svelte +30 -0
  111. package/dist/fields/RichTextCell.svelte.d.ts +5 -0
  112. package/dist/fields/RowAdder.svelte +93 -0
  113. package/dist/fields/RowAdder.svelte.d.ts +7 -0
  114. package/dist/fields/RowHeader.svelte +99 -0
  115. package/dist/fields/RowHeader.svelte.d.ts +11 -0
  116. package/dist/fields/SelectCell.svelte +35 -0
  117. package/dist/fields/SelectCell.svelte.d.ts +15 -0
  118. package/dist/fields/SelectEditor.svelte +350 -0
  119. package/dist/fields/SelectEditor.svelte.d.ts +17 -0
  120. package/dist/fields/SummaryCell.svelte +160 -0
  121. package/dist/fields/SummaryCell.svelte.d.ts +11 -0
  122. package/dist/fields/SvBase.svelte +545 -0
  123. package/dist/fields/SvBase.svelte.d.ts +30 -0
  124. package/dist/fields/SvBaseForm.svelte +295 -0
  125. package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
  126. package/dist/fields/SvBaseViewBar.svelte +644 -0
  127. package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
  128. package/dist/fields/UserCell.svelte +39 -0
  129. package/dist/fields/UserCell.svelte.d.ts +9 -0
  130. package/dist/fields/UserFace.svelte +30 -0
  131. package/dist/fields/UserFace.svelte.d.ts +12 -0
  132. package/dist/fields/activity.svelte.d.ts +50 -0
  133. package/dist/fields/activity.svelte.js +110 -0
  134. package/dist/fields/attachments.d.ts +95 -0
  135. package/dist/fields/attachments.js +145 -0
  136. package/dist/fields/automation-outbound.d.ts +46 -0
  137. package/dist/fields/automation-outbound.js +61 -0
  138. package/dist/fields/automation-server.d.ts +53 -0
  139. package/dist/fields/automation-server.js +333 -0
  140. package/dist/fields/automations.svelte.d.ts +208 -0
  141. package/dist/fields/automations.svelte.js +437 -0
  142. package/dist/fields/bar-request.svelte.d.ts +16 -0
  143. package/dist/fields/bar-request.svelte.js +15 -0
  144. package/dist/fields/base-alerts.d.ts +24 -0
  145. package/dist/fields/base-alerts.js +64 -0
  146. package/dist/fields/base-connect.d.ts +82 -0
  147. package/dist/fields/base-connect.js +271 -0
  148. package/dist/fields/base-schema.d.ts +104 -0
  149. package/dist/fields/base-schema.js +266 -0
  150. package/dist/fields/base-sql.d.ts +62 -0
  151. package/dist/fields/base-sql.js +251 -0
  152. package/dist/fields/base-sync.d.ts +142 -0
  153. package/dist/fields/base-sync.js +499 -0
  154. package/dist/fields/base-tables.d.ts +89 -0
  155. package/dist/fields/base-tables.js +349 -0
  156. package/dist/fields/builtin-types.d.ts +13 -0
  157. package/dist/fields/builtin-types.js +773 -0
  158. package/dist/fields/button.d.ts +2 -0
  159. package/dist/fields/button.js +17 -0
  160. package/dist/fields/chart-view.d.ts +35 -0
  161. package/dist/fields/chart-view.js +154 -0
  162. package/dist/fields/column-order.d.ts +6 -0
  163. package/dist/fields/column-order.js +12 -0
  164. package/dist/fields/convert.d.ts +16 -0
  165. package/dist/fields/convert.js +176 -0
  166. package/dist/fields/copy-text.d.ts +13 -0
  167. package/dist/fields/copy-text.js +45 -0
  168. package/dist/fields/editor-request.svelte.d.ts +71 -0
  169. package/dist/fields/editor-request.svelte.js +52 -0
  170. package/dist/fields/field-ops.d.ts +72 -0
  171. package/dist/fields/field-ops.js +334 -0
  172. package/dist/fields/filter-model.d.ts +84 -0
  173. package/dist/fields/filter-model.js +446 -0
  174. package/dist/fields/format.d.ts +109 -0
  175. package/dist/fields/format.js +288 -0
  176. package/dist/fields/formula.d.ts +50 -0
  177. package/dist/fields/formula.js +530 -0
  178. package/dist/fields/grid-access.d.ts +60 -0
  179. package/dist/fields/grid-access.js +120 -0
  180. package/dist/fields/index.d.ts +34 -0
  181. package/dist/fields/index.js +312 -0
  182. package/dist/fields/linked-fields.d.ts +8 -0
  183. package/dist/fields/linked-fields.js +40 -0
  184. package/dist/fields/links.d.ts +10 -0
  185. package/dist/fields/links.js +46 -0
  186. package/dist/fields/name-list.d.ts +16 -0
  187. package/dist/fields/name-list.js +49 -0
  188. package/dist/fields/palette.d.ts +47 -0
  189. package/dist/fields/palette.js +69 -0
  190. package/dist/fields/record-delete.svelte.d.ts +12 -0
  191. package/dist/fields/record-delete.svelte.js +5 -0
  192. package/dist/fields/record-menu.d.ts +9 -0
  193. package/dist/fields/record-menu.js +78 -0
  194. package/dist/fields/registry.svelte.d.ts +176 -0
  195. package/dist/fields/registry.svelte.js +51 -0
  196. package/dist/fields/rich-text.d.ts +23 -0
  197. package/dist/fields/rich-text.js +48 -0
  198. package/dist/fields/row-color-choice.svelte.d.ts +31 -0
  199. package/dist/fields/row-color-choice.svelte.js +25 -0
  200. package/dist/fields/row-colors.d.ts +36 -0
  201. package/dist/fields/row-colors.js +90 -0
  202. package/dist/fields/row-height.d.ts +28 -0
  203. package/dist/fields/row-height.js +50 -0
  204. package/dist/fields/sources.svelte.d.ts +80 -0
  205. package/dist/fields/sources.svelte.js +131 -0
  206. package/dist/fields/summaries.d.ts +18 -0
  207. package/dist/fields/summaries.js +138 -0
  208. package/dist/fields/view-model.d.ts +129 -0
  209. package/dist/fields/view-model.js +492 -0
  210. package/dist/fields/viewbar/ChartPanel.svelte +61 -0
  211. package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
  212. package/dist/fields/viewbar/ColorPanel.svelte +112 -0
  213. package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
  214. package/dist/fields/viewbar/ConditionValue.svelte +153 -0
  215. package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
  216. package/dist/fields/viewbar/FieldIcon.svelte +18 -0
  217. package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
  218. package/dist/fields/viewbar/FilterPanel.svelte +192 -0
  219. package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
  220. package/dist/fields/viewbar/GanttPanel.svelte +60 -0
  221. package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
  222. package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
  223. package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
  224. package/dist/fields/viewbar/SortPanel.svelte +85 -0
  225. package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
  226. package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
  227. package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
  228. package/dist/fields/viewbar/panel-focus.d.ts +3 -0
  229. package/dist/fields/viewbar/panel-focus.js +22 -0
  230. package/dist/fields/viewbar/sort-state.d.ts +20 -0
  231. package/dist/fields/viewbar/sort-state.js +24 -0
  232. package/dist/gallery.d.ts +7 -0
  233. package/dist/gallery.js +36 -0
  234. package/dist/gantt/SvGridGantt.svelte +88 -16
  235. package/dist/gantt/gantt-config.d.ts +28 -0
  236. package/dist/index.d.ts +3 -0
  237. package/dist/index.js +6 -0
  238. package/dist/install.js +3 -0
  239. package/dist/license-core.d.ts +3 -3
  240. package/dist/license.js +1 -0
  241. package/dist/node/studio.js +141 -20
  242. package/dist/scheduler-config.d.ts +11 -0
  243. package/dist/sources/introspect-supabase.js +4 -1
  244. package/dist/sources/rest.js +6 -2
  245. package/dist/studio/emit-project.js +25 -6
  246. package/dist/studio/sveltekit3.js +13 -7
  247. package/dist/studio/ui-components.generated.js +94 -2
  248. package/dist/upgrade-prompt.d.ts +1 -1
  249. package/dist/version.d.ts +2 -2
  250. package/dist/version.js +2 -2
  251. package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
  252. package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
  253. package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
  254. package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
  255. package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
  256. package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
  257. package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
  258. package/dist/wc/sv-sheet-element.js +7561 -7618
  259. package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
  260. package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
  261. package/package.json +14 -3
  262. package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
  263. package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
  264. package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
  265. package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
  266. package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
  267. package/dist/designer/assets/src-DqHCqori.js +0 -58
  268. package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
  269. package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
  270. package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
@@ -0,0 +1,185 @@
1
+ <script lang="ts">
2
+ /**
3
+ * One table of a base: the view bar over a `<SvGrid>`, and the form view
4
+ * beside it. Grid, Kanban, calendar, Gantt and gallery views are the grid
5
+ * itself in another mode (`board`, `scheduler`, `gantt`, `gallery`); a form
6
+ * view asks for the same fields. Every prop it does not name goes to the
7
+ * grid. `<SvBase>` draws one, under its table tabs when it has several.
8
+ *
9
+ * With a `runner`, the view bar has the table's Automations; an
10
+ * automation's "Color the record" action paints the grid while a record
11
+ * matches its condition.
12
+ */
13
+ import { SvGrid, hasGanttView } from '@svgrid/grid'
14
+ import SvBaseViewBar from './SvBaseViewBar.svelte'
15
+ import SvBaseForm from './SvBaseForm.svelte'
16
+ import BaseChartView from './BaseChartView.svelte'
17
+ import BaseAutomations from './BaseAutomations.svelte'
18
+ import { gridOf } from './grid-access'
19
+ import { fieldCopyText } from './copy-text'
20
+ import { recordMenu } from './record-menu'
21
+ import { matches, type AutomationRun, type AutomationRunner, type BaseAutomation } from './automations.svelte'
22
+ import type { BaseTable } from './base-tables'
23
+ import type { FieldView, FieldViewType } from './view-model'
24
+
25
+ let {
26
+ data,
27
+ columns,
28
+ views = $bindable<FieldView[]>([{ id: 'grid', name: 'Grid view', type: 'grid' }]),
29
+ activeView = $bindable<string>(''),
30
+ onViewsChange,
31
+ viewTypes,
32
+ rowHeights,
33
+ containerHeight = 560,
34
+ onApiReady,
35
+ bare = false,
36
+ table,
37
+ tables,
38
+ runner,
39
+ serverRuns,
40
+ onAutomationsChange,
41
+ onFormSubmit,
42
+ newRecord,
43
+ ...grid
44
+ }: {
45
+ /** The grid's rows and columns. */
46
+ data: any[]
47
+ columns: any[]
48
+ views?: FieldView[]
49
+ activeView?: string
50
+ onViewsChange?: (views: FieldView[]) => void
51
+ viewTypes?: ReadonlyArray<FieldViewType>
52
+ /** The row heights the view bar's row height menu offers. */
53
+ rowHeights?: ReadonlyArray<{ label: string; height: number }>
54
+ containerHeight?: number | string
55
+ onApiReady?: (api: any) => void
56
+ /** No border of its own: it sits under the table tabs, inside their frame. */
57
+ bare?: boolean
58
+ /** The table shown, with its automations (from `<SvBase tables>`). */
59
+ table?: BaseTable
60
+ /** Every table of the base, for the automations that create records. */
61
+ tables?: () => BaseTable[]
62
+ /** Runs the base's automations; without it there is no Automations button. */
63
+ runner?: AutomationRunner
64
+ serverRuns?: () => Promise<ReadonlyArray<AutomationRun>>
65
+ onAutomationsChange?: (automations: BaseAutomation[]) => void
66
+ /** A form view sent a record. A Promise keeps the form busy until it settles; a rejection shows its message. */
67
+ onFormSubmit?: (row: Record<string, unknown>, viewId: string) => void | Promise<unknown>
68
+ /** Values a record from a form view starts with (`<SvBase tables>` gives an id). */
69
+ newRecord?: () => Record<string, unknown>
70
+ [prop: string]: any
71
+ } = $props()
72
+
73
+ // Raw: a proxied api is another object, and the view bar finds the grid by the api itself.
74
+ let api = $state.raw<any>(null)
75
+ // Right-click on a record: expand, insert, duplicate, copy, paste, delete. The app's own `contextMenu` wins.
76
+ const menu = recordMenu(() => api, () => (api ? gridOf(api) : undefined))
77
+ const view = $derived(views.find((v) => v.id === activeView) ?? views[0])
78
+ const type = $derived(view?.type ?? 'grid')
79
+ // A form or a chart draws over the grid, which stays mounted: it holds the
80
+ // records, the filter and the api they read.
81
+ const besideGrid = $derived(type === 'form' || type === 'chart')
82
+ // Gantt only once the app has turned it on.
83
+ const types = $derived(viewTypes ?? (['grid', 'kanban', 'calendar', ...(hasGanttView() ? ['gantt'] : []), 'gallery', 'chart', 'form'] as FieldViewType[]))
84
+ const height = $derived(typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight)
85
+
86
+ // ---- Automations ----------------------------------------------------------
87
+ let automationsOpen = $state(false)
88
+ const automationList = $derived(table?.automations ?? [])
89
+ // "Color the record": a format per action, drawn while the record matches.
90
+ const highlightFormats = $derived.by(() => {
91
+ const t = table
92
+ if (!t) return []
93
+ const out: any[] = []
94
+ for (const a of automationList) {
95
+ if (!a.enabled || !a.condition) continue
96
+ for (const action of a.actions) {
97
+ if (action.type !== 'highlight') continue
98
+ out.push({
99
+ type: 'rule',
100
+ ...(action.fields?.length ? { columns: action.fields } : {}),
101
+ when: ({ row }: { row: Record<string, unknown> }) => matches(t, a.condition, row),
102
+ background: action.background,
103
+ ...(action.color ? { color: action.color } : {}),
104
+ })
105
+ }
106
+ }
107
+ return out
108
+ })
109
+ const conditionalFormats = $derived(highlightFormats.length ? [...((grid.conditionalFormats as any[]) ?? []), ...highlightFormats] : grid.conditionalFormats)
110
+ </script>
111
+
112
+ <div class="sv-fbase" class:is-bare={bare} style:height>
113
+ {#if api}
114
+ <SvBaseViewBar
115
+ {api}
116
+ bind:views
117
+ bind:activeView
118
+ {onViewsChange}
119
+ viewTypes={types}
120
+ {rowHeights}
121
+ automations={runner && table ? { count: automationList.length, onManage: () => (automationsOpen = true) } : null}
122
+ />
123
+ {/if}
124
+ <div class="sv-fbase-body">
125
+ <!-- The grid stays mounted under a form: it holds the api the form adds rows through. -->
126
+ <div class="sv-fbase-grid" class:is-hidden={besideGrid}>
127
+ <SvGrid
128
+ {...grid}
129
+ {data}
130
+ {columns}
131
+ {conditionalFormats}
132
+ contextMenu={grid.contextMenu ?? menu}
133
+ processCellForClipboard={grid.processCellForClipboard ?? fieldCopyText}
134
+ containerHeight="100%"
135
+ onApiReady={(a: any) => {
136
+ api = a
137
+ onApiReady?.(a)
138
+ }}
139
+ />
140
+ </div>
141
+ {#if api && type === 'chart'}
142
+ <BaseChartView {api} setting={view?.chart} />
143
+ {/if}
144
+ {#if api && type === 'form'}
145
+ <!-- One form per view: another form view starts with its own empty fields. -->
146
+ {#key view?.id}
147
+ <SvBaseForm
148
+ {api}
149
+ fields={view?.formFields}
150
+ required={view?.formRequired}
151
+ title={view?.formTitle ?? view?.name}
152
+ description={view?.formDescription}
153
+ submitLabel={view?.formSubmitLabel}
154
+ defaults={newRecord}
155
+ onSubmit={(row) => (view ? onFormSubmit?.(row, view.id) : undefined)}
156
+ />
157
+ {/key}
158
+ {/if}
159
+ </div>
160
+ {#if runner && table && api && automationsOpen}
161
+ <BaseAutomations
162
+ bind:open={automationsOpen}
163
+ {table}
164
+ tables={tables?.() ?? [table]}
165
+ ctrl={gridOf(api)}
166
+ {views}
167
+ {runner}
168
+ {serverRuns}
169
+ onChange={(list) => onAutomationsChange?.(list)}
170
+ />
171
+ {/if}
172
+ </div>
173
+
174
+ <style>
175
+ .sv-fbase {
176
+ display: flex; flex-direction: column; min-height: 0; box-sizing: border-box;
177
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius-lg, 10px); overflow: hidden;
178
+ background: var(--sg-bg, #fff);
179
+ }
180
+ .sv-fbase.is-bare { border: 0; border-radius: 0; }
181
+ .sv-fbase-body { position: relative; flex: 1 1 auto; min-height: 0; }
182
+ .sv-fbase-grid { height: 100%; }
183
+ .sv-fbase-grid.is-hidden { visibility: hidden; position: absolute; inset: 0; pointer-events: none; }
184
+ .sv-fbase-body > :global(.sv-fform-wrap) { position: absolute; inset: 0; }
185
+ </style>
@@ -0,0 +1,37 @@
1
+ import { type AutomationRun, type AutomationRunner, type BaseAutomation } from './automations.svelte.js';
2
+ import type { BaseTable } from './base-tables.js';
3
+ import type { FieldView, FieldViewType } from './view-model.js';
4
+ type $$ComponentProps = {
5
+ /** The grid's rows and columns. */
6
+ data: any[];
7
+ columns: any[];
8
+ views?: FieldView[];
9
+ activeView?: string;
10
+ onViewsChange?: (views: FieldView[]) => void;
11
+ viewTypes?: ReadonlyArray<FieldViewType>;
12
+ /** The row heights the view bar's row height menu offers. */
13
+ rowHeights?: ReadonlyArray<{
14
+ label: string;
15
+ height: number;
16
+ }>;
17
+ containerHeight?: number | string;
18
+ onApiReady?: (api: any) => void;
19
+ /** No border of its own: it sits under the table tabs, inside their frame. */
20
+ bare?: boolean;
21
+ /** The table shown, with its automations (from `<SvBase tables>`). */
22
+ table?: BaseTable;
23
+ /** Every table of the base, for the automations that create records. */
24
+ tables?: () => BaseTable[];
25
+ /** Runs the base's automations; without it there is no Automations button. */
26
+ runner?: AutomationRunner;
27
+ serverRuns?: () => Promise<ReadonlyArray<AutomationRun>>;
28
+ onAutomationsChange?: (automations: BaseAutomation[]) => void;
29
+ /** A form view sent a record. A Promise keeps the form busy until it settles; a rejection shows its message. */
30
+ onFormSubmit?: (row: Record<string, unknown>, viewId: string) => void | Promise<unknown>;
31
+ /** Values a record from a form view starts with (`<SvBase tables>` gives an id). */
32
+ newRecord?: () => Record<string, unknown>;
33
+ [prop: string]: any;
34
+ };
35
+ declare const BaseTableView: import("svelte").Component<$$ComponentProps, {}, "views" | "activeView">;
36
+ type BaseTableView = ReturnType<typeof BaseTableView>;
37
+ export default BaseTableView;
@@ -0,0 +1,308 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The tables of a base as tabs: click one to show it, double-click to
4
+ * rename it, and the active tab's menu renames, duplicates or deletes it.
5
+ * "+ Add table" makes a new one. Deleting asks first and says which link
6
+ * fields of other tables go with it, and the lookups, rollups and counts
7
+ * that read them.
8
+ */
9
+ import { tick } from 'svelte'
10
+ import { SvModal, SvPopover } from '@svgrid/grid'
11
+ import { linkReaders, type BaseTable } from './base-tables'
12
+
13
+ let {
14
+ tables,
15
+ activeId,
16
+ onSwitch,
17
+ onAdd,
18
+ onRename,
19
+ onDuplicate,
20
+ onDelete,
21
+ onExportSql,
22
+ status = null,
23
+ }: {
24
+ tables: ReadonlyArray<BaseTable>
25
+ activeId: string
26
+ onSwitch: (id: string) => void
27
+ onAdd: () => void
28
+ onRename: (id: string, name: string) => void
29
+ onDuplicate: (id: string) => void
30
+ onDelete: (id: string) => void
31
+ /** Download the base as Postgres SQL (from the list of tables). */
32
+ onExportSql?: () => void
33
+ /** A line at the end of the bar: loading, saving, a failed save. */
34
+ status?: { text: string; tone?: 'muted' | 'error'; title?: string } | null
35
+ } = $props()
36
+
37
+ let menuOpen = $state(false)
38
+ let listOpen = $state(false)
39
+ let renaming = $state<string | null>(null)
40
+ let draft = $state('')
41
+ let deleting = $state<BaseTable | null>(null)
42
+ let confirmOpen = $state(false)
43
+ let strip = $state<HTMLElement | null>(null)
44
+
45
+ const active = $derived(tables.find((t) => t.id === activeId))
46
+
47
+ /**
48
+ * The fields of other tables that link to `t`, as "Deals: Account", each
49
+ * followed by the fields that read it, as "Deals: Industry (reads Account)".
50
+ */
51
+ function linksTo(t: BaseTable): string[] {
52
+ const out: string[] = []
53
+ for (const o of tables) {
54
+ if (o.id === t.id) continue
55
+ for (const d of o.columns) {
56
+ if (d.fieldType !== 'link' || d.fieldProps?.table !== t.id) continue
57
+ const name = String(d.header ?? d.field)
58
+ out.push(`${o.name}: ${name}`)
59
+ for (const r of linkReaders(o, d)) out.push(`${o.name}: ${String(r.header ?? r.field)} (reads ${name})`)
60
+ }
61
+ }
62
+ return out
63
+ }
64
+
65
+ async function startRename(id: string) {
66
+ menuOpen = false
67
+ renaming = id
68
+ draft = tables.find((t) => t.id === id)?.name ?? ''
69
+ await tick()
70
+ const input = strip?.querySelector<HTMLInputElement>('.sv-btabs-input')
71
+ input?.focus()
72
+ input?.select()
73
+ }
74
+ function commitRename() {
75
+ const id = renaming
76
+ renaming = null
77
+ const name = draft.trim()
78
+ if (id && name && name !== tables.find((t) => t.id === id)?.name) onRename(id, name)
79
+ }
80
+ function renameKey(e: KeyboardEvent) {
81
+ if (e.key === 'Enter') commitRename()
82
+ else if (e.key === 'Escape') renaming = null
83
+ }
84
+
85
+ function askDelete() {
86
+ menuOpen = false
87
+ deleting = active ?? null
88
+ confirmOpen = !!deleting
89
+ }
90
+ function confirmDelete() {
91
+ const t = deleting
92
+ confirmOpen = false
93
+ deleting = null
94
+ if (t) onDelete(t.id)
95
+ }
96
+
97
+ /** Arrow keys walk the tabs, Home and End jump, as a tab list does. */
98
+ function tabKey(e: KeyboardEvent, i: number) {
99
+ let to = -1
100
+ if (e.key === 'ArrowRight') to = (i + 1) % tables.length
101
+ else if (e.key === 'ArrowLeft') to = (i - 1 + tables.length) % tables.length
102
+ else if (e.key === 'Home') to = 0
103
+ else if (e.key === 'End') to = tables.length - 1
104
+ else if (e.key === 'F2') {
105
+ e.preventDefault()
106
+ void startRename(tables[i]!.id)
107
+ return
108
+ }
109
+ if (to < 0) return
110
+ e.preventDefault()
111
+ onSwitch(tables[to]!.id)
112
+ void tick().then(() => strip?.querySelectorAll<HTMLElement>('[role="tab"]')[to]?.focus())
113
+ }
114
+ const inputValue = (e: Event) => (e.currentTarget as HTMLInputElement).value
115
+ </script>
116
+
117
+ <div class="sv-btabs" bind:this={strip}>
118
+ <div class="sv-btabs-list" role="tablist" aria-label="Tables">
119
+ {#each tables as t, i (t.id)}
120
+ {@const on = t.id === activeId}
121
+ <div class="sv-btabs-tab" class:is-active={on}>
122
+ {#if renaming === t.id}
123
+ <input
124
+ class="sv-btabs-input"
125
+ aria-label="Table name"
126
+ value={draft}
127
+ oninput={(e) => (draft = inputValue(e))}
128
+ onkeydown={renameKey}
129
+ onblur={commitRename}
130
+ />
131
+ {:else}
132
+ <button
133
+ type="button"
134
+ role="tab"
135
+ class="sv-btabs-name"
136
+ aria-selected={on}
137
+ tabindex={on ? 0 : -1}
138
+ title={t.name}
139
+ onclick={() => onSwitch(t.id)}
140
+ ondblclick={() => startRename(t.id)}
141
+ onkeydown={(e) => tabKey(e, i)}>{t.name}</button
142
+ >
143
+ {#if on}
144
+ <SvPopover bind:open={menuOpen} arrow={false} placement="bottom-start" ariaLabel="Table actions">
145
+ {#snippet anchor()}
146
+ <button type="button" class="sv-btabs-more" aria-label={`${t.name} table options`} aria-haspopup="menu" aria-expanded={menuOpen}>
147
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
148
+ </button>
149
+ {/snippet}
150
+ <div class="sv-btabs-menu" role="menu">
151
+ <button type="button" role="menuitem" class="sv-btabs-item" onclick={() => startRename(t.id)}>Rename table</button>
152
+ <button type="button" role="menuitem" class="sv-btabs-item" onclick={() => { menuOpen = false; onDuplicate(t.id) }}>Duplicate table</button>
153
+ <button type="button" role="menuitem" class="sv-btabs-item is-danger" disabled={tables.length < 2} title={tables.length < 2 ? 'A base keeps at least one table' : undefined} onclick={askDelete}>Delete table</button>
154
+ </div>
155
+ </SvPopover>
156
+ {/if}
157
+ {/if}
158
+ </div>
159
+ {/each}
160
+ </div>
161
+ <div class="sv-btabs-tools">
162
+ <!-- Every table in a list: the way to one whose tab has scrolled out of sight. -->
163
+ <SvPopover bind:open={listOpen} arrow={false} placement="bottom-start" ariaLabel="All tables">
164
+ {#snippet anchor()}
165
+ <button type="button" class="sv-btabs-tool sv-btabs-icon" aria-label="All tables" title="All tables" aria-haspopup="menu" aria-expanded={listOpen}>
166
+ <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
167
+ </button>
168
+ {/snippet}
169
+ <div class="sv-btabs-menu" role="menu">
170
+ {#each tables as t (t.id)}
171
+ <button type="button" role="menuitem" class="sv-btabs-item" class:is-current={t.id === activeId} onclick={() => { listOpen = false; onSwitch(t.id) }}>
172
+ <span class="sv-btabs-check" aria-hidden="true">{t.id === activeId ? '✓' : ''}</span>{t.name}
173
+ </button>
174
+ {/each}
175
+ {#if onExportSql}
176
+ <div class="sv-btabs-sep" role="separator"></div>
177
+ <button type="button" role="menuitem" class="sv-btabs-item" title="The tables, links, records, views and automations, ready for Postgres or Supabase" onclick={() => { listOpen = false; onExportSql() }}>
178
+ <span class="sv-btabs-check" aria-hidden="true"></span>Download as SQL
179
+ </button>
180
+ {/if}
181
+ </div>
182
+ </SvPopover>
183
+ <button type="button" class="sv-btabs-tool sv-btabs-add" onclick={onAdd}>
184
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
185
+ <span>Add table</span>
186
+ </button>
187
+ </div>
188
+ {#if status}
189
+ <span class="sv-btabs-status" class:is-error={status.tone === 'error'} role="status" title={status.title ?? status.text}>{status.text}</span>
190
+ {/if}
191
+ </div>
192
+
193
+ {#if deleting}
194
+ {@const links = linksTo(deleting)}
195
+ <SvModal bind:open={confirmOpen} title={`Delete ${deleting.name}?`} width={440} onClose={() => (deleting = null)}>
196
+ <p class="sv-btabs-warn">
197
+ Its {deleting.data.length} {deleting.data.length === 1 ? 'record goes' : 'records go'} with it, and so do its views.
198
+ </p>
199
+ {#if links.length}
200
+ <p class="sv-btabs-warn">These fields of other tables link to it, or read a link to it, and are deleted too:</p>
201
+ <ul class="sv-btabs-links">
202
+ {#each links as l (l)}<li>{l}</li>{/each}
203
+ </ul>
204
+ {/if}
205
+ {#snippet footer()}
206
+ <div class="sv-btabs-foot">
207
+ <button type="button" class="sv-btabs-btn" onclick={() => { confirmOpen = false; deleting = null }}>Cancel</button>
208
+ <button type="button" class="sv-btabs-btn is-danger" onclick={confirmDelete}>Delete table</button>
209
+ </div>
210
+ {/snippet}
211
+ </SvModal>
212
+ {/if}
213
+
214
+ <style>
215
+ /* The table bar, from the theme's own tokens. The tables sit on a band of
216
+ the theme's muted surface, closed by a hairline. The other tables are
217
+ muted text divided by hairlines. The shown table is a tab of the
218
+ toolbar's surface, outlined like the grid, open at the bottom: it covers
219
+ the band's hairline and runs into the toolbar below. The theme's accent
220
+ marks it along the top, as the accent marks a selection everywhere. */
221
+ .sv-btabs {
222
+ --sv-btabs-h: 33px;
223
+ display: flex; align-items: flex-end; gap: 2px; box-sizing: border-box;
224
+ height: calc(var(--sv-btabs-h) + 7px); padding: 0 6px 0 0;
225
+ font-family: var(--sg-font, inherit); font-size: 13px; line-height: 1;
226
+ background: var(--sg-muted-bg, var(--sg-header-bg, #f5f5f4));
227
+ box-shadow: inset 0 -1px 0 var(--sg-border, #e7e5e4);
228
+ }
229
+ .sv-btabs-list { display: flex; align-items: flex-end; min-width: 0; height: 100%; padding: 0 0 0 8px; overflow-x: auto; scrollbar-width: none; }
230
+ .sv-btabs-list::-webkit-scrollbar { display: none; }
231
+ .sv-btabs-tab {
232
+ position: relative; flex: none; display: flex; align-items: center; gap: 2px; box-sizing: border-box;
233
+ height: var(--sv-btabs-h); padding: 0 6px 1px 4px;
234
+ border: 1px solid transparent; border-bottom: 0; border-radius: var(--sg-radius, 6px) var(--sg-radius, 6px) 0 0;
235
+ color: var(--sg-muted, #78716c);
236
+ transition: color 120ms ease, background-color 120ms ease;
237
+ }
238
+ /* A hairline between two tables, unless one of them is the shown one. */
239
+ .sv-btabs-tab:not(.is-active) + .sv-btabs-tab:not(.is-active)::before {
240
+ content: ''; position: absolute; left: -2px; top: 10px; bottom: 10px; width: 1px;
241
+ background: var(--sg-border, #e7e5e4);
242
+ }
243
+ /* No hairline beside a hovered tab: its own surface marks it. */
244
+ .sv-btabs-tab:hover::before, .sv-btabs-tab:hover + .sv-btabs-tab::before { opacity: 0; }
245
+ .sv-btabs-tab:not(.is-active):hover {
246
+ color: var(--sg-fg, #1c1917);
247
+ background: color-mix(in srgb, var(--sg-bg, #fff) 55%, transparent);
248
+ }
249
+ .sv-btabs-tab.is-active {
250
+ z-index: 1; height: calc(var(--sv-btabs-h) + 1px); margin-bottom: -1px; padding-bottom: 2px;
251
+ color: var(--sg-fg, #1c1917); background: var(--sg-bg, #fff); border-color: var(--sg-border, #e7e5e4);
252
+ box-shadow: inset 0 2px 0 var(--sg-accent, #2563eb);
253
+ }
254
+ .sv-btabs-name {
255
+ max-width: 220px; height: 26px; padding: 0 8px; border: 0; border-radius: var(--sg-radius, 6px); background: none;
256
+ color: inherit; font: inherit; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;
257
+ }
258
+ .is-active .sv-btabs-name { font-weight: 600; padding-right: 2px; }
259
+ .sv-btabs-more {
260
+ display: grid; place-items: center; width: 22px; height: 22px; padding: 0; border: 0; border-radius: var(--sg-radius, 6px);
261
+ background: none; color: var(--sg-muted, #78716c); cursor: pointer;
262
+ }
263
+ /* A tint of the text color, not a surface token: it reads on any theme, light or dark. */
264
+ .sv-btabs-more:hover { background: color-mix(in srgb, var(--sg-fg, #1c1917) 10%, transparent); color: var(--sg-fg, #1c1917); }
265
+ .sv-btabs-input {
266
+ width: 160px; height: 24px; margin: 0 2px; padding: 0 6px; box-sizing: border-box; font: inherit; font-weight: 600;
267
+ color: var(--sg-fg, #1c1917); background: var(--sg-input-bg, var(--sg-bg, #fff));
268
+ border: 1px solid var(--sg-accent, #2563eb); border-radius: var(--sg-radius, 6px); outline: none;
269
+ }
270
+ .sv-btabs-tools { flex: none; display: flex; align-items: center; gap: 2px; height: var(--sv-btabs-h); margin-left: 0; }
271
+ .sv-btabs-status {
272
+ flex: 0 1 auto; align-self: center; min-width: 0; margin-left: auto; padding: 0 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
273
+ font-size: 12px; color: var(--sg-muted, #78716c);
274
+ }
275
+ .sv-btabs-status.is-error { color: var(--sg-danger, #dc2626); }
276
+ .sv-btabs-tool {
277
+ display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 8px; border: 0; border-radius: var(--sg-radius, 6px);
278
+ background: none; color: var(--sg-muted, #78716c); font: inherit; cursor: pointer; white-space: nowrap;
279
+ transition: color 120ms ease, background-color 120ms ease;
280
+ }
281
+ .sv-btabs-icon { width: 26px; padding: 0; justify-content: center; }
282
+ .sv-btabs-tool:hover { color: var(--sg-fg, #1c1917); background: color-mix(in srgb, var(--sg-bg, #fff) 55%, transparent); }
283
+ .sv-btabs-name:focus-visible, .sv-btabs-more:focus-visible, .sv-btabs-tool:focus-visible {
284
+ outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: -2px;
285
+ }
286
+ @media (prefers-reduced-motion: reduce) {
287
+ .sv-btabs-tab, .sv-btabs-tool { transition: none; }
288
+ }
289
+ .sv-btabs-menu { display: flex; flex-direction: column; min-width: 190px; max-height: 320px; overflow-y: auto; padding: 4px; font-size: 13px; }
290
+ .sv-btabs-sep { height: 1px; margin: 4px 6px; background: var(--sg-border, #e2e8f0); }
291
+ .sv-btabs-item {
292
+ display: flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; text-align: start; border: 0; border-radius: 6px;
293
+ background: none; color: var(--sg-fg, #0f172a); font: inherit; cursor: pointer;
294
+ }
295
+ .sv-btabs-item:hover:not(:disabled), .sv-btabs-item:focus-visible { background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); outline: none; }
296
+ .sv-btabs-item:disabled { color: var(--sg-muted, #64748b); cursor: default; }
297
+ .sv-btabs-item.is-danger:not(:disabled) { color: var(--sg-danger, #dc2626); }
298
+ .sv-btabs-item.is-current { font-weight: 600; }
299
+ .sv-btabs-check { width: 14px; flex: none; color: var(--sg-muted, #64748b); }
300
+ .sv-btabs-warn { margin: 0 0 8px; font-size: 14px; color: var(--sg-fg, #0f172a); }
301
+ .sv-btabs-links { margin: 0 0 4px; padding-left: 18px; font-size: 13px; color: var(--sg-muted, #64748b); }
302
+ .sv-btabs-foot { display: flex; justify-content: flex-end; gap: 8px; width: 100%; }
303
+ .sv-btabs-btn {
304
+ height: 32px; padding: 0 14px; font: inherit; font-size: 13px; cursor: pointer; border-radius: var(--sg-radius, 6px);
305
+ background: transparent; color: var(--sg-fg, #0f172a); border: 1px solid var(--sg-border, #e2e8f0);
306
+ }
307
+ .sv-btabs-btn.is-danger { background: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626); color: #fff; }
308
+ </style>
@@ -0,0 +1,21 @@
1
+ import { type BaseTable } from './base-tables.js';
2
+ type $$ComponentProps = {
3
+ tables: ReadonlyArray<BaseTable>;
4
+ activeId: string;
5
+ onSwitch: (id: string) => void;
6
+ onAdd: () => void;
7
+ onRename: (id: string, name: string) => void;
8
+ onDuplicate: (id: string) => void;
9
+ onDelete: (id: string) => void;
10
+ /** Download the base as Postgres SQL (from the list of tables). */
11
+ onExportSql?: () => void;
12
+ /** A line at the end of the bar: loading, saving, a failed save. */
13
+ status?: {
14
+ text: string;
15
+ tone?: 'muted' | 'error';
16
+ title?: string;
17
+ } | null;
18
+ };
19
+ declare const BaseTabs: import("svelte").Component<$$ComponentProps, {}, "">;
20
+ type BaseTabs = ReturnType<typeof BaseTabs>;
21
+ export default BaseTabs;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Button cells. The action is the column's `fieldProps.onClick` when the app
4
+ * set one, then an automation that runs on this button (`<SvBase>` hands the
5
+ * grid `fieldButtons`), otherwise the URL template with `{Field name}` filled in from the
6
+ * row (values URL-encoded); only an http(s) result opens.
7
+ */
8
+ import type { FieldCellProps } from './registry.svelte'
9
+ import { buttonUrl } from './button'
10
+
11
+ // `onClick` gets the row's id and the grid's api too, so it can call
12
+ // `openRecord({ api, rowId })`.
13
+ type ButtonClick = { row: unknown; columnId: string; rowId: string; api: unknown }
14
+ let { ctrl, row, column, props }: FieldCellProps<{ label?: string; color?: string; url?: string; onClick?: (ctx: ButtonClick) => void }> = $props()
15
+ const g = $derived(row as { original: Record<string, unknown> })
16
+ const automation = $derived(props.onClick ? null : ((ctrl as { props?: { fieldButtons?: (id: string) => ((row: unknown) => void) | null } })?.props?.fieldButtons?.(column.id) ?? null))
17
+ const href = $derived(props.onClick || automation ? null : buttonUrl(String(props.url ?? ''), g.original, (column.columnDef as { __fieldNames?: Record<string, string> }).__fieldNames))
18
+ const disabled = $derived(!props.onClick && !automation && !href)
19
+
20
+ function run(e: MouseEvent) {
21
+ e.stopPropagation()
22
+ if (props.onClick) props.onClick({ row: g.original, columnId: column.id, rowId: (row as { id: string }).id, api: (ctrl as { buildApi?: () => unknown })?.buildApi?.() })
23
+ else if (automation) automation(g.original)
24
+ else if (href) window.open(href, '_blank', 'noopener,noreferrer')
25
+ }
26
+ </script>
27
+
28
+ <button
29
+ type="button"
30
+ class="sv-fbt"
31
+ style:--c={props.color || '#2d7ff9'}
32
+ {disabled}
33
+ title={href ?? undefined}
34
+ onpointerdown={(e) => e.stopPropagation()}
35
+ onclick={run}
36
+ >{props.label || 'Open'}</button>
37
+
38
+ <style>
39
+ .sv-fbt {
40
+ max-width: 100%; height: 24px; padding: 0 10px; border-radius: 6px; font: inherit; font-size: 12px; font-weight: 500;
41
+ border: 1px solid color-mix(in srgb, var(--c) 45%, transparent);
42
+ background: color-mix(in srgb, var(--c) 12%, transparent); color: var(--sg-fg, #0f172a);
43
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;
44
+ }
45
+ .sv-fbt:hover:not(:disabled) { background: color-mix(in srgb, var(--c) 22%, transparent); }
46
+ .sv-fbt:disabled { opacity: 0.45; cursor: default; }
47
+ </style>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Button cells. The action is the column's `fieldProps.onClick` when the app
3
+ * set one, then an automation that runs on this button (`<SvBase>` hands the
4
+ * grid `fieldButtons`), otherwise the URL template with `{Field name}` filled in from the
5
+ * row (values URL-encoded); only an http(s) result opens.
6
+ */
7
+ import type { FieldCellProps } from './registry.svelte.js';
8
+ declare const ButtonCell: import("svelte").Component<FieldCellProps<{
9
+ label?: string;
10
+ color?: string;
11
+ url?: string;
12
+ onClick?: (ctx: {
13
+ row: unknown;
14
+ columnId: string;
15
+ rowId: string;
16
+ api: unknown;
17
+ }) => void;
18
+ }>, {}, "">;
19
+ type ButtonCell = ReturnType<typeof ButtonCell>;
20
+ export default ButtonCell;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Checkbox cells with the field's icon and color. The default icon is a
4
+ * box with a check; star, heart and the rest show filled when checked and as
5
+ * a faint outline when not. Clicking and Space still toggle through the grid
6
+ * (the column's editor is 'checkbox'); this only draws.
7
+ */
8
+ import type { FieldCellProps } from './registry.svelte'
9
+ import { ICONS, STROKE_ICONS, type IconName } from './palette'
10
+
11
+ let { value, props }: FieldCellProps<{ icon?: IconName; color?: string }> = $props()
12
+ const on = $derived(value === true || value === 'true' || value === 1)
13
+ const icon = $derived<IconName>(props.icon && props.icon in ICONS ? props.icon : 'check')
14
+ const color = $derived(props.color || '#20c933')
15
+ </script>
16
+
17
+ <span class="sv-fcb" role="checkbox" aria-checked={on} aria-readonly="true" aria-label={on ? 'Checked' : 'Not checked'} style:--c={color}>
18
+ {#if icon === 'check' || icon === 'x'}
19
+ <span class="sv-fcb-box" class:is-on={on}>
20
+ {#if on}<svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true"><path d={ICONS[icon]} fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" /></svg>{/if}
21
+ </span>
22
+ {:else}
23
+ <svg class="sv-fcb-icon" class:is-on={on} viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
24
+ <path
25
+ d={ICONS[icon]}
26
+ fill={on && !STROKE_ICONS.has(icon) ? 'var(--c)' : 'none'}
27
+ stroke={on ? 'var(--c)' : 'currentColor'}
28
+ stroke-width="1.8"
29
+ stroke-linecap="round"
30
+ stroke-linejoin="round"
31
+ />
32
+ </svg>
33
+ {/if}
34
+ </span>
35
+
36
+ <style>
37
+ .sv-fcb { display: inline-flex; align-items: center; justify-content: center; width: 100%; color: var(--sg-muted, #94a3b8); }
38
+ .sv-fcb-box {
39
+ display: inline-flex; align-items: center; justify-content: center;
40
+ width: 16px; height: 16px; border-radius: 4px; box-sizing: border-box;
41
+ border: 1.5px solid color-mix(in srgb, var(--sg-muted, #94a3b8) 70%, transparent);
42
+ }
43
+ .sv-fcb-box.is-on { background: var(--c); border-color: var(--c); }
44
+ .sv-fcb-icon { opacity: 0.45; }
45
+ .sv-fcb-icon.is-on { opacity: 1; }
46
+ </style>