@svgrid/grid 2.6.23 → 3.0.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.
- package/CHANGELOG.md +88 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/SvAutoComplete.svelte +169 -169
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -279
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -378
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGridCellEditor.svelte +744 -744
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -270
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -334
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -293
- package/dist/SvNumberInput.svelte +172 -172
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- 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 +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -239
- package/dist/cdn/{GridMenus-BzWXQjv3.js → GridMenus-DMihUtma.js} +3 -3
- package/dist/cdn/{GridMenus-xr_rHx8F.js → GridMenus-nmNDj1a3.js} +3 -3
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-Bh0A0JkF.js} +1 -1
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-DflbiP7N.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-BWwpfB_o.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-CCbDZNZB.js → SvDateTimePicker-Bivn8dAP.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-BAPSC7EL.js → SvGridCellEditor-AXL8cHGO.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-BwzmUWtL.js → SvGridCellEditor-Ba7rY3eu.js} +1 -1
- package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
- package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CcP1tafP.js} +2 -2
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-DZT7T1wf.js} +2 -2
- package/dist/cdn/{row-drag-touch-Ddaa-QeB.js → row-drag-touch-CycRR65n.js} +12 -10
- package/dist/cdn/{src-skQN5eqh.js → src-B_YS5AOc.js} +45 -45
- package/dist/cdn/{src-DGo7IHug.js → src-DYXXpuSk.js} +44 -44
- package/dist/cdn/svgrid.js +7 -7
- package/dist/cdn/svgrid.svelte-external.js +7 -7
- package/dist/chart-export.js +8 -8
- package/dist/row-drag-touch.d.ts +5 -1
- package/dist/row-drag-touch.js +15 -4
- package/dist/row-drag.js +7 -3
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/SvAutoComplete.svelte +169 -169
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -279
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -378
- 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.types.ts +2092 -2092
- package/src/SvGridCellEditor.svelte +744 -744
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -270
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -334
- 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 +293 -293
- package/src/SvNumberInput.svelte +172 -172
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- 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 +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -239
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- 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 +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -148
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- 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/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- 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 +322 -322
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -974
- package/src/editing.ts +609 -609
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- 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 +179 -179
- package/src/menus.ts +597 -597
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag-touch.ts +20 -5
- package/src/row-drag.test.ts +401 -353
- package/src/row-drag.ts +419 -415
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection.test.ts +885 -885
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -377
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- 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 +322 -322
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvAutoComplete - a free-text input with a live-filtered suggestion list.
|
|
4
|
-
* Unlike SvComboBox, it accepts ANY value (the text), suggestions are just
|
|
5
|
-
* shortcuts. Parity: Smart `smart-input` (autocomplete). Emits the text string.
|
|
6
|
-
*
|
|
7
|
-
* One styled renderer over the headless `createAutocomplete` core; only
|
|
8
|
-
* portal/measure render concerns live here.
|
|
9
|
-
*/
|
|
10
|
-
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
11
|
-
import { startPanelResize } from './panel-resize'
|
|
12
|
-
import { createDismissableLayer } from './a11y/dismissable'
|
|
13
|
-
import { type ListOption } from './list-option'
|
|
14
|
-
import SvField from './SvField.svelte'
|
|
15
|
-
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
16
|
-
import { createAutocomplete } from './createAutocomplete.svelte'
|
|
17
|
-
|
|
18
|
-
type Props = SvEditorProps & {
|
|
19
|
-
value?: string
|
|
20
|
-
onChange?: (value: string) => void
|
|
21
|
-
/** Suggestions - strings or {value,label} (label is shown, value inserted). */
|
|
22
|
-
suggestions?: ReadonlyArray<string | ListOption>
|
|
23
|
-
minChars?: number
|
|
24
|
-
placeholder?: string
|
|
25
|
-
/** Show a bottom drag grip so the user can resize the open suggestion panel's
|
|
26
|
-
* height (hidden when the panel flips upward, where there is no room to grow). */
|
|
27
|
-
resizable?: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let {
|
|
31
|
-
value = $bindable(''),
|
|
32
|
-
onChange,
|
|
33
|
-
suggestions = [],
|
|
34
|
-
minChars = 1,
|
|
35
|
-
placeholder,
|
|
36
|
-
resizable = false,
|
|
37
|
-
disabled = false,
|
|
38
|
-
name,
|
|
39
|
-
size = 'md',
|
|
40
|
-
ariaLabel,
|
|
41
|
-
invalid = false,
|
|
42
|
-
block = false,
|
|
43
|
-
required = false,
|
|
44
|
-
error,
|
|
45
|
-
label,
|
|
46
|
-
hint,
|
|
47
|
-
dir,
|
|
48
|
-
id,
|
|
49
|
-
loading = false,
|
|
50
|
-
}: Props = $props()
|
|
51
|
-
|
|
52
|
-
const autoId = nextEditorId('sv-ac')
|
|
53
|
-
const uid = $derived(id ?? autoId)
|
|
54
|
-
|
|
55
|
-
let fieldEl = $state<HTMLInputElement | null>(null)
|
|
56
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
57
|
-
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
58
|
-
// User-chosen panel height (px) once they drag the resize grip; null = auto.
|
|
59
|
-
let userHeight = $state<number | null>(null)
|
|
60
|
-
const DEFAULT_CAP = 288
|
|
61
|
-
const panelMaxH = $derived(
|
|
62
|
-
resizable && userHeight != null ? Math.min(userHeight, rect.availHeight) : Math.min(rect.availHeight, DEFAULT_CAP),
|
|
63
|
-
)
|
|
64
|
-
const showGrip = $derived(resizable && !rect.openUpward)
|
|
65
|
-
|
|
66
|
-
const ac = createAutocomplete({
|
|
67
|
-
value: () => value,
|
|
68
|
-
onChange: (v) => { value = v; onChange?.(v) },
|
|
69
|
-
suggestions: () => suggestions,
|
|
70
|
-
minChars: () => minChars,
|
|
71
|
-
disabled: () => disabled,
|
|
72
|
-
ariaLabel: () => ariaLabel,
|
|
73
|
-
focusInput: () => fieldEl?.focus(),
|
|
74
|
-
id: () => uid,
|
|
75
|
-
invalid: () => invalid,
|
|
76
|
-
required: () => required,
|
|
77
|
-
error: () => error,
|
|
78
|
-
hint: () => hint,
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
function updatePos() {
|
|
82
|
-
if (!fieldEl) return
|
|
83
|
-
// estimatedHeight only picks the flip direction; real max-height is availHeight
|
|
84
|
-
// below, so the panel takes its natural content height. See SvComboBox. When
|
|
85
|
-
// the user has dragged the grip, anchor the flip decision to THAT height.
|
|
86
|
-
const est = resizable && userHeight != null ? userHeight : Math.min(ac.filtered.length, 8) * 34 + 12
|
|
87
|
-
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: est })
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function startResize(e: PointerEvent) {
|
|
91
|
-
if (!panelEl) return
|
|
92
|
-
startPanelResize(e, {
|
|
93
|
-
startHeight: panelEl.clientHeight,
|
|
94
|
-
min: 96,
|
|
95
|
-
max: () => rect.availHeight,
|
|
96
|
-
onHeight: (h) => { userHeight = h; updatePos() },
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
$effect(() => {
|
|
101
|
-
if (!ac.open) return
|
|
102
|
-
updatePos()
|
|
103
|
-
const rp = () => updatePos()
|
|
104
|
-
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
105
|
-
const layer = createDismissableLayer({ element: () => [fieldEl, panelEl], onDismiss: () => ac.close(), closeOnEscape: false })
|
|
106
|
-
layer.activate()
|
|
107
|
-
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); layer.release() }
|
|
108
|
-
})
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
112
|
-
<input
|
|
113
|
-
bind:this={fieldEl}
|
|
114
|
-
class="sv-ac sv-ac--{size}"
|
|
115
|
-
class:is-block={block}
|
|
116
|
-
class:is-invalid={invalid}
|
|
117
|
-
type="text"
|
|
118
|
-
{placeholder}
|
|
119
|
-
{...ac.inputProps()}
|
|
120
|
-
/>
|
|
121
|
-
</SvField>
|
|
122
|
-
|
|
123
|
-
{#if ac.open}
|
|
124
|
-
<div bind:this={panelEl} class="sv-ddl__panel" class:is-resizable={showGrip} use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${panelMaxH}px`} style:height={resizable && userHeight != null && !rect.openUpward ? `${panelMaxH}px` : undefined} {...ac.listboxProps()}>
|
|
125
|
-
{#each ac.filtered as opt, i (i)}
|
|
126
|
-
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
127
|
-
<div class="sv-ddl__opt" class:is-active={ac.isActive(i)} {...ac.optionProps(i)}>{opt.label}</div>
|
|
128
|
-
{/each}
|
|
129
|
-
{#if showGrip}
|
|
130
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
131
|
-
<div class="sv-ddl__grip" onpointerdown={startResize} title="Drag to resize" aria-hidden="true">
|
|
132
|
-
<span class="sv-ddl__grip-dots"></span>
|
|
133
|
-
</div>
|
|
134
|
-
{/if}
|
|
135
|
-
</div>
|
|
136
|
-
{/if}
|
|
137
|
-
{#if name}<input type="hidden" {name} value={value} />{/if}
|
|
138
|
-
|
|
139
|
-
<style>
|
|
140
|
-
.sv-ac {
|
|
141
|
-
--_accent: var(--sg-accent, #2563eb);
|
|
142
|
-
box-sizing: border-box; width: 220px;
|
|
143
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
144
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
145
|
-
font: inherit; padding: 0 10px; outline: none;
|
|
146
|
-
}
|
|
147
|
-
.sv-ac--sm { height: 28px; font-size: 12px; }
|
|
148
|
-
.sv-ac--md { height: 34px; font-size: 13px; }
|
|
149
|
-
.sv-ac--lg { height: 40px; font-size: 15px; }
|
|
150
|
-
.sv-ac:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
151
|
-
.sv-ac.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
152
|
-
.sv-ac.is-invalid:focus { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
153
|
-
/* Bottom resize grip (shared "····" handle - see SvDropDownList / panel-resize). */
|
|
154
|
-
:global(.sv-ddl__panel.is-resizable) { padding-bottom: 0; }
|
|
155
|
-
:global(.sv-ddl__grip) {
|
|
156
|
-
position: sticky; bottom: 0; z-index: 1; margin: 2px -4px -4px; height: 15px;
|
|
157
|
-
display: flex; align-items: center; justify-content: center; cursor: ns-resize;
|
|
158
|
-
background: var(--sg-bg, #fff); border-top: 1px solid var(--sg-border, #e2e8f0);
|
|
159
|
-
border-radius: 0 0 10px 10px; touch-action: none;
|
|
160
|
-
}
|
|
161
|
-
:global(.sv-ddl__grip-dots) {
|
|
162
|
-
width: 26px; height: 4px; color: var(--sg-muted, #94a3b8); opacity: 0.6;
|
|
163
|
-
background-image: radial-gradient(currentColor 1px, transparent 1.6px);
|
|
164
|
-
background-size: 6px 4px; background-position: center; background-repeat: repeat-x;
|
|
165
|
-
}
|
|
166
|
-
:global(.sv-ddl__grip:hover .sv-ddl__grip-dots) { opacity: 1; }
|
|
167
|
-
/* Fill the container - see `block` in SvEditorProps. */
|
|
168
|
-
.sv-ac.is-block { width: 100%; max-width: 100%; }
|
|
169
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvAutoComplete - a free-text input with a live-filtered suggestion list.
|
|
4
|
+
* Unlike SvComboBox, it accepts ANY value (the text), suggestions are just
|
|
5
|
+
* shortcuts. Parity: Smart `smart-input` (autocomplete). Emits the text string.
|
|
6
|
+
*
|
|
7
|
+
* One styled renderer over the headless `createAutocomplete` core; only
|
|
8
|
+
* portal/measure render concerns live here.
|
|
9
|
+
*/
|
|
10
|
+
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
11
|
+
import { startPanelResize } from './panel-resize'
|
|
12
|
+
import { createDismissableLayer } from './a11y/dismissable'
|
|
13
|
+
import { type ListOption } from './list-option'
|
|
14
|
+
import SvField from './SvField.svelte'
|
|
15
|
+
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
16
|
+
import { createAutocomplete } from './createAutocomplete.svelte'
|
|
17
|
+
|
|
18
|
+
type Props = SvEditorProps & {
|
|
19
|
+
value?: string
|
|
20
|
+
onChange?: (value: string) => void
|
|
21
|
+
/** Suggestions - strings or {value,label} (label is shown, value inserted). */
|
|
22
|
+
suggestions?: ReadonlyArray<string | ListOption>
|
|
23
|
+
minChars?: number
|
|
24
|
+
placeholder?: string
|
|
25
|
+
/** Show a bottom drag grip so the user can resize the open suggestion panel's
|
|
26
|
+
* height (hidden when the panel flips upward, where there is no room to grow). */
|
|
27
|
+
resizable?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
value = $bindable(''),
|
|
32
|
+
onChange,
|
|
33
|
+
suggestions = [],
|
|
34
|
+
minChars = 1,
|
|
35
|
+
placeholder,
|
|
36
|
+
resizable = false,
|
|
37
|
+
disabled = false,
|
|
38
|
+
name,
|
|
39
|
+
size = 'md',
|
|
40
|
+
ariaLabel,
|
|
41
|
+
invalid = false,
|
|
42
|
+
block = false,
|
|
43
|
+
required = false,
|
|
44
|
+
error,
|
|
45
|
+
label,
|
|
46
|
+
hint,
|
|
47
|
+
dir,
|
|
48
|
+
id,
|
|
49
|
+
loading = false,
|
|
50
|
+
}: Props = $props()
|
|
51
|
+
|
|
52
|
+
const autoId = nextEditorId('sv-ac')
|
|
53
|
+
const uid = $derived(id ?? autoId)
|
|
54
|
+
|
|
55
|
+
let fieldEl = $state<HTMLInputElement | null>(null)
|
|
56
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
57
|
+
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
58
|
+
// User-chosen panel height (px) once they drag the resize grip; null = auto.
|
|
59
|
+
let userHeight = $state<number | null>(null)
|
|
60
|
+
const DEFAULT_CAP = 288
|
|
61
|
+
const panelMaxH = $derived(
|
|
62
|
+
resizable && userHeight != null ? Math.min(userHeight, rect.availHeight) : Math.min(rect.availHeight, DEFAULT_CAP),
|
|
63
|
+
)
|
|
64
|
+
const showGrip = $derived(resizable && !rect.openUpward)
|
|
65
|
+
|
|
66
|
+
const ac = createAutocomplete({
|
|
67
|
+
value: () => value,
|
|
68
|
+
onChange: (v) => { value = v; onChange?.(v) },
|
|
69
|
+
suggestions: () => suggestions,
|
|
70
|
+
minChars: () => minChars,
|
|
71
|
+
disabled: () => disabled,
|
|
72
|
+
ariaLabel: () => ariaLabel,
|
|
73
|
+
focusInput: () => fieldEl?.focus(),
|
|
74
|
+
id: () => uid,
|
|
75
|
+
invalid: () => invalid,
|
|
76
|
+
required: () => required,
|
|
77
|
+
error: () => error,
|
|
78
|
+
hint: () => hint,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function updatePos() {
|
|
82
|
+
if (!fieldEl) return
|
|
83
|
+
// estimatedHeight only picks the flip direction; real max-height is availHeight
|
|
84
|
+
// below, so the panel takes its natural content height. See SvComboBox. When
|
|
85
|
+
// the user has dragged the grip, anchor the flip decision to THAT height.
|
|
86
|
+
const est = resizable && userHeight != null ? userHeight : Math.min(ac.filtered.length, 8) * 34 + 12
|
|
87
|
+
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: est })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startResize(e: PointerEvent) {
|
|
91
|
+
if (!panelEl) return
|
|
92
|
+
startPanelResize(e, {
|
|
93
|
+
startHeight: panelEl.clientHeight,
|
|
94
|
+
min: 96,
|
|
95
|
+
max: () => rect.availHeight,
|
|
96
|
+
onHeight: (h) => { userHeight = h; updatePos() },
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$effect(() => {
|
|
101
|
+
if (!ac.open) return
|
|
102
|
+
updatePos()
|
|
103
|
+
const rp = () => updatePos()
|
|
104
|
+
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
105
|
+
const layer = createDismissableLayer({ element: () => [fieldEl, panelEl], onDismiss: () => ac.close(), closeOnEscape: false })
|
|
106
|
+
layer.activate()
|
|
107
|
+
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); layer.release() }
|
|
108
|
+
})
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
112
|
+
<input
|
|
113
|
+
bind:this={fieldEl}
|
|
114
|
+
class="sv-ac sv-ac--{size}"
|
|
115
|
+
class:is-block={block}
|
|
116
|
+
class:is-invalid={invalid}
|
|
117
|
+
type="text"
|
|
118
|
+
{placeholder}
|
|
119
|
+
{...ac.inputProps()}
|
|
120
|
+
/>
|
|
121
|
+
</SvField>
|
|
122
|
+
|
|
123
|
+
{#if ac.open}
|
|
124
|
+
<div bind:this={panelEl} class="sv-ddl__panel" class:is-resizable={showGrip} use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${panelMaxH}px`} style:height={resizable && userHeight != null && !rect.openUpward ? `${panelMaxH}px` : undefined} {...ac.listboxProps()}>
|
|
125
|
+
{#each ac.filtered as opt, i (i)}
|
|
126
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
127
|
+
<div class="sv-ddl__opt" class:is-active={ac.isActive(i)} {...ac.optionProps(i)}>{opt.label}</div>
|
|
128
|
+
{/each}
|
|
129
|
+
{#if showGrip}
|
|
130
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
131
|
+
<div class="sv-ddl__grip" onpointerdown={startResize} title="Drag to resize" aria-hidden="true">
|
|
132
|
+
<span class="sv-ddl__grip-dots"></span>
|
|
133
|
+
</div>
|
|
134
|
+
{/if}
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
{#if name}<input type="hidden" {name} value={value} />{/if}
|
|
138
|
+
|
|
139
|
+
<style>
|
|
140
|
+
.sv-ac {
|
|
141
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
142
|
+
box-sizing: border-box; width: 220px;
|
|
143
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
144
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
145
|
+
font: inherit; padding: 0 10px; outline: none;
|
|
146
|
+
}
|
|
147
|
+
.sv-ac--sm { height: 28px; font-size: 12px; }
|
|
148
|
+
.sv-ac--md { height: 34px; font-size: 13px; }
|
|
149
|
+
.sv-ac--lg { height: 40px; font-size: 15px; }
|
|
150
|
+
.sv-ac:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
151
|
+
.sv-ac.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
152
|
+
.sv-ac.is-invalid:focus { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
153
|
+
/* Bottom resize grip (shared "····" handle - see SvDropDownList / panel-resize). */
|
|
154
|
+
:global(.sv-ddl__panel.is-resizable) { padding-bottom: 0; }
|
|
155
|
+
:global(.sv-ddl__grip) {
|
|
156
|
+
position: sticky; bottom: 0; z-index: 1; margin: 2px -4px -4px; height: 15px;
|
|
157
|
+
display: flex; align-items: center; justify-content: center; cursor: ns-resize;
|
|
158
|
+
background: var(--sg-bg, #fff); border-top: 1px solid var(--sg-border, #e2e8f0);
|
|
159
|
+
border-radius: 0 0 10px 10px; touch-action: none;
|
|
160
|
+
}
|
|
161
|
+
:global(.sv-ddl__grip-dots) {
|
|
162
|
+
width: 26px; height: 4px; color: var(--sg-muted, #94a3b8); opacity: 0.6;
|
|
163
|
+
background-image: radial-gradient(currentColor 1px, transparent 1.6px);
|
|
164
|
+
background-size: 6px 4px; background-position: center; background-repeat: repeat-x;
|
|
165
|
+
}
|
|
166
|
+
:global(.sv-ddl__grip:hover .sv-ddl__grip-dots) { opacity: 1; }
|
|
167
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
168
|
+
.sv-ac.is-block { width: 100%; max-width: 100%; }
|
|
169
|
+
</style>
|
package/dist/SvAvatar.svelte
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvAvatar - a user/entity avatar: an image with an initials + colour-hash
|
|
4
|
-
* fallback (used when there is no `src` or the image fails to load), plus an
|
|
5
|
-
* optional presence status dot. Parity: Smart avatar.
|
|
6
|
-
*
|
|
7
|
-
* ```svelte
|
|
8
|
-
* <SvAvatar name="Ada Lovelace" src="/ada.jpg" status="online" />
|
|
9
|
-
* ```
|
|
10
|
-
*/
|
|
11
|
-
import { avatarInitials, avatarColorHue } from './avatar'
|
|
12
|
-
|
|
13
|
-
type Props = {
|
|
14
|
-
name?: string
|
|
15
|
-
src?: string
|
|
16
|
-
alt?: string
|
|
17
|
-
/** Preset (sm 28 / md 36 / lg 48) or an explicit pixel size. */
|
|
18
|
-
size?: 'sm' | 'md' | 'lg' | number
|
|
19
|
-
shape?: 'circle' | 'square'
|
|
20
|
-
status?: 'online' | 'offline' | 'busy' | 'away'
|
|
21
|
-
/** Override the hash-derived fallback colour (any CSS colour). */
|
|
22
|
-
color?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let { name = '', src, alt, size = 'md', shape = 'circle', status, color }: Props = $props()
|
|
26
|
-
|
|
27
|
-
const px = $derived(typeof size === 'number' ? size : size === 'sm' ? 28 : size === 'lg' ? 48 : 36)
|
|
28
|
-
const initials = $derived(avatarInitials(name))
|
|
29
|
-
const bg = $derived(color ?? `hsl(${avatarColorHue(name || '?')} 62% 45%)`)
|
|
30
|
-
// Fall back to initials when the image errors.
|
|
31
|
-
let broken = $state(false)
|
|
32
|
-
const showImg = $derived(!!src && !broken)
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<span
|
|
36
|
-
class="sv-avatar sv-avatar--{shape}"
|
|
37
|
-
style:width={`${px}px`}
|
|
38
|
-
style:height={`${px}px`}
|
|
39
|
-
style:font-size={`${Math.round(px * 0.4)}px`}
|
|
40
|
-
style:background={showImg ? undefined : bg}
|
|
41
|
-
role="img"
|
|
42
|
-
aria-label={alt ?? (name || 'avatar')}
|
|
43
|
-
title={name || undefined}
|
|
44
|
-
>
|
|
45
|
-
{#if showImg}
|
|
46
|
-
<img class="sv-avatar__img" src={src} alt={alt ?? name} onerror={() => (broken = true)} />
|
|
47
|
-
{:else if initials}
|
|
48
|
-
<span class="sv-avatar__initials" aria-hidden="true">{initials}</span>
|
|
49
|
-
{:else}
|
|
50
|
-
<svg class="sv-avatar__ph" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4.4 0-8 2.5-8 5.5V22h16v-2.5C20 16.5 16.4 14 12 14Z" /></svg>
|
|
51
|
-
{/if}
|
|
52
|
-
{#if status}<span class="sv-avatar__status is-{status}" aria-hidden="true"></span>{/if}
|
|
53
|
-
</span>
|
|
54
|
-
|
|
55
|
-
<style>
|
|
56
|
-
.sv-avatar {
|
|
57
|
-
position: relative; display: inline-grid; place-items: center; overflow: visible;
|
|
58
|
-
color: #fff; font-weight: 600; line-height: 1; user-select: none; flex: none;
|
|
59
|
-
vertical-align: middle;
|
|
60
|
-
}
|
|
61
|
-
.sv-avatar--circle { border-radius: 50%; }
|
|
62
|
-
.sv-avatar--square { border-radius: 22%; }
|
|
63
|
-
.sv-avatar__img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }
|
|
64
|
-
.sv-avatar--circle .sv-avatar__img, .sv-avatar--circle { }
|
|
65
|
-
.sv-avatar__initials { letter-spacing: 0.02em; }
|
|
66
|
-
.sv-avatar__ph { width: 62%; height: 62%; opacity: 0.9; }
|
|
67
|
-
.sv-avatar__status {
|
|
68
|
-
position: absolute; right: -1px; bottom: -1px; width: 28%; height: 28%; min-width: 8px; min-height: 8px;
|
|
69
|
-
border-radius: 50%; border: 2px solid var(--sg-bg, #fff);
|
|
70
|
-
}
|
|
71
|
-
.sv-avatar__status.is-online { background: var(--sg-success, #16a34a); }
|
|
72
|
-
.sv-avatar__status.is-offline { background: var(--sg-muted, #94a3b8); }
|
|
73
|
-
.sv-avatar__status.is-busy { background: var(--sg-danger, #dc2626); }
|
|
74
|
-
.sv-avatar__status.is-away { background: var(--sg-warning, #d97706); }
|
|
75
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvAvatar - a user/entity avatar: an image with an initials + colour-hash
|
|
4
|
+
* fallback (used when there is no `src` or the image fails to load), plus an
|
|
5
|
+
* optional presence status dot. Parity: Smart avatar.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <SvAvatar name="Ada Lovelace" src="/ada.jpg" status="online" />
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import { avatarInitials, avatarColorHue } from './avatar'
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
name?: string
|
|
15
|
+
src?: string
|
|
16
|
+
alt?: string
|
|
17
|
+
/** Preset (sm 28 / md 36 / lg 48) or an explicit pixel size. */
|
|
18
|
+
size?: 'sm' | 'md' | 'lg' | number
|
|
19
|
+
shape?: 'circle' | 'square'
|
|
20
|
+
status?: 'online' | 'offline' | 'busy' | 'away'
|
|
21
|
+
/** Override the hash-derived fallback colour (any CSS colour). */
|
|
22
|
+
color?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { name = '', src, alt, size = 'md', shape = 'circle', status, color }: Props = $props()
|
|
26
|
+
|
|
27
|
+
const px = $derived(typeof size === 'number' ? size : size === 'sm' ? 28 : size === 'lg' ? 48 : 36)
|
|
28
|
+
const initials = $derived(avatarInitials(name))
|
|
29
|
+
const bg = $derived(color ?? `hsl(${avatarColorHue(name || '?')} 62% 45%)`)
|
|
30
|
+
// Fall back to initials when the image errors.
|
|
31
|
+
let broken = $state(false)
|
|
32
|
+
const showImg = $derived(!!src && !broken)
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<span
|
|
36
|
+
class="sv-avatar sv-avatar--{shape}"
|
|
37
|
+
style:width={`${px}px`}
|
|
38
|
+
style:height={`${px}px`}
|
|
39
|
+
style:font-size={`${Math.round(px * 0.4)}px`}
|
|
40
|
+
style:background={showImg ? undefined : bg}
|
|
41
|
+
role="img"
|
|
42
|
+
aria-label={alt ?? (name || 'avatar')}
|
|
43
|
+
title={name || undefined}
|
|
44
|
+
>
|
|
45
|
+
{#if showImg}
|
|
46
|
+
<img class="sv-avatar__img" src={src} alt={alt ?? name} onerror={() => (broken = true)} />
|
|
47
|
+
{:else if initials}
|
|
48
|
+
<span class="sv-avatar__initials" aria-hidden="true">{initials}</span>
|
|
49
|
+
{:else}
|
|
50
|
+
<svg class="sv-avatar__ph" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4.4 0-8 2.5-8 5.5V22h16v-2.5C20 16.5 16.4 14 12 14Z" /></svg>
|
|
51
|
+
{/if}
|
|
52
|
+
{#if status}<span class="sv-avatar__status is-{status}" aria-hidden="true"></span>{/if}
|
|
53
|
+
</span>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.sv-avatar {
|
|
57
|
+
position: relative; display: inline-grid; place-items: center; overflow: visible;
|
|
58
|
+
color: #fff; font-weight: 600; line-height: 1; user-select: none; flex: none;
|
|
59
|
+
vertical-align: middle;
|
|
60
|
+
}
|
|
61
|
+
.sv-avatar--circle { border-radius: 50%; }
|
|
62
|
+
.sv-avatar--square { border-radius: 22%; }
|
|
63
|
+
.sv-avatar__img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }
|
|
64
|
+
.sv-avatar--circle .sv-avatar__img, .sv-avatar--circle { }
|
|
65
|
+
.sv-avatar__initials { letter-spacing: 0.02em; }
|
|
66
|
+
.sv-avatar__ph { width: 62%; height: 62%; opacity: 0.9; }
|
|
67
|
+
.sv-avatar__status {
|
|
68
|
+
position: absolute; right: -1px; bottom: -1px; width: 28%; height: 28%; min-width: 8px; min-height: 8px;
|
|
69
|
+
border-radius: 50%; border: 2px solid var(--sg-bg, #fff);
|
|
70
|
+
}
|
|
71
|
+
.sv-avatar__status.is-online { background: var(--sg-success, #16a34a); }
|
|
72
|
+
.sv-avatar__status.is-offline { background: var(--sg-muted, #94a3b8); }
|
|
73
|
+
.sv-avatar__status.is-busy { background: var(--sg-danger, #dc2626); }
|
|
74
|
+
.sv-avatar__status.is-away { background: var(--sg-warning, #d97706); }
|
|
75
|
+
</style>
|