@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,242 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Attachment cells in read mode: images as square thumbnails, other files as
4
+ * chips (type badge + name), as many as the column width holds, then "+N".
5
+ * Thumbnails are a fixed size with lazy loading and async decoding, so a
6
+ * scroll never waits on an image or re-lays out the row.
7
+ *
8
+ * Files dropped on the cell are added to it (uploaded with the column's
9
+ * uploader, written through the grid's edit path). Each file uploads on
10
+ * its own: the ones that land are written, a refused or failed one is
11
+ * reported in a toast, and uploads still running when the cell goes away
12
+ * are aborted.
13
+ */
14
+ import { toast } from '@svgrid/grid'
15
+ import type { FieldCellProps } from './registry.svelte'
16
+ import { isImage, limitAttachments, rejectReason, safeUrl, toAttachments, uploadAttachment, type Attachment, type AttachmentFieldProps } from './attachments'
17
+ import { writeCell } from './grid-access'
18
+
19
+ let { ctrl, row, column, value, props }: FieldCellProps<AttachmentFieldProps> = $props()
20
+
21
+ const THUMB = 26
22
+ const CHIP = 96
23
+ const GAP = 4
24
+ const PLUS = 30
25
+
26
+ const items = $derived(toAttachments(value))
27
+ const width = $derived(
28
+ Number((ctrl as { getColumnWidth?: (id: string) => number }).getColumnWidth?.(column.id)) ||
29
+ Number(column.columnDef.width) ||
30
+ 200,
31
+ )
32
+ // How many fit, leaving room for the "+N" badge when some do not.
33
+ const shown = $derived.by(() => {
34
+ const room = width - 12
35
+ let used = 0
36
+ let n = 0
37
+ for (const a of items) {
38
+ const w = (isImage(a) ? THUMB : CHIP) + GAP
39
+ const rest = items.length - n - 1
40
+ if (used + w + (rest > 0 ? PLUS : 0) > room) break
41
+ used += w
42
+ n += 1
43
+ }
44
+ return Math.max(items.length ? 1 : 0, n)
45
+ })
46
+ const label = $derived(
47
+ items.length === 0
48
+ ? 'No attachments'
49
+ : `${items.length} attachment${items.length === 1 ? '' : 's'}: ${items.map((a) => a.name).join(', ')}`,
50
+ )
51
+ const ext = (a: Attachment) => (a.name.split('.').pop() ?? '').slice(0, 4).toUpperCase() || 'FILE'
52
+
53
+ // ---- Files dropped on the cell ----
54
+ const canDrop = $derived(!!(ctrl as { editingEnabled?: boolean } | null)?.editingEnabled && column.columnDef.editable !== false && !!row?.id)
55
+ let over = $state(false)
56
+ const hasFiles = (e: DragEvent) => !!e.dataTransfer && [...e.dataTransfer.types].includes('Files')
57
+ function dragOver(e: DragEvent) {
58
+ if (!canDrop || !hasFiles(e)) return
59
+ e.preventDefault()
60
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
61
+ over = true
62
+ }
63
+ // Each running upload's progress (0..1), averaged into the bar under the cell.
64
+ let uploads = $state<Record<string, number>>({})
65
+ const progress = $derived.by(() => {
66
+ const parts = Object.values(uploads)
67
+ return parts.length ? parts.reduce((s, p) => s + p, 0) / parts.length : null
68
+ })
69
+ const running = new Set<AbortController>()
70
+ $effect(() => () => {
71
+ for (const ctl of running) ctl.abort()
72
+ })
73
+ let seq = 0
74
+
75
+ async function drop(e: DragEvent) {
76
+ over = false
77
+ if (!canDrop || !hasFiles(e)) return
78
+ e.preventDefault()
79
+ e.stopPropagation()
80
+ // The row as it was dropped on; the write goes to it even if the cell is reused.
81
+ const rowId = row.id
82
+ const original = row.original as Record<string, unknown> | undefined
83
+ const field = column.columnDef.field as string | undefined
84
+ const current = () => (original && field ? toAttachments(original[field]) : items)
85
+ const refused: string[] = []
86
+ const queue: File[] = []
87
+ for (const file of [...(e.dataTransfer?.files ?? [])]) {
88
+ const why = rejectReason(file, props, items.length + queue.length)
89
+ if (why) refused.push(`${file.name}: ${why}`)
90
+ else queue.push(file)
91
+ }
92
+ if (refused.length) toast.warning(refused.join('\n'), { title: 'Not added' })
93
+ if (!queue.length) return
94
+ const ctl = new AbortController()
95
+ running.add(ctl)
96
+ const keys = queue.map(() => `u${++seq}`)
97
+ uploads = { ...uploads, ...Object.fromEntries(keys.map((k) => [k, 0])) }
98
+ const results = await Promise.allSettled(
99
+ queue.map((file, i) =>
100
+ uploadAttachment(file, props, {
101
+ row: original,
102
+ columnId: column.id,
103
+ signal: ctl.signal,
104
+ onProgress: (f) => {
105
+ if (keys[i]! in uploads) uploads = { ...uploads, [keys[i]!]: Math.max(0, Math.min(1, f)) }
106
+ },
107
+ }),
108
+ ),
109
+ )
110
+ running.delete(ctl)
111
+ if (ctl.signal.aborted) return
112
+ uploads = Object.fromEntries(Object.entries(uploads).filter(([k]) => !keys.includes(k)))
113
+ const failed: string[] = []
114
+ const added: Attachment[] = []
115
+ results.forEach((r, i) => {
116
+ if (r.status === 'fulfilled') added.push(r.value)
117
+ else failed.push(`${queue[i]!.name}: ${r.reason instanceof Error && r.reason.message ? r.reason.message : 'Upload failed'}`)
118
+ })
119
+ // Another drop may have filled the cell meanwhile; maxFiles still holds.
120
+ const base = current()
121
+ const { kept, rejected } = limitAttachments(added, props, base.length)
122
+ if (kept.length) writeCell(ctrl, rowId, column.id, [...base, ...kept])
123
+ if (rejected.length) toast.warning(rejected.join('\n'), { title: 'Not added' })
124
+ if (failed.length) toast.error(failed.join('\n'), { title: failed.length === 1 ? 'Upload failed' : `${failed.length} uploads failed` })
125
+ }
126
+ </script>
127
+
128
+ <span
129
+ class="sv-att"
130
+ class:is-drop={over}
131
+ role="img"
132
+ aria-label={label}
133
+ title={items.map((a) => a.name).join('\n')}
134
+ ondragover={dragOver}
135
+ ondragleave={() => (over = false)}
136
+ ondrop={drop}
137
+ >
138
+ {#each items.slice(0, shown) as a (a.id)}
139
+ {#if isImage(a) && safeUrl(a.thumbnailUrl ?? a.url, true)}
140
+ <img
141
+ class="sv-att-thumb"
142
+ src={safeUrl(a.thumbnailUrl ?? a.url, true)}
143
+ alt=""
144
+ width={THUMB}
145
+ height={THUMB}
146
+ loading="lazy"
147
+ decoding="async"
148
+ draggable="false"
149
+ />
150
+ {:else}
151
+ <span class="sv-att-chip">
152
+ <span class="sv-att-ext">{ext(a)}</span>
153
+ <span class="sv-att-name">{a.name}</span>
154
+ </span>
155
+ {/if}
156
+ {/each}
157
+ {#if items.length > shown}
158
+ <span class="sv-att-more">+{items.length - shown}</span>
159
+ {/if}
160
+ {#if progress !== null}
161
+ <span
162
+ class="sv-att-progress"
163
+ role="progressbar"
164
+ aria-label="Uploading"
165
+ aria-valuemin="0"
166
+ aria-valuemax="100"
167
+ aria-valuenow={Math.round(progress * 100)}
168
+ style:--sv-att-done={`${Math.round(progress * 100)}%`}
169
+ ></span>
170
+ {/if}
171
+ </span>
172
+
173
+ <style>
174
+ .sv-att {
175
+ position: relative;
176
+ display: inline-flex;
177
+ align-items: center;
178
+ gap: 4px;
179
+ min-width: 0;
180
+ width: 100%;
181
+ min-height: 100%;
182
+ overflow: hidden;
183
+ }
184
+ /* Files over the cell: where they will land. */
185
+ .sv-att.is-drop {
186
+ outline: 2px dashed var(--sg-accent, #2563eb);
187
+ outline-offset: -2px;
188
+ border-radius: 4px;
189
+ }
190
+ .sv-att-thumb {
191
+ flex: none;
192
+ width: 26px;
193
+ height: 26px;
194
+ object-fit: cover;
195
+ border-radius: 4px;
196
+ border: 1px solid var(--sg-border, #e2e8f0);
197
+ background: var(--sg-header-bg, #f8fafc);
198
+ }
199
+ .sv-att-chip {
200
+ flex: none;
201
+ display: inline-flex;
202
+ align-items: center;
203
+ gap: 4px;
204
+ width: 96px;
205
+ height: 24px;
206
+ padding: 0 6px 0 3px;
207
+ border-radius: 4px;
208
+ border: 1px solid var(--sg-border, #e2e8f0);
209
+ background: var(--sg-header-bg, #f8fafc);
210
+ font-size: 11px;
211
+ }
212
+ .sv-att-ext {
213
+ flex: none;
214
+ padding: 1px 3px;
215
+ border-radius: 3px;
216
+ background: var(--sg-bg, #fff);
217
+ color: var(--sg-muted, #64748b);
218
+ font-size: 9px;
219
+ font-weight: 600;
220
+ letter-spacing: 0.02em;
221
+ }
222
+ .sv-att-name {
223
+ min-width: 0;
224
+ overflow: hidden;
225
+ text-overflow: ellipsis;
226
+ white-space: nowrap;
227
+ }
228
+ .sv-att-more {
229
+ flex: none;
230
+ font-size: 12px;
231
+ color: var(--sg-muted, #64748b);
232
+ }
233
+ /* Uploads from a drop: a thin bar along the bottom of the cell. */
234
+ .sv-att-progress {
235
+ position: absolute;
236
+ left: 0;
237
+ right: 0;
238
+ bottom: 0;
239
+ height: 2px;
240
+ background: linear-gradient(to right, var(--sg-accent, #2563eb) var(--sv-att-done), transparent var(--sv-att-done));
241
+ }
242
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { FieldCellProps } from './registry.svelte.js';
2
+ import { type AttachmentFieldProps } from './attachments.js';
3
+ declare const AttachmentCell: import("svelte").Component<FieldCellProps<AttachmentFieldProps>, {}, "">;
4
+ type AttachmentCell = ReturnType<typeof AttachmentCell>;
5
+ export default AttachmentCell;
@@ -0,0 +1,357 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The attachment field's editor: every file in the cell as a tile, with
4
+ * add (file picker, drop, paste, URL), remove, reorder (drag, or Alt+Left /
5
+ * Alt+Right on a tile) and preview. Uploads run through the column's
6
+ * uploader with progress; a failed one stays as a tile with Retry. Done
7
+ * commits the list, Cancel (or Escape) leaves the cell as it was.
8
+ */
9
+ import { SvModal } from '@svgrid/grid'
10
+ import AttachmentPreview from './AttachmentPreview.svelte'
11
+ import {
12
+ attachmentsFromText,
13
+ formatBytes,
14
+ isImage,
15
+ limitAttachments,
16
+ rejectReason,
17
+ safeUrl,
18
+ toAttachments,
19
+ uploadAttachment,
20
+ type Attachment,
21
+ type AttachmentFieldProps,
22
+ } from './attachments'
23
+
24
+ let {
25
+ value,
26
+ title = 'Attachments',
27
+ props,
28
+ row,
29
+ columnId,
30
+ onDone,
31
+ onCancel,
32
+ }: {
33
+ value: unknown
34
+ title?: string
35
+ props: AttachmentFieldProps
36
+ row: unknown
37
+ columnId: string
38
+ onDone: (next: Attachment[]) => void
39
+ onCancel: () => void
40
+ } = $props()
41
+
42
+ type Pending = { key: string; file: File; progress: number; error: string | null; ctl: AbortController }
43
+
44
+ // svelte-ignore state_referenced_locally
45
+ let items = $state<Attachment[]>(toAttachments(value))
46
+ let pending = $state<Pending[]>([])
47
+ let rejected = $state<string[]>([])
48
+ let urlText = $state('')
49
+ let previewIndex = $state<number | null>(null)
50
+ let dragFrom = $state<number | null>(null)
51
+ let dropping = $state(false)
52
+ let fileInput = $state<HTMLInputElement | null>(null)
53
+ let open = $state(true)
54
+ let finished = false
55
+
56
+ const busy = $derived(pending.some((p) => !p.error))
57
+
58
+ function addFiles(files: Iterable<File>) {
59
+ rejected = []
60
+ for (const file of files) {
61
+ const why = rejectReason(file, props, items.length + pending.length)
62
+ if (why) {
63
+ rejected = [...rejected, `${file.name}: ${why}`]
64
+ continue
65
+ }
66
+ start({ key: `${file.name}-${file.size}-${Math.random()}`, file, progress: 0, error: null, ctl: new AbortController() })
67
+ }
68
+ }
69
+
70
+ function start(p: Pending) {
71
+ pending = [...pending.filter((x) => x.key !== p.key), p]
72
+ const set = (patch: Partial<Pending>) => (pending = pending.map((x) => (x.key === p.key ? { ...x, ...patch } : x)))
73
+ uploadAttachment(p.file, props, {
74
+ row,
75
+ columnId,
76
+ signal: p.ctl.signal,
77
+ onProgress: (f) => set({ progress: Math.max(0, Math.min(1, f)) }),
78
+ }).then(
79
+ (att) => {
80
+ if (p.ctl.signal.aborted) return
81
+ items = [...items, att]
82
+ pending = pending.filter((x) => x.key !== p.key)
83
+ },
84
+ (err) => {
85
+ if (p.ctl.signal.aborted) return
86
+ set({ error: err instanceof Error ? err.message : 'Upload failed' })
87
+ },
88
+ )
89
+ }
90
+
91
+ function retry(p: Pending) {
92
+ start({ ...p, progress: 0, error: null, ctl: new AbortController() })
93
+ }
94
+
95
+ function drop(p: Pending) {
96
+ p.ctl.abort()
97
+ pending = pending.filter((x) => x.key !== p.key)
98
+ }
99
+
100
+ // Kept out of the markup: AGENTS.md keeps `as` casts in <script>.
101
+ function onPick(e: Event) {
102
+ const input = e.currentTarget as HTMLInputElement
103
+ if (input.files) addFiles(input.files)
104
+ input.value = ''
105
+ }
106
+
107
+ function addUrls() {
108
+ const found = attachmentsFromText(urlText)
109
+ if (!found.length) {
110
+ rejected = ['No http(s) URL found']
111
+ return
112
+ }
113
+ // The same checks as a picked file: accept (by the name's extension) and maxFiles.
114
+ const { kept, rejected: refused } = limitAttachments(found, props, items.length + pending.length)
115
+ items = [...items, ...kept]
116
+ urlText = ''
117
+ rejected = refused
118
+ }
119
+
120
+ function remove(i: number) {
121
+ items = items.filter((_, j) => j !== i)
122
+ }
123
+
124
+ function move(from: number, to: number) {
125
+ if (to < 0 || to >= items.length || from === to) return
126
+ const next = items.slice()
127
+ const [it] = next.splice(from, 1)
128
+ next.splice(to, 0, it!)
129
+ items = next
130
+ }
131
+
132
+ function onTileKey(e: KeyboardEvent, i: number) {
133
+ if (e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
134
+ e.preventDefault()
135
+ const to = i + (e.key === 'ArrowRight' ? 1 : -1)
136
+ move(i, to)
137
+ queueMicrotask(() => (document.querySelectorAll<HTMLElement>('.sv-attd-tile')[to])?.focus())
138
+ } else if (e.key === 'Delete' || e.key === 'Backspace') {
139
+ e.preventDefault()
140
+ remove(i)
141
+ } else if (e.key === 'Enter' || e.key === ' ') {
142
+ e.preventDefault()
143
+ previewIndex = i
144
+ }
145
+ }
146
+
147
+ function onPaste(e: ClipboardEvent) {
148
+ const files = [...(e.clipboardData?.files ?? [])]
149
+ if (files.length) {
150
+ e.preventDefault()
151
+ addFiles(files)
152
+ return
153
+ }
154
+ const text = e.clipboardData?.getData('text') ?? ''
155
+ if (!(e.target as HTMLElement).closest('input') && attachmentsFromText(text).length) {
156
+ e.preventDefault()
157
+ urlText = text
158
+ addUrls()
159
+ }
160
+ }
161
+
162
+ function done() {
163
+ if (busy) return
164
+ finished = true
165
+ open = false
166
+ onDone(items)
167
+ }
168
+
169
+ function cancel() {
170
+ if (finished) return
171
+ finished = true
172
+ for (const p of pending) p.ctl.abort()
173
+ open = false
174
+ onCancel()
175
+ }
176
+
177
+ const ext = (a: Attachment) => (a.name.split('.').pop() ?? '').slice(0, 4).toUpperCase() || 'FILE'
178
+ </script>
179
+
180
+ <SvModal bind:open {title} width={680} onClose={cancel}>
181
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
182
+ <div
183
+ class="sv-attd"
184
+ class:is-dropping={dropping}
185
+ onpaste={onPaste}
186
+ ondragover={(e) => {
187
+ if (dragFrom !== null || !e.dataTransfer?.types.includes('Files')) return
188
+ e.preventDefault()
189
+ dropping = true
190
+ }}
191
+ ondragleave={(e) => {
192
+ if (e.currentTarget === e.target) dropping = false
193
+ }}
194
+ ondrop={(e) => {
195
+ dropping = false
196
+ if (dragFrom !== null || !e.dataTransfer?.files.length) return
197
+ e.preventDefault()
198
+ addFiles(e.dataTransfer.files)
199
+ }}
200
+ >
201
+ {#if items.length === 0 && pending.length === 0}
202
+ <p class="sv-attd-empty">No files yet. Drop files here, paste them, or use Add files.</p>
203
+ {/if}
204
+ <ul class="sv-attd-tiles" aria-label="Files">
205
+ {#each items as a, i (a.id)}
206
+ <li
207
+ class="sv-attd-tile"
208
+ class:is-dragging={dragFrom === i}
209
+ tabindex="0"
210
+ draggable="true"
211
+ aria-label={`${a.name}${a.size ? `, ${formatBytes(a.size)}` : ''}. Enter to preview, Delete to remove, Alt+Arrow to move.`}
212
+ onkeydown={(e) => onTileKey(e, i)}
213
+ ondragstart={(e) => {
214
+ dragFrom = i
215
+ e.dataTransfer?.setData('text/plain', a.name)
216
+ }}
217
+ ondragend={() => (dragFrom = null)}
218
+ ondragover={(e) => {
219
+ if (dragFrom === null) return
220
+ e.preventDefault()
221
+ }}
222
+ ondrop={(e) => {
223
+ if (dragFrom === null) return
224
+ e.preventDefault()
225
+ move(dragFrom, i)
226
+ dragFrom = null
227
+ }}
228
+ >
229
+ <button type="button" class="sv-attd-face" onclick={() => (previewIndex = i)} tabindex="-1" aria-hidden="true">
230
+ {#if isImage(a) && safeUrl(a.thumbnailUrl ?? a.url, true)}
231
+ <img src={safeUrl(a.thumbnailUrl ?? a.url, true)} alt="" loading="lazy" decoding="async" draggable="false" />
232
+ {:else}
233
+ <span class="sv-attd-ext">{ext(a)}</span>
234
+ {/if}
235
+ </button>
236
+ <span class="sv-attd-name" title={a.name}>{a.name}</span>
237
+ <span class="sv-attd-size">{formatBytes(a.size)}</span>
238
+ <button type="button" class="sv-attd-remove" aria-label={`Remove ${a.name}`} onclick={() => remove(i)}>
239
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
240
+ </button>
241
+ </li>
242
+ {/each}
243
+ {#each pending as p (p.key)}
244
+ <li class="sv-attd-tile is-pending" aria-label={p.error ? `${p.file.name}: ${p.error}` : `Uploading ${p.file.name}`}>
245
+ <span class="sv-attd-face"><span class="sv-attd-ext">{p.error ? '!' : `${Math.round(p.progress * 100)}%`}</span></span>
246
+ <span class="sv-attd-name" title={p.file.name}>{p.file.name}</span>
247
+ {#if p.error}
248
+ <span class="sv-attd-error">{p.error}</span>
249
+ <span class="sv-attd-actions">
250
+ <button type="button" class="sv-attd-link" onclick={() => retry(p)}>Retry</button>
251
+ <button type="button" class="sv-attd-link" onclick={() => drop(p)}>Remove</button>
252
+ </span>
253
+ {:else}
254
+ <span class="sv-attd-bar"><span style:width={`${Math.round(p.progress * 100)}%`}></span></span>
255
+ <button type="button" class="sv-attd-remove" aria-label={`Cancel ${p.file.name}`} onclick={() => drop(p)}>
256
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
257
+ </button>
258
+ {/if}
259
+ </li>
260
+ {/each}
261
+ </ul>
262
+ {#if rejected.length}
263
+ <ul class="sv-attd-rejected" role="alert">
264
+ {#each rejected as r, i (i)}<li>{r}</li>{/each}
265
+ </ul>
266
+ {/if}
267
+ </div>
268
+
269
+ {#snippet footer()}
270
+ <div class="sv-attd-footer">
271
+ <input
272
+ bind:this={fileInput}
273
+ type="file"
274
+ multiple
275
+ accept={props.accept}
276
+ hidden
277
+ onchange={onPick}
278
+ />
279
+ <button type="button" class="sv-attd-btn" onclick={() => fileInput?.click()}>Add files</button>
280
+ <input
281
+ class="sv-attd-url"
282
+ type="url"
283
+ placeholder="or paste a URL"
284
+ aria-label="Add a file by URL"
285
+ bind:value={urlText}
286
+ onkeydown={(e) => {
287
+ if (e.key === 'Enter') {
288
+ e.preventDefault()
289
+ addUrls()
290
+ }
291
+ }}
292
+ />
293
+ <span class="sv-attd-spacer"></span>
294
+ <button type="button" class="sv-attd-btn" onclick={cancel}>Cancel</button>
295
+ <button type="button" class="sv-attd-btn is-primary" onclick={done} disabled={busy}>
296
+ {busy ? `Uploading ${pending.filter((p) => !p.error).length}...` : 'Done'}
297
+ </button>
298
+ </div>
299
+ {/snippet}
300
+ </SvModal>
301
+
302
+ {#if previewIndex !== null}
303
+ <AttachmentPreview items={items} bind:index={previewIndex} />
304
+ {/if}
305
+
306
+ <style>
307
+ .sv-attd { min-height: 160px; border: 2px dashed transparent; border-radius: 8px; padding: 4px; }
308
+ .sv-attd.is-dropping { border-color: var(--sg-accent, #2563eb); }
309
+ .sv-attd-empty { margin: 40px 0; text-align: center; color: var(--sg-muted, #64748b); font-size: 13px; }
310
+ .sv-attd-tiles {
311
+ list-style: none; margin: 0; padding: 0;
312
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px;
313
+ }
314
+ .sv-attd-tile {
315
+ position: relative; display: flex; flex-direction: column; gap: 3px;
316
+ padding: 6px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px;
317
+ background: var(--sg-bg, #fff); cursor: grab; min-width: 0;
318
+ }
319
+ .sv-attd-tile:focus-visible { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
320
+ .sv-attd-tile.is-dragging { opacity: 0.4; }
321
+ .sv-attd-tile.is-pending { cursor: default; }
322
+ .sv-attd-face {
323
+ display: flex; align-items: center; justify-content: center;
324
+ height: 96px; border: 0; padding: 0; border-radius: 6px; overflow: hidden;
325
+ background: var(--sg-header-bg, #f8fafc); cursor: zoom-in;
326
+ }
327
+ .sv-attd-face img { width: 100%; height: 100%; object-fit: cover; }
328
+ .sv-attd-ext { font-size: 13px; font-weight: 600; color: var(--sg-muted, #64748b); }
329
+ .sv-attd-name { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
330
+ .sv-attd-size, .sv-attd-error { font-size: 11px; color: var(--sg-muted, #64748b); }
331
+ .sv-attd-error { color: var(--sg-danger, #dc2626); }
332
+ .sv-attd-actions { display: flex; gap: 8px; }
333
+ .sv-attd-link { border: 0; padding: 0; background: none; color: inherit; text-decoration: underline; font: inherit; font-size: 12px; cursor: pointer; }
334
+ .sv-attd-bar { height: 4px; border-radius: 2px; background: var(--sg-header-bg, #f8fafc); overflow: hidden; }
335
+ .sv-attd-bar > span { display: block; height: 100%; background: var(--sg-muted, #64748b); transition: width 120ms linear; }
336
+ .sv-attd-remove {
337
+ position: absolute; top: 10px; inset-inline-end: 10px;
338
+ display: inline-flex; padding: 3px; border: 0; border-radius: 999px;
339
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); cursor: pointer; opacity: 0.85;
340
+ }
341
+ .sv-attd-remove:hover { opacity: 1; }
342
+ .sv-attd-rejected { margin: 10px 0 0; padding-inline-start: 18px; font-size: 12px; color: var(--sg-danger, #dc2626); }
343
+ .sv-attd-footer { display: flex; align-items: center; gap: 8px; width: 100%; }
344
+ .sv-attd-spacer { flex: 1; }
345
+ .sv-attd-url {
346
+ width: 220px; height: 30px; padding: 0 8px; font: inherit; font-size: 13px;
347
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px;
348
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
349
+ }
350
+ .sv-attd-btn {
351
+ height: 30px; padding: 0 12px; font: inherit; font-size: 13px; cursor: pointer;
352
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px;
353
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
354
+ }
355
+ .sv-attd-btn.is-primary { background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff); border-color: transparent; }
356
+ .sv-attd-btn:disabled { opacity: 0.6; cursor: default; }
357
+ </style>
@@ -0,0 +1,13 @@
1
+ import { type Attachment, type AttachmentFieldProps } from './attachments.js';
2
+ type $$ComponentProps = {
3
+ value: unknown;
4
+ title?: string;
5
+ props: AttachmentFieldProps;
6
+ row: unknown;
7
+ columnId: string;
8
+ onDone: (next: Attachment[]) => void;
9
+ onCancel: () => void;
10
+ };
11
+ declare const AttachmentDialog: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type AttachmentDialog = ReturnType<typeof AttachmentDialog>;
13
+ export default AttachmentDialog;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The grid mounts this when an attachment cell is edited (Enter, double
4
+ * click, typing): it opens the attachment dialog over the page and commits
5
+ * the dialog's list, or cancels. Registered with registerCellEditor as
6
+ * 'attachment' by enableFields().
7
+ */
8
+ import AttachmentDialog from './AttachmentDialog.svelte'
9
+ import { fieldPropsOf } from './registry.svelte'
10
+ import type { Attachment, AttachmentFieldProps } from './attachments'
11
+
12
+ let {
13
+ value,
14
+ columnDef,
15
+ columnId,
16
+ row,
17
+ onCommit,
18
+ onCancel,
19
+ }: {
20
+ value: unknown
21
+ columnDef?: Record<string, unknown>
22
+ columnId: string
23
+ row?: unknown
24
+ onCommit: (v?: unknown) => void
25
+ onCancel: () => void
26
+ } = $props()
27
+
28
+ const props = $derived(fieldPropsOf((columnDef ?? {}) as { fieldType?: unknown; fieldProps?: Record<string, unknown> }) as AttachmentFieldProps)
29
+ const title = $derived(typeof columnDef?.header === 'string' ? columnDef.header : 'Attachments')
30
+ </script>
31
+
32
+ <span class="sv-att-editing" aria-hidden="true"></span>
33
+ <AttachmentDialog
34
+ {value}
35
+ {title}
36
+ {props}
37
+ {row}
38
+ {columnId}
39
+ onDone={(next: Attachment[]) => onCommit(next)}
40
+ onCancel={() => onCancel()}
41
+ />
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ value: unknown;
3
+ columnDef?: Record<string, unknown>;
4
+ columnId: string;
5
+ row?: unknown;
6
+ onCommit: (v?: unknown) => void;
7
+ onCancel: () => void;
8
+ };
9
+ declare const AttachmentEditor: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type AttachmentEditor = ReturnType<typeof AttachmentEditor>;
11
+ export default AttachmentEditor;