@svgrid/grid 2.2.25 → 2.2.27
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 +199 -145
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -728
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +250 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2723 -2719
- package/dist/SvGrid.svelte +3333 -3333
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +287 -287
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +131 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/a11y/dismissable.d.ts +11 -0
- package/dist/a11y/dismissable.js +21 -0
- package/dist/ai.js +3 -3
- package/dist/cdn/GridMenus-BzxEYNtW.js +421 -0
- package/dist/cdn/GridMenus-Dr-nGEXn.js +417 -0
- package/dist/cdn/{SvDateTimePicker-CYC7gRER.js → SvDateTimePicker-B1sDc9Pz.js} +451 -444
- package/dist/cdn/{SvDateTimePicker-DtQKd4Ns.js → SvDateTimePicker-Bh0FTs1E.js} +114 -107
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-BKQsO5fZ.js} +1 -1
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bgk4hDs8.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-DSkHIQDd.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-DkCc2Z7b.js} +1 -1
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-BrbVaELm.js} +1 -1
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-i9rWvSKg.js} +1 -1
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CGxxopQ4.js} +1 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D08sVmAy.js} +1 -1
- package/dist/cdn/{src-DysW9qbk.js → src-Cw8d9U8j.js} +5753 -5753
- package/dist/cdn/{src-bYfr_7aR.js → src-cgs1wcUS.js} +8102 -8102
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +30 -13
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -728
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +250 -250
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3239 -3239
- package/src/SvGrid.css +2723 -2719
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +287 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +131 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -95
- package/src/a11y/dismissable.ts +114 -94
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -712
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-scroll.test.ts +102 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-GfCAHgkZ.js +0 -420
- package/dist/cdn/GridMenus-GiVNSeDU.js +0 -416
package/dist/SvTreeSelect.svelte
CHANGED
|
@@ -1,233 +1,233 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type { TreeSelectNode } from './ui-app.types'
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<script lang="ts">
|
|
6
|
-
/**
|
|
7
|
-
* SvTreeSelect - a single-select dropdown that shows an indented, collapsible
|
|
8
|
-
* tree in its panel (pick a node from a hierarchy). The open / position /
|
|
9
|
-
* dismiss / roving / keyboard / ARIA mechanics come from the shared
|
|
10
|
-
* `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
|
|
11
|
-
* expand/collapse); this component adds the tree flattening + rendering. Covers
|
|
12
|
-
* the "cascader / tree select" pattern without depending on SvTree.
|
|
13
|
-
*/
|
|
14
|
-
import type { TreeSelectNode } from './ui-app.types'
|
|
15
|
-
import SvField from './SvField.svelte'
|
|
16
|
-
import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
|
|
17
|
-
import { portalToBody, popIn } from './popover'
|
|
18
|
-
import { createPopoverSelect } from './createPopoverSelect.svelte'
|
|
19
|
-
import { flattenVisible, findNodePath } from './tree-select'
|
|
20
|
-
|
|
21
|
-
type Props = Pick<
|
|
22
|
-
SvEditorProps,
|
|
23
|
-
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
|
-
> & {
|
|
25
|
-
nodes: ReadonlyArray<TreeSelectNode>
|
|
26
|
-
value?: string | number | null
|
|
27
|
-
onChange?: (value: string | number) => void
|
|
28
|
-
onCommit?: (value: string | number) => void
|
|
29
|
-
onCancel?: () => void
|
|
30
|
-
placeholder?: string
|
|
31
|
-
showPath?: boolean
|
|
32
|
-
expandedIds?: ReadonlyArray<string | number>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let {
|
|
36
|
-
nodes,
|
|
37
|
-
value = $bindable<string | number | null>(null),
|
|
38
|
-
onChange,
|
|
39
|
-
onCommit,
|
|
40
|
-
onCancel,
|
|
41
|
-
placeholder = 'Select…',
|
|
42
|
-
showPath = false,
|
|
43
|
-
expandedIds = [],
|
|
44
|
-
disabled = false,
|
|
45
|
-
readonly = false,
|
|
46
|
-
label,
|
|
47
|
-
hint,
|
|
48
|
-
error,
|
|
49
|
-
required = false,
|
|
50
|
-
invalid = false,
|
|
51
|
-
size = 'md',
|
|
52
|
-
id,
|
|
53
|
-
name,
|
|
54
|
-
dir,
|
|
55
|
-
ariaLabel,
|
|
56
|
-
loading = false,
|
|
57
|
-
}: Props = $props()
|
|
58
|
-
|
|
59
|
-
const autoId = nextEditorId('sv-ts')
|
|
60
|
-
const uid = $derived(id ?? autoId)
|
|
61
|
-
|
|
62
|
-
let expanded = $state(new Set<string | number>(expandedIds))
|
|
63
|
-
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
64
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
65
|
-
let treeEl = $state<HTMLUListElement | null>(null)
|
|
66
|
-
|
|
67
|
-
const rows = $derived(flattenVisible(nodes, expanded))
|
|
68
|
-
const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
|
|
69
|
-
const triggerText = $derived(
|
|
70
|
-
!selectedPath
|
|
71
|
-
? ''
|
|
72
|
-
: showPath
|
|
73
|
-
? selectedPath.map((n) => n.label).join(' / ')
|
|
74
|
-
: selectedPath[selectedPath.length - 1]!.label,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
function setExpanded(v: string | number, on: boolean) {
|
|
78
|
-
const next = new Set(expanded)
|
|
79
|
-
on ? next.add(v) : next.delete(v)
|
|
80
|
-
expanded = next
|
|
81
|
-
}
|
|
82
|
-
function toggleExpand(v: string | number) {
|
|
83
|
-
setExpanded(v, !expanded.has(v))
|
|
84
|
-
}
|
|
85
|
-
function pick(node: TreeSelectNode) {
|
|
86
|
-
if (node.disabled) return
|
|
87
|
-
value = node.value // enables bind:value
|
|
88
|
-
onChange?.(node.value)
|
|
89
|
-
ts.closePanel()
|
|
90
|
-
}
|
|
91
|
-
function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
|
|
92
|
-
if (row.hasChildren) toggleExpand(row.node.value)
|
|
93
|
-
else pick(row.node)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const ts = createPopoverSelect({
|
|
97
|
-
itemCount: () => rows.length,
|
|
98
|
-
disabled: (i) => !!rows[i]?.node.disabled,
|
|
99
|
-
readonly: () => readonly,
|
|
100
|
-
onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
|
|
101
|
-
getTrigger: () => triggerEl,
|
|
102
|
-
getPanel: () => panelEl,
|
|
103
|
-
getInitialFocus: () => treeEl,
|
|
104
|
-
closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
|
|
105
|
-
initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
|
|
106
|
-
minWidth: () => 220,
|
|
107
|
-
estimatedHeight: () => 300,
|
|
108
|
-
onExtraKey: (e, act) => {
|
|
109
|
-
const row = rows[act]
|
|
110
|
-
if (!row) return false
|
|
111
|
-
// "expand" opens the node or steps into the first child; "collapse" closes
|
|
112
|
-
// it or jumps to the parent - same fallback as createTree's core.
|
|
113
|
-
const expand = () => {
|
|
114
|
-
if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
|
|
115
|
-
else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
|
|
116
|
-
}
|
|
117
|
-
const collapse = () => {
|
|
118
|
-
if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
|
|
119
|
-
else {
|
|
120
|
-
let pi = act - 1
|
|
121
|
-
while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
|
|
122
|
-
if (pi >= 0) ts.active = pi
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
|
|
126
|
-
if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
|
|
127
|
-
return false
|
|
128
|
-
},
|
|
129
|
-
idPrefix: 'sv-ts',
|
|
130
|
-
})
|
|
131
|
-
</script>
|
|
132
|
-
|
|
133
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
134
|
-
<div class="sv-ts sv-ts--{size}" class:is-disabled={disabled}>
|
|
135
|
-
<button
|
|
136
|
-
bind:this={triggerEl}
|
|
137
|
-
type="button"
|
|
138
|
-
class="sv-ts__trigger"
|
|
139
|
-
class:is-invalid={invalid}
|
|
140
|
-
{disabled}
|
|
141
|
-
onclick={() => ts.toggle()}
|
|
142
|
-
{...ts.triggerProps('tree')}
|
|
143
|
-
{...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
|
|
144
|
-
>
|
|
145
|
-
<span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
|
|
146
|
-
<span class="sv-ts__arrow" aria-hidden="true">▾</span>
|
|
147
|
-
</button>
|
|
148
|
-
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
149
|
-
</div>
|
|
150
|
-
</SvField>
|
|
151
|
-
|
|
152
|
-
{#if ts.open}
|
|
153
|
-
<div
|
|
154
|
-
bind:this={panelEl}
|
|
155
|
-
class="sv-ts__panel"
|
|
156
|
-
dir={dir}
|
|
157
|
-
use:portalToBody
|
|
158
|
-
use:popIn={{ up: ts.rect.openUpward }}
|
|
159
|
-
style:position="fixed"
|
|
160
|
-
style:top={`${ts.rect.top}px`}
|
|
161
|
-
style:left={`${ts.rect.left}px`}
|
|
162
|
-
style:min-width={`${ts.rect.width}px`}
|
|
163
|
-
>
|
|
164
|
-
<ul
|
|
165
|
-
bind:this={treeEl}
|
|
166
|
-
id={ts.panelId}
|
|
167
|
-
class="sv-ts__tree"
|
|
168
|
-
role="tree"
|
|
169
|
-
aria-label={label ?? ariaLabel ?? 'Tree'}
|
|
170
|
-
aria-activedescendant={ts.activeDescendant()}
|
|
171
|
-
tabindex="0"
|
|
172
|
-
style:max-height={`${ts.rect.maxHeight}px`}
|
|
173
|
-
onkeydown={ts.onPanelKeydown}
|
|
174
|
-
>
|
|
175
|
-
{#each rows as row, i (row.node.value)}
|
|
176
|
-
<li
|
|
177
|
-
class="sv-ts__row"
|
|
178
|
-
class:is-active={ts.isActive(i)}
|
|
179
|
-
class:is-selected={row.node.value === value}
|
|
180
|
-
class:is-disabled={row.node.disabled}
|
|
181
|
-
role="treeitem"
|
|
182
|
-
aria-selected={row.node.value === value}
|
|
183
|
-
aria-expanded={row.hasChildren ? row.expanded : undefined}
|
|
184
|
-
style:padding-inline-start={`${8 + row.depth * 16}px`}
|
|
185
|
-
onclick={() => onRowClick(row)}
|
|
186
|
-
{...ts.itemProps(i)}
|
|
187
|
-
>
|
|
188
|
-
{#if row.hasChildren}
|
|
189
|
-
<span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">▸</span>
|
|
190
|
-
{:else}
|
|
191
|
-
<span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
|
|
192
|
-
{/if}
|
|
193
|
-
<span class="sv-ts__label">{row.node.label}</span>
|
|
194
|
-
</li>
|
|
195
|
-
{/each}
|
|
196
|
-
</ul>
|
|
197
|
-
</div>
|
|
198
|
-
{/if}
|
|
199
|
-
|
|
200
|
-
<style>
|
|
201
|
-
.sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
|
|
202
|
-
.sv-ts.is-disabled { opacity: 0.6; }
|
|
203
|
-
.sv-ts__trigger {
|
|
204
|
-
flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
|
|
205
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
206
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
207
|
-
font: inherit; cursor: pointer; padding: 0 9px;
|
|
208
|
-
}
|
|
209
|
-
.sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
|
|
210
|
-
.sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
|
|
211
|
-
.sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
|
|
212
|
-
.sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
213
|
-
.sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
214
|
-
.sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
215
|
-
.sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
|
|
216
|
-
.sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
|
|
217
|
-
|
|
218
|
-
:global(.sv-ts__panel) {
|
|
219
|
-
z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
|
|
220
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
221
|
-
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
222
|
-
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
223
|
-
}
|
|
224
|
-
:global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
|
|
225
|
-
:global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
226
|
-
:global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
227
|
-
:global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
228
|
-
: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; font-size: 10px; }
|
|
230
|
-
:global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
|
|
231
|
-
:global(.sv-ts__chev--leaf) { visibility: hidden; }
|
|
232
|
-
:global(.sv-ts__label) { flex: 1; white-space: nowrap; }
|
|
233
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type { TreeSelectNode } from './ui-app.types'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvTreeSelect - a single-select dropdown that shows an indented, collapsible
|
|
8
|
+
* tree in its panel (pick a node from a hierarchy). The open / position /
|
|
9
|
+
* dismiss / roving / keyboard / ARIA mechanics come from the shared
|
|
10
|
+
* `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
|
|
11
|
+
* expand/collapse); this component adds the tree flattening + rendering. Covers
|
|
12
|
+
* the "cascader / tree select" pattern without depending on SvTree.
|
|
13
|
+
*/
|
|
14
|
+
import type { TreeSelectNode } from './ui-app.types'
|
|
15
|
+
import SvField from './SvField.svelte'
|
|
16
|
+
import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
|
|
17
|
+
import { portalToBody, popIn } from './popover'
|
|
18
|
+
import { createPopoverSelect } from './createPopoverSelect.svelte'
|
|
19
|
+
import { flattenVisible, findNodePath } from './tree-select'
|
|
20
|
+
|
|
21
|
+
type Props = Pick<
|
|
22
|
+
SvEditorProps,
|
|
23
|
+
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
|
+
> & {
|
|
25
|
+
nodes: ReadonlyArray<TreeSelectNode>
|
|
26
|
+
value?: string | number | null
|
|
27
|
+
onChange?: (value: string | number) => void
|
|
28
|
+
onCommit?: (value: string | number) => void
|
|
29
|
+
onCancel?: () => void
|
|
30
|
+
placeholder?: string
|
|
31
|
+
showPath?: boolean
|
|
32
|
+
expandedIds?: ReadonlyArray<string | number>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
nodes,
|
|
37
|
+
value = $bindable<string | number | null>(null),
|
|
38
|
+
onChange,
|
|
39
|
+
onCommit,
|
|
40
|
+
onCancel,
|
|
41
|
+
placeholder = 'Select…',
|
|
42
|
+
showPath = false,
|
|
43
|
+
expandedIds = [],
|
|
44
|
+
disabled = false,
|
|
45
|
+
readonly = false,
|
|
46
|
+
label,
|
|
47
|
+
hint,
|
|
48
|
+
error,
|
|
49
|
+
required = false,
|
|
50
|
+
invalid = false,
|
|
51
|
+
size = 'md',
|
|
52
|
+
id,
|
|
53
|
+
name,
|
|
54
|
+
dir,
|
|
55
|
+
ariaLabel,
|
|
56
|
+
loading = false,
|
|
57
|
+
}: Props = $props()
|
|
58
|
+
|
|
59
|
+
const autoId = nextEditorId('sv-ts')
|
|
60
|
+
const uid = $derived(id ?? autoId)
|
|
61
|
+
|
|
62
|
+
let expanded = $state(new Set<string | number>(expandedIds))
|
|
63
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
64
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
65
|
+
let treeEl = $state<HTMLUListElement | null>(null)
|
|
66
|
+
|
|
67
|
+
const rows = $derived(flattenVisible(nodes, expanded))
|
|
68
|
+
const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
|
|
69
|
+
const triggerText = $derived(
|
|
70
|
+
!selectedPath
|
|
71
|
+
? ''
|
|
72
|
+
: showPath
|
|
73
|
+
? selectedPath.map((n) => n.label).join(' / ')
|
|
74
|
+
: selectedPath[selectedPath.length - 1]!.label,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
function setExpanded(v: string | number, on: boolean) {
|
|
78
|
+
const next = new Set(expanded)
|
|
79
|
+
on ? next.add(v) : next.delete(v)
|
|
80
|
+
expanded = next
|
|
81
|
+
}
|
|
82
|
+
function toggleExpand(v: string | number) {
|
|
83
|
+
setExpanded(v, !expanded.has(v))
|
|
84
|
+
}
|
|
85
|
+
function pick(node: TreeSelectNode) {
|
|
86
|
+
if (node.disabled) return
|
|
87
|
+
value = node.value // enables bind:value
|
|
88
|
+
onChange?.(node.value)
|
|
89
|
+
ts.closePanel()
|
|
90
|
+
}
|
|
91
|
+
function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
|
|
92
|
+
if (row.hasChildren) toggleExpand(row.node.value)
|
|
93
|
+
else pick(row.node)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ts = createPopoverSelect({
|
|
97
|
+
itemCount: () => rows.length,
|
|
98
|
+
disabled: (i) => !!rows[i]?.node.disabled,
|
|
99
|
+
readonly: () => readonly,
|
|
100
|
+
onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
|
|
101
|
+
getTrigger: () => triggerEl,
|
|
102
|
+
getPanel: () => panelEl,
|
|
103
|
+
getInitialFocus: () => treeEl,
|
|
104
|
+
closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
|
|
105
|
+
initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
|
|
106
|
+
minWidth: () => 220,
|
|
107
|
+
estimatedHeight: () => 300,
|
|
108
|
+
onExtraKey: (e, act) => {
|
|
109
|
+
const row = rows[act]
|
|
110
|
+
if (!row) return false
|
|
111
|
+
// "expand" opens the node or steps into the first child; "collapse" closes
|
|
112
|
+
// it or jumps to the parent - same fallback as createTree's core.
|
|
113
|
+
const expand = () => {
|
|
114
|
+
if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
|
|
115
|
+
else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
|
|
116
|
+
}
|
|
117
|
+
const collapse = () => {
|
|
118
|
+
if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
|
|
119
|
+
else {
|
|
120
|
+
let pi = act - 1
|
|
121
|
+
while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
|
|
122
|
+
if (pi >= 0) ts.active = pi
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
|
|
126
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
|
|
127
|
+
return false
|
|
128
|
+
},
|
|
129
|
+
idPrefix: 'sv-ts',
|
|
130
|
+
})
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
134
|
+
<div class="sv-ts sv-ts--{size}" class:is-disabled={disabled}>
|
|
135
|
+
<button
|
|
136
|
+
bind:this={triggerEl}
|
|
137
|
+
type="button"
|
|
138
|
+
class="sv-ts__trigger"
|
|
139
|
+
class:is-invalid={invalid}
|
|
140
|
+
{disabled}
|
|
141
|
+
onclick={() => ts.toggle()}
|
|
142
|
+
{...ts.triggerProps('tree')}
|
|
143
|
+
{...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
|
|
144
|
+
>
|
|
145
|
+
<span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
|
|
146
|
+
<span class="sv-ts__arrow" aria-hidden="true">▾</span>
|
|
147
|
+
</button>
|
|
148
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
149
|
+
</div>
|
|
150
|
+
</SvField>
|
|
151
|
+
|
|
152
|
+
{#if ts.open}
|
|
153
|
+
<div
|
|
154
|
+
bind:this={panelEl}
|
|
155
|
+
class="sv-ts__panel"
|
|
156
|
+
dir={dir}
|
|
157
|
+
use:portalToBody
|
|
158
|
+
use:popIn={{ up: ts.rect.openUpward }}
|
|
159
|
+
style:position="fixed"
|
|
160
|
+
style:top={`${ts.rect.top}px`}
|
|
161
|
+
style:left={`${ts.rect.left}px`}
|
|
162
|
+
style:min-width={`${ts.rect.width}px`}
|
|
163
|
+
>
|
|
164
|
+
<ul
|
|
165
|
+
bind:this={treeEl}
|
|
166
|
+
id={ts.panelId}
|
|
167
|
+
class="sv-ts__tree"
|
|
168
|
+
role="tree"
|
|
169
|
+
aria-label={label ?? ariaLabel ?? 'Tree'}
|
|
170
|
+
aria-activedescendant={ts.activeDescendant()}
|
|
171
|
+
tabindex="0"
|
|
172
|
+
style:max-height={`${ts.rect.maxHeight}px`}
|
|
173
|
+
onkeydown={ts.onPanelKeydown}
|
|
174
|
+
>
|
|
175
|
+
{#each rows as row, i (row.node.value)}
|
|
176
|
+
<li
|
|
177
|
+
class="sv-ts__row"
|
|
178
|
+
class:is-active={ts.isActive(i)}
|
|
179
|
+
class:is-selected={row.node.value === value}
|
|
180
|
+
class:is-disabled={row.node.disabled}
|
|
181
|
+
role="treeitem"
|
|
182
|
+
aria-selected={row.node.value === value}
|
|
183
|
+
aria-expanded={row.hasChildren ? row.expanded : undefined}
|
|
184
|
+
style:padding-inline-start={`${8 + row.depth * 16}px`}
|
|
185
|
+
onclick={() => onRowClick(row)}
|
|
186
|
+
{...ts.itemProps(i)}
|
|
187
|
+
>
|
|
188
|
+
{#if row.hasChildren}
|
|
189
|
+
<span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">▸</span>
|
|
190
|
+
{:else}
|
|
191
|
+
<span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
|
|
192
|
+
{/if}
|
|
193
|
+
<span class="sv-ts__label">{row.node.label}</span>
|
|
194
|
+
</li>
|
|
195
|
+
{/each}
|
|
196
|
+
</ul>
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
|
|
200
|
+
<style>
|
|
201
|
+
.sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
|
|
202
|
+
.sv-ts.is-disabled { opacity: 0.6; }
|
|
203
|
+
.sv-ts__trigger {
|
|
204
|
+
flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
|
|
205
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
206
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
207
|
+
font: inherit; cursor: pointer; padding: 0 9px;
|
|
208
|
+
}
|
|
209
|
+
.sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
|
|
210
|
+
.sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
|
|
211
|
+
.sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
|
|
212
|
+
.sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
213
|
+
.sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
214
|
+
.sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
215
|
+
.sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
|
|
216
|
+
.sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
|
|
217
|
+
|
|
218
|
+
:global(.sv-ts__panel) {
|
|
219
|
+
z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
|
|
220
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
221
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
222
|
+
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
223
|
+
}
|
|
224
|
+
:global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
|
|
225
|
+
:global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
226
|
+
:global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
227
|
+
:global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
228
|
+
: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; font-size: 10px; }
|
|
230
|
+
:global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
|
|
231
|
+
:global(.sv-ts__chev--leaf) { visibility: hidden; }
|
|
232
|
+
:global(.sv-ts__label) { flex: 1; white-space: nowrap; }
|
|
233
|
+
</style>
|
|
@@ -29,4 +29,15 @@ export type DismissableLayer = {
|
|
|
29
29
|
export declare function createDismissableLayer(options: DismissableOptions): DismissableLayer;
|
|
30
30
|
/** Test/introspection helper: current number of active layers. */
|
|
31
31
|
export declare function dismissableDepth(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Close-on-scroll for popovers that are position:fixed and anchored by a
|
|
34
|
+
* one-time measurement taken when they open. Scrolling an ancestor slides the
|
|
35
|
+
* anchor out from under them, so they have to close - but scrolling INSIDE
|
|
36
|
+
* them (a long menu, a facet list) must not, which is what a bare capture-phase
|
|
37
|
+
* window listener gets wrong: inner scroll containers don't bubble, so the
|
|
38
|
+
* capture listener sees them and dismisses the panel the user is scrolling.
|
|
39
|
+
*
|
|
40
|
+
* Returns the unsubscribe function.
|
|
41
|
+
*/
|
|
42
|
+
export declare function onScrollOutside(element: () => DismissTarget, onOutside: () => void): () => void;
|
|
32
43
|
export {};
|
package/dist/a11y/dismissable.js
CHANGED
|
@@ -52,3 +52,24 @@ export function createDismissableLayer(options) {
|
|
|
52
52
|
export function dismissableDepth() {
|
|
53
53
|
return stack.length;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Close-on-scroll for popovers that are position:fixed and anchored by a
|
|
57
|
+
* one-time measurement taken when they open. Scrolling an ancestor slides the
|
|
58
|
+
* anchor out from under them, so they have to close - but scrolling INSIDE
|
|
59
|
+
* them (a long menu, a facet list) must not, which is what a bare capture-phase
|
|
60
|
+
* window listener gets wrong: inner scroll containers don't bubble, so the
|
|
61
|
+
* capture listener sees them and dismisses the panel the user is scrolling.
|
|
62
|
+
*
|
|
63
|
+
* Returns the unsubscribe function.
|
|
64
|
+
*/
|
|
65
|
+
export function onScrollOutside(element, onOutside) {
|
|
66
|
+
if (typeof window === 'undefined')
|
|
67
|
+
return () => { };
|
|
68
|
+
const handler = (event) => {
|
|
69
|
+
if (containsTarget(element(), event.target))
|
|
70
|
+
return;
|
|
71
|
+
onOutside();
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener('scroll', handler, true);
|
|
74
|
+
return () => window.removeEventListener('scroll', handler, true);
|
|
75
|
+
}
|
package/dist/ai.js
CHANGED
|
@@ -42,14 +42,14 @@ export function hasAIProvider() {
|
|
|
42
42
|
}
|
|
43
43
|
class NoProviderError extends Error {
|
|
44
44
|
constructor() {
|
|
45
|
-
super('@svgrid/
|
|
45
|
+
super('@svgrid/grid ai: no AI provider registered. Call setAIProvider(fn) ' +
|
|
46
46
|
'with an adapter that talks to OpenAI / Anthropic / your proxy.');
|
|
47
47
|
this.name = 'NoProviderError';
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
class BadJsonError extends Error {
|
|
51
51
|
constructor(raw) {
|
|
52
|
-
super('@svgrid/
|
|
52
|
+
super('@svgrid/grid ai: provider returned non-JSON for a json-format request. ' +
|
|
53
53
|
`First 200 chars: ${raw.slice(0, 200)}`);
|
|
54
54
|
this.name = 'BadJsonError';
|
|
55
55
|
}
|
|
@@ -183,7 +183,7 @@ export async function aiFilter(api, query, opts = {}) {
|
|
|
183
183
|
*/
|
|
184
184
|
export async function aiSmartFill(api, opts) {
|
|
185
185
|
if (opts.examples.length === 0) {
|
|
186
|
-
throw new Error('@svgrid/
|
|
186
|
+
throw new Error('@svgrid/grid ai: aiSmartFill requires at least one example.');
|
|
187
187
|
}
|
|
188
188
|
const data = api.getData();
|
|
189
189
|
const targets = opts.targetRowIndices ??
|