@svgrid/enterprise 2.6.4 → 2.7.0

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 (59) hide show
  1. package/dist/SvGridBulkEditDrawer.svelte +158 -0
  2. package/dist/SvGridBulkEditDrawer.svelte.d.ts +9 -0
  3. package/dist/SvGridSelectionBar.svelte +452 -0
  4. package/dist/SvGridSelectionBar.svelte.d.ts +6 -0
  5. package/dist/SvPivotDesigner.svelte +3 -3
  6. package/dist/SvPivotDesigner.svelte.d.ts +3 -3
  7. package/dist/bulk-edit.d.ts +64 -0
  8. package/dist/bulk-edit.js +152 -0
  9. package/dist/cdn/svgrid-enterprise.svelte-external.js +6182 -5779
  10. package/dist/designer/assets/GridMenus-Cgilexpk.js +8 -0
  11. package/dist/designer/assets/SvDateRangeInput-BV5OXYQ2.css +1 -0
  12. package/dist/designer/assets/SvDateRangeInput-CVn-yqrQ.js +1 -0
  13. package/dist/designer/assets/SvDateTimePicker-BdioTrJQ.js +1 -0
  14. package/dist/designer/assets/SvDateTimePicker-CY8TK4BT.css +1 -0
  15. package/dist/designer/assets/SvField-BsqiA5me.js +1 -0
  16. package/dist/designer/assets/SvGridCellEditor-dLY7_kk9.js +2 -0
  17. package/dist/designer/assets/SvListBox-CVYQcLX9.js +1 -0
  18. package/dist/designer/assets/{SvListBox-BMSfFJJT.css → SvListBox-DgN8kvjt.css} +1 -1
  19. package/dist/designer/assets/column-resize-C_inr5zT.js +1 -0
  20. package/dist/designer/assets/date-format-4DJvNtG2.js +1 -0
  21. package/dist/designer/assets/{SvDateTimePicker-Cygij-dr.css → date-format-Dzy4mTGh.css} +1 -1
  22. package/dist/designer/assets/dismissable-BO547MH9.js +1 -0
  23. package/dist/designer/assets/editor-registry-BjP16H0v.js +1 -0
  24. package/dist/designer/assets/{index-DxBZuosR.js → index-BvaLmQSd.js} +129 -129
  25. package/dist/designer/assets/index-Cp7fDJ3b.css +1 -0
  26. package/dist/designer/assets/{js-scroller.svelte-03aI6ovu.js → js-scroller.svelte-Bhxqb3-c.js} +2 -2
  27. package/dist/designer/assets/preload-helper-CG0n22O7.js +1 -0
  28. package/dist/designer/assets/row-resize-B0FWHcNr.js +1 -0
  29. package/dist/designer/assets/src-7qOYsBug.js +2896 -0
  30. package/dist/designer/assets/src-B7DNcoQK.css +1 -0
  31. package/dist/designer/assets/validate-h-aslRV9.js +1 -0
  32. package/dist/designer/index.html +18 -13
  33. package/dist/export.d.ts +4 -0
  34. package/dist/import.d.ts +7 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +8 -0
  37. package/dist/install.d.ts +3 -0
  38. package/dist/install.js +2 -0
  39. package/dist/node/studio.js +65 -3
  40. package/dist/pivot.d.ts +11 -0
  41. package/dist/pivot.js +6 -1
  42. package/dist/selection-bar.d.ts +7 -0
  43. package/dist/selection-bar.js +42 -0
  44. package/dist/studio/emit-project.js +6 -0
  45. package/dist/studio/ui-components.generated.js +64 -2
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +8 -3
  49. package/dist/designer/assets/GridMenus-BIU7Nfbr.js +0 -7
  50. package/dist/designer/assets/SvDateTimePicker-DOBYyLeN.js +0 -1
  51. package/dist/designer/assets/SvGrid.helpers-C6wxF55r.js +0 -1
  52. package/dist/designer/assets/SvGridCellEditor-CMtmQINJ.js +0 -2
  53. package/dist/designer/assets/SvListBox-D-_I_Aff.js +0 -1
  54. package/dist/designer/assets/dismissable-2EZKoens.js +0 -1
  55. package/dist/designer/assets/editor-registry-BB1Qb5mp.js +0 -1
  56. package/dist/designer/assets/index-BfzoL904.css +0 -1
  57. package/dist/designer/assets/src-D8qr49rE.css +0 -1
  58. package/dist/designer/assets/src-Dn1Axx6T.js +0 -2896
  59. /package/dist/designer/assets/{dismissable-BF-9TJSd.css → SvField-BF-9TJSd.css} +0 -0
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The bulk-edit drawer behind the selection bar's `editFields` button.
4
+ *
5
+ * Edits ONE OR MORE fields across ONE OR MORE selected rows. It is the same
6
+ * side drawer + schema form the grid already uses for single-row editing
7
+ * (`SvDrawer` + `SvForm`), so a user who has edited one row here already
8
+ * knows this screen.
9
+ *
10
+ * The rule that makes multi-field bulk edit safe: **only fields you actually
11
+ * change are written.** A field every selected row already agrees on opens
12
+ * showing that value; a field they disagree on opens blank and labelled
13
+ * "Multiple values". Leave either alone and every row keeps what it had.
14
+ */
15
+ import { untrack } from 'svelte'
16
+ import { SvDrawer, SvForm, type FormField, type FormFieldType } from '@svgrid/grid'
17
+ import {
18
+ applyBulkEdit,
19
+ bulkEditInitialValues,
20
+ bulkEditableFields,
21
+ coerceBulkValue,
22
+ type BulkEditField,
23
+ type BulkEditResult,
24
+ } from './bulk-edit'
25
+
26
+ let {
27
+ ctrl,
28
+ open = $bindable(false),
29
+ onApplied,
30
+ }: {
31
+ ctrl: any
32
+ open?: boolean
33
+ onApplied?: (r: BulkEditResult) => void
34
+ } = $props()
35
+
36
+ const messages = $derived(ctrl.messages)
37
+ const columns = $derived(bulkEditableFields(ctrl) as BulkEditField[])
38
+ const count = $derived((ctrl.selectionBarTarget?.ids ?? []).length as number)
39
+
40
+ /** A column's editor type mapped onto the form's control vocabulary. */
41
+ function controlFor(editorType: string, hasOptions: boolean): FormFieldType {
42
+ if (hasOptions) return 'select'
43
+ switch (editorType) {
44
+ case 'number': return 'number'
45
+ case 'checkbox': return 'checkbox'
46
+ case 'date': return 'date'
47
+ case 'datetime': return 'datetime'
48
+ case 'time': return 'time'
49
+ case 'textarea': return 'textarea'
50
+ case 'color': return 'color'
51
+ default: return 'text'
52
+ }
53
+ }
54
+
55
+ // Snapshotted when the drawer opens rather than derived: the form is
56
+ // uncontrolled once mounted, and a baseline that kept re-deriving as rows
57
+ // changed underneath would make "did the user change this?" unanswerable.
58
+ let baseline = $state<Record<string, unknown>>({})
59
+ let mixed = $state<Set<string>>(new Set())
60
+ let formKey = $state(0)
61
+
62
+ // Only `open` is tracked. The snapshot reads `columns` and the grid's rows,
63
+ // and writes state that `fields` derives from - tracking either would make
64
+ // this effect depend on what it writes, which Svelte rejects.
65
+ $effect(() => {
66
+ if (!open) return
67
+ untrack(() => {
68
+ const snapshot = bulkEditInitialValues(ctrl, columns)
69
+ baseline = snapshot.values
70
+ mixed = snapshot.mixed
71
+ // Remount the form so it picks the new initial values up.
72
+ formKey += 1
73
+ })
74
+ })
75
+
76
+ const fields = $derived<FormField[]>(
77
+ columns.map((c) => ({
78
+ name: c.id,
79
+ label: c.label,
80
+ type: controlFor(c.editorType, !!c.options?.length),
81
+ // Placeholder only. The same sentence repeated under every differing
82
+ // field was five identical lines of noise - the lead says it once.
83
+ placeholder: mixed.has(c.id) ? messages.bulkEditMixed : undefined,
84
+ options: c.options?.map((o) => ({ value: o.value as string | number, label: o.label })),
85
+ })),
86
+ )
87
+
88
+ const fill = (t: string, n: number) => t.replace(/\{count\}/g, String(n))
89
+
90
+ function submit(values: Record<string, unknown>) {
91
+ const byId = new Map(columns.map((c) => [c.id, c]))
92
+ const edits: Record<string, unknown> = {}
93
+ for (const [name, raw] of Object.entries(values)) {
94
+ const column = byId.get(name)
95
+ if (!column) continue
96
+ const next = coerceBulkValue(column.editorType, raw)
97
+ // Untouched fields are the ones that keep every row's own value. A field
98
+ // that opened "Multiple values" and was left blank is untouched too.
99
+ if (next === baseline[name]) continue
100
+ if (mixed.has(name) && (raw === '' || raw == null)) continue
101
+ edits[name] = next
102
+ }
103
+ const result = applyBulkEdit(ctrl, edits)
104
+ open = false
105
+ onApplied?.(result)
106
+ }
107
+ </script>
108
+
109
+ <SvDrawer
110
+ bind:open
111
+ side="right"
112
+ title={messages.bulkEditTitle}
113
+ onClose={() => (open = false)}
114
+ >
115
+ <div class="sv-bulk">
116
+ {#if columns.length === 0}
117
+ <p class="sv-bulk-empty">{messages.bulkEditNoFields}</p>
118
+ {:else}
119
+ <p class="sv-bulk-lead">{fill(messages.bulkEditLead, count)}</p>
120
+ {#key formKey}
121
+ <SvForm
122
+ {fields}
123
+ initial={baseline}
124
+ columns={1}
125
+ submitLabel={fill(messages.bulkEditApply, count)}
126
+ cancelLabel={messages.bulkEditCancel}
127
+ onSubmit={submit}
128
+ onCancel={() => (open = false)}
129
+ />
130
+ {/key}
131
+ {/if}
132
+ </div>
133
+ </SvDrawer>
134
+
135
+ <style>
136
+ /* Fill the drawer so the actions can sit against its bottom edge. Without
137
+ this the buttons stop wherever the last field happens to end, with the
138
+ rest of the panel empty below them - which reads as an unfinished panel
139
+ rather than a short form. */
140
+ .sv-bulk { display: flex; flex-direction: column; min-height: 100%; }
141
+ .sv-bulk :global(.sv-form) { flex: 1; }
142
+ .sv-bulk :global(.sv-form__actions) {
143
+ margin-top: auto;
144
+ padding-top: 14px;
145
+ border-top: 1px solid var(--sg-border, #e2e8f0);
146
+ }
147
+
148
+ .sv-bulk-lead {
149
+ margin: 0 0 14px;
150
+ font-size: 13px;
151
+ color: var(--sg-muted, #64748b);
152
+ }
153
+ .sv-bulk-empty {
154
+ margin: 0;
155
+ font-size: 13px;
156
+ color: var(--sg-muted, #64748b);
157
+ }
158
+ </style>
@@ -0,0 +1,9 @@
1
+ import { type BulkEditResult } from './bulk-edit';
2
+ type $$ComponentProps = {
3
+ ctrl: any;
4
+ open?: boolean;
5
+ onApplied?: (r: BulkEditResult) => void;
6
+ };
7
+ declare const SvGridBulkEditDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
8
+ type SvGridBulkEditDrawer = ReturnType<typeof SvGridBulkEditDrawer>;
9
+ export default SvGridBulkEditDrawer;
@@ -0,0 +1,452 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The bulk-action bar (Pro renderer for `<SvGrid selectionBar={...}>`).
4
+ *
5
+ * Floats over the grid while rows are selected: a count chip, then the
6
+ * actions that apply to the whole selection, then a clear button - the shape
7
+ * an issue tracker uses for bulk edit.
8
+ *
9
+ * The grid decides WHETHER this exists and normalises the prop; this renders
10
+ * it. Everything it reads comes off the controller handle, so the free grid
11
+ * needs no knowledge of what lives here.
12
+ */
13
+ import { onMount } from 'svelte'
14
+ import SvGridBulkEditDrawer from './SvGridBulkEditDrawer.svelte'
15
+
16
+ let { ctrl }: { ctrl: any } = $props()
17
+
18
+ type Target = { rows: unknown[]; ids: string[] }
19
+ type Action = {
20
+ key: string
21
+ label: string
22
+ icon?: string
23
+ danger?: boolean
24
+ hidden?: (t: Target) => boolean
25
+ disabled?: (t: Target) => boolean
26
+ action: (t: Target) => void
27
+ }
28
+
29
+ const target = $derived(ctrl.selectionBarTarget as Target)
30
+ const count = $derived(target.ids.length)
31
+ const messages = $derived(ctrl.messages)
32
+ const position = $derived(ctrl.selectionBarPosition as 'top' | 'bottom')
33
+ const maxVisible = $derived(ctrl.selectionBarMaxVisible as number)
34
+ const hideClear = $derived(ctrl.selectionBarHideClear as boolean)
35
+
36
+ let bulkEditOpen = $state(false)
37
+
38
+ // Single-path 24x24 icons, matching the stroke style the grid uses elsewhere.
39
+ const ICON = {
40
+ selectAll: 'M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11',
41
+ editFields: 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4Z',
42
+ }
43
+
44
+ /** Turn the built-in string keys into real actions. */
45
+ function resolve(entry: Action | string): Action | 'separator' | null {
46
+ if (typeof entry !== 'string') return entry
47
+ if (entry === 'separator') return 'separator'
48
+ if (entry === 'selectAll') {
49
+ return {
50
+ key: 'selectAll',
51
+ label: messages.selectionBarSelectAll,
52
+ icon: ICON.selectAll,
53
+ // Nothing to do once the whole view is already picked.
54
+ disabled: () => ctrl.headerSelectionState === 'all',
55
+ action: () => ctrl.toggleSelectAllRows(),
56
+ }
57
+ }
58
+ if (entry === 'editFields') {
59
+ return {
60
+ key: 'editFields',
61
+ label: messages.selectionBarEditFields,
62
+ icon: ICON.editFields,
63
+ action: () => (bulkEditOpen = true),
64
+ }
65
+ }
66
+ return null
67
+ }
68
+
69
+ // Predicates run per render against the LIVE selection, so a rule like "only
70
+ // at two or more rows" flips as the user ticks rather than freezing at the
71
+ // first paint.
72
+ const entries = $derived(
73
+ (ctrl.selectionBarActions as Array<Action | string>)
74
+ .map(resolve)
75
+ .filter((e): e is Action | 'separator' => e != null)
76
+ .filter((e) => e === 'separator' || !e.hidden?.(target)),
77
+ )
78
+ // Separators do not count towards the limit - they are punctuation, not
79
+ // buttons, and letting them push a real action into the menu would be odd.
80
+ const buttons = $derived(entries.filter((e): e is Action => e !== 'separator'))
81
+ const overflow = $derived(buttons.length > maxVisible ? buttons.slice(maxVisible) : [])
82
+ const overflowKeys = $derived(new Set(overflow.map((a) => a.key)))
83
+ const inline = $derived(entries.filter((e) => e === 'separator' || !overflowKeys.has(e.key)))
84
+
85
+ let overflowOpen = $state(false)
86
+ let barEl = $state<HTMLElement | null>(null)
87
+
88
+ const fill = (template: string, n: number) => template.replace(/\{count\}/g, String(n))
89
+ const clearSelection = () => ctrl.grid.setRowSelection(() => ({}))
90
+
91
+ function run(a: Action) {
92
+ overflowOpen = false
93
+ a.action(target)
94
+ }
95
+
96
+ /**
97
+ * Roving focus, which is what `role="toolbar"` promises a screen reader:
98
+ * one tab stop for the bar, then Left/Right between its buttons. Without it
99
+ * a bar with six actions puts six stops between the grid and whatever comes
100
+ * after it.
101
+ */
102
+ function onKeydown(event: KeyboardEvent) {
103
+ if (event.key === 'Escape') {
104
+ if (overflowOpen) {
105
+ overflowOpen = false
106
+ event.stopPropagation()
107
+ return
108
+ }
109
+ clearSelection()
110
+ event.stopPropagation()
111
+ return
112
+ }
113
+ if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return
114
+ const items = [...(barEl?.querySelectorAll<HTMLButtonElement>('button:not(:disabled)') ?? [])]
115
+ if (items.length < 2) return
116
+ const at = items.indexOf(document.activeElement as HTMLButtonElement)
117
+ if (at < 0) return
118
+ event.preventDefault()
119
+ const step = event.key === 'ArrowRight' ? 1 : -1
120
+ items[(at + step + items.length) % items.length]!.focus()
121
+ }
122
+
123
+ // Close the overflow menu on any pointerdown that is not inside the bar.
124
+ onMount(() => {
125
+ const onDown = (e: PointerEvent) => {
126
+ if (!overflowOpen) return
127
+ if (barEl && e.composedPath().includes(barEl)) return
128
+ overflowOpen = false
129
+ }
130
+ window.addEventListener('pointerdown', onDown, true)
131
+ return () => window.removeEventListener('pointerdown', onDown, true)
132
+ })
133
+ </script>
134
+
135
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
136
+ <div
137
+ bind:this={barEl}
138
+ class="sv-selbar"
139
+ data-position={position}
140
+ role="toolbar"
141
+ aria-label={messages.selectionBarLabel}
142
+ aria-orientation="horizontal"
143
+ onkeydown={onKeydown}
144
+ >
145
+ <span class="sv-selbar-count" aria-live="polite">
146
+ <span class="sv-selbar-chip">{count}</span>
147
+ <span class="sv-selbar-count-label">{messages.selectionBarCount}</span>
148
+ </span>
149
+
150
+ {#if inline.length}
151
+ <span class="sv-selbar-sep" aria-hidden="true"></span>
152
+ {/if}
153
+
154
+ {#each inline as entry, i (entry === 'separator' ? `sep${i}` : entry.key)}
155
+ {#if entry === 'separator'}
156
+ <span class="sv-selbar-sep" aria-hidden="true"></span>
157
+ {:else}
158
+ {@const isDisabled = entry.disabled?.(target) ?? false}
159
+ <button
160
+ type="button"
161
+ class="sv-selbar-btn"
162
+ class:is-danger={entry.danger}
163
+ class:has-icon={!!entry.icon}
164
+ title={entry.label}
165
+ aria-label={entry.label}
166
+ disabled={isDisabled}
167
+ onclick={() => run(entry)}
168
+ >
169
+ {#if entry.icon}
170
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
171
+ <path d={entry.icon} />
172
+ </svg>
173
+ {/if}
174
+ <span>{entry.label}</span>
175
+ </button>
176
+ {/if}
177
+ {/each}
178
+
179
+ {#if overflow.length}
180
+ <div class="sv-selbar-more">
181
+ <button
182
+ type="button"
183
+ class="sv-selbar-btn sv-selbar-more-btn"
184
+ aria-haspopup="menu"
185
+ aria-expanded={overflowOpen}
186
+ aria-label={messages.selectionBarMore}
187
+ title={messages.selectionBarMore}
188
+ onclick={() => (overflowOpen = !overflowOpen)}
189
+ >
190
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true">
191
+ <circle cx="5" cy="12" r="1.8" /><circle cx="12" cy="12" r="1.8" /><circle cx="19" cy="12" r="1.8" />
192
+ </svg>
193
+ </button>
194
+ {#if overflowOpen}
195
+ <div class="sv-selbar-menu" data-position={position} role="menu">
196
+ {#each overflow as a (a.key)}
197
+ {@const isDisabled = a.disabled?.(target) ?? false}
198
+ <button
199
+ type="button"
200
+ class="sv-selbar-menu-item"
201
+ class:is-danger={a.danger}
202
+ role="menuitem"
203
+ disabled={isDisabled}
204
+ onclick={() => run(a)}
205
+ >
206
+ {#if a.icon}
207
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
208
+ <path d={a.icon} />
209
+ </svg>
210
+ {/if}
211
+ <span>{a.label}</span>
212
+ </button>
213
+ {/each}
214
+ </div>
215
+ {/if}
216
+ </div>
217
+ {/if}
218
+
219
+ {#if !hideClear}
220
+ <span class="sv-selbar-sep" aria-hidden="true"></span>
221
+ <button
222
+ type="button"
223
+ class="sv-selbar-clear"
224
+ aria-label={messages.selectionBarClear}
225
+ title={messages.selectionBarClear}
226
+ onclick={clearSelection}
227
+ >
228
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
229
+ <path d="M18 6 6 18M6 6l12 12" />
230
+ </svg>
231
+ </button>
232
+ {/if}
233
+ </div>
234
+
235
+ <SvGridBulkEditDrawer {ctrl} bind:open={bulkEditOpen} />
236
+
237
+ <style>
238
+ /* `absolute` against `.sv-grid-root`, not `fixed`: fixed escapes the grid and
239
+ floats over the page, which is wrong when there are two grids on a screen,
240
+ and it anchors to the wrong box inside any ancestor with a transform. */
241
+ .sv-selbar {
242
+ position: absolute;
243
+ left: 50%;
244
+ /*
245
+ * Above the grid's own custom scrollbars, which sit at 40 / 41 / 42.
246
+ *
247
+ * At 30 the bar tied with pinned cells and the sticky header and lost to
248
+ * the scrollbars outright: a bottom-anchored bar overlaps the horizontal
249
+ * scrollbar by a pixel and the vertical one by a few, and the scrollbar
250
+ * painted over the bar's corner and shadow.
251
+ *
252
+ * Deliberately BELOW find-in-grid (60) and the menus / backdrops (900+):
253
+ * an overlay the user just opened should win over persistent chrome.
254
+ */
255
+ z-index: 45;
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 2px;
259
+ max-width: calc(100% - 24px);
260
+ --selbar-pad-y: 6px;
261
+ padding: var(--selbar-pad-y) 8px;
262
+ border: 1px solid var(--sg-border, #e2e8f0);
263
+ border-radius: 8px;
264
+ background: var(--sg-bg, #fff);
265
+ box-shadow: 0 8px 28px rgb(15 23 42 / 20%), 0 1px 2px rgb(15 23 42 / 10%);
266
+ font-family: var(--sg-font, inherit);
267
+ font-size: 13px;
268
+ color: var(--sg-fg, #0f172a);
269
+ /* NO overflow here. Any value but `visible` clips absolutely-positioned
270
+ descendants, and the "..." menu is one - setting `overflow-x: auto` as
271
+ an anti-overlap backstop made the menu invisible and unclickable.
272
+ Buttons shrink instead; see .sv-selbar-btn. */
273
+ }
274
+ .sv-selbar[data-position='bottom'] {
275
+ bottom: 16px;
276
+ transform: translateX(-50%);
277
+ animation: sv-selbar-up 140ms ease-out;
278
+ }
279
+ .sv-selbar[data-position='top'] {
280
+ top: 16px;
281
+ transform: translateX(-50%);
282
+ animation: sv-selbar-down 140ms ease-out;
283
+ }
284
+ /* The entrance is what makes the bar read as arriving rather than as a row
285
+ that was always there. Both keyframes keep the -50% X so the slide is
286
+ vertical only. */
287
+ @keyframes sv-selbar-up {
288
+ from { transform: translate(-50%, 10px); opacity: 0; }
289
+ to { transform: translate(-50%, 0); opacity: 1; }
290
+ }
291
+ @keyframes sv-selbar-down {
292
+ from { transform: translate(-50%, -10px); opacity: 0; }
293
+ to { transform: translate(-50%, 0); opacity: 1; }
294
+ }
295
+ @media (prefers-reduced-motion: reduce) {
296
+ .sv-selbar[data-position='bottom'],
297
+ .sv-selbar[data-position='top'] { animation: none; }
298
+ }
299
+
300
+ /* Count as a chip + label rather than one string: the number is the thing a
301
+ user checks before hitting a destructive button, so it gets its own
302
+ high-contrast block instead of blending into the sentence. */
303
+ .sv-selbar-count {
304
+ display: inline-flex;
305
+ align-items: center;
306
+ gap: 7px;
307
+ padding: 0 6px 0 4px;
308
+ white-space: nowrap;
309
+ }
310
+ .sv-selbar-chip {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ min-width: 20px;
315
+ height: 20px;
316
+ padding: 0 6px;
317
+ border-radius: 4px;
318
+ background: var(--sg-accent, #2563eb);
319
+ color: var(--sg-on-accent, #fff);
320
+ font-size: 12px;
321
+ font-weight: 700;
322
+ font-variant-numeric: tabular-nums;
323
+ }
324
+ .sv-selbar-count-label { color: var(--sg-muted, #64748b); }
325
+
326
+ .sv-selbar-sep {
327
+ width: 1px;
328
+ align-self: stretch;
329
+ margin: 3px 4px;
330
+ background: var(--sg-border, #e2e8f0);
331
+ flex: none;
332
+ }
333
+
334
+ .sv-selbar-btn {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 7px;
338
+ /* A flex item will not shrink below its content unless told to, which is
339
+ how six nowrap labels ended up drawn on top of each other on a phone.
340
+ With min-width 0 the label ellipsises instead. */
341
+ min-width: 0;
342
+ border: 1px solid transparent;
343
+ border-radius: 6px;
344
+ background: transparent;
345
+ color: var(--sg-fg, #0f172a);
346
+ padding: 5px 9px;
347
+ font: inherit;
348
+ white-space: nowrap;
349
+ cursor: pointer;
350
+ }
351
+ .sv-selbar-btn:hover:not(:disabled) {
352
+ background: var(--sg-row-hover-bg, rgb(148 163 184 / 14%));
353
+ }
354
+ .sv-selbar-btn:disabled { opacity: 0.4; cursor: default; }
355
+ /* Not red by default. An issue tracker's Delete sits in the same neutral row
356
+ as everything else, and a bar of one red button trains people to ignore
357
+ the colour. `danger: true` is there for when an action really warrants it. */
358
+ .sv-selbar-btn.is-danger { color: var(--sg-danger, #dc2626); }
359
+ .sv-selbar-btn.is-danger:hover:not(:disabled) {
360
+ background: color-mix(in srgb, var(--sg-danger, #dc2626) 12%, transparent 88%);
361
+ }
362
+ .sv-selbar-more-btn { padding: 5px 7px; flex: none; }
363
+ .sv-selbar-btn > span {
364
+ overflow: hidden;
365
+ text-overflow: ellipsis;
366
+ }
367
+ .sv-selbar-btn > svg { flex: none; }
368
+
369
+ .sv-selbar-more { position: relative; display: inline-flex; }
370
+ .sv-selbar-menu {
371
+ position: absolute;
372
+ right: 0;
373
+ min-width: 172px;
374
+ padding: 4px;
375
+ border: 1px solid var(--sg-border, #e2e8f0);
376
+ border-radius: 8px;
377
+ background: var(--sg-bg, #fff);
378
+ box-shadow: 0 8px 28px rgb(15 23 42 / 20%);
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 1px;
382
+ z-index: 1;
383
+ }
384
+ /* Open away from the edge the bar is pinned to, so the menu never falls off
385
+ the grid: a bottom bar opens upward, a top bar opens downward.
386
+
387
+ The offset clears the BAR, not the button. `100%` is the trigger wrapper,
388
+ which sits inside the bar's vertical padding - so a plain `100% + 6px`
389
+ left the menu overlapping the bar by the padding. Hence + --selbar-pad-y. */
390
+ .sv-selbar-menu[data-position='bottom'] { bottom: calc(100% + var(--selbar-pad-y) + 6px); }
391
+ .sv-selbar-menu[data-position='top'] { top: calc(100% + var(--selbar-pad-y) + 6px); }
392
+
393
+ .sv-selbar-menu-item {
394
+ display: flex;
395
+ align-items: center;
396
+ gap: 8px;
397
+ width: 100%;
398
+ border: 0;
399
+ border-radius: 5px;
400
+ background: transparent;
401
+ color: var(--sg-fg, #0f172a);
402
+ padding: 7px 9px;
403
+ font: inherit;
404
+ text-align: left;
405
+ cursor: pointer;
406
+ }
407
+ .sv-selbar-menu-item:hover:not(:disabled) {
408
+ background: var(--sg-row-hover-bg, rgb(148 163 184 / 14%));
409
+ }
410
+ .sv-selbar-menu-item:disabled { opacity: 0.4; cursor: default; }
411
+ .sv-selbar-menu-item.is-danger { color: var(--sg-danger, #dc2626); }
412
+
413
+ .sv-selbar-clear {
414
+ display: inline-flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ width: 26px;
418
+ height: 26px;
419
+ border: 0;
420
+ border-radius: 6px;
421
+ background: transparent;
422
+ color: var(--sg-muted, #64748b);
423
+ cursor: pointer;
424
+ flex: none;
425
+ }
426
+ .sv-selbar-clear:hover {
427
+ background: var(--sg-row-hover-bg, rgb(148 163 184 / 14%));
428
+ color: var(--sg-fg, #0f172a);
429
+ }
430
+
431
+ .sv-selbar button:focus-visible {
432
+ outline: 2px solid var(--sg-accent, #2563eb);
433
+ outline-offset: 1px;
434
+ }
435
+
436
+ /* Narrow: drop to icons and keep the count chip. Six labelled buttons do not
437
+ fit a phone, and a bar you have to scroll sideways is a bar whose last
438
+ action nobody reaches.
439
+
440
+ Only buttons that HAVE an icon lose their label - an icon-less action would
441
+ otherwise render as an empty box. The label survives as the tooltip and as
442
+ the accessible name, which is on the button itself, so nothing is lost to a
443
+ screen reader. */
444
+ @media (max-width: 640px) {
445
+ .sv-selbar { gap: 0; padding-left: 6px; }
446
+ .sv-selbar-btn.has-icon span { display: none; }
447
+ .sv-selbar-btn.has-icon { padding: 6px 8px; }
448
+ .sv-selbar-count { padding-right: 2px; }
449
+ .sv-selbar-count-label { display: none; }
450
+ .sv-selbar-sep { margin: 3px 2px; }
451
+ }
452
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ ctrl: any;
3
+ };
4
+ declare const SvGridSelectionBar: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type SvGridSelectionBar = ReturnType<typeof SvGridSelectionBar>;
6
+ export default SvGridSelectionBar;
@@ -100,7 +100,7 @@
100
100
  * grid. `'top'` (default) keeps the classic Excel layout: left rail +
101
101
  * a horizontal row of wells above the grid. `'right'` docks everything
102
102
  * as a single vertical tool panel on the right of the grid - the
103
- * "enterprise data grid" / AG-Grid pivot-panel arrangement.
103
+ * conventional "enterprise data grid" pivot-panel arrangement.
104
104
  */
105
105
  panelPosition?: 'top' | 'right'
106
106
  /** Width of the docked panel when `panelPosition='right'`. Default 280. */
@@ -131,7 +131,7 @@
131
131
  */
132
132
  columnVirtualization?: boolean
133
133
  /**
134
- * Render the field picker as an AG-Grid-style **Columns** tool panel: a
134
+ * Render the field picker as a **Columns** tool panel: a
135
135
  * collapsible column-group TREE whose checkboxes toggle column VISIBILITY
136
136
  * (with a select-all), instead of the flat "tick to add to a well" list.
137
137
  * Fields still drag into the Rows / Columns / Values wells. The embedded
@@ -144,7 +144,7 @@
144
144
  onHiddenFieldsChange?: (hidden: string[]) => void
145
145
  /**
146
146
  * Show a vertical Columns / Filters tab rail on the docked panel
147
- * (`panelPosition='right'`). The Filters tab is an AG-Grid-style set-filter
147
+ * (`panelPosition='right'`). The Filters tab is a set-filter
148
148
  * builder: "Add Filter" -> pick a column -> tick which values pass. Filters
149
149
  * narrow the source rows (they drive `layout.filters`). Default false.
150
150
  */
@@ -95,7 +95,7 @@ declare function $$render<T extends PivotInputRow>(): {
95
95
  * grid. `'top'` (default) keeps the classic Excel layout: left rail +
96
96
  * a horizontal row of wells above the grid. `'right'` docks everything
97
97
  * as a single vertical tool panel on the right of the grid - the
98
- * "enterprise data grid" / AG-Grid pivot-panel arrangement.
98
+ * conventional "enterprise data grid" pivot-panel arrangement.
99
99
  */
100
100
  panelPosition?: "top" | "right";
101
101
  /** Width of the docked panel when `panelPosition='right'`. Default 280. */
@@ -133,7 +133,7 @@ declare function $$render<T extends PivotInputRow>(): {
133
133
  */
134
134
  columnVirtualization?: boolean;
135
135
  /**
136
- * Render the field picker as an AG-Grid-style **Columns** tool panel: a
136
+ * Render the field picker as a **Columns** tool panel: a
137
137
  * collapsible column-group TREE whose checkboxes toggle column VISIBILITY
138
138
  * (with a select-all), instead of the flat "tick to add to a well" list.
139
139
  * Fields still drag into the Rows / Columns / Values wells. The embedded
@@ -146,7 +146,7 @@ declare function $$render<T extends PivotInputRow>(): {
146
146
  onHiddenFieldsChange?: (hidden: string[]) => void;
147
147
  /**
148
148
  * Show a vertical Columns / Filters tab rail on the docked panel
149
- * (`panelPosition='right'`). The Filters tab is an AG-Grid-style set-filter
149
+ * (`panelPosition='right'`). The Filters tab is a set-filter
150
150
  * builder: "Add Filter" -> pick a column -> tick which values pass. Filters
151
151
  * narrow the source rows (they drive `layout.filters`). Default false.
152
152
  */