@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.
- package/README.md +1 -0
- package/dist/SvAlertRuleEditor.svelte +16 -5
- package/dist/SvGridAlerts.svelte +8 -0
- package/dist/SvGridAlerts.svelte.d.ts +4 -0
- package/dist/SvGridBoard.svelte +69 -10
- package/dist/SvGridBulkEditDrawer.svelte +13 -5
- package/dist/SvGridGallery.svelte +286 -0
- package/dist/SvGridGallery.svelte.d.ts +30 -0
- package/dist/SvGridScheduler.svelte +33 -11
- package/dist/SvGridSelectionBar.svelte +47 -0
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
- package/dist/advanced-filter-enable.d.ts +6 -0
- package/dist/advanced-filter-enable.js +8 -0
- package/dist/alerts/alert-engine.d.ts +7 -0
- package/dist/alerts/alert-engine.js +5 -2
- package/dist/alerts/condition-editor.d.ts +26 -0
- package/dist/alerts/condition-editor.js +17 -0
- package/dist/base/index.d.ts +20 -0
- package/dist/base/index.js +20 -0
- package/dist/bulk-edit.d.ts +11 -0
- package/dist/bulk-edit.js +18 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +11197 -10639
- package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
- package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
- package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
- package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
- package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
- package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
- package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
- package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
- package/dist/designer/assets/{index-Obk8sI77.js → index-BPU2KNc5.js} +307 -307
- package/dist/designer/assets/index-BmvG99kR.css +1 -0
- package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
- package/dist/designer/assets/src-C_U1NKOY.js +58 -0
- package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
- package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
- package/dist/designer/index.html +11 -7
- package/dist/expressions/compile.js +10 -0
- package/dist/expressions/evaluate.js +18 -0
- package/dist/expressions/expression-types.d.ts +10 -0
- package/dist/expressions/kinds.d.ts +28 -0
- package/dist/expressions/kinds.js +19 -0
- package/dist/expressions/parse.js +9 -1
- package/dist/fields/AttachmentCell.svelte +242 -0
- package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
- package/dist/fields/AttachmentDialog.svelte +357 -0
- package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
- package/dist/fields/AttachmentEditor.svelte +41 -0
- package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
- package/dist/fields/AttachmentPreview.svelte +72 -0
- package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
- package/dist/fields/BarcodeCell.svelte +20 -0
- package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
- package/dist/fields/BaseAutomations.svelte +558 -0
- package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
- package/dist/fields/BaseChartView.svelte +47 -0
- package/dist/fields/BaseChartView.svelte.d.ts +8 -0
- package/dist/fields/BaseTableView.svelte +185 -0
- package/dist/fields/BaseTableView.svelte.d.ts +37 -0
- package/dist/fields/BaseTabs.svelte +308 -0
- package/dist/fields/BaseTabs.svelte.d.ts +21 -0
- package/dist/fields/ButtonCell.svelte +47 -0
- package/dist/fields/ButtonCell.svelte.d.ts +20 -0
- package/dist/fields/CheckboxCell.svelte +46 -0
- package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
- package/dist/fields/DeleteFieldConfirm.svelte +80 -0
- package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
- package/dist/fields/DeleteRecordConfirm.svelte +48 -0
- package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
- package/dist/fields/FieldCell.svelte +80 -0
- package/dist/fields/FieldCell.svelte.d.ts +9 -0
- package/dist/fields/FieldEditor.svelte +321 -0
- package/dist/fields/FieldEditor.svelte.d.ts +10 -0
- package/dist/fields/FieldInput.svelte +188 -0
- package/dist/fields/FieldInput.svelte.d.ts +29 -0
- package/dist/fields/FieldsHeaderEnd.svelte +146 -0
- package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
- package/dist/fields/FootRowAdder.svelte +8 -0
- package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
- package/dist/fields/GroupHeader.svelte +127 -0
- package/dist/fields/GroupHeader.svelte.d.ts +24 -0
- package/dist/fields/LinkCell.svelte +46 -0
- package/dist/fields/LinkCell.svelte.d.ts +9 -0
- package/dist/fields/LinkPickerEditor.svelte +172 -0
- package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
- package/dist/fields/LinkPreview.svelte +60 -0
- package/dist/fields/LinkPreview.svelte.d.ts +7 -0
- package/dist/fields/LinkedRecordView.svelte +94 -0
- package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
- package/dist/fields/LinkedRecordsCell.svelte +59 -0
- package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
- package/dist/fields/LongTextEditor.svelte +157 -0
- package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
- package/dist/fields/NumberEditor.svelte +309 -0
- package/dist/fields/NumberEditor.svelte.d.ts +12 -0
- package/dist/fields/OptionsEditor.svelte +117 -0
- package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
- package/dist/fields/PercentCell.svelte +21 -0
- package/dist/fields/PercentCell.svelte.d.ts +8 -0
- package/dist/fields/PropertyInput.svelte +204 -0
- package/dist/fields/PropertyInput.svelte.d.ts +11 -0
- package/dist/fields/RatingCell.svelte +75 -0
- package/dist/fields/RatingCell.svelte.d.ts +16 -0
- package/dist/fields/RatingEditor.svelte +82 -0
- package/dist/fields/RatingEditor.svelte.d.ts +9 -0
- package/dist/fields/RecordView.svelte +310 -0
- package/dist/fields/RecordView.svelte.d.ts +11 -0
- package/dist/fields/RichTextCell.svelte +30 -0
- package/dist/fields/RichTextCell.svelte.d.ts +5 -0
- package/dist/fields/RowAdder.svelte +93 -0
- package/dist/fields/RowAdder.svelte.d.ts +7 -0
- package/dist/fields/RowHeader.svelte +99 -0
- package/dist/fields/RowHeader.svelte.d.ts +11 -0
- package/dist/fields/SelectCell.svelte +35 -0
- package/dist/fields/SelectCell.svelte.d.ts +15 -0
- package/dist/fields/SelectEditor.svelte +350 -0
- package/dist/fields/SelectEditor.svelte.d.ts +17 -0
- package/dist/fields/SummaryCell.svelte +160 -0
- package/dist/fields/SummaryCell.svelte.d.ts +11 -0
- package/dist/fields/SvBase.svelte +545 -0
- package/dist/fields/SvBase.svelte.d.ts +30 -0
- package/dist/fields/SvBaseForm.svelte +295 -0
- package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
- package/dist/fields/SvBaseViewBar.svelte +644 -0
- package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
- package/dist/fields/UserCell.svelte +39 -0
- package/dist/fields/UserCell.svelte.d.ts +9 -0
- package/dist/fields/UserFace.svelte +30 -0
- package/dist/fields/UserFace.svelte.d.ts +12 -0
- package/dist/fields/activity.svelte.d.ts +50 -0
- package/dist/fields/activity.svelte.js +110 -0
- package/dist/fields/attachments.d.ts +95 -0
- package/dist/fields/attachments.js +145 -0
- package/dist/fields/automation-outbound.d.ts +46 -0
- package/dist/fields/automation-outbound.js +61 -0
- package/dist/fields/automation-server.d.ts +53 -0
- package/dist/fields/automation-server.js +333 -0
- package/dist/fields/automations.svelte.d.ts +208 -0
- package/dist/fields/automations.svelte.js +437 -0
- package/dist/fields/bar-request.svelte.d.ts +16 -0
- package/dist/fields/bar-request.svelte.js +15 -0
- package/dist/fields/base-alerts.d.ts +24 -0
- package/dist/fields/base-alerts.js +64 -0
- package/dist/fields/base-connect.d.ts +82 -0
- package/dist/fields/base-connect.js +271 -0
- package/dist/fields/base-schema.d.ts +104 -0
- package/dist/fields/base-schema.js +266 -0
- package/dist/fields/base-sql.d.ts +62 -0
- package/dist/fields/base-sql.js +251 -0
- package/dist/fields/base-sync.d.ts +142 -0
- package/dist/fields/base-sync.js +499 -0
- package/dist/fields/base-tables.d.ts +89 -0
- package/dist/fields/base-tables.js +349 -0
- package/dist/fields/builtin-types.d.ts +13 -0
- package/dist/fields/builtin-types.js +773 -0
- package/dist/fields/button.d.ts +2 -0
- package/dist/fields/button.js +17 -0
- package/dist/fields/chart-view.d.ts +35 -0
- package/dist/fields/chart-view.js +154 -0
- package/dist/fields/column-order.d.ts +6 -0
- package/dist/fields/column-order.js +12 -0
- package/dist/fields/convert.d.ts +16 -0
- package/dist/fields/convert.js +176 -0
- package/dist/fields/copy-text.d.ts +13 -0
- package/dist/fields/copy-text.js +45 -0
- package/dist/fields/editor-request.svelte.d.ts +71 -0
- package/dist/fields/editor-request.svelte.js +52 -0
- package/dist/fields/field-ops.d.ts +72 -0
- package/dist/fields/field-ops.js +334 -0
- package/dist/fields/filter-model.d.ts +84 -0
- package/dist/fields/filter-model.js +446 -0
- package/dist/fields/format.d.ts +109 -0
- package/dist/fields/format.js +288 -0
- package/dist/fields/formula.d.ts +50 -0
- package/dist/fields/formula.js +530 -0
- package/dist/fields/grid-access.d.ts +60 -0
- package/dist/fields/grid-access.js +120 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +312 -0
- package/dist/fields/linked-fields.d.ts +8 -0
- package/dist/fields/linked-fields.js +40 -0
- package/dist/fields/links.d.ts +10 -0
- package/dist/fields/links.js +46 -0
- package/dist/fields/name-list.d.ts +16 -0
- package/dist/fields/name-list.js +49 -0
- package/dist/fields/palette.d.ts +47 -0
- package/dist/fields/palette.js +69 -0
- package/dist/fields/record-delete.svelte.d.ts +12 -0
- package/dist/fields/record-delete.svelte.js +5 -0
- package/dist/fields/record-menu.d.ts +9 -0
- package/dist/fields/record-menu.js +78 -0
- package/dist/fields/registry.svelte.d.ts +176 -0
- package/dist/fields/registry.svelte.js +51 -0
- package/dist/fields/rich-text.d.ts +23 -0
- package/dist/fields/rich-text.js +48 -0
- package/dist/fields/row-color-choice.svelte.d.ts +31 -0
- package/dist/fields/row-color-choice.svelte.js +25 -0
- package/dist/fields/row-colors.d.ts +36 -0
- package/dist/fields/row-colors.js +90 -0
- package/dist/fields/row-height.d.ts +28 -0
- package/dist/fields/row-height.js +50 -0
- package/dist/fields/sources.svelte.d.ts +80 -0
- package/dist/fields/sources.svelte.js +131 -0
- package/dist/fields/summaries.d.ts +18 -0
- package/dist/fields/summaries.js +138 -0
- package/dist/fields/view-model.d.ts +129 -0
- package/dist/fields/view-model.js +492 -0
- package/dist/fields/viewbar/ChartPanel.svelte +61 -0
- package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ColorPanel.svelte +112 -0
- package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ConditionValue.svelte +153 -0
- package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
- package/dist/fields/viewbar/FieldIcon.svelte +18 -0
- package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
- package/dist/fields/viewbar/FilterPanel.svelte +192 -0
- package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
- package/dist/fields/viewbar/GanttPanel.svelte +60 -0
- package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
- package/dist/fields/viewbar/SortPanel.svelte +85 -0
- package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
- package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
- package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
- package/dist/fields/viewbar/panel-focus.d.ts +3 -0
- package/dist/fields/viewbar/panel-focus.js +22 -0
- package/dist/fields/viewbar/sort-state.d.ts +20 -0
- package/dist/fields/viewbar/sort-state.js +24 -0
- package/dist/gallery.d.ts +7 -0
- package/dist/gallery.js +36 -0
- package/dist/gantt/SvGridGantt.svelte +88 -16
- package/dist/gantt/gantt-config.d.ts +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/install.js +3 -0
- package/dist/license-core.d.ts +3 -3
- package/dist/license.js +1 -0
- package/dist/node/studio.js +105 -4
- package/dist/scheduler-config.d.ts +11 -0
- package/dist/sources/introspect-supabase.js +4 -1
- package/dist/sources/rest.js +6 -2
- package/dist/studio/ui-components.generated.js +93 -1
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
- package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
- package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
- package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
- package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
- package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
- package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
- package/dist/wc/sv-sheet-element.js +7556 -7617
- package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
- package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
- package/package.json +14 -3
- package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
- package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
- package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
- package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
- package/dist/designer/assets/src-DqHCqori.js +0 -58
- package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
- package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
- package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
- **Scheduling automation** - a client-side cron / one-off scheduler (`createScheduler`, `parseCron`, `CRON_PRESETS`) to drive recurring exports and alerts with no backend.
|
|
36
36
|
- **Spreadsheet** (Suite) - `<SvSheet>` and the `<sv-sheet>` custom element over a built-in formula engine: around 185 functions, cross-sheet references, named ranges, absolute and relative refs, dynamic arrays with spill, iterative calculation with convergence, circular-reference detection, volatile-function tracking, and dependency-graph recalculation. In-house, with no runtime dependency.
|
|
37
37
|
- **Spreadsheet file formats** (Suite) - read and write `.xlsx`, `.xls` (BIFF8) and `.ods`, plus `.csv`. Formulas, tables, named ranges, merges, frozen panes, sparklines, charts and images survive an `.xlsx` round trip. Pivot tables round-trip as their computed cells, not as live PivotTable parts, and `.xls` keeps the BIFF8 row and column ceilings.
|
|
38
|
+
- **SvGrid Base** (Suite) - `<SvBase>` from `@svgrid/enterprise/base`: a table database inside your app. Typed fields (select, user, attachment, rating, linked record, lookup, rollup, formula and more), a primary field, saved views (grid, Kanban, calendar, gallery, form) each with its own filter, sorts, groups and row colors, and an expanded record with changes and comments.
|
|
38
39
|
- **SvGrid Studio** (Suite) - a schema-driven data-app layer: `EntitySchema`, memory / SQL / Supabase / REST data sources, a SvelteKit adapter, edit panels, master/detail, schema charts + KPI dashboards, and the project model + codegen behind the visual designer. See [`@svgrid/studio`](https://www.npmjs.com/package/@svgrid/studio).
|
|
39
40
|
|
|
40
41
|
## Install
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* severity / scope, the trigger (data change, relative change, validation,
|
|
5
5
|
* scheduled), the predicate (embedded SvExpressionEditor), and the actions
|
|
6
6
|
* (toast / badge / highlight / flash / prevent-edit / log). Reuses the grid
|
|
7
|
-
* UI kit so it matches the rest of the app.
|
|
7
|
+
* UI kit so it matches the rest of the app. Under an SvGrid Base table the
|
|
8
|
+
* condition is written with the Base's filter instead (`condition-editor.ts`).
|
|
8
9
|
*/
|
|
9
10
|
import { SvModal, SvField, SvTextInput, SvDropDownList, SvColorInput, SvTagsInput } from '@svgrid/grid'
|
|
10
11
|
import SvExpressionEditor from './SvExpressionEditor.svelte'
|
|
11
12
|
import { CRON_PRESETS } from './scheduling'
|
|
13
|
+
import { getAlertConditionEditor } from './alerts/condition-editor'
|
|
12
14
|
import type { ExprColumn } from './expressions/expression-columns'
|
|
13
15
|
import type { PredicateExpr } from './expressions/expression-types'
|
|
14
16
|
import type {
|
|
@@ -61,6 +63,10 @@
|
|
|
61
63
|
{ value: 'crossed', label: 'crossed a threshold' },
|
|
62
64
|
]
|
|
63
65
|
|
|
66
|
+
const custom = getAlertConditionEditor()
|
|
67
|
+
const DEFAULT_TOKENS = ['{value}', '{column}', '{field}']
|
|
68
|
+
const tokens = $derived(custom?.tokens?.() ?? DEFAULT_TOKENS)
|
|
69
|
+
|
|
64
70
|
// --- Draft state ---------------------------------------------------------
|
|
65
71
|
|
|
66
72
|
let name = $state('')
|
|
@@ -100,7 +106,7 @@
|
|
|
100
106
|
severity = rule?.severity ?? 'warning'
|
|
101
107
|
scope = rule?.scope ?? 'row'
|
|
102
108
|
triggerType = rule?.trigger.type ?? 'dataChange'
|
|
103
|
-
predicate = rule?.predicate ?? { kind: 'const', value: true }
|
|
109
|
+
predicate = rule?.predicate ?? custom?.initial() ?? { kind: 'const', value: true }
|
|
104
110
|
selected = new Set(rule?.actions.map((a) => a.kind) ?? ['toast'])
|
|
105
111
|
message = rule?.actions.find((a) => a.message)?.message ?? ''
|
|
106
112
|
const styled = rule?.actions.find((a) => a.style)
|
|
@@ -133,6 +139,7 @@
|
|
|
133
139
|
selected = next
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
const ownEditor = $derived(custom && custom.accepts(predicate) ? custom : null)
|
|
136
143
|
const showStyle = $derived(selected.has('highlight') || selected.has('badge'))
|
|
137
144
|
const showTargets = $derived(showStyle || selected.has('cellFlash') || scope === 'cell')
|
|
138
145
|
|
|
@@ -230,7 +237,11 @@
|
|
|
230
237
|
{/if}
|
|
231
238
|
|
|
232
239
|
<SvField label={triggerType === 'validation' ? 'Reject the edit when' : 'Condition'}>
|
|
233
|
-
|
|
240
|
+
{#if ownEditor}
|
|
241
|
+
<ownEditor.component value={predicate} onChange={(next) => (predicate = next)} />
|
|
242
|
+
{:else}
|
|
243
|
+
<SvExpressionEditor {columns} bind:value={predicate} {rows} />
|
|
244
|
+
{/if}
|
|
234
245
|
</SvField>
|
|
235
246
|
|
|
236
247
|
<fieldset class="ar-fieldset">
|
|
@@ -244,8 +255,8 @@
|
|
|
244
255
|
{/each}
|
|
245
256
|
</div>
|
|
246
257
|
<div class="ar-message">
|
|
247
|
-
<SvField label="Message
|
|
248
|
-
<SvTextInput value={message} onChange={(v) => (message = v)} placeholder=
|
|
258
|
+
<SvField label="Message" hint={`Tokens: ${tokens.slice(0, 4).join(', ')}`}>
|
|
259
|
+
<SvTextInput value={message} onChange={(v) => (message = v)} placeholder={custom ? `e.g. ${tokens[0] ?? ''} is overdue` : 'e.g. {region} price hit {value}'} />
|
|
249
260
|
</SvField>
|
|
250
261
|
</div>
|
|
251
262
|
{#if showStyle}
|
package/dist/SvGridAlerts.svelte
CHANGED
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
storageKey?: string
|
|
62
62
|
getValue?: (row: TRow, columnId: string) => unknown
|
|
63
63
|
locale?: string | ReadonlyArray<string>
|
|
64
|
+
/** A message token the built-in ones do not cover, as text (`{Due date}` in SvGrid Base). */
|
|
65
|
+
token?: (row: TRow, name: string) => string | undefined
|
|
66
|
+
/** The rules changed in the manager: added, edited, turned on or off, deleted. */
|
|
67
|
+
onRulesChange?: (rules: AlertRule[]) => void
|
|
64
68
|
/**
|
|
65
69
|
* Reactively scan `data` for changes (default). Set `false` when a consumer
|
|
66
70
|
* drives evaluation through `pushChanged` (large / streaming feeds) - the
|
|
@@ -92,6 +96,8 @@
|
|
|
92
96
|
storageKey,
|
|
93
97
|
getValue,
|
|
94
98
|
locale,
|
|
99
|
+
token,
|
|
100
|
+
onRulesChange,
|
|
95
101
|
watch = true,
|
|
96
102
|
onReady,
|
|
97
103
|
toastCooldownMs = 0,
|
|
@@ -131,6 +137,7 @@
|
|
|
131
137
|
getRowId: rowIdOf,
|
|
132
138
|
get getValue() { return getValue },
|
|
133
139
|
get locale() { return locale },
|
|
140
|
+
get token() { return token },
|
|
134
141
|
})
|
|
135
142
|
|
|
136
143
|
// The observer owns all scheduling + change tracking. It runs evaluation on a
|
|
@@ -239,6 +246,7 @@
|
|
|
239
246
|
function onRulesChanged(next: AlertRule[]) {
|
|
240
247
|
rules = next
|
|
241
248
|
rulesVersion += 1
|
|
249
|
+
onRulesChange?.(next)
|
|
242
250
|
}
|
|
243
251
|
</script>
|
|
244
252
|
|
|
@@ -17,6 +17,10 @@ declare function $$render<TRow extends Record<string, unknown>>(): {
|
|
|
17
17
|
storageKey?: string;
|
|
18
18
|
getValue?: (row: TRow, columnId: string) => unknown;
|
|
19
19
|
locale?: string | ReadonlyArray<string>;
|
|
20
|
+
/** A message token the built-in ones do not cover, as text (`{Due date}` in SvGrid Base). */
|
|
21
|
+
token?: (row: TRow, name: string) => string | undefined;
|
|
22
|
+
/** The rules changed in the manager: added, edited, turned on or off, deleted. */
|
|
23
|
+
onRulesChange?: (rules: AlertRule[]) => void;
|
|
20
24
|
/**
|
|
21
25
|
* Reactively scan `data` for changes (default). Set `false` when a consumer
|
|
22
26
|
* drives evaluation through `pushChanged` (large / streaming feeds) - the
|
package/dist/SvGridBoard.svelte
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
popIn,
|
|
34
34
|
createDismissableLayer,
|
|
35
35
|
onScrollOutside,
|
|
36
|
+
getGridExtension,
|
|
36
37
|
} from "@svgrid/grid";
|
|
37
38
|
|
|
38
39
|
let {
|
|
@@ -258,6 +259,36 @@
|
|
|
258
259
|
const fieldColumns = $derived(
|
|
259
260
|
columns.filter((c) => typeof c.field === "string"),
|
|
260
261
|
);
|
|
262
|
+
// Columns with a `fieldType` (the fields module) filled in as the grid fills
|
|
263
|
+
// them, so a card shows a select as its pill, files as thumbnails, a date
|
|
264
|
+
// in its format - not the raw value.
|
|
265
|
+
const gridExt = $derived(getGridExtension());
|
|
266
|
+
const filledByField = $derived.by(() => {
|
|
267
|
+
const fill = gridExt?.column;
|
|
268
|
+
const out = new Map<string, Record<string, any>>();
|
|
269
|
+
for (const c of fieldColumns as Array<Record<string, any>>) {
|
|
270
|
+
out.set(c.field as string, c.fieldType && fill ? fill(c, fieldColumns) : c);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
});
|
|
274
|
+
// What a field cell reads off the grid it is in (the grid itself when the
|
|
275
|
+
// board hands it over); a card has a width of its own.
|
|
276
|
+
// `board` is gone for a moment while the view switches away: cells still read it.
|
|
277
|
+
const CARD_CELL_GRID = $derived(board?.cellGrid?.(220) ?? { getColumnWidth: () => 220 });
|
|
278
|
+
function drawsFieldCell(field: string): Record<string, any> | null {
|
|
279
|
+
const def = filledByField.get(field);
|
|
280
|
+
if (!def?.fieldType || def.cell != null || !gridExt?.cell || gridExt.hasCell?.(def) === false) return null;
|
|
281
|
+
return def;
|
|
282
|
+
}
|
|
283
|
+
function cardText(row: TData, field: string): string {
|
|
284
|
+
const def = filledByField.get(field);
|
|
285
|
+
const value = fieldValue(row, field);
|
|
286
|
+
const f = def?.formatter as ((x: { value: unknown; row: unknown; column: unknown }) => unknown) | undefined;
|
|
287
|
+
// The row as a grid cell hands it to a formatter: its id and the record.
|
|
288
|
+
if (f) return String(f({ value, row: { id: key(row), original: row }, column: { id: def?.id ?? field, columnDef: def } }) ?? "");
|
|
289
|
+
return fmt(value);
|
|
290
|
+
}
|
|
291
|
+
const isBlank = (v: unknown) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
261
292
|
function headerLabel(field: string): string {
|
|
262
293
|
const col = fieldColumns.find((c) => c.field === field);
|
|
263
294
|
return col && typeof col.header === "string" ? col.header : field;
|
|
@@ -647,6 +678,11 @@
|
|
|
647
678
|
}
|
|
648
679
|
}
|
|
649
680
|
}
|
|
681
|
+
// A lane marked hideWhenEmpty shows only while a card is in it.
|
|
682
|
+
if (base.some((l) => l.hideWhenEmpty)) {
|
|
683
|
+
const used = new Set(data.map((r) => effectiveLane(r)));
|
|
684
|
+
base = base.filter((l) => !l.hideWhenEmpty || used.has(l.id));
|
|
685
|
+
}
|
|
650
686
|
if (!laneOrder.length) return base;
|
|
651
687
|
// Apply the drag-reorder overlay: ordered ids first, then any new lanes.
|
|
652
688
|
const byId = new Map(base.map((l) => [l.id, l]));
|
|
@@ -1445,12 +1481,23 @@
|
|
|
1445
1481
|
{#if metaFields.length}
|
|
1446
1482
|
<dl class="sv-board-card-meta">
|
|
1447
1483
|
{#each metaFields as field (field)}
|
|
1448
|
-
{@const
|
|
1449
|
-
{#if
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
<
|
|
1453
|
-
|
|
1484
|
+
{@const cellDef = drawsFieldCell(field)}
|
|
1485
|
+
{#if cellDef && gridExt?.cell}
|
|
1486
|
+
{#if !isBlank(fieldValue(row, field))}
|
|
1487
|
+
{@const FieldCellView = gridExt.cell}
|
|
1488
|
+
<div class="sv-board-card-metarow">
|
|
1489
|
+
<dt>{headerLabel(field)}</dt>
|
|
1490
|
+
<dd class="sv-board-card-fieldcell"><FieldCellView ctrl={CARD_CELL_GRID} row={{ id: key(row), original: row }} column={{ id: cellDef.id ?? field, columnDef: cellDef }} value={fieldValue(row, field)} /></dd>
|
|
1491
|
+
</div>
|
|
1492
|
+
{/if}
|
|
1493
|
+
{:else}
|
|
1494
|
+
{@const v = cardText(row, field)}
|
|
1495
|
+
{#if v !== ""}
|
|
1496
|
+
<div class="sv-board-card-metarow">
|
|
1497
|
+
<dt>{headerLabel(field)}</dt>
|
|
1498
|
+
<dd>{v}</dd>
|
|
1499
|
+
</div>
|
|
1500
|
+
{/if}
|
|
1454
1501
|
{/if}
|
|
1455
1502
|
{/each}
|
|
1456
1503
|
</dl>
|
|
@@ -1583,7 +1630,7 @@
|
|
|
1583
1630
|
{@const labels = labelsOf(row)}
|
|
1584
1631
|
{@const due = dueOf(row)}
|
|
1585
1632
|
{@const asg = assigneesOf(row)}
|
|
1586
|
-
{@const comments = countOf(row, board.commentsField)}
|
|
1633
|
+
{@const comments = board.commentCount ? board.commentCount(row) : countOf(row, board.commentsField)}
|
|
1587
1634
|
{@const attachments = countOf(row, board.attachmentsField)}
|
|
1588
1635
|
{@const age = ageOf(row)}
|
|
1589
1636
|
{@const blocked = isFlagged(row)}
|
|
@@ -1608,9 +1655,11 @@
|
|
|
1608
1655
|
type="button"
|
|
1609
1656
|
class="sv-board-badge is-meta is-btn"
|
|
1610
1657
|
title="Comments"
|
|
1658
|
+
aria-label={`${comments} ${comments === 1 ? "comment" : "comments"}`}
|
|
1611
1659
|
onclick={(e) => {
|
|
1612
1660
|
e.stopPropagation();
|
|
1613
|
-
|
|
1661
|
+
if (board.onCommentsOpen) board.onCommentsOpen(row);
|
|
1662
|
+
else commentsOpen[key(row)] = !commentsOpen[key(row)];
|
|
1614
1663
|
}}
|
|
1615
1664
|
>
|
|
1616
1665
|
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true"><path d="M2 3h12v8H8l-3 2v-2H2z" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
|
|
@@ -1943,10 +1992,10 @@
|
|
|
1943
1992
|
line-height: 1.3;
|
|
1944
1993
|
}
|
|
1945
1994
|
.sv-board-card-meta {
|
|
1946
|
-
margin:
|
|
1995
|
+
margin: 8px 0 0;
|
|
1947
1996
|
display: flex;
|
|
1948
1997
|
flex-direction: column;
|
|
1949
|
-
gap:
|
|
1998
|
+
gap: 6px;
|
|
1950
1999
|
}
|
|
1951
2000
|
.sv-board-card-metarow {
|
|
1952
2001
|
display: flex;
|
|
@@ -1964,6 +2013,16 @@
|
|
|
1964
2013
|
text-overflow: ellipsis;
|
|
1965
2014
|
white-space: nowrap;
|
|
1966
2015
|
}
|
|
2016
|
+
/* A field drawn as its cell draws it (pills, avatars, thumbnails), on one line. */
|
|
2017
|
+
.sv-board-card-metarow {
|
|
2018
|
+
align-items: center;
|
|
2019
|
+
}
|
|
2020
|
+
.sv-board-card-fieldcell {
|
|
2021
|
+
display: flex;
|
|
2022
|
+
align-items: center;
|
|
2023
|
+
min-width: 0;
|
|
2024
|
+
flex: 1;
|
|
2025
|
+
}
|
|
1967
2026
|
.sv-board-lane-empty {
|
|
1968
2027
|
text-align: center;
|
|
1969
2028
|
font-size: 0.76rem;
|
|
@@ -48,8 +48,14 @@
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
/** A column's editor type mapped onto the form's control vocabulary. */
|
|
51
|
-
function controlFor(
|
|
52
|
-
|
|
51
|
+
function controlFor(c: BulkEditField): FormFieldType {
|
|
52
|
+
const { editorType } = c
|
|
53
|
+
if (c.options?.length) return c.multiple ? 'multiselect' : 'select'
|
|
54
|
+
// Field editors: a number typed as the field writes it ("$1,200", "1:30"),
|
|
55
|
+
// read by the column's own parser; a rating as stars; long text as lines.
|
|
56
|
+
if (editorType === 'fieldNumber') return 'text'
|
|
57
|
+
if (editorType === 'fieldRating') return 'rating'
|
|
58
|
+
if (editorType === 'fieldLongText') return 'textarea'
|
|
53
59
|
switch (editorType) {
|
|
54
60
|
case 'number': return 'number'
|
|
55
61
|
case 'checkbox': return 'checkbox'
|
|
@@ -87,7 +93,8 @@
|
|
|
87
93
|
columns.map((c) => ({
|
|
88
94
|
name: c.id,
|
|
89
95
|
label: c.label,
|
|
90
|
-
type: controlFor(c
|
|
96
|
+
type: controlFor(c),
|
|
97
|
+
...(c.max ? { max: c.max } : {}),
|
|
91
98
|
// Placeholder only. The same sentence repeated under every differing
|
|
92
99
|
// field was five identical lines of noise - the lead says it once.
|
|
93
100
|
placeholder: mixed.has(c.id) ? messages.bulkEditMixed : undefined,
|
|
@@ -104,10 +111,11 @@
|
|
|
104
111
|
for (const [name, raw] of Object.entries(values)) {
|
|
105
112
|
const column = byId.get(name)
|
|
106
113
|
if (!column) continue
|
|
107
|
-
const next = coerceBulkValue(column.editorType, raw)
|
|
114
|
+
const next = column.parse && raw !== '' && raw != null ? column.parse(raw) : coerceBulkValue(column.editorType, raw)
|
|
108
115
|
// Untouched fields are the ones that keep every row's own value. A field
|
|
109
116
|
// that opened "Multiple values" and was left blank is untouched too.
|
|
110
|
-
|
|
117
|
+
// By content: a multiple select's list is a new array each time.
|
|
118
|
+
if (next === baseline[name] || JSON.stringify(next) === JSON.stringify(baseline[name])) continue
|
|
111
119
|
if (mixed.has(name) && (raw === '' || raw == null)) continue
|
|
112
120
|
edits[name] = next
|
|
113
121
|
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<script
|
|
2
|
+
lang="ts"
|
|
3
|
+
generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData"
|
|
4
|
+
>
|
|
5
|
+
// Gallery renderer for SvGrid: rendered in place of the table when the
|
|
6
|
+
// `gallery` prop is set. Each row is a card - a cover image, a title and a
|
|
7
|
+
// few fields - in a wall that fills the width. The rows are the grid's
|
|
8
|
+
// filtered + sorted rows, so its search box, filters and sort flow through.
|
|
9
|
+
// - the cover is an image URL or the first image of a list of files
|
|
10
|
+
// - a column with a `fieldType` (the fields module) draws as its cell does
|
|
11
|
+
// (select pills, avatars, stars), other columns through their formatter
|
|
12
|
+
// or `format`
|
|
13
|
+
// - cards render in batches as the wall scrolls, so a big table does not
|
|
14
|
+
// mount thousands of cards up front
|
|
15
|
+
// Themes entirely from the grid's `--sg-*` tokens.
|
|
16
|
+
import type { ColumnDef, GalleryConfig, RowData, TableFeatures } from "@svgrid/grid";
|
|
17
|
+
import { formatValueForExport, getGridExtension } from "@svgrid/grid";
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
data,
|
|
21
|
+
columns,
|
|
22
|
+
gallery,
|
|
23
|
+
getRowId,
|
|
24
|
+
}: {
|
|
25
|
+
data: ReadonlyArray<TData>;
|
|
26
|
+
columns: ReadonlyArray<ColumnDef<TFeatures, TData>>;
|
|
27
|
+
gallery: GalleryConfig<TFeatures, TData>;
|
|
28
|
+
getRowId?: (row: TData, index: number) => string;
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
type Def = Record<string, any>;
|
|
32
|
+
const ext = $derived(getGridExtension());
|
|
33
|
+
const leaves = $derived.by(() => {
|
|
34
|
+
const out: Def[] = [];
|
|
35
|
+
const walk = (cols: ReadonlyArray<Def>) => {
|
|
36
|
+
for (const c of cols) {
|
|
37
|
+
if (Array.isArray(c.columns) && c.columns.length) walk(c.columns);
|
|
38
|
+
else if (typeof c.field === "string") out.push(c);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
walk(columns as unknown as Def[]);
|
|
42
|
+
// Field-typed columns filled in as the grid fills them (formatter, cell).
|
|
43
|
+
const fill = ext?.column;
|
|
44
|
+
return fill ? out.map((c) => (c.fieldType ? fill(c, out) : c)) : out;
|
|
45
|
+
});
|
|
46
|
+
const byField = $derived(new Map(leaves.map((c) => [c.field as string, c])));
|
|
47
|
+
const titleField = $derived((gallery.titleField as string | undefined) ?? (leaves[0]?.field as string | undefined));
|
|
48
|
+
const coverField = $derived(gallery.coverField as string | undefined);
|
|
49
|
+
const MAX_FIELDS = 6;
|
|
50
|
+
const cardFields = $derived(
|
|
51
|
+
(gallery.cardFields as ReadonlyArray<string> | undefined) ??
|
|
52
|
+
leaves
|
|
53
|
+
.map((c) => c.field as string)
|
|
54
|
+
.filter((f) => f !== titleField && f !== coverField)
|
|
55
|
+
.slice(0, MAX_FIELDS),
|
|
56
|
+
);
|
|
57
|
+
const SIZES = { small: 180, medium: 240, large: 320 } as const;
|
|
58
|
+
const cardWidth = $derived(
|
|
59
|
+
typeof gallery?.cardSize === "number" ? gallery.cardSize : SIZES[gallery?.cardSize ?? "medium"],
|
|
60
|
+
);
|
|
61
|
+
const showLabels = $derived(gallery.showLabels !== false);
|
|
62
|
+
|
|
63
|
+
// ---- values ----
|
|
64
|
+
const valueOf = (row: TData, field: string) => {
|
|
65
|
+
const def = byField.get(field);
|
|
66
|
+
return def?.fieldFn ? def.fieldFn(row) : (row as Record<string, unknown>)[field];
|
|
67
|
+
};
|
|
68
|
+
// `id`: the card's key, so a formatter gets the row as a grid cell hands it over.
|
|
69
|
+
function text(row: TData, field: string, id = ""): string {
|
|
70
|
+
const def = byField.get(field);
|
|
71
|
+
const value = valueOf(row, field);
|
|
72
|
+
if (typeof def?.formatter === "function") {
|
|
73
|
+
return String(def.formatter({ value, row: { id, original: row }, column: { id: def.id ?? field, columnDef: def } }) ?? "");
|
|
74
|
+
}
|
|
75
|
+
if (def?.format) return formatValueForExport(value, def.format);
|
|
76
|
+
if (value == null) return "";
|
|
77
|
+
if (value instanceof Date) return value.toLocaleDateString();
|
|
78
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
79
|
+
if (Array.isArray(value)) return value.map((v) => (v && typeof v === "object" ? (v.name ?? v.label ?? "") : String(v))).join(", ");
|
|
80
|
+
return String(value);
|
|
81
|
+
}
|
|
82
|
+
const isBlank = (v: unknown) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
83
|
+
/** The column when a field type draws its own cell, else null. */
|
|
84
|
+
function cellDef(field: string): Def | null {
|
|
85
|
+
const def = byField.get(field);
|
|
86
|
+
if (!def?.fieldType || def.cell != null || !ext?.cell || ext.hasCell?.(def) === false) return null;
|
|
87
|
+
return def;
|
|
88
|
+
}
|
|
89
|
+
// What a field cell reads off the grid it is in (the grid itself when the
|
|
90
|
+
// gallery hands it over); a card has a width of its own.
|
|
91
|
+
// `gallery` is gone for a moment while the view switches away: cells still read it.
|
|
92
|
+
const CARD_GRID = $derived(gallery?.cellGrid?.(cardWidth - 24) ?? { getColumnWidth: () => cardWidth - 24 });
|
|
93
|
+
|
|
94
|
+
// ---- cover ----
|
|
95
|
+
const IMAGE_URL = /^(https?:|blob:|data:image\/(png|jpeg|gif|webp|avif|svg\+xml);)/i;
|
|
96
|
+
const isImageFile = (f: { type?: string; name?: string; url?: string }) =>
|
|
97
|
+
(f.type ?? "").startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg)$/i.test(f.name ?? f.url ?? "");
|
|
98
|
+
function coverUrl(row: TData): string | null {
|
|
99
|
+
if (!coverField) return null;
|
|
100
|
+
const v = valueOf(row, coverField) as unknown;
|
|
101
|
+
const pick = (u: unknown) => (typeof u === "string" && IMAGE_URL.test(u.trim()) ? u.trim() : null);
|
|
102
|
+
if (typeof v === "string") return pick(v);
|
|
103
|
+
if (Array.isArray(v)) {
|
|
104
|
+
for (const f of v) {
|
|
105
|
+
if (typeof f === "string" && pick(f)) return pick(f);
|
|
106
|
+
if (f && typeof f === "object" && isImageFile(f)) return pick(f.thumbnailUrl ?? f.url);
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (v && typeof v === "object") return pick((v as { url?: unknown }).url);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- identity + batches ----
|
|
115
|
+
const keyOf = (row: TData, i: number) => (getRowId ? getRowId(row, i) : String(i));
|
|
116
|
+
const BATCH = 120;
|
|
117
|
+
let shownCount = $state(BATCH);
|
|
118
|
+
const shown = $derived(data.slice(0, shownCount));
|
|
119
|
+
// A new filter or search starts the wall from the top batch again.
|
|
120
|
+
$effect(() => {
|
|
121
|
+
void data;
|
|
122
|
+
shownCount = BATCH;
|
|
123
|
+
});
|
|
124
|
+
function more(node: HTMLElement) {
|
|
125
|
+
const io = new IntersectionObserver((entries) => {
|
|
126
|
+
if (entries.some((e) => e.isIntersecting)) shownCount += BATCH;
|
|
127
|
+
}, { rootMargin: "400px" });
|
|
128
|
+
io.observe(node);
|
|
129
|
+
return { destroy: () => io.disconnect() };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const open = (row: TData) => gallery.onCardClick?.(row);
|
|
133
|
+
const fieldLabel = (field: string) => {
|
|
134
|
+
const h = byField.get(field)?.header;
|
|
135
|
+
return typeof h === "string" ? h : field;
|
|
136
|
+
};
|
|
137
|
+
const iconOf = (field: string) => {
|
|
138
|
+
const def = byField.get(field);
|
|
139
|
+
return def ? (ext?.headerIcon?.(def) ?? null) : null;
|
|
140
|
+
};
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<div class="sv-gal" style:--sv-gal-card={`${cardWidth}px`} role="list" aria-label="Records">
|
|
144
|
+
{#each shown as row, i (keyOf(row, i))}
|
|
145
|
+
{@const url = coverUrl(row)}
|
|
146
|
+
{@const title = titleField ? text(row, titleField, keyOf(row, i)) : ""}
|
|
147
|
+
{#if gallery.card}
|
|
148
|
+
<div class="sv-gal-custom" role="listitem">
|
|
149
|
+
{#if gallery.onCardClick}
|
|
150
|
+
<!-- A drawn card is one button: a click or Enter opens it. -->
|
|
151
|
+
<div
|
|
152
|
+
class="sv-gal-custom-hit"
|
|
153
|
+
role="button"
|
|
154
|
+
tabindex="0"
|
|
155
|
+
onclick={() => open(row)}
|
|
156
|
+
onkeydown={(e) => {
|
|
157
|
+
if (e.target !== e.currentTarget || (e.key !== 'Enter' && e.key !== ' ')) return
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
open(row)
|
|
160
|
+
}}
|
|
161
|
+
>{@render gallery.card(row)}</div>
|
|
162
|
+
{:else}
|
|
163
|
+
{@render gallery.card(row)}
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
{:else}
|
|
167
|
+
<!-- The title is the card's button; a click anywhere else on the card does the same. -->
|
|
168
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
169
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
170
|
+
<div class="sv-gal-card" class:is-clickable={!!gallery.onCardClick} role="listitem" onclick={() => open(row)}>
|
|
171
|
+
{#if coverField}
|
|
172
|
+
<div class="sv-gal-cover" class:is-contain={gallery.coverFit === "contain"} class:is-empty={!url}>
|
|
173
|
+
{#if url}<img src={url} alt="" loading="lazy" decoding="async" />{/if}
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
176
|
+
<div class="sv-gal-body">
|
|
177
|
+
{#if gallery.onCardClick}
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
class="sv-gal-title"
|
|
181
|
+
onclick={(e) => {
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
open(row);
|
|
184
|
+
}}>{title || "Untitled"}</button
|
|
185
|
+
>
|
|
186
|
+
{:else}
|
|
187
|
+
<div class="sv-gal-title">{title || "Untitled"}</div>
|
|
188
|
+
{/if}
|
|
189
|
+
{#each cardFields as field (field)}
|
|
190
|
+
{@const def = cellDef(field)}
|
|
191
|
+
{@const value = valueOf(row, field)}
|
|
192
|
+
{#if !isBlank(value)}
|
|
193
|
+
<div class="sv-gal-field">
|
|
194
|
+
{#if showLabels}
|
|
195
|
+
<div class="sv-gal-label">
|
|
196
|
+
{#if iconOf(field)}
|
|
197
|
+
<svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true"><path d={iconOf(field)} fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
198
|
+
{/if}
|
|
199
|
+
{fieldLabel(field)}
|
|
200
|
+
</div>
|
|
201
|
+
{/if}
|
|
202
|
+
<div class="sv-gal-value">
|
|
203
|
+
{#if def && ext?.cell}
|
|
204
|
+
{@const Cell = ext.cell}
|
|
205
|
+
<Cell ctrl={CARD_GRID} row={{ id: keyOf(row, i), original: row }} column={{ id: def.id ?? field, columnDef: def }} {value} />
|
|
206
|
+
{:else}
|
|
207
|
+
{text(row, field, keyOf(row, i))}
|
|
208
|
+
{/if}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
{/if}
|
|
212
|
+
{/each}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
{/if}
|
|
216
|
+
{:else}
|
|
217
|
+
<p class="sv-gal-empty">{gallery.emptyText ?? "No records"}</p>
|
|
218
|
+
{/each}
|
|
219
|
+
{#if shownCount < data.length}
|
|
220
|
+
<div class="sv-gal-more" use:more aria-hidden="true"></div>
|
|
221
|
+
{/if}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<style>
|
|
225
|
+
.sv-gal {
|
|
226
|
+
display: grid;
|
|
227
|
+
grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--sv-gal-card)), 1fr));
|
|
228
|
+
grid-auto-rows: max-content;
|
|
229
|
+
gap: 18px;
|
|
230
|
+
padding: 18px;
|
|
231
|
+
height: 100%;
|
|
232
|
+
box-sizing: border-box;
|
|
233
|
+
overflow: auto;
|
|
234
|
+
align-content: start;
|
|
235
|
+
background: color-mix(in srgb, var(--sg-fg, #0f172a) 3%, var(--sg-bg, #fff));
|
|
236
|
+
color: var(--sg-fg, #0f172a);
|
|
237
|
+
font-family: var(--sg-font, inherit);
|
|
238
|
+
font-size: 13px;
|
|
239
|
+
}
|
|
240
|
+
.sv-gal-card {
|
|
241
|
+
min-width: 0;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
background: var(--sg-bg, #fff);
|
|
244
|
+
border: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 9%, transparent);
|
|
245
|
+
border-radius: 12px;
|
|
246
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
|
247
|
+
transition: box-shadow 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
|
|
248
|
+
}
|
|
249
|
+
.sv-gal-card.is-clickable { cursor: pointer; }
|
|
250
|
+
.sv-gal-card.is-clickable:hover {
|
|
251
|
+
transform: translateY(-2px);
|
|
252
|
+
border-color: color-mix(in srgb, var(--sg-fg, #0f172a) 14%, transparent);
|
|
253
|
+
box-shadow: 0 2px 4px rgba(15, 23, 42, 0.06), 0 12px 28px -12px rgba(15, 23, 42, 0.32);
|
|
254
|
+
}
|
|
255
|
+
.sv-gal-card:has(.sv-gal-title:focus-visible) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 2px; }
|
|
256
|
+
.sv-gal-cover {
|
|
257
|
+
aspect-ratio: 4 / 3;
|
|
258
|
+
overflow: hidden;
|
|
259
|
+
background: color-mix(in srgb, var(--sg-fg, #0f172a) 5%, transparent);
|
|
260
|
+
}
|
|
261
|
+
.sv-gal-cover img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
|
|
262
|
+
.sv-gal-cover.is-contain img { object-fit: contain; }
|
|
263
|
+
.sv-gal-card.is-clickable:hover .sv-gal-cover img { transform: scale(1.04); }
|
|
264
|
+
.sv-gal-body { padding: 12px 14px 14px; display: grid; gap: 9px; min-width: 0; }
|
|
265
|
+
.sv-gal-title {
|
|
266
|
+
padding: 0; border: 0; background: none; color: inherit; font: inherit; font-weight: 600; font-size: 14.5px; line-height: 1.3;
|
|
267
|
+
letter-spacing: -0.005em; text-align: start; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: inherit; outline: none;
|
|
268
|
+
}
|
|
269
|
+
.sv-gal-field { min-width: 0; }
|
|
270
|
+
.sv-gal-label {
|
|
271
|
+
display: flex; align-items: center; gap: 4px; margin-bottom: 2px;
|
|
272
|
+
font-size: 10.5px; font-weight: 500; letter-spacing: 0.03em; text-transform: uppercase; color: var(--sg-muted, #64748b);
|
|
273
|
+
}
|
|
274
|
+
.sv-gal-label svg { flex: none; }
|
|
275
|
+
/* Values are read here, not edited: a click on one opens the card. */
|
|
276
|
+
.sv-gal-value { min-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; pointer-events: none; }
|
|
277
|
+
.sv-gal-custom { min-width: 0; }
|
|
278
|
+
.sv-gal-custom-hit { cursor: pointer; border-radius: 12px; outline: none; }
|
|
279
|
+
.sv-gal-custom-hit:focus-visible { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 2px; }
|
|
280
|
+
.sv-gal-empty { grid-column: 1 / -1; color: var(--sg-muted, #64748b); margin: 8px 2px; }
|
|
281
|
+
.sv-gal-more { grid-column: 1 / -1; height: 1px; }
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
.sv-gal-card, .sv-gal-cover img { transition: none; }
|
|
284
|
+
.sv-gal-card.is-clickable:hover, .sv-gal-card.is-clickable:hover .sv-gal-cover img { transform: none; }
|
|
285
|
+
}
|
|
286
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ColumnDef, GalleryConfig, RowData, TableFeatures } from "@svgrid/grid";
|
|
2
|
+
declare function $$render<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(): {
|
|
3
|
+
props: {
|
|
4
|
+
data: ReadonlyArray<TData>;
|
|
5
|
+
columns: ReadonlyArray<ColumnDef<TFeatures, TData>>;
|
|
6
|
+
gallery: GalleryConfig<TFeatures, TData>;
|
|
7
|
+
getRowId?: (row: TData, index: number) => string;
|
|
8
|
+
};
|
|
9
|
+
exports: {};
|
|
10
|
+
bindings: "";
|
|
11
|
+
slots: {};
|
|
12
|
+
events: {};
|
|
13
|
+
};
|
|
14
|
+
declare class __sveltets_Render<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> {
|
|
15
|
+
props(): ReturnType<typeof $$render<TFeatures, TData>>['props'];
|
|
16
|
+
events(): ReturnType<typeof $$render<TFeatures, TData>>['events'];
|
|
17
|
+
slots(): ReturnType<typeof $$render<TFeatures, TData>>['slots'];
|
|
18
|
+
bindings(): "";
|
|
19
|
+
exports(): {};
|
|
20
|
+
}
|
|
21
|
+
interface $$IsomorphicComponent {
|
|
22
|
+
new <TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TFeatures, TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TFeatures, TData>['props']>, ReturnType<__sveltets_Render<TFeatures, TData>['events']>, ReturnType<__sveltets_Render<TFeatures, TData>['slots']>> & {
|
|
23
|
+
$$bindings?: ReturnType<__sveltets_Render<TFeatures, TData>['bindings']>;
|
|
24
|
+
} & ReturnType<__sveltets_Render<TFeatures, TData>['exports']>;
|
|
25
|
+
<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(internal: unknown, props: ReturnType<__sveltets_Render<TFeatures, TData>['props']> & {}): ReturnType<__sveltets_Render<TFeatures, TData>['exports']>;
|
|
26
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
|
|
27
|
+
}
|
|
28
|
+
declare const SvGridGallery: $$IsomorphicComponent;
|
|
29
|
+
type SvGridGallery<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = InstanceType<typeof SvGridGallery<TFeatures, TData>>;
|
|
30
|
+
export default SvGridGallery;
|