@svgrid/enterprise 3.1.8 → 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 (268) 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 +11197 -10639
  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-Obk8sI77.js → index-BPU2KNc5.js} +307 -307
  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 +105 -4
  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/ui-components.generated.js +93 -1
  246. package/dist/upgrade-prompt.d.ts +1 -1
  247. package/dist/version.d.ts +2 -2
  248. package/dist/version.js +2 -2
  249. package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
  250. package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
  251. package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
  252. package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
  253. package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
  254. package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
  255. package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
  256. package/dist/wc/sv-sheet-element.js +7556 -7617
  257. package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
  258. package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
  259. package/package.json +14 -3
  260. package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
  261. package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
  262. package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
  263. package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
  264. package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
  265. package/dist/designer/assets/src-DqHCqori.js +0 -58
  266. package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
  267. package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
  268. package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
@@ -0,0 +1,350 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The select and user fields' editor (registered as 'fieldSelect'): the
4
+ * cell grows into a panel over itself, with the chosen items on top (each
5
+ * with a remove button), a search box, and the items drawn as the cells
6
+ * draw them: options as colored pills, people as avatar and name.
7
+ *
8
+ * Typing filters the options; one that does not exist yet is offered as
9
+ * "Add option", which adds it to the field (onColumnChange) and picks it
10
+ * (not for people: they come from setFieldUsers; not with `addOptions`
11
+ * false, as in a form, where only the field's options can be picked).
12
+ * Up / Down move, Enter picks. A single select commits on the pick; a
13
+ * multiple select keeps the dropdown open, Backspace in the empty search box
14
+ * removes the last pill, and Enter on nothing, Escape, Tab or a click
15
+ * outside keep what is chosen. Escape on a single select leaves the cell as
16
+ * it was.
17
+ */
18
+ import { portalToBody } from '@svgrid/grid'
19
+ import { onMount } from 'svelte'
20
+ import { fieldPropsOf } from './registry.svelte'
21
+ import { addSelectOptions } from './field-ops'
22
+ import { OPTION_CYCLE, textOn } from './palette'
23
+ import { getFieldUsers, type FieldUser } from './sources.svelte'
24
+ import UserFace from './UserFace.svelte'
25
+
26
+ type Option = { value: string; label?: string; color?: string; user?: FieldUser }
27
+ let {
28
+ value,
29
+ typed = false,
30
+ row,
31
+ columnDef,
32
+ columnId,
33
+ grid,
34
+ onChange,
35
+ onCommit,
36
+ onCancel,
37
+ onCommitAndMove,
38
+ addOptions = true,
39
+ }: {
40
+ value: unknown
41
+ typed?: boolean
42
+ row?: unknown
43
+ columnDef?: Record<string, unknown>
44
+ columnId: string
45
+ grid?: unknown
46
+ onChange?: (v: unknown) => void
47
+ onCommit: (v?: unknown) => void
48
+ onCancel: () => void
49
+ onCommitAndMove?: (v: unknown, direction: 1 | -1) => void
50
+ /** False: no "Add option", only the field's own options (a form). */
51
+ addOptions?: boolean
52
+ } = $props()
53
+
54
+ const def = $derived((columnDef ?? {}) as { fieldType?: unknown; fieldProps?: Record<string, unknown> })
55
+ // A user field picks people: from setFieldUsers, one or (with `multiple`) several.
56
+ const isUser = $derived(def.fieldType === 'user')
57
+ const multiple = $derived(isUser ? !!fieldPropsOf(def).multiple : def.fieldType === 'multiSelect')
58
+ // svelte-ignore state_referenced_locally
59
+ const declared: Option[] =
60
+ def.fieldType === 'user'
61
+ ? getFieldUsers().map((u) => ({ value: u.id, label: u.name, user: u }))
62
+ : ((fieldPropsOf(def).options as Array<{ value: unknown; label?: string; color?: string }> | undefined) ?? []).map(
63
+ (o): Option => ({ value: String(o.value), ...(o.label ? { label: o.label } : {}), ...(o.color ? { color: o.color } : {}) }),
64
+ )
65
+ // New options show here at once; they reach the field when the edit ends.
66
+ let added = $state<Option[]>([])
67
+ const options = $derived([...declared, ...added])
68
+ // Typed on the cell: the text searches the options, and the choices
69
+ // already made are the row's.
70
+ // svelte-ignore state_referenced_locally
71
+ const start = typed ? (row as Record<string, unknown> | undefined)?.[String(columnDef?.field ?? '')] : value
72
+ // svelte-ignore state_referenced_locally
73
+ let picked = $state<string[]>((Array.isArray(start) ? start : start == null || start === '' ? [] : [start]).map(String))
74
+ // svelte-ignore state_referenced_locally
75
+ let search = $state(typed ? String(value ?? '') : '')
76
+ let active = $state(0)
77
+ let finished = false
78
+
79
+ const labelOf = (o: Option) => o.label ?? o.value
80
+ const byValue = $derived(new Map(options.map((o) => [o.value, o])))
81
+ const shown = $derived.by(() => {
82
+ const q = search.trim().toLowerCase()
83
+ return options.filter((o) => (!multiple || !picked.includes(o.value)) && (!q || labelOf(o).toLowerCase().includes(q)))
84
+ })
85
+ const newValue = $derived.by(() => {
86
+ const t = search.trim()
87
+ return t && addOptions && !isUser && !options.some((o) => labelOf(o).toLowerCase() === t.toLowerCase() || o.value.toLowerCase() === t.toLowerCase()) ? t : null
88
+ })
89
+ const count = $derived(shown.length + (newValue ? 1 : 0))
90
+ $effect(() => {
91
+ if (active >= count) active = Math.max(0, count - 1)
92
+ })
93
+
94
+ // ---- the panel: the cell grown in place, over the cell and down from it;
95
+ // in the body, so the cell cannot clip it
96
+ let anchor = $state<HTMLElement | null>(null)
97
+ let input = $state<HTMLInputElement | null>(null)
98
+ let list = $state<HTMLElement | null>(null)
99
+ let panel = $state<HTMLElement | null>(null)
100
+ let rect = $state<{ top: number; left: number; width: number; maxHeight: number } | null>(null)
101
+ const PANEL_HEIGHT = 340
102
+ function place() {
103
+ const cell = anchor?.closest('td') ?? anchor
104
+ if (!cell) return
105
+ const r = cell.getBoundingClientRect()
106
+ const width = Math.min(Math.max(r.width + 2, 260), window.innerWidth - 16)
107
+ const left = Math.max(8, Math.min(r.left - 1, window.innerWidth - width - 8))
108
+ // Near the bottom of the window the panel moves up rather than shrink.
109
+ const top = Math.max(8, Math.min(r.top - 1, window.innerHeight - PANEL_HEIGHT - 8))
110
+ rect = { top, left, width, maxHeight: window.innerHeight - top - 8 }
111
+ }
112
+ onMount(() => {
113
+ // A single select opens on its current choice.
114
+ if (!multiple && picked.length) active = Math.max(0, shown.findIndex((o) => o.value === picked[0]))
115
+ place()
116
+ // The search box (caret after any typed text); once more on the next
117
+ // frame, as the double-click that opened the editor can take focus back.
118
+ const takeFocus = () => {
119
+ if (!input || document.activeElement === input) return
120
+ input.focus({ preventScroll: true })
121
+ input.setSelectionRange(search.length, search.length)
122
+ }
123
+ queueMicrotask(takeFocus)
124
+ const frame = requestAnimationFrame(takeFocus)
125
+ const onScroll = (e: Event) => {
126
+ if (!panel?.contains(e.target as Node)) place()
127
+ }
128
+ const onDown = (e: PointerEvent) => {
129
+ const t = e.target as Node
130
+ if (panel?.contains(t) || anchor?.contains(t)) return
131
+ keep()
132
+ }
133
+ window.addEventListener('resize', place)
134
+ window.addEventListener('scroll', onScroll, true)
135
+ document.addEventListener('pointerdown', onDown, true)
136
+ return () => {
137
+ cancelAnimationFrame(frame)
138
+ window.removeEventListener('resize', place)
139
+ window.removeEventListener('scroll', onScroll, true)
140
+ document.removeEventListener('pointerdown', onDown, true)
141
+ }
142
+ })
143
+
144
+ // ---- picking
145
+ const current = (): unknown => (multiple ? [...picked] : (picked[0] ?? null))
146
+ function finish(how: 'commit' | 'cancel' | 'next' | 'prev') {
147
+ if (finished) return
148
+ finished = true
149
+ if (how === 'cancel') return onCancel()
150
+ // Read everything first: committing ends the edit and unmounts this
151
+ // editor, and its props are gone after that.
152
+ const v = current()
153
+ const g = grid
154
+ const id = columnId
155
+ const used = added.filter((o) => picked.includes(o.value))
156
+ if (how === 'commit') onCommit(v)
157
+ else if (onCommitAndMove) onCommitAndMove(v, how === 'next' ? 1 : -1)
158
+ else onCommit(v)
159
+ // New options reach the field after the value, so the grid is not
160
+ // refilling the column while the edit is still open.
161
+ if (used.length && g) addSelectOptions(g as never, id, used)
162
+ }
163
+ const keep = () => finish('commit')
164
+ function choose(i: number) {
165
+ let v: string
166
+ if (i < shown.length) v = shown[i]!.value
167
+ else if (newValue) {
168
+ v = newValue
169
+ added = [...added, { value: v, color: OPTION_CYCLE[options.length % OPTION_CYCLE.length] }]
170
+ } else return
171
+ search = ''
172
+ if (!multiple) {
173
+ picked = [v]
174
+ return keep()
175
+ }
176
+ picked = [...picked, v]
177
+ onChange?.(current())
178
+ active = 0
179
+ }
180
+ function remove(v: string) {
181
+ picked = picked.filter((x) => x !== v)
182
+ onChange?.(current())
183
+ input?.focus()
184
+ }
185
+ function move(by: number) {
186
+ if (!count) return
187
+ active = (active + by + count) % count
188
+ queueMicrotask(() => list?.querySelector(`[data-i="${active}"]`)?.scrollIntoView({ block: 'nearest' }))
189
+ }
190
+ function onKey(e: KeyboardEvent) {
191
+ if (e.key === 'ArrowDown') move(1)
192
+ else if (e.key === 'ArrowUp') move(-1)
193
+ else if (e.key === 'Enter') count ? choose(active) : keep()
194
+ else if (e.key === 'Escape') multiple ? keep() : finish('cancel')
195
+ else if (e.key === 'Tab') finish(e.shiftKey ? 'prev' : 'next')
196
+ else if (e.key === 'Backspace' && !search && multiple && picked.length) remove(picked[picked.length - 1]!)
197
+ else {
198
+ e.stopPropagation()
199
+ return
200
+ }
201
+ e.preventDefault()
202
+ e.stopPropagation()
203
+ }
204
+ const inputValue = (e: Event) => (e.currentTarget as HTMLInputElement).value
205
+ const listId = `sv-fse-${Math.random().toString(36).slice(2, 8)}`
206
+ </script>
207
+
208
+ <!-- One chosen or offered item, drawn as its cell draws it. -->
209
+ {#snippet item(v: string, removable: boolean)}
210
+ {@const o = byValue.get(v)}
211
+ {@const label = o ? labelOf(o) : v}
212
+ {#if isUser}
213
+ <span class="sv-fse-user" class:is-chip={removable}>
214
+ <UserFace user={o?.user ?? { id: v, name: v }} />
215
+ <span class="sv-fse-user-name">{label}</span>
216
+ {#if removable}{@render removeButton(v, label)}{/if}
217
+ </span>
218
+ {:else}
219
+ <span class="sv-fse-pill" class:is-plain={!o?.color} style:background={o?.color} style:color={o?.color ? textOn(o.color) : undefined}>
220
+ {label}
221
+ {#if removable}{@render removeButton(v, label)}{/if}
222
+ </span>
223
+ {/if}
224
+ {/snippet}
225
+ {#snippet removeButton(v: string, label: string)}
226
+ <button type="button" class="sv-fse-x" aria-label={`Remove ${label}`} onmousedown={(e) => e.preventDefault()} onclick={() => remove(v)}>
227
+ <svg viewBox="0 0 24 24" width="10" height="10" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" /></svg>
228
+ </button>
229
+ {/snippet}
230
+
231
+ <span class="sv-fse-cell" bind:this={anchor}>
232
+ {#each picked as v (v)}
233
+ {@render item(v, false)}
234
+ {/each}
235
+ </span>
236
+
237
+ {#if rect}
238
+ <div
239
+ class="sv-fse-panel"
240
+ bind:this={panel}
241
+ use:portalToBody
242
+ style:top={`${rect.top}px`}
243
+ style:left={`${rect.left}px`}
244
+ style:width={`${rect.width}px`}
245
+ style:max-height={`${rect.maxHeight}px`}
246
+ >
247
+ {#if picked.length}
248
+ <div class="sv-fse-chosen">
249
+ {#each picked as v (v)}
250
+ {@render item(v, true)}
251
+ {/each}
252
+ </div>
253
+ {/if}
254
+ <input
255
+ bind:this={input}
256
+ class="sv-fse-search"
257
+ placeholder={isUser ? 'Find a person' : 'Find an option'}
258
+ role="combobox"
259
+ aria-expanded="true"
260
+ aria-controls={listId}
261
+ aria-activedescendant={count ? `${listId}-${active}` : undefined}
262
+ aria-label={isUser ? (multiple ? 'Add a person' : 'Choose a person') : multiple ? 'Add an option' : 'Choose an option'}
263
+ value={search}
264
+ oninput={(e) => {
265
+ search = inputValue(e)
266
+ active = 0
267
+ }}
268
+ onkeydown={onKey}
269
+ />
270
+ <ul class="sv-fse-list" id={listId} role="listbox" bind:this={list}>
271
+ {#each shown as o, i (o.value)}
272
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
273
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
274
+ <li
275
+ id={`${listId}-${i}`}
276
+ data-i={i}
277
+ role="option"
278
+ aria-selected={picked.includes(o.value)}
279
+ class="sv-fse-option"
280
+ class:is-active={i === active}
281
+ onpointermove={() => (active = i)}
282
+ onmousedown={(e) => e.preventDefault()}
283
+ onclick={() => choose(i)}
284
+ >
285
+ {@render item(o.value, false)}
286
+ {#if !multiple && picked[0] === o.value}
287
+ <svg class="sv-fse-check" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M5 12l5 5 9-10" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" /></svg>
288
+ {/if}
289
+ </li>
290
+ {/each}
291
+ {#if newValue}
292
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
293
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
294
+ <li
295
+ id={`${listId}-${shown.length}`}
296
+ data-i={shown.length}
297
+ role="option"
298
+ aria-selected="false"
299
+ class="sv-fse-option sv-fse-add"
300
+ class:is-active={active === shown.length}
301
+ onpointermove={() => (active = shown.length)}
302
+ onmousedown={(e) => e.preventDefault()}
303
+ onclick={() => choose(shown.length)}
304
+ >
305
+ <span class="sv-fse-add-label">Add option</span>
306
+ <span class="sv-fse-pill" style:background={OPTION_CYCLE[options.length % OPTION_CYCLE.length]} style:color={textOn(OPTION_CYCLE[options.length % OPTION_CYCLE.length])}>{newValue}</span>
307
+ </li>
308
+ {/if}
309
+ {#if !count}
310
+ <li class="sv-fse-empty">
311
+ {#if isUser}{multiple && options.length && !search ? 'Everyone is chosen' : options.length ? 'Nobody matches' : 'No people (setFieldUsers)'}
312
+ {:else}{multiple && options.length && !search ? 'Every option is chosen' : search && options.length ? 'No option matches' : 'No options'}{/if}
313
+ </li>
314
+ {/if}
315
+ </ul>
316
+ </div>
317
+ {/if}
318
+
319
+ <style>
320
+ .sv-fse-cell { display: flex; align-items: center; gap: 4px; height: 100%; padding: 0 6px; box-sizing: border-box; overflow: hidden; }
321
+ .sv-fse-chosen { display: flex; flex-wrap: wrap; gap: 4px; padding: 2px 2px 8px; }
322
+ .sv-fse-pill {
323
+ display: inline-flex; align-items: center; gap: 4px; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
324
+ padding: 0 8px; border-radius: 999px; font-size: 12px; line-height: 20px;
325
+ }
326
+ .sv-fse-pill.is-plain { background: var(--sg-header-bg, #eef0f3); color: var(--sg-fg, #0f172a); border: 1px solid var(--sg-border, #e2e8f0); line-height: 18px; }
327
+ .sv-fse-user { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; font-size: 13px; }
328
+ .sv-fse-user.is-chip { padding: 2px 6px 2px 2px; border-radius: 999px; background: var(--sg-header-bg, #eef0f3); border: 1px solid var(--sg-border, #e2e8f0); }
329
+ .sv-fse-user-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
330
+ .sv-fse-x { display: inline-flex; padding: 2px; margin-inline-end: -4px; border: 0; border-radius: 999px; background: none; color: inherit; opacity: 0.7; cursor: pointer; }
331
+ .sv-fse-x:hover { opacity: 1; background: color-mix(in srgb, currentColor 18%, transparent); }
332
+ .sv-fse-panel {
333
+ position: fixed; z-index: 2147483646; display: flex; flex-direction: column; box-sizing: border-box; padding: 6px;
334
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 6px);
335
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
336
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.16); font-size: 13px;
337
+ }
338
+ .sv-fse-search {
339
+ flex: none; height: 30px; padding: 0 8px; font: inherit; box-sizing: border-box; width: 100%;
340
+ border: 1px solid var(--sg-input-border, var(--sg-border, #e2e8f0)); border-radius: var(--sg-radius, 6px);
341
+ background: var(--sg-input-bg, var(--sg-bg, #fff)); color: var(--sg-fg, #0f172a);
342
+ }
343
+ .sv-fse-search:focus { outline: none; border-color: var(--sg-accent, #2563eb); }
344
+ .sv-fse-list { list-style: none; margin: 6px 0 0; padding: 0; overflow: auto; min-height: 0; }
345
+ .sv-fse-option { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 6px; border-radius: 4px; cursor: pointer; }
346
+ .sv-fse-option.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
347
+ .sv-fse-check { margin-inline-start: auto; flex: none; }
348
+ .sv-fse-add-label { flex: none; color: var(--sg-muted, #64748b); }
349
+ .sv-fse-empty { padding: 8px 6px; color: var(--sg-muted, #64748b); }
350
+ </style>
@@ -0,0 +1,17 @@
1
+ type $$ComponentProps = {
2
+ value: unknown;
3
+ typed?: boolean;
4
+ row?: unknown;
5
+ columnDef?: Record<string, unknown>;
6
+ columnId: string;
7
+ grid?: unknown;
8
+ onChange?: (v: unknown) => void;
9
+ onCommit: (v?: unknown) => void;
10
+ onCancel: () => void;
11
+ onCommitAndMove?: (v: unknown, direction: 1 | -1) => void;
12
+ /** False: no "Add option", only the field's own options (a form). */
13
+ addOptions?: boolean;
14
+ };
15
+ declare const SelectEditor: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type SelectEditor = ReturnType<typeof SelectEditor>;
17
+ export default SelectEditor;
@@ -0,0 +1,160 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A field's cell in the footer summary row (the grid's `summary` prop): the
4
+ * summary it shows, named ("Sum $4,320", "Empty 3"), or a quiet
5
+ * "Summarize" on hover. A click opens the summaries that fit the field
6
+ * (sum, average... for numbers; checked for a checkbox; earliest, latest
7
+ * for dates; empty, filled, unique for all); the choice is kept in the
8
+ * field (`fieldProps.summary`, through onColumnChange). The primary field,
9
+ * until one is picked for it, shows how many records the view holds.
10
+ */
11
+ import { portalToBody } from '@svgrid/grid'
12
+ import { fieldPropsOf } from './registry.svelte'
13
+ import { setFieldSummary } from './field-ops'
14
+ import { summaryChoices, summaryLabel, type SummaryKind } from './summaries'
15
+ import { groupLeaves, leafFilter, primaryIdOf } from './grid-access'
16
+
17
+ let { ctrl, column, text }: { ctrl: any; column: { id: string; columnDef: Record<string, any> }; text: string } = $props()
18
+
19
+ const isField = $derived(!!column.columnDef.fieldType)
20
+ const kind = $derived((fieldPropsOf(column.columnDef as never).summary as SummaryKind | undefined) ?? 'none')
21
+ const choices = $derived(isField ? summaryChoices(column.columnDef) : [])
22
+ // The record count, in the primary field's cell while it has no summary of its own.
23
+ const showsCount = $derived(kind === 'none' && primaryIdOf(ctrl) === column.id)
24
+ type CountRow = { depth?: number; leafCount?: number; getCanExpand?: () => boolean; __treeRow?: boolean }
25
+ const count = $derived.by(() => {
26
+ if (!showsCount) return 0
27
+ const rows = (ctrl.allRows ?? []) as CountRow[]
28
+ // Grouped: a collapsed group hides its rows, which are still records; the
29
+ // grid groups before it filters, so a group's size counts the view's filter.
30
+ const tops = rows.filter((r) => (r.depth ?? 0) === 0 && !r.__treeRow && typeof r.getCanExpand === 'function' && r.getCanExpand() && r.leafCount != null)
31
+ if (!tops.length) return rows.length
32
+ const keep = leafFilter(ctrl)
33
+ return keep ? tops.reduce((n, r) => n + groupLeaves(r as never, keep).length, 0) : tops.reduce((n, r) => n + (r.leafCount ?? 0), 0)
34
+ })
35
+ const right = $derived(column.columnDef.align === 'right' || (column.columnDef.align == null && column.columnDef.cellDataType === 'number'))
36
+
37
+ let button = $state<HTMLButtonElement | null>(null)
38
+ let menu = $state<HTMLElement | null>(null)
39
+ let at = $state<{ left: number; bottom: number; width: number } | null>(null)
40
+
41
+ function toggle() {
42
+ if (at) return close()
43
+ const r = button!.getBoundingClientRect()
44
+ // The footer sits at the grid's bottom: the menu opens upward.
45
+ at = { left: Math.max(8, Math.min(r.left, window.innerWidth - 220)), bottom: window.innerHeight - r.top + 4, width: Math.max(r.width, 200) }
46
+ queueMicrotask(() => menu?.querySelector<HTMLElement>('[aria-checked="true"]')?.focus())
47
+ }
48
+ function close() {
49
+ at = null
50
+ }
51
+ function pick(value: SummaryKind) {
52
+ close()
53
+ setFieldSummary(ctrl, column.id, value)
54
+ button?.focus()
55
+ }
56
+ function onMenuKey(e: KeyboardEvent) {
57
+ const items = [...(menu?.querySelectorAll<HTMLElement>('[role="menuitemradio"]') ?? [])]
58
+ const i = items.indexOf(document.activeElement as HTMLElement)
59
+ if (e.key === 'Escape') {
60
+ close()
61
+ button?.focus()
62
+ } else if (e.key === 'ArrowDown') items[(i + 1) % items.length]?.focus()
63
+ else if (e.key === 'ArrowUp') items[(i - 1 + items.length) % items.length]?.focus()
64
+ else return
65
+ e.preventDefault()
66
+ }
67
+ function dismissOutside(node: HTMLElement) {
68
+ const onDown = (e: PointerEvent) => {
69
+ if (!node.contains(e.target as Node) && !button?.contains(e.target as Node)) close()
70
+ }
71
+ document.addEventListener('pointerdown', onDown, true)
72
+ return { destroy: () => document.removeEventListener('pointerdown', onDown, true) }
73
+ }
74
+ </script>
75
+
76
+ {#if !isField}
77
+ <span class="sv-grid-cell-text">{text}</span>
78
+ {:else}
79
+ <button
80
+ type="button"
81
+ class="sv-fsm"
82
+ class:is-right={right}
83
+ class:is-set={kind !== 'none' || showsCount}
84
+ bind:this={button}
85
+ aria-haspopup="menu"
86
+ aria-expanded={!!at}
87
+ aria-label={showsCount ? `${count} ${count === 1 ? 'record' : 'records'}, summarize ${column.columnDef.header ?? column.id}` : kind === 'none' ? `Summarize ${column.columnDef.header ?? column.id}` : `${summaryLabel(kind)} ${text}, change summary`}
88
+ onclick={toggle}
89
+ >
90
+ {#if showsCount}
91
+ <span class="sv-fsm-value">{count}</span>
92
+ <span class="sv-fsm-label">{count === 1 ? 'record' : 'records'}</span>
93
+ {:else if kind === 'none'}
94
+ <span class="sv-fsm-hint">Summarize</span>
95
+ <svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
96
+ {:else}
97
+ <span class="sv-fsm-label">{summaryLabel(kind)}</span>
98
+ <span class="sv-fsm-value">{text}</span>
99
+ {/if}
100
+ </button>
101
+ {#if at}
102
+ <div
103
+ class="sv-fsm-menu"
104
+ role="menu"
105
+ tabindex="-1"
106
+ aria-label="Summary"
107
+ bind:this={menu}
108
+ use:portalToBody
109
+ use:dismissOutside
110
+ style:left={`${at.left}px`}
111
+ style:bottom={`${at.bottom}px`}
112
+ style:min-width={`${at.width}px`}
113
+ onkeydown={onMenuKey}
114
+ >
115
+ {#each choices as c (c.value)}
116
+ <button type="button" role="menuitemradio" aria-checked={c.value === kind} class="sv-fsm-item" onclick={() => pick(c.value)}>
117
+ <span>{c.label}</span>
118
+ {#if c.value === kind}<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M5 12l5 5 9-10" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" /></svg>{/if}
119
+ </button>
120
+ {/each}
121
+ </div>
122
+ {/if}
123
+ {/if}
124
+
125
+ <style>
126
+ .sv-fsm {
127
+ display: flex; align-items: center; gap: 6px; width: 100%; height: 100%; min-height: 28px; padding: 0 8px; box-sizing: border-box;
128
+ border: 0; background: none; color: var(--sg-fg, #0f172a); font: inherit; font-size: 13px; font-weight: 400; cursor: pointer; text-align: start;
129
+ }
130
+ .sv-fsm.is-right { justify-content: flex-end; }
131
+ .sv-fsm-hint { color: var(--sg-muted, #64748b); }
132
+ /* "Summarize" is quiet: it shows on the footer row's hover and on focus. */
133
+ .sv-fsm:not(.is-set) { opacity: 0; }
134
+ :global(.sv-grid-summary-row:hover) .sv-fsm:not(.is-set),
135
+ .sv-fsm:not(.is-set):focus-visible,
136
+ .sv-fsm[aria-expanded='true'] { opacity: 1; }
137
+ /* A base's summary row is drawn as the "+ Add row" line under it is: on
138
+ the column headers' ground, one line above, no column lines through it.
139
+ Every cell of it (row number, pinned, spacers) the same; `--sg-summary-bg`
140
+ picks a ground of its own. */
141
+ :global(.sv-fbase .sv-grid-foot > .sv-grid-summary-row > :is(th, td)) {
142
+ background: var(--sg-summary-bg, var(--sg-header-bg, #f5f7fb));
143
+ border-top: 1px solid var(--sg-border, #e2e8f0);
144
+ border-bottom: 1px solid var(--sg-border, #e2e8f0);
145
+ /* One band: no column lines through it, no seam by the row numbers. */
146
+ border-inline-end-color: transparent;
147
+ }
148
+ .sv-fsm-label { color: var(--sg-muted, #64748b); font-weight: 400; }
149
+ .sv-fsm-value { font-weight: 600; font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
150
+ .sv-fsm-menu {
151
+ position: fixed; z-index: 2147483646; display: flex; flex-direction: column; padding: 4px; max-height: 60vh; overflow: auto;
152
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 6px);
153
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
154
+ }
155
+ .sv-fsm-item {
156
+ display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 30px; padding: 0 10px;
157
+ border: 0; border-radius: 4px; background: none; color: inherit; font: inherit; font-size: 13px; text-align: start; cursor: pointer;
158
+ }
159
+ .sv-fsm-item:hover, .sv-fsm-item:focus-visible { background: var(--sg-row-hover-bg, #f1f5f9); outline: none; }
160
+ </style>
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ ctrl: any;
3
+ column: {
4
+ id: string;
5
+ columnDef: Record<string, any>;
6
+ };
7
+ text: string;
8
+ };
9
+ declare const SummaryCell: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type SummaryCell = ReturnType<typeof SummaryCell>;
11
+ export default SummaryCell;