@svgrid/grid 2.2.30 → 2.2.32
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 -1
- package/dist/SvAvatar.svelte +1 -1
- package/dist/SvCheckBox.svelte +2 -1
- package/dist/SvCircularProgress.svelte +1 -1
- package/dist/SvGrid.controller.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.js +358 -18
- package/dist/SvGrid.css +103 -3
- package/dist/SvGrid.svelte +162 -10
- package/dist/SvGrid.types.d.ts +81 -0
- package/dist/SvGridDropdown.svelte +38 -6
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvGridSelect.svelte +12 -3
- package/dist/SvGridSelect.svelte.d.ts +6 -0
- package/dist/SvGroupCell.svelte +9 -2
- package/dist/SvMultiSelect.svelte +12 -3
- package/dist/SvMultiSelect.svelte.d.ts +6 -0
- package/dist/SvPasswordInput.svelte +1 -1
- package/dist/SvProgress.svelte +1 -1
- package/dist/SvRating.svelte +1 -1
- package/dist/SvResult.svelte +1 -1
- package/dist/SvRichText.svelte +31 -16
- package/dist/SvRichText.svelte.d.ts +9 -0
- package/dist/SvSwitchButton.svelte +2 -1
- package/dist/SvToaster.svelte +1 -1
- package/dist/SvTree.svelte +9 -2
- package/dist/SvTree.svelte.d.ts +5 -0
- package/dist/SvTreeSelect.svelte +6 -2
- package/dist/build-api.js +16 -0
- package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-C6ixSGWW.js} +33 -33
- package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-Cyk3qpJ7.js} +33 -33
- package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-CdqaP61H.js} +2 -1
- package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-Dh-2ej8j.js} +92 -87
- package/dist/cdn/{src-BLJfdyL0.js → src-BgHjWF0Q.js} +4193 -3837
- package/dist/cdn/{src-DYDCiC0D.js → src-CgD-wiuS.js} +5956 -5602
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/cell-render.d.ts +1 -0
- package/dist/cell-render.js +115 -1
- package/dist/cell-values.js +5 -0
- package/dist/core.d.ts +80 -9
- package/dist/core.js +89 -0
- package/dist/editor-contract.d.ts +12 -5
- package/dist/editor-registry.d.ts +18 -7
- package/dist/editor-registry.js +10 -1
- package/dist/group-display.d.ts +39 -0
- package/dist/group-display.js +52 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/keyboard-handlers.js +16 -0
- package/dist/recurrence.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +8 -2
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +26 -0
- package/package.json +1 -1
- package/src/SvAccordion.test.ts +5 -5
- package/src/SvAvatar.svelte +1 -1
- package/src/SvButtonGroup.test.ts +6 -6
- package/src/SvCheckBox.svelte +2 -1
- package/src/SvCircularProgress.svelte +1 -1
- package/src/SvFileUpload.test.ts +1 -1
- package/src/SvGrid.controller.svelte.ts +367 -17
- package/src/SvGrid.css +103 -3
- package/src/SvGrid.svelte +162 -10
- package/src/SvGrid.types.ts +81 -0
- package/src/SvGridDropdown.svelte +38 -6
- package/src/SvGridSelect.svelte +12 -3
- package/src/SvGroupCell.svelte +9 -2
- package/src/SvMultiSelect.svelte +12 -3
- package/src/SvPasswordInput.svelte +1 -1
- package/src/SvProgress.svelte +1 -1
- package/src/SvRating.svelte +1 -1
- package/src/SvResult.svelte +1 -1
- package/src/SvRichText.svelte +31 -16
- package/src/SvSwitchButton.svelte +2 -1
- package/src/SvToaster.svelte +1 -1
- package/src/SvTree.svelte +9 -2
- package/src/SvTreeSelect.svelte +6 -2
- package/src/build-api.ts +15 -0
- package/src/builtin-editors.test.ts +3 -1
- package/src/cell-render.test.ts +27 -3
- package/src/cell-render.ts +122 -1
- package/src/cell-values.ts +4 -0
- package/src/chart-export.test.ts +2 -2
- package/src/clipboard.test.ts +0 -2
- package/src/core.ts +157 -8
- package/src/editing.test.ts +1 -1
- package/src/editor-contract.ts +12 -5
- package/src/editor-registry.grid.test.ts +48 -0
- package/src/editor-registry.test.ts +3 -1
- package/src/editor-registry.ts +23 -7
- package/src/facet-buckets.test.ts +33 -33
- package/src/group-display.test.ts +91 -1
- package/src/group-display.ts +83 -0
- package/src/index.ts +9 -0
- package/src/keyboard-handlers.ts +17 -0
- package/src/list-power.test.ts +2 -2
- package/src/menus.test.ts +4 -4
- package/src/named-views.coverage.test.ts +3 -3
- package/src/recurrence.ts +3 -2
- package/src/row-resize.test.ts +40 -40
- package/src/scheduler-model.ts +1 -1
- package/src/server-data-source.coverage.test.ts +1 -1
- package/src/server-group-model.test.ts +6 -6
- package/src/spreadsheet.test.ts +0 -1
- package/src/svgrid-wrapper.types.ts +11 -9
- package/src/svgrid.async-editor-options.test.ts +271 -0
- package/src/svgrid.auto-row-height.test.ts +204 -0
- package/src/svgrid.charting.test.ts +4 -4
- package/src/svgrid.comments-autocomplete.test.ts +8 -2
- package/src/svgrid.context-menu.test.ts +25 -9
- package/src/svgrid.filter-depth.test.ts +163 -0
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
- package/src/svgrid.filter-menu-scroll.test.ts +10 -2
- package/src/svgrid.grand-total.test.ts +188 -0
- package/src/svgrid.group-display-mode.test.ts +171 -0
- package/src/svgrid.group-footers.test.ts +121 -0
- package/src/svgrid.group-pagination.test.ts +153 -0
- package/src/svgrid.tree-data.test.ts +186 -0
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
- package/src/themes/index.ts +42 -0
- package/src/tree-edit.test.ts +4 -4
- package/src/tree-row-model.test.ts +169 -0
- package/src/ui-localization.test.ts +113 -0
- package/themes/ag-alpine.css +22 -0
- package/themes/antd.css +22 -0
- package/themes/atlassian.css +22 -0
- package/themes/bootstrap.css +22 -0
- package/themes/carbon.css +22 -0
- package/themes/catppuccin.css +22 -0
- package/themes/dracula.css +22 -0
- package/themes/ember.css +22 -0
- package/themes/excel.css +22 -0
- package/themes/fluent.css +22 -0
- package/themes/github.css +22 -0
- package/themes/linear.css +22 -0
- package/themes/material.css +22 -0
- package/themes/nord.css +22 -0
- package/themes/notion.css +22 -0
- package/themes/salesforce.css +22 -0
- package/themes/sap.css +22 -0
- package/themes/shadcn.css +22 -0
- package/themes/tailwind.css +22 -0
- package/themes/vercel.css +22 -0
package/src/SvGridSelect.svelte
CHANGED
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
loadOptions?: (query: string) => Promise<Row[]>
|
|
39
39
|
debounceMs?: number
|
|
40
40
|
loadingText?: string
|
|
41
|
+
/** Shown when the list is empty / a search matches nothing (localization). */
|
|
42
|
+
emptyText?: string
|
|
43
|
+
/** Placeholder in the panel's search box (localization). */
|
|
44
|
+
searchPlaceholder?: string
|
|
45
|
+
/** Accessible name for the panel's search box (localization). */
|
|
46
|
+
searchLabel?: string
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
let {
|
|
@@ -54,6 +60,9 @@
|
|
|
54
60
|
loadOptions,
|
|
55
61
|
debounceMs = 250,
|
|
56
62
|
loadingText = 'Loading…',
|
|
63
|
+
emptyText = 'No matches',
|
|
64
|
+
searchPlaceholder = 'Search…',
|
|
65
|
+
searchLabel = 'Search options',
|
|
57
66
|
disabled = false,
|
|
58
67
|
readonly = false,
|
|
59
68
|
label,
|
|
@@ -168,8 +177,8 @@
|
|
|
168
177
|
aria-expanded="true"
|
|
169
178
|
aria-controls={gs.panelId}
|
|
170
179
|
aria-activedescendant={gs.activeDescendant()}
|
|
171
|
-
aria-label=
|
|
172
|
-
placeholder=
|
|
180
|
+
aria-label={searchLabel}
|
|
181
|
+
placeholder={searchPlaceholder}
|
|
173
182
|
value={search}
|
|
174
183
|
oninput={(e) => { search = e.currentTarget.value; gs.active = 0; if (loadOptions) runLoad(search) }}
|
|
175
184
|
/>
|
|
@@ -205,7 +214,7 @@
|
|
|
205
214
|
{/each}
|
|
206
215
|
</div>
|
|
207
216
|
{:else}
|
|
208
|
-
<div class="sv-gs__empty" role="row">{loading ? loadingText :
|
|
217
|
+
<div class="sv-gs__empty" role="row">{loading ? loadingText : emptyText}</div>
|
|
209
218
|
{/each}
|
|
210
219
|
{#if loading && filtered.length}
|
|
211
220
|
<div class="sv-gs__empty" role="row" aria-live="polite">{loadingText}</div>
|
package/src/SvGroupCell.svelte
CHANGED
|
@@ -39,7 +39,12 @@
|
|
|
39
39
|
aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
|
|
40
40
|
onclick={() => onToggle(row)}
|
|
41
41
|
>
|
|
42
|
-
|
|
42
|
+
<!-- Same chevron the grid's own expanders and SvTree draw, so every
|
|
43
|
+
expander in the library looks identical. -->
|
|
44
|
+
<span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">
|
|
45
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
|
|
46
|
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
|
47
|
+
</span>
|
|
43
48
|
<span class="sv-group-key">{meta.key}</span>
|
|
44
49
|
{#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
|
|
45
50
|
</button>
|
|
@@ -76,7 +81,9 @@
|
|
|
76
81
|
cursor: pointer;
|
|
77
82
|
}
|
|
78
83
|
.sv-group-chev {
|
|
79
|
-
display: inline-
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
80
87
|
transition: transform 0.15s;
|
|
81
88
|
color: var(--sg-muted, #64748b);
|
|
82
89
|
}
|
package/src/SvMultiSelect.svelte
CHANGED
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
/** Debounce for remote search. Default 250ms. */
|
|
38
38
|
debounceMs?: number
|
|
39
39
|
loadingText?: string
|
|
40
|
+
/** Shown when the list is empty / a search matches nothing (localization). */
|
|
41
|
+
emptyText?: string
|
|
42
|
+
/** Placeholder in the panel's search box (localization). */
|
|
43
|
+
searchPlaceholder?: string
|
|
44
|
+
/** Accessible name for the panel's search box (localization). */
|
|
45
|
+
searchLabel?: string
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
let {
|
|
@@ -52,6 +58,9 @@
|
|
|
52
58
|
loadOptions,
|
|
53
59
|
debounceMs = 250,
|
|
54
60
|
loadingText = 'Loading…',
|
|
61
|
+
emptyText = 'No matches',
|
|
62
|
+
searchPlaceholder = 'Search…',
|
|
63
|
+
searchLabel = 'Search options',
|
|
55
64
|
disabled = false,
|
|
56
65
|
readonly = false,
|
|
57
66
|
label,
|
|
@@ -193,8 +202,8 @@
|
|
|
193
202
|
aria-expanded="true"
|
|
194
203
|
aria-controls={ms.panelId}
|
|
195
204
|
aria-activedescendant={ms.activeDescendant()}
|
|
196
|
-
aria-label=
|
|
197
|
-
placeholder=
|
|
205
|
+
aria-label={searchLabel}
|
|
206
|
+
placeholder={searchPlaceholder}
|
|
198
207
|
value={search}
|
|
199
208
|
oninput={(e) => { search = e.currentTarget.value; ms.active = 0; if (loadOptions) runLoad(search) }}
|
|
200
209
|
/>
|
|
@@ -224,7 +233,7 @@
|
|
|
224
233
|
<span class="sv-ms__label">{opt.label}</span>
|
|
225
234
|
</li>
|
|
226
235
|
{:else}
|
|
227
|
-
<li class="sv-ms__empty" aria-disabled="true">{loading ? loadingText :
|
|
236
|
+
<li class="sv-ms__empty" aria-disabled="true">{loading ? loadingText : emptyText}</li>
|
|
228
237
|
{/each}
|
|
229
238
|
{#if loading && filtered.length}
|
|
230
239
|
<li class="sv-ms__loading" aria-live="polite">{loadingText}</li>
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
.sv-pw__meter { display: flex; gap: 4px; }
|
|
143
143
|
.sv-pw__bar { flex: 1; height: 4px; border-radius: 2px; background: var(--sg-border, #e2e8f0); transition: background 0.15s; }
|
|
144
144
|
.sv-pw__bar.on.is-lvl1 { background: var(--sg-danger, #dc2626); }
|
|
145
|
-
.sv-pw__bar.on.is-lvl2 { background: var(--sg-warning, #
|
|
145
|
+
.sv-pw__bar.on.is-lvl2 { background: var(--sg-warning, #d97706); }
|
|
146
146
|
.sv-pw__bar.on.is-lvl3 { background: #eab308; }
|
|
147
147
|
.sv-pw__bar.on.is-lvl4 { background: var(--sg-success, #16a34a); }
|
|
148
148
|
.sv-pw__strength { font-size: 11px; font-weight: 600; color: var(--sg-muted, #64748b); }
|
package/src/SvProgress.svelte
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
display: flex; align-items: center; gap: 10px; width: 100%;
|
|
76
76
|
}
|
|
77
77
|
.sv-prog--success { --_c: var(--sg-success, #16a34a); }
|
|
78
|
-
.sv-prog--warning { --_c: var(--sg-warning, #
|
|
78
|
+
.sv-prog--warning { --_c: var(--sg-warning, #d97706); }
|
|
79
79
|
.sv-prog--danger { --_c: var(--sg-danger, #dc2626); }
|
|
80
80
|
.sv-prog__track {
|
|
81
81
|
position: relative; flex: 1; overflow: hidden;
|
package/src/SvRating.svelte
CHANGED
package/src/SvResult.svelte
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
}
|
|
60
60
|
.sv-result--success { --_c: var(--sg-success, #16a34a); }
|
|
61
61
|
.sv-result--error { --_c: var(--sg-danger, #dc2626); }
|
|
62
|
-
.sv-result--warning { --_c: var(--sg-warning, #
|
|
62
|
+
.sv-result--warning { --_c: var(--sg-warning, #d97706); }
|
|
63
63
|
.sv-result--notfound { --_c: var(--sg-muted, #64748b); }
|
|
64
64
|
.sv-result__icon {
|
|
65
65
|
display: grid; place-items: center; width: 60px; height: 60px; border-radius: 50%; margin-bottom: 4px;
|
package/src/SvRichText.svelte
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* <SvRichText bind:value={html} placeholder="Write something…" />
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
-
import { nextEditorId } from './editor-contract'
|
|
22
|
+
import { nextEditorId, resolveMessages } from './editor-contract'
|
|
23
23
|
|
|
24
24
|
const DEFAULT_TOOLS: RichTextTool[] = [
|
|
25
25
|
'bold', 'italic', 'underline', 'strike', '|',
|
|
@@ -29,16 +29,27 @@
|
|
|
29
29
|
'link', 'clear', '|', 'undo', 'redo',
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ul:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
/** Toolbar glyphs. Mode-independent and not language-dependent, so they stay
|
|
33
|
+
* fixed; the LABELS beside them are localizable via `messages`. */
|
|
34
|
+
const GLYPH: Record<Exclude<RichTextTool, '|'>, string> = {
|
|
35
|
+
bold: 'B', italic: 'I', underline: 'U', strike: 'S',
|
|
36
|
+
h1: 'H1', h2: 'H2', h3: 'H3', p: '¶',
|
|
37
|
+
ul: '•', ol: '1.', quote: '❝', code: '</>',
|
|
38
|
+
alignLeft: '≡', alignCenter: '≣', alignRight: '≡', link: '🔗',
|
|
39
|
+
clear: '⌫', undo: '↶', redo: '↷',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** User-facing strings (localizable via `messages`). Every one is a tooltip +
|
|
43
|
+
* accessible name on a toolbar button, so leaving them hard-coded made the
|
|
44
|
+
* whole toolbar English-only for screen-reader users. */
|
|
45
|
+
type RichTextMessages = Record<Exclude<RichTextTool, '|'>, string> & { linkPrompt: string; toolbar: string }
|
|
46
|
+
const DEFAULT_MESSAGES: RichTextMessages = {
|
|
47
|
+
bold: 'Bold', italic: 'Italic', underline: 'Underline', strike: 'Strikethrough',
|
|
48
|
+
h1: 'Heading 1', h2: 'Heading 2', h3: 'Heading 3', p: 'Paragraph',
|
|
49
|
+
ul: 'Bullet list', ol: 'Numbered list', quote: 'Quote', code: 'Code block',
|
|
50
|
+
alignLeft: 'Align left', alignCenter: 'Align center', alignRight: 'Align right',
|
|
51
|
+
link: 'Link', clear: 'Clear formatting', undo: 'Undo', redo: 'Redo',
|
|
52
|
+
linkPrompt: 'Link URL', toolbar: 'Formatting',
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
type Props = {
|
|
@@ -52,6 +63,8 @@
|
|
|
52
63
|
/** Toolbar tools + order; use '|' for a separator. */
|
|
53
64
|
tools?: RichTextTool[]
|
|
54
65
|
ariaLabel?: string
|
|
66
|
+
/** Override any toolbar label / prompt (localization). */
|
|
67
|
+
messages?: Partial<RichTextMessages>
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
let {
|
|
@@ -63,8 +76,10 @@
|
|
|
63
76
|
minHeight = '160px',
|
|
64
77
|
tools = DEFAULT_TOOLS,
|
|
65
78
|
ariaLabel = 'Rich text editor',
|
|
79
|
+
messages,
|
|
66
80
|
}: Props = $props()
|
|
67
81
|
|
|
82
|
+
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
68
83
|
const uid = nextEditorId('sv-rt')
|
|
69
84
|
let editorEl = $state<HTMLDivElement | null>(null)
|
|
70
85
|
let focused = $state(false)
|
|
@@ -109,14 +124,14 @@
|
|
|
109
124
|
ul: () => exec('insertUnorderedList'), ol: () => exec('insertOrderedList'),
|
|
110
125
|
quote: () => exec('formatBlock', 'BLOCKQUOTE'), code: () => exec('formatBlock', 'PRE'),
|
|
111
126
|
alignLeft: () => exec('justifyLeft'), alignCenter: () => exec('justifyCenter'), alignRight: () => exec('justifyRight'),
|
|
112
|
-
link: () => { const url = typeof prompt === 'function' ? prompt(
|
|
127
|
+
link: () => { const url = typeof prompt === 'function' ? prompt(M.linkPrompt, 'https://') : null; if (url) exec('createLink', url) },
|
|
113
128
|
clear: () => exec('removeFormat'), undo: () => exec('undo'), redo: () => exec('redo'),
|
|
114
129
|
}
|
|
115
130
|
</script>
|
|
116
131
|
|
|
117
132
|
<div class="sv-rt" class:is-disabled={disabled} class:is-readonly={readonly}>
|
|
118
133
|
{#if !readonly}
|
|
119
|
-
<div class="sv-rt__toolbar" role="toolbar" aria-label=
|
|
134
|
+
<div class="sv-rt__toolbar" role="toolbar" aria-label={M.toolbar} aria-controls={uid}>
|
|
120
135
|
{#each tools as t, i (i)}
|
|
121
136
|
{#if t === '|'}
|
|
122
137
|
<span class="sv-rt__sep" aria-hidden="true"></span>
|
|
@@ -126,13 +141,13 @@
|
|
|
126
141
|
class="sv-rt__btn"
|
|
127
142
|
class:is-on={activeState[t]}
|
|
128
143
|
data-tool={t}
|
|
129
|
-
title={
|
|
130
|
-
aria-label={
|
|
144
|
+
title={M[t]}
|
|
145
|
+
aria-label={M[t]}
|
|
131
146
|
aria-pressed={activeState[t] ? true : undefined}
|
|
132
147
|
{disabled}
|
|
133
148
|
onmousedown={(e) => e.preventDefault()}
|
|
134
149
|
onclick={run[t]}
|
|
135
|
-
>{
|
|
150
|
+
>{GLYPH[t]}</button>
|
|
136
151
|
{/if}
|
|
137
152
|
{/each}
|
|
138
153
|
</div>
|
|
@@ -53,8 +53,9 @@
|
|
|
53
53
|
</script>
|
|
54
54
|
|
|
55
55
|
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
56
|
+
<!-- role="switch" arrives via switchProps(), which the a11y pass cannot see through -->
|
|
57
|
+
<!-- svelte-ignore a11y_role_supports_aria_props_implicit -->
|
|
56
58
|
<button
|
|
57
|
-
role="switch"
|
|
58
59
|
class="sv-switch sv-switch--{size}"
|
|
59
60
|
class:is-on={checked}
|
|
60
61
|
class:is-readonly={readonly}
|
package/src/SvToaster.svelte
CHANGED
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
|
|
136
136
|
:global(.sv-toast--success) { --_accent: var(--sg-success, #16a34a); }
|
|
137
137
|
:global(.sv-toast--error) { --_accent: var(--sg-danger, #dc2626); }
|
|
138
|
-
:global(.sv-toast--warning) { --_accent: var(--sg-warning, #
|
|
138
|
+
:global(.sv-toast--warning) { --_accent: var(--sg-warning, #d97706); }
|
|
139
139
|
:global(.sv-toast--info) { --_accent: var(--sg-accent, #2563eb); }
|
|
140
140
|
|
|
141
141
|
:global(.sv-toast__icon) { flex: none; width: 18px; height: 18px; display: grid; place-items: center; color: var(--_accent); font-weight: 700; }
|
package/src/SvTree.svelte
CHANGED
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
/** Render a built-in search box that drives the filter. */
|
|
49
49
|
searchable?: boolean
|
|
50
50
|
searchPlaceholder?: string
|
|
51
|
+
/** Placeholder row shown under a lazy node while its children load
|
|
52
|
+
* (localization). */
|
|
53
|
+
loadingText?: string
|
|
54
|
+
/** Accessible name for the search box's clear button (localization). */
|
|
55
|
+
clearSearchLabel?: string
|
|
51
56
|
/** Sort siblings by label ('asc'/'desc') or a custom comparator. */
|
|
52
57
|
sort?: 'asc' | 'desc' | ((a: TreeNode, b: TreeNode) => number)
|
|
53
58
|
/** Allow inline rename (double-click or F2). Fires onRename. */
|
|
@@ -77,6 +82,8 @@
|
|
|
77
82
|
filter,
|
|
78
83
|
searchable = false,
|
|
79
84
|
searchPlaceholder = 'Search...',
|
|
85
|
+
loadingText = 'Loading...',
|
|
86
|
+
clearSearchLabel = 'Clear search',
|
|
80
87
|
sort,
|
|
81
88
|
editable = false,
|
|
82
89
|
onRename,
|
|
@@ -173,7 +180,7 @@
|
|
|
173
180
|
if (!node?.lazy || loaded.has(id) || loadingIds.has(id)) return
|
|
174
181
|
loadingIds = new Set(loadingIds).add(id)
|
|
175
182
|
// A placeholder row shows immediately while the promise resolves.
|
|
176
|
-
loaded = new Map(loaded).set(id, [{ id: `${id}__loading`, label:
|
|
183
|
+
loaded = new Map(loaded).set(id, [{ id: `${id}__loading`, label: loadingText, disabled: true }])
|
|
177
184
|
try {
|
|
178
185
|
const kids = await loadChildren(node)
|
|
179
186
|
loaded = new Map(loaded).set(id, kids)
|
|
@@ -268,7 +275,7 @@
|
|
|
268
275
|
<div class="sv-tree__search">
|
|
269
276
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></svg>
|
|
270
277
|
<input class="sv-tree__search-input" type="text" placeholder={searchPlaceholder} bind:value={query} aria-label={searchPlaceholder} />
|
|
271
|
-
{#if query}<button type="button" class="sv-tree__search-clear" aria-label=
|
|
278
|
+
{#if query}<button type="button" class="sv-tree__search-clear" aria-label={clearSearchLabel} onclick={() => (query = '')}>×</button>{/if}
|
|
272
279
|
</div>
|
|
273
280
|
{/if}
|
|
274
281
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
package/src/SvTreeSelect.svelte
CHANGED
|
@@ -186,7 +186,11 @@
|
|
|
186
186
|
{...ts.itemProps(i)}
|
|
187
187
|
>
|
|
188
188
|
{#if row.hasChildren}
|
|
189
|
-
|
|
189
|
+
<!-- Same chevron as SvTree and the grid's expanders. -->
|
|
190
|
+
<span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">
|
|
191
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
|
|
192
|
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
|
193
|
+
</span>
|
|
190
194
|
{:else}
|
|
191
195
|
<span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
|
|
192
196
|
{/if}
|
|
@@ -226,7 +230,7 @@
|
|
|
226
230
|
:global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
227
231
|
:global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
228
232
|
:global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
|
|
229
|
-
:global(.sv-ts__chev) { width: 14px; flex: none; color: var(--sg-muted, #64748b); transition: transform 0.12s;
|
|
233
|
+
:global(.sv-ts__chev) { width: 14px; flex: none; display: inline-flex; align-items: center; justify-content: center; color: var(--sg-muted, #64748b); transition: transform 0.12s; }
|
|
230
234
|
:global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
|
|
231
235
|
:global(.sv-ts__chev--leaf) { visibility: hidden; }
|
|
232
236
|
:global(.sv-ts__label) { flex: 1; white-space: nowrap; }
|
package/src/build-api.ts
CHANGED
|
@@ -455,6 +455,21 @@ export function createGridApi<
|
|
|
455
455
|
clearFilter(columnId) {
|
|
456
456
|
ctx.clearColumnFilter(columnId);
|
|
457
457
|
},
|
|
458
|
+
refreshEditorOptions(columnId) {
|
|
459
|
+
// Async `editorOptions` are cached per column (or per column+row for a
|
|
460
|
+
// per-row source) so opening an editor twice does not refetch. This
|
|
461
|
+
// drops those entries, so the next render re-runs the source - use it
|
|
462
|
+
// when the underlying list changed server-side.
|
|
463
|
+
if (columnId == null) {
|
|
464
|
+
ctx.asyncEditorOptions = {};
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const next = { ...ctx.asyncEditorOptions };
|
|
468
|
+
for (const key of Object.keys(next)) {
|
|
469
|
+
if (key === columnId || key.startsWith(`${columnId}::`)) delete next[key];
|
|
470
|
+
}
|
|
471
|
+
ctx.asyncEditorOptions = next;
|
|
472
|
+
},
|
|
458
473
|
clearAllFilters() {
|
|
459
474
|
// See `setFilter` for why the read-modify-write is untracked.
|
|
460
475
|
untrack(() => {
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type CellEditorContext,
|
|
10
10
|
} from './editor-registry'
|
|
11
11
|
|
|
12
|
+
// onCommitAndMove / onRequestClose / inCell are the hooks the grid injects at
|
|
13
|
+
// mount; the registry tests here never exercise them.
|
|
12
14
|
function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
|
|
13
15
|
return {
|
|
14
16
|
value: null,
|
|
@@ -18,7 +20,7 @@ function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
|
|
|
18
20
|
onCommit: () => {},
|
|
19
21
|
onCancel: () => {},
|
|
20
22
|
...overrides,
|
|
21
|
-
}
|
|
23
|
+
} as CellEditorContext
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
afterEach(() => {
|
package/src/cell-render.test.ts
CHANGED
|
@@ -9,16 +9,35 @@ import { createCellRender } from './cell-render'
|
|
|
9
9
|
// shape that mirrors the @svgrid/grid runtime closely enough to exercise
|
|
10
10
|
// every branch.
|
|
11
11
|
|
|
12
|
+
// Only `id` / `original` / `getCellValueByColumnId` are read by the functions
|
|
13
|
+
// under test, but the stub carries the full `Row` shape so it is assignable
|
|
14
|
+
// without a cast at every call site.
|
|
12
15
|
type AnyRow = {
|
|
13
16
|
id: string
|
|
17
|
+
index: number
|
|
14
18
|
original: Record<string, unknown>
|
|
19
|
+
depth: number
|
|
20
|
+
getCanExpand: () => boolean
|
|
21
|
+
getIsExpanded: () => boolean
|
|
22
|
+
toggleExpanded: () => void
|
|
23
|
+
getIsSelected: () => boolean
|
|
24
|
+
toggleSelected: () => void
|
|
25
|
+
getAllCells: () => never[]
|
|
15
26
|
getCellValueByColumnId: (colId: string) => unknown
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
function makeRow(id: string, data: Record<string, unknown>): AnyRow {
|
|
19
30
|
return {
|
|
20
31
|
id,
|
|
32
|
+
index: 0,
|
|
21
33
|
original: data,
|
|
34
|
+
depth: 0,
|
|
35
|
+
getCanExpand: () => false,
|
|
36
|
+
getIsExpanded: () => false,
|
|
37
|
+
toggleExpanded: () => {},
|
|
38
|
+
getIsSelected: () => false,
|
|
39
|
+
toggleSelected: () => {},
|
|
40
|
+
getAllCells: () => [],
|
|
22
41
|
getCellValueByColumnId: (colId: string) => data[colId],
|
|
23
42
|
}
|
|
24
43
|
}
|
|
@@ -34,6 +53,11 @@ function makeCtx(overrides: Partial<any> = {}): any {
|
|
|
34
53
|
conditionalColumnStats: new Map(),
|
|
35
54
|
noteOverrides: {},
|
|
36
55
|
editorOptionsCache: {},
|
|
56
|
+
// Async `editorOptions` bookkeeping: resolved lists (reactive in the real
|
|
57
|
+
// controller) plus two plain Sets used as dedupe guards.
|
|
58
|
+
asyncEditorOptions: {} as Record<string, unknown>,
|
|
59
|
+
asyncEditorOptionsPending: new Set<string>(),
|
|
60
|
+
asyncEditorColumns: new Set<string>(),
|
|
37
61
|
...overrides,
|
|
38
62
|
}
|
|
39
63
|
}
|
|
@@ -172,7 +196,7 @@ describe('createCellRender / computeCellClass', () => {
|
|
|
172
196
|
const row = makeRow('r', { score: 9 })
|
|
173
197
|
expect(cr.computeCellClass(row, makeColumn('score', { cellClass }))).toBe('big')
|
|
174
198
|
expect(cellClass).toHaveBeenCalledTimes(1)
|
|
175
|
-
const passed = cellClass.mock.calls[0][0]
|
|
199
|
+
const passed = cellClass.mock.calls[0]![0]
|
|
176
200
|
expect(passed.row).toBe(row)
|
|
177
201
|
expect(passed.getValue()).toBe(9)
|
|
178
202
|
})
|
|
@@ -423,7 +447,7 @@ describe('createCellRender / formatPinnedValue', () => {
|
|
|
423
447
|
const formatter = vi.fn(({ value, row }: any) => `${value}:${row}`)
|
|
424
448
|
const col = makeColumn('a', { formatter })
|
|
425
449
|
expect(cr.formatPinnedValue(col, 5)).toBe('5:null')
|
|
426
|
-
expect(formatter.mock.calls[0][0].row).toBeNull()
|
|
450
|
+
expect(formatter.mock.calls[0]![0].row).toBeNull()
|
|
427
451
|
})
|
|
428
452
|
|
|
429
453
|
it('masks password pinned values', () => {
|
|
@@ -478,7 +502,7 @@ describe('createCellRender / computePinnedCellClass', () => {
|
|
|
478
502
|
field: 'a',
|
|
479
503
|
})
|
|
480
504
|
expect(cr.computePinnedCellClass({ a: 3 }, col)).toBe('pos')
|
|
481
|
-
expect(cellClass.mock.calls[0][0].row).toBeNull()
|
|
505
|
+
expect(cellClass.mock.calls[0]![0].row).toBeNull()
|
|
482
506
|
})
|
|
483
507
|
|
|
484
508
|
it('coerces a falsy cellClass result to empty string', () => {
|
package/src/cell-render.ts
CHANGED
|
@@ -263,6 +263,77 @@ export function createCellRender<
|
|
|
263
263
|
return v && v.trim() ? v : null
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
const isThenable = (v: unknown): v is Promise<unknown> =>
|
|
267
|
+
typeof (v as { then?: unknown } | null)?.then === "function";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve a promised option list once per key, then keep it.
|
|
271
|
+
*
|
|
272
|
+
* Render paths call `getColumnEditorOptions` synchronously and often, so an
|
|
273
|
+
* async source cannot block: the first call starts the fetch and returns an
|
|
274
|
+
* empty list, and the resolved value lands in reactive state which re-renders
|
|
275
|
+
* the editor. `asyncEditorOptionsPending` tracks in-flight keys so repeated
|
|
276
|
+
* renders do not fire the request again.
|
|
277
|
+
*/
|
|
278
|
+
function resolveAsyncOptions(
|
|
279
|
+
key: string,
|
|
280
|
+
promise: Promise<unknown>,
|
|
281
|
+
/** Drop other cached entries starting with this prefix when the result
|
|
282
|
+
* lands - used to evict a cascade's superseded signatures. */
|
|
283
|
+
prunePrefix?: string,
|
|
284
|
+
): CellEditorOption[] {
|
|
285
|
+
const done = ctx.asyncEditorOptions[key];
|
|
286
|
+
if (done) return done;
|
|
287
|
+
const pending: Set<string> = ctx.asyncEditorOptionsPending;
|
|
288
|
+
if (!pending.has(key)) {
|
|
289
|
+
// Plain Set, not reactive state: this runs inside the template, and a
|
|
290
|
+
// $state write during render is a Svelte 5 error.
|
|
291
|
+
pending.add(key);
|
|
292
|
+
const store = (value: CellEditorOption[]) => {
|
|
293
|
+
const next: Record<string, CellEditorOption[]> = {};
|
|
294
|
+
for (const [k, v] of Object.entries(ctx.asyncEditorOptions as Record<string, CellEditorOption[]>)) {
|
|
295
|
+
// Keep everything except this cell's stale signatures.
|
|
296
|
+
if (prunePrefix && k !== key && k.startsWith(prunePrefix)) continue;
|
|
297
|
+
next[k] = v;
|
|
298
|
+
}
|
|
299
|
+
next[key] = value;
|
|
300
|
+
ctx.asyncEditorOptions = next;
|
|
301
|
+
};
|
|
302
|
+
promise
|
|
303
|
+
.then((value) => store(normalizeEditorOptions(value as never)))
|
|
304
|
+
// A failed lookup settles on "no options" rather than leaving the
|
|
305
|
+
// editor spinning forever.
|
|
306
|
+
.catch(() => store([]))
|
|
307
|
+
.finally(() => {
|
|
308
|
+
pending.delete(key);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Cache key for a per-row async source.
|
|
316
|
+
*
|
|
317
|
+
* A cascade depends on the row's OTHER cells - City is a function of Country -
|
|
318
|
+
* so keying by row id alone made the first result permanent: changing the
|
|
319
|
+
* country left the old city list cached forever. The row's own values are
|
|
320
|
+
* folded into the key, so editing the row naturally supersedes the entry.
|
|
321
|
+
*
|
|
322
|
+
* Only reached for columns already known to be async, so the stringify cost
|
|
323
|
+
* is bounded by the rows actually rendering such a column.
|
|
324
|
+
*/
|
|
325
|
+
function asyncRowKey(column: Column<TData>, row: Row<TData>): string {
|
|
326
|
+
let signature: string;
|
|
327
|
+
try {
|
|
328
|
+
signature = JSON.stringify(row.original);
|
|
329
|
+
} catch {
|
|
330
|
+
// Circular / non-serializable row: fall back to identity, which at least
|
|
331
|
+
// invalidates when the consumer replaces the object immutably.
|
|
332
|
+
signature = String(row.index);
|
|
333
|
+
}
|
|
334
|
+
return `${column.id}::${row.id}::${signature}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
266
337
|
function getColumnEditorOptions(
|
|
267
338
|
column: Column<TData>,
|
|
268
339
|
row?: Row<TData> | null,
|
|
@@ -272,8 +343,36 @@ export function createCellRender<
|
|
|
272
343
|
// Dynamic per-row: must be re-evaluated because the row's other
|
|
273
344
|
// cells may have just changed (cascade).
|
|
274
345
|
if (!row?.original) return [];
|
|
275
|
-
|
|
346
|
+
// Once a column is known to hand back promises, answer from the cache
|
|
347
|
+
// WITHOUT calling the source again. Render reads options constantly, so
|
|
348
|
+
// calling an async source every time fires a real request per render -
|
|
349
|
+
// and any state the source touches is being mutated mid-render, which
|
|
350
|
+
// Svelte rejects outright. The key folds in the row's values, so editing
|
|
351
|
+
// the row supersedes the entry and the cascade refetches.
|
|
352
|
+
const known = (ctx.asyncEditorColumns as Set<string>).has(column.id);
|
|
353
|
+
if (known) {
|
|
354
|
+
const key = asyncRowKey(column, row);
|
|
355
|
+
const done = ctx.asyncEditorOptions[key];
|
|
356
|
+
if (done) return done;
|
|
357
|
+
if ((ctx.asyncEditorOptionsPending as Set<string>).has(key)) return [];
|
|
358
|
+
const produced = def(row.original as TData);
|
|
359
|
+
if (isThenable(produced)) {
|
|
360
|
+
return resolveAsyncOptions(key, produced, `${column.id}::${row.id}::`);
|
|
361
|
+
}
|
|
362
|
+
return normalizeEditorOptions(produced);
|
|
363
|
+
}
|
|
364
|
+
const produced = def(row.original as TData);
|
|
365
|
+
if (isThenable(produced)) {
|
|
366
|
+
(ctx.asyncEditorColumns as Set<string>).add(column.id);
|
|
367
|
+
return resolveAsyncOptions(
|
|
368
|
+
asyncRowKey(column, row),
|
|
369
|
+
produced,
|
|
370
|
+
`${column.id}::${row.id}::`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return normalizeEditorOptions(produced);
|
|
276
374
|
}
|
|
375
|
+
if (isThenable(def)) return resolveAsyncOptions(column.id, def);
|
|
277
376
|
const id = column.id;
|
|
278
377
|
if (
|
|
279
378
|
!ctx.editorOptionsCache[id] ||
|
|
@@ -285,6 +384,27 @@ export function createCellRender<
|
|
|
285
384
|
return ctx.editorOptionsCache[id];
|
|
286
385
|
}
|
|
287
386
|
|
|
387
|
+
/**
|
|
388
|
+
* True while a column's (or cell's) async option list is still loading.
|
|
389
|
+
* Derived from "the source is async and no result has landed" rather than
|
|
390
|
+
* from the pending Set, so it stays reactive - `asyncEditorOptions` is the
|
|
391
|
+
* reactive half, the Set is only a dedupe guard.
|
|
392
|
+
*/
|
|
393
|
+
function areEditorOptionsLoading(
|
|
394
|
+
column: Column<TData>,
|
|
395
|
+
row?: Row<TData> | null,
|
|
396
|
+
): boolean {
|
|
397
|
+
const def = column.columnDef.editorOptions;
|
|
398
|
+
if (typeof def === "function") {
|
|
399
|
+
if (!row?.original) return false;
|
|
400
|
+
// Never call the source here - that would fire another request just to
|
|
401
|
+
// answer "are we loading?". The resolver records which columns are async.
|
|
402
|
+
if (!(ctx.asyncEditorColumns as Set<string>).has(column.id)) return false;
|
|
403
|
+
return !ctx.asyncEditorOptions[asyncRowKey(column, row)];
|
|
404
|
+
}
|
|
405
|
+
return isThenable(def) && !ctx.asyncEditorOptions[column.id];
|
|
406
|
+
}
|
|
407
|
+
|
|
288
408
|
/** Joined display string for list/chips cells. */
|
|
289
409
|
function formatListCellValue(
|
|
290
410
|
column: Column<TData>,
|
|
@@ -461,6 +581,7 @@ export function createCellRender<
|
|
|
461
581
|
computeCellValidity,
|
|
462
582
|
computeCellNote,
|
|
463
583
|
getColumnEditorOptions,
|
|
584
|
+
areEditorOptionsLoading,
|
|
464
585
|
formatListCellValue,
|
|
465
586
|
formatCellValue,
|
|
466
587
|
formatPinnedValue,
|
package/src/cell-values.ts
CHANGED
|
@@ -19,6 +19,10 @@ export function getColumnBaseValue<TData extends RowData>(row: Row<TData>, colum
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function isGroupRow<TData extends RowData>(row: Row<TData>) {
|
|
22
|
+
// Tree rows are expandable too, but they are real data rows - rendering one as
|
|
23
|
+
// a full-width banner would swallow its cells. Only grouping's synthetic rows
|
|
24
|
+
// are banners.
|
|
25
|
+
if ((row as { __treeRow?: boolean }).__treeRow) return false;
|
|
22
26
|
return typeof row.getCanExpand === "function" && row.getCanExpand();
|
|
23
27
|
}
|
|
24
28
|
|
package/src/chart-export.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
chartToSvgString,
|
|
4
4
|
chartToPngBlob,
|
|
@@ -125,7 +125,7 @@ describe('chartToSvgString', () => {
|
|
|
125
125
|
describe('downloadChartSvg', () => {
|
|
126
126
|
let createSpy: ReturnType<typeof vi.spyOn>
|
|
127
127
|
let revokeSpy: ReturnType<typeof vi.spyOn>
|
|
128
|
-
let clickSpy:
|
|
128
|
+
let clickSpy: Mock<() => void>
|
|
129
129
|
|
|
130
130
|
beforeEach(() => {
|
|
131
131
|
vi.useFakeTimers()
|
package/src/clipboard.test.ts
CHANGED
|
@@ -614,7 +614,6 @@ describe('copySelectionToClipboard', () => {
|
|
|
614
614
|
configurable: true,
|
|
615
615
|
})
|
|
616
616
|
const execCommand = vi.fn().mockReturnValue(true)
|
|
617
|
-
// @ts-expect-error - jsdom lacks execCommand by default
|
|
618
617
|
document.execCommand = execCommand
|
|
619
618
|
const ctx = makeCtx()
|
|
620
619
|
ctx.selectionRange = {
|
|
@@ -631,7 +630,6 @@ describe('copySelectionToClipboard', () => {
|
|
|
631
630
|
value: undefined,
|
|
632
631
|
configurable: true,
|
|
633
632
|
})
|
|
634
|
-
// @ts-expect-error - override execCommand to throw
|
|
635
633
|
document.execCommand = () => {
|
|
636
634
|
throw new Error('blocked')
|
|
637
635
|
}
|