@svgrid/grid 2.2.15 → 2.2.17
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/GridFooter.svelte +12 -11
- package/dist/SvAnchor.svelte +65 -0
- package/dist/SvAnchor.svelte.d.ts +24 -0
- package/dist/SvAspectRatio.svelte +38 -0
- package/dist/SvAspectRatio.svelte.d.ts +16 -0
- package/dist/SvAutoComplete.svelte +52 -7
- package/dist/SvAutoComplete.svelte.d.ts +4 -1
- package/dist/SvBlockquote.svelte +43 -0
- package/dist/SvBlockquote.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +33 -2
- package/dist/SvCalendar.svelte.d.ts +8 -1
- package/dist/SvCheckBox.svelte +6 -2
- package/dist/SvCode.svelte +52 -0
- package/dist/SvCode.svelte.d.ts +19 -0
- package/dist/SvCollapsible.svelte +75 -0
- package/dist/SvCollapsible.svelte.d.ts +27 -0
- package/dist/SvColorInput.svelte +11 -17
- package/dist/SvColorInput.svelte.d.ts +2 -0
- package/dist/SvComboBox.svelte +85 -8
- package/dist/SvComboBox.svelte.d.ts +14 -0
- package/dist/SvCountryInput.svelte +2 -2
- package/dist/SvCountryInput.svelte.d.ts +1 -1
- package/dist/SvDropDownList.svelte +154 -51
- package/dist/SvDropDownList.svelte.d.ts +17 -3
- package/dist/SvDurationInput.svelte +59 -41
- package/dist/SvDurationInput.svelte.d.ts +18 -0
- package/dist/SvField.svelte +203 -12
- package/dist/SvField.svelte.d.ts +45 -8
- package/dist/SvForm.svelte +195 -21
- package/dist/SvForm.svelte.d.ts +23 -0
- package/dist/SvGrid.controller.svelte.d.ts +12 -1
- package/dist/SvGrid.controller.svelte.js +87 -7
- package/dist/SvGrid.svelte +383 -181
- package/dist/SvGrid.types.d.ts +123 -2
- package/dist/SvGridChartView.svelte +70 -0
- package/dist/SvGridChartView.svelte.d.ts +11 -0
- package/dist/SvGridDropdown.svelte +9 -2
- package/dist/SvGridSelect.svelte +5 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroup.svelte +41 -0
- package/dist/SvGroup.svelte.d.ts +27 -0
- package/dist/SvHoverCard.svelte +70 -0
- package/dist/SvHoverCard.svelte.d.ts +41 -0
- package/dist/SvKbd.svelte +51 -0
- package/dist/SvKbd.svelte.d.ts +18 -0
- package/dist/SvList.svelte +55 -0
- package/dist/SvList.svelte.d.ts +23 -0
- package/dist/SvListBox.svelte +166 -77
- package/dist/SvListBox.svelte.d.ts +17 -4
- package/dist/SvLoadingOverlay.svelte +53 -0
- package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
- package/dist/SvMark.svelte +36 -0
- package/dist/SvMark.svelte.d.ts +14 -0
- package/dist/SvMaskedInput.svelte +50 -35
- package/dist/SvMaskedInput.svelte.d.ts +17 -2
- package/dist/SvMenuList.svelte +32 -3
- package/dist/SvMenubar.svelte +200 -0
- package/dist/SvMenubar.svelte.d.ts +18 -0
- package/dist/SvMultiSelect.svelte +7 -4
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +79 -42
- package/dist/SvNumberInput.svelte.d.ts +25 -0
- package/dist/SvOtpInput.svelte +2 -2
- package/dist/SvPasswordInput.svelte +51 -31
- package/dist/SvPasswordInput.svelte.d.ts +17 -0
- package/dist/SvPhoneInput.svelte +39 -28
- package/dist/SvPhoneInput.svelte.d.ts +4 -0
- package/dist/SvPopconfirm.svelte +79 -0
- package/dist/SvPopconfirm.svelte.d.ts +42 -0
- package/dist/SvPopover.svelte +87 -29
- package/dist/SvPopover.svelte.d.ts +18 -4
- package/dist/SvRadioGroup.svelte +5 -1
- package/dist/SvResult.svelte +73 -0
- package/dist/SvResult.svelte.d.ts +28 -0
- package/dist/SvSegmented.svelte +114 -0
- package/dist/SvSegmented.svelte.d.ts +29 -0
- package/dist/SvSimpleGrid.svelte +41 -0
- package/dist/SvSimpleGrid.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +23 -10
- package/dist/SvSpinner.svelte +43 -0
- package/dist/SvSpinner.svelte.d.ts +20 -0
- package/dist/SvStack.svelte +37 -0
- package/dist/SvStack.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +6 -1
- package/dist/SvTagsInput.svelte +9 -5
- package/dist/SvText.svelte +76 -0
- package/dist/SvText.svelte.d.ts +31 -0
- package/dist/SvTextInput.svelte +75 -43
- package/dist/SvTextInput.svelte.d.ts +28 -1
- package/dist/SvTimePicker.svelte +32 -2
- package/dist/SvTimePicker.svelte.d.ts +8 -9
- package/dist/SvTitle.svelte +57 -0
- package/dist/SvTitle.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +41 -6
- package/dist/SvToggleButton.svelte +4 -1
- package/dist/SvTooltip.svelte +68 -34
- package/dist/SvTooltip.svelte.d.ts +14 -2
- package/dist/SvTree.svelte +81 -21
- package/dist/SvTree.svelte.d.ts +3 -2
- package/dist/SvTreeSelect.svelte +5 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/SvVisuallyHidden.svelte +49 -0
- package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
- package/dist/ai.d.ts +289 -0
- package/dist/ai.js +1013 -0
- package/dist/board-view.svelte.d.ts +22 -0
- package/dist/board-view.svelte.js +13 -0
- package/dist/build-api.js +36 -6
- package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
- package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
- package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
- package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
- package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
- package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
- package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
- package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
- package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
- package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
- package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
- package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
- package/dist/cdn/chart-T1usDN0o.js +1212 -0
- package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
- package/dist/cdn/export-format-JT5pWVT8.js +561 -0
- package/dist/cdn/popover-P-9P2YYn.js +69 -0
- package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/cdn/src-BLhdLmoN.js +25707 -0
- package/dist/cdn/src-BwnUVj3T.js +25623 -0
- package/dist/cdn/svgrid.js +9 -32897
- package/dist/cdn/svgrid.svelte-external.js +9 -30052
- package/dist/cell-formatting.d.ts +2 -2
- package/dist/cell-render.js +4 -4
- package/dist/chart-view.svelte.d.ts +25 -0
- package/dist/chart-view.svelte.js +14 -0
- package/dist/createCombobox.svelte.d.ts +5 -0
- package/dist/createCombobox.svelte.js +21 -5
- package/dist/createDropdownList.svelte.d.ts +3 -0
- package/dist/createDropdownList.svelte.js +8 -2
- package/dist/createForm.svelte.d.ts +18 -0
- package/dist/createForm.svelte.js +51 -6
- package/dist/createMenu.svelte.js +27 -0
- package/dist/createPopoverSelect.svelte.d.ts +3 -0
- package/dist/createPopoverSelect.svelte.js +9 -1
- package/dist/createSlider.svelte.d.ts +8 -21
- package/dist/createSlider.svelte.js +34 -0
- package/dist/createTooltip.svelte.d.ts +12 -0
- package/dist/createTooltip.svelte.js +72 -9
- package/dist/editor-contract.d.ts +15 -0
- package/dist/export-provider.d.ts +27 -0
- package/dist/export-provider.js +9 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +33 -0
- package/dist/form-field.d.ts +45 -2
- package/dist/grid-messages.d.ts +61 -0
- package/dist/grid-messages.js +72 -0
- package/dist/group-display.d.ts +62 -0
- package/dist/group-display.js +78 -0
- package/dist/index.d.ts +36 -4
- package/dist/index.js +43 -2
- package/dist/js-scroller.svelte.d.ts +35 -0
- package/dist/js-scroller.svelte.js +154 -0
- package/dist/list-option.d.ts +46 -0
- package/dist/list-option.js +53 -0
- package/dist/menus.js +25 -11
- package/dist/pivot-view.svelte.d.ts +72 -0
- package/dist/pivot-view.svelte.js +13 -0
- package/dist/positioning.d.ts +111 -0
- package/dist/positioning.js +191 -0
- package/dist/scheduler-model.d.ts +3 -0
- package/dist/scheduler-model.js +22 -5
- package/dist/svgrid-wrapper.types.d.ts +18 -0
- package/dist/toast-store.svelte.d.ts +54 -0
- package/dist/toast-store.svelte.js +65 -15
- package/package.json +9 -1
- package/src/GridFooter.svelte +12 -11
- package/src/SvAnchor.svelte +65 -0
- package/src/SvAspectRatio.svelte +38 -0
- package/src/SvAutoComplete.svelte +52 -7
- package/src/SvBlockquote.svelte +43 -0
- package/src/SvCalendar.svelte +33 -2
- package/src/SvCalendar.test.ts +15 -0
- package/src/SvCheckBox.svelte +6 -2
- package/src/SvCode.svelte +52 -0
- package/src/SvCollapsible.svelte +75 -0
- package/src/SvColorInput.svelte +11 -17
- package/src/SvComboBox.svelte +85 -8
- package/src/SvCountryInput.svelte +2 -2
- package/src/SvDropDownList.svelte +154 -51
- package/src/SvDurationInput.svelte +59 -41
- package/src/SvField.svelte +203 -12
- package/src/SvForm.svelte +195 -21
- package/src/SvForm.test.ts +112 -0
- package/src/SvGrid.controller.svelte.ts +88 -7
- package/src/SvGrid.svelte +383 -181
- package/src/SvGrid.types.ts +125 -1
- package/src/SvGridChartView.svelte +70 -0
- package/src/SvGridDropdown.svelte +9 -2
- package/src/SvGridSelect.svelte +5 -2
- package/src/SvGroup.svelte +41 -0
- package/src/SvHoverCard.svelte +70 -0
- package/src/SvKbd.svelte +51 -0
- package/src/SvList.svelte +55 -0
- package/src/SvListBox.svelte +166 -77
- package/src/SvLoadingOverlay.svelte +53 -0
- package/src/SvMark.svelte +36 -0
- package/src/SvMaskedInput.svelte +50 -35
- package/src/SvMenuList.svelte +32 -3
- package/src/SvMenubar.svelte +200 -0
- package/src/SvMultiSelect.svelte +7 -4
- package/src/SvNumberInput.svelte +79 -42
- package/src/SvOtpInput.svelte +2 -2
- package/src/SvPasswordInput.svelte +51 -31
- package/src/SvPhoneInput.svelte +39 -28
- package/src/SvPopconfirm.svelte +79 -0
- package/src/SvPopover.svelte +87 -29
- package/src/SvRadioGroup.svelte +5 -1
- package/src/SvResult.svelte +73 -0
- package/src/SvSegmented.svelte +114 -0
- package/src/SvSimpleGrid.svelte +41 -0
- package/src/SvSlider.svelte +23 -10
- package/src/SvSpinner.svelte +43 -0
- package/src/SvStack.svelte +37 -0
- package/src/SvSwitchButton.svelte +6 -1
- package/src/SvTagsInput.svelte +9 -5
- package/src/SvText.svelte +76 -0
- package/src/SvTextInput.svelte +75 -43
- package/src/SvTimePicker.svelte +32 -2
- package/src/SvTitle.svelte +57 -0
- package/src/SvToaster.svelte +41 -6
- package/src/SvToaster.test.ts +32 -0
- package/src/SvToggleButton.svelte +4 -1
- package/src/SvTooltip.svelte +68 -34
- package/src/SvTree.svelte +81 -21
- package/src/SvTreeSelect.svelte +5 -2
- package/src/SvVisuallyHidden.svelte +49 -0
- package/src/ai.test.ts +502 -0
- package/src/ai.ts +1391 -0
- package/src/board-view.svelte.ts +33 -0
- package/src/build-api.coverage.test.ts +101 -0
- package/src/build-api.ts +43 -18
- package/src/builtin-editors.grid.test.ts +1 -1
- package/src/cell-formatting.ts +5 -5
- package/src/cell-render.ts +4 -4
- package/src/chart-view.svelte.ts +36 -0
- package/src/createCombobox.svelte.ts +17 -5
- package/src/createDropdownList.svelte.ts +8 -2
- package/src/createForm.svelte.ts +59 -6
- package/src/createMenu.svelte.ts +23 -0
- package/src/createPopoverSelect.svelte.ts +8 -1
- package/src/createSlider.svelte.ts +20 -0
- package/src/createTooltip.svelte.ts +67 -9
- package/src/editor-contract.ts +16 -0
- package/src/export-provider.ts +40 -0
- package/src/filter-operators.ts +37 -0
- package/src/form-field.ts +42 -2
- package/src/grid-messages.test.ts +24 -0
- package/src/grid-messages.ts +137 -0
- package/src/group-display.test.ts +77 -0
- package/src/group-display.ts +117 -0
- package/src/i18n.grid.test.ts +73 -0
- package/src/index.ts +91 -2
- package/src/js-scroller.svelte.ts +173 -0
- package/src/list-option.test.ts +56 -0
- package/src/list-option.ts +81 -0
- package/src/menus.test.ts +32 -0
- package/src/menus.ts +25 -12
- package/src/pivot-view.svelte.ts +90 -0
- package/src/pivot-view.test.ts +38 -0
- package/src/pivot.grid.test.ts +102 -0
- package/src/positioning.test.ts +141 -0
- package/src/positioning.ts +266 -0
- package/src/scheduler-model.test.ts +16 -0
- package/src/scheduler-model.ts +24 -5
- package/src/svgrid-wrapper.types.ts +23 -0
- package/src/svgrid.charting.test.ts +8 -1
- package/src/svgrid.comments-autocomplete.test.ts +7 -2
- package/src/svgrid.context-menu.test.ts +8 -3
- package/src/toast-store.svelte.ts +98 -0
- package/src/toast-store.test.ts +62 -1
- package/src/ui-breadth.test.ts +138 -0
- package/src/ui-buttons.test.ts +39 -0
- package/src/ui-inputs.test.ts +2 -2
- package/src/ui-range.test.ts +9 -0
- package/src/ui-selection.test.ts +36 -0
- package/src/ui-tier1.test.ts +1 -1
- package/src/virtual.test.ts +20 -6
- package/dist/SvGridBoard.svelte +0 -2339
- package/dist/SvGridBoard.svelte.d.ts +0 -31
- package/src/SvGridBoard.svelte +0 -2339
- package/src/board.test.ts +0 -927
package/src/SvPhoneInput.svelte
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
placeholder?: string
|
|
25
25
|
/** Override the built-in strings (e.g. the country selector aria-label). */
|
|
26
26
|
messages?: Partial<PhoneMessages>
|
|
27
|
+
/** Stretch to the container width. */
|
|
28
|
+
block?: boolean
|
|
29
|
+
/** Control width in px (ignored when `block`). Default 250. */
|
|
30
|
+
width?: number
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
let {
|
|
@@ -43,7 +47,10 @@
|
|
|
43
47
|
hint,
|
|
44
48
|
dir,
|
|
45
49
|
id,
|
|
50
|
+
loading = false,
|
|
46
51
|
messages,
|
|
52
|
+
block = false,
|
|
53
|
+
width = 250,
|
|
47
54
|
}: Props = $props()
|
|
48
55
|
|
|
49
56
|
const autoId = nextEditorId('sv-phone')
|
|
@@ -68,53 +75,57 @@
|
|
|
68
75
|
})
|
|
69
76
|
</script>
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
<div class="sv-phone__country">
|
|
78
|
+
{#snippet countryChip()}
|
|
79
|
+
<span class="sv-phone__country">
|
|
74
80
|
<span class="sv-phone__flag" aria-hidden="true">{ph.flag}</span>
|
|
75
81
|
<span class="sv-phone__dial">{ph.dial}</span>
|
|
82
|
+
<svg class="sv-phone__chev" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
76
83
|
<select class="sv-phone__select" {...ph.selectProps()}>
|
|
77
84
|
{#each COUNTRIES as c (c.code)}
|
|
78
85
|
<option value={c.code}>{flagEmoji(c.code)} {c.name} ({c.dial})</option>
|
|
79
86
|
{/each}
|
|
80
87
|
</select>
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
</span>
|
|
89
|
+
{/snippet}
|
|
90
|
+
|
|
91
|
+
<SvField
|
|
92
|
+
frame
|
|
93
|
+
id={uid}
|
|
94
|
+
{label}
|
|
95
|
+
{hint}
|
|
96
|
+
{error}
|
|
97
|
+
{required}
|
|
98
|
+
{dir}
|
|
99
|
+
{size}
|
|
100
|
+
{invalid}
|
|
101
|
+
{disabled}
|
|
102
|
+
{readonly}
|
|
103
|
+
{loading}
|
|
104
|
+
{block}
|
|
105
|
+
{width}
|
|
106
|
+
leading={countryChip}
|
|
107
|
+
>
|
|
83
108
|
<input class="sv-phone__number" {...ph.inputProps()} />
|
|
84
109
|
{#if name}<input type="hidden" {name} value={ph.value} />{/if}
|
|
85
|
-
</div>
|
|
86
110
|
</SvField>
|
|
87
111
|
|
|
88
112
|
<style>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
display: inline-flex; align-items: stretch; width: 250px;
|
|
92
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
93
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
94
|
-
overflow: hidden;
|
|
95
|
-
}
|
|
96
|
-
.sv-phone:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
97
|
-
.sv-phone.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
98
|
-
.sv-phone.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
99
|
-
.sv-phone.is-disabled { opacity: 0.6; }
|
|
113
|
+
/* A compact country chip in the leading slot: flag + dial + chevron, with the
|
|
114
|
+
native <select> as an invisible overlay covering it. */
|
|
100
115
|
.sv-phone__country {
|
|
101
116
|
position: relative; display: inline-flex; align-items: center; gap: 4px;
|
|
102
|
-
padding-
|
|
103
|
-
background: var(--sg-header-bg, #f8fafc); cursor: pointer;
|
|
117
|
+
padding-inline-end: 8px; border-inline-end: 1px solid var(--sg-border, #e2e8f0); cursor: pointer;
|
|
104
118
|
}
|
|
105
119
|
.sv-phone__flag { font-size: 15px; }
|
|
106
120
|
.sv-phone__dial { font-size: 13px; font-weight: 600; }
|
|
107
121
|
.sv-phone__chev { color: var(--sg-muted, #64748b); }
|
|
108
122
|
/* The native <select> is an invisible overlay, but its option popup is drawn by
|
|
109
|
-
the OS using the control's own color/background - so theme them explicitly
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
the OS using the control's own color/background - so theme them explicitly (a
|
|
124
|
+
2-class selector, so it beats SvField frame's generic `select` reset) or dark
|
|
125
|
+
themes get light text on the default white popup. */
|
|
126
|
+
.sv-phone__country .sv-phone__select {
|
|
127
|
+
position: absolute; inset: 0; opacity: 0; cursor: pointer; font-size: 13px; padding: 0;
|
|
113
128
|
color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff);
|
|
114
129
|
}
|
|
115
|
-
.sv-phone__select option { color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff); }
|
|
116
|
-
.sv-phone__number { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 10px; }
|
|
117
|
-
.sv-phone--sm { height: 28px; font-size: 12px; }
|
|
118
|
-
.sv-phone--md { height: 34px; font-size: 13px; }
|
|
119
|
-
.sv-phone--lg { height: 40px; font-size: 15px; }
|
|
130
|
+
.sv-phone__country .sv-phone__select option { color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff); }
|
|
120
131
|
</style>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvPopconfirm - a lightweight confirm step in a popover anchored to its
|
|
4
|
+
* trigger, for a low-stakes destructive action ("Delete this row?") where a
|
|
5
|
+
* full modal dialog would be too heavy. Click the anchor to reveal a short
|
|
6
|
+
* message plus Cancel / Confirm buttons; confirming runs `onConfirm` and closes.
|
|
7
|
+
* Built on SvPopover, so it is portalled, positioned by the shared engine, and
|
|
8
|
+
* dismisses on Escape / outside-click.
|
|
9
|
+
*
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <SvPopconfirm title="Delete this row?" confirmVariant="danger"
|
|
12
|
+
* onConfirm={() => remove(row)}>
|
|
13
|
+
* {#snippet anchor()}<SvButton variant="danger" size="sm">Delete</SvButton>{/snippet}
|
|
14
|
+
* </SvPopconfirm>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte'
|
|
18
|
+
import SvPopover from './SvPopover.svelte'
|
|
19
|
+
import SvButton from './SvButton.svelte'
|
|
20
|
+
import type { Placement } from './positioning'
|
|
21
|
+
|
|
22
|
+
type Props = {
|
|
23
|
+
/** Controlled open state (bindable). */
|
|
24
|
+
open?: boolean
|
|
25
|
+
onOpenChange?: (open: boolean) => void
|
|
26
|
+
/** Bold prompt, e.g. "Delete this row?". */
|
|
27
|
+
title?: string
|
|
28
|
+
/** Optional longer explanation under the title. */
|
|
29
|
+
description?: string
|
|
30
|
+
confirmLabel?: string
|
|
31
|
+
cancelLabel?: string
|
|
32
|
+
/** Variant for the confirm button (use `danger` for destructive actions). */
|
|
33
|
+
confirmVariant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
|
|
34
|
+
/** Runs when the user confirms; the popover then closes. */
|
|
35
|
+
onConfirm?: () => void
|
|
36
|
+
/** Runs when the user cancels (or dismisses); the popover then closes. */
|
|
37
|
+
onCancel?: () => void
|
|
38
|
+
/** Preferred placement; flips when there is no room. Default `top`. */
|
|
39
|
+
placement?: Placement
|
|
40
|
+
ariaLabel?: string
|
|
41
|
+
/** The trigger that opens the confirm. */
|
|
42
|
+
anchor?: Snippet
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
open = $bindable(false),
|
|
47
|
+
onOpenChange,
|
|
48
|
+
title,
|
|
49
|
+
description,
|
|
50
|
+
confirmLabel = 'Confirm',
|
|
51
|
+
cancelLabel = 'Cancel',
|
|
52
|
+
confirmVariant = 'primary',
|
|
53
|
+
onConfirm,
|
|
54
|
+
onCancel,
|
|
55
|
+
placement = 'top',
|
|
56
|
+
ariaLabel,
|
|
57
|
+
anchor,
|
|
58
|
+
}: Props = $props()
|
|
59
|
+
|
|
60
|
+
function confirm() { onConfirm?.(); open = false }
|
|
61
|
+
function cancel() { onCancel?.(); open = false }
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<SvPopover bind:open {onOpenChange} trigger="click" {placement} minWidth={230} ariaLabel={ariaLabel ?? title} {anchor}>
|
|
65
|
+
<div class="sv-popconfirm">
|
|
66
|
+
{#if title}<div class="sv-popconfirm__title">{title}</div>{/if}
|
|
67
|
+
{#if description}<div class="sv-popconfirm__desc">{description}</div>{/if}
|
|
68
|
+
<div class="sv-popconfirm__actions">
|
|
69
|
+
<SvButton size="sm" variant="ghost" onclick={cancel}>{cancelLabel}</SvButton>
|
|
70
|
+
<SvButton size="sm" variant={confirmVariant} onclick={confirm}>{confirmLabel}</SvButton>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</SvPopover>
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.sv-popconfirm__title { font-weight: 650; font-size: 13px; color: var(--sg-fg, #0f172a); }
|
|
77
|
+
.sv-popconfirm__desc { margin-top: 4px; font-size: 12.5px; color: var(--sg-muted, #64748b); line-height: 1.45; }
|
|
78
|
+
.sv-popconfirm__actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
|
79
|
+
</style>
|
package/src/SvPopover.svelte
CHANGED
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SvPopover - a floating panel anchored to a trigger, portalled to <body> so it
|
|
4
4
|
* escapes any `overflow:hidden` ancestor. Open on click (default), hover, or
|
|
5
|
-
* manually via `open`.
|
|
6
|
-
*
|
|
5
|
+
* manually via `open`. Positioned by the shared engine (`computePosition`): full
|
|
6
|
+
* placement matrix + flip + shift + an optional arrow, kept in sync with
|
|
7
|
+
* `autoUpdate`. Closes on outside-click / Escape via the dismissable layer
|
|
8
|
+
* stack, and animates in. The panel is hoverable (interactive) under the hover
|
|
9
|
+
* trigger. Parity: Smart popover.
|
|
7
10
|
*
|
|
8
11
|
* ```svelte
|
|
9
|
-
* <SvPopover>
|
|
12
|
+
* <SvPopover placement="bottom" arrow>
|
|
10
13
|
* {#snippet anchor()}<button>Open</button>{/snippet}
|
|
11
14
|
* <p>Panel content</p>
|
|
12
15
|
* </SvPopover>
|
|
13
16
|
* ```
|
|
14
17
|
*/
|
|
15
18
|
import type { Snippet } from 'svelte'
|
|
16
|
-
import {
|
|
19
|
+
import { portalToBody, popIn } from './popover'
|
|
20
|
+
import { computePosition, autoUpdate, type Placement, type ComputePositionResult } from './positioning'
|
|
17
21
|
import { createDismissableLayer } from './a11y/dismissable'
|
|
18
22
|
|
|
19
23
|
type Props = {
|
|
@@ -22,7 +26,17 @@
|
|
|
22
26
|
onOpenChange?: (open: boolean) => void
|
|
23
27
|
/** How the anchor opens the panel. `manual` = only `open` drives it. */
|
|
24
28
|
trigger?: 'click' | 'hover' | 'manual'
|
|
25
|
-
/**
|
|
29
|
+
/** Preferred placement (flips when there is no room). Default `bottom-start`. */
|
|
30
|
+
placement?: Placement
|
|
31
|
+
/** Main-axis gap between trigger and panel. Default 8 (room for the arrow). */
|
|
32
|
+
offset?: number
|
|
33
|
+
/** Render a pointer arrow toward the trigger. Default true. */
|
|
34
|
+
arrow?: boolean
|
|
35
|
+
/** Hover trigger: delay (ms) before opening. Default 0. */
|
|
36
|
+
openDelay?: number
|
|
37
|
+
/** Hover trigger: delay (ms) before closing after leave. Default 120. */
|
|
38
|
+
closeDelay?: number
|
|
39
|
+
/** Estimated panel height, used before the panel has been measured. */
|
|
26
40
|
estimatedHeight?: number
|
|
27
41
|
/** Force a minimum panel width (else it hugs its content). */
|
|
28
42
|
minWidth?: number
|
|
@@ -39,6 +53,11 @@
|
|
|
39
53
|
open = $bindable(false),
|
|
40
54
|
onOpenChange,
|
|
41
55
|
trigger = 'click',
|
|
56
|
+
placement = 'bottom-start',
|
|
57
|
+
offset = 8,
|
|
58
|
+
arrow = true,
|
|
59
|
+
openDelay = 0,
|
|
60
|
+
closeDelay = 120,
|
|
42
61
|
estimatedHeight = 220,
|
|
43
62
|
minWidth,
|
|
44
63
|
closeOnOutsideClick = true,
|
|
@@ -47,43 +66,60 @@
|
|
|
47
66
|
children,
|
|
48
67
|
}: Props = $props()
|
|
49
68
|
|
|
69
|
+
const ARROW = 10
|
|
70
|
+
|
|
50
71
|
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
51
72
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
52
|
-
let
|
|
73
|
+
let pos = $state<ComputePositionResult>({
|
|
74
|
+
x: 0, y: 0, placement, side: 'bottom', align: 'start', maxWidth: 0, maxHeight: 0,
|
|
75
|
+
})
|
|
53
76
|
|
|
54
77
|
function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
|
|
55
78
|
function toggle() { setOpen(!open) }
|
|
56
79
|
|
|
57
80
|
function updatePos() {
|
|
58
81
|
if (!anchorEl) return
|
|
59
|
-
|
|
82
|
+
const r = anchorEl.getBoundingClientRect()
|
|
83
|
+
const f = panelEl?.getBoundingClientRect()
|
|
84
|
+
const floating = {
|
|
85
|
+
width: f?.width || minWidth || r.width,
|
|
86
|
+
height: f?.height || estimatedHeight,
|
|
87
|
+
}
|
|
88
|
+
pos = computePosition(
|
|
89
|
+
{ x: r.left, y: r.top, width: r.width, height: r.height },
|
|
90
|
+
floating,
|
|
91
|
+
{ placement, offset, padding: 8, minMainAxis: 96, arrow: arrow ? { size: ARROW } : undefined },
|
|
92
|
+
)
|
|
60
93
|
}
|
|
61
94
|
|
|
95
|
+
// Reposition whenever the trigger/panel resize or an ancestor scrolls, and wire
|
|
96
|
+
// Escape + outside-click dismissal via the shared layer stack (so a popover
|
|
97
|
+
// nested in a dialog closes top-first). Re-runs once `panelEl` mounts.
|
|
62
98
|
$effect(() => {
|
|
63
|
-
if (!open) return
|
|
64
|
-
updatePos
|
|
65
|
-
const rp = () => updatePos()
|
|
66
|
-
window.addEventListener('scroll', rp, true)
|
|
67
|
-
window.addEventListener('resize', rp)
|
|
68
|
-
// Escape + outside-click dismissal via the shared layer stack (so a popover
|
|
69
|
-
// nested in a dialog closes top-first). The trigger is passed alongside the
|
|
70
|
-
// panel so clicking it toggles rather than dismiss-then-reopen.
|
|
99
|
+
if (!open || !anchorEl || !panelEl) return
|
|
100
|
+
const stop = autoUpdate(anchorEl, panelEl, updatePos)
|
|
71
101
|
const layer = createDismissableLayer({
|
|
72
102
|
element: () => [anchorEl, panelEl],
|
|
73
103
|
onDismiss: () => setOpen(false),
|
|
74
104
|
closeOnOutside: closeOnOutsideClick,
|
|
75
105
|
})
|
|
76
106
|
layer.activate()
|
|
77
|
-
return () => {
|
|
78
|
-
window.removeEventListener('scroll', rp, true)
|
|
79
|
-
window.removeEventListener('resize', rp)
|
|
80
|
-
layer.release()
|
|
81
|
-
}
|
|
107
|
+
return () => { stop(); layer.release() }
|
|
82
108
|
})
|
|
83
109
|
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
function
|
|
110
|
+
let openTimer: ReturnType<typeof setTimeout> | undefined
|
|
111
|
+
let closeTimer: ReturnType<typeof setTimeout> | undefined
|
|
112
|
+
function onEnter() {
|
|
113
|
+
if (trigger !== 'hover') return
|
|
114
|
+
clearTimeout(closeTimer)
|
|
115
|
+
if (open) return
|
|
116
|
+
openTimer = setTimeout(() => setOpen(true), openDelay)
|
|
117
|
+
}
|
|
118
|
+
function onLeave() {
|
|
119
|
+
if (trigger !== 'hover') return
|
|
120
|
+
clearTimeout(openTimer)
|
|
121
|
+
closeTimer = setTimeout(() => setOpen(false), closeDelay)
|
|
122
|
+
}
|
|
87
123
|
|
|
88
124
|
/** Fallback accessible name for the panel when `ariaLabel` isn't given: the
|
|
89
125
|
* anchor's own aria-label, else its trimmed text content. */
|
|
@@ -112,11 +148,11 @@
|
|
|
112
148
|
bind:this={panelEl}
|
|
113
149
|
class="sv-pop__panel"
|
|
114
150
|
use:portalToBody
|
|
115
|
-
use:popIn={{ up:
|
|
151
|
+
use:popIn={{ up: pos.side === 'top' }}
|
|
116
152
|
style:position="fixed"
|
|
117
|
-
style:top={`${
|
|
118
|
-
style:left={`${
|
|
119
|
-
style:min-width={minWidth ? `${
|
|
153
|
+
style:top={`${pos.y}px`}
|
|
154
|
+
style:left={`${pos.x}px`}
|
|
155
|
+
style:min-width={minWidth ? `${minWidth}px` : undefined}
|
|
120
156
|
role="dialog"
|
|
121
157
|
aria-label={ariaLabel ?? anchorFallbackLabel()}
|
|
122
158
|
onpointerenter={onEnter}
|
|
@@ -124,16 +160,38 @@
|
|
|
124
160
|
onfocusin={onEnter}
|
|
125
161
|
onfocusout={onLeave}
|
|
126
162
|
>
|
|
127
|
-
{
|
|
163
|
+
{#if arrow}
|
|
164
|
+
<span
|
|
165
|
+
class="sv-pop__arrow sv-pop__arrow--{pos.side}"
|
|
166
|
+
style:left={pos.arrow?.x != null ? `${pos.arrow.x}px` : undefined}
|
|
167
|
+
style:top={pos.arrow?.y != null ? `${pos.arrow.y}px` : undefined}
|
|
168
|
+
></span>
|
|
169
|
+
{/if}
|
|
170
|
+
<div class="sv-pop__content" style:max-height={pos.maxHeight ? `${pos.maxHeight}px` : undefined}>
|
|
171
|
+
{@render children?.()}
|
|
172
|
+
</div>
|
|
128
173
|
</div>
|
|
129
174
|
{/if}
|
|
130
175
|
|
|
131
176
|
<style>
|
|
132
177
|
.sv-pop__anchor { display: inline-flex; }
|
|
133
178
|
:global(.sv-pop__panel) {
|
|
134
|
-
z-index: 2147483646; max-width: min(92vw, 360px);
|
|
179
|
+
z-index: 2147483646; max-width: min(92vw, 360px);
|
|
135
180
|
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
136
181
|
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
137
182
|
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px; line-height: 1.55;
|
|
138
183
|
}
|
|
184
|
+
:global(.sv-pop__content) { padding: 12px 14px; overflow: auto; }
|
|
185
|
+
/* Arrow: a rotated square straddling the panel edge, two borders showing so it
|
|
186
|
+
reads as a continuation of the panel outline. Not clipped (sibling of the
|
|
187
|
+
scrollable content, panel itself has no overflow). */
|
|
188
|
+
:global(.sv-pop__arrow) {
|
|
189
|
+
position: absolute; width: 10px; height: 10px;
|
|
190
|
+
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
|
191
|
+
transform: rotate(45deg);
|
|
192
|
+
}
|
|
193
|
+
:global(.sv-pop__arrow--bottom) { top: -6px; border-right: 0; border-bottom: 0; }
|
|
194
|
+
:global(.sv-pop__arrow--top) { bottom: -6px; border-left: 0; border-top: 0; }
|
|
195
|
+
:global(.sv-pop__arrow--right) { left: -6px; border-top: 0; border-right: 0; }
|
|
196
|
+
:global(.sv-pop__arrow--left) { right: -6px; border-bottom: 0; border-left: 0; }
|
|
139
197
|
</style>
|
package/src/SvRadioGroup.svelte
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
onChange,
|
|
26
26
|
name,
|
|
27
27
|
disabled = false,
|
|
28
|
+
readonly = false,
|
|
28
29
|
orientation = 'vertical',
|
|
29
30
|
size = 'md',
|
|
30
31
|
ariaLabel,
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
const rg = createRadioGroup({
|
|
45
46
|
options: () => options,
|
|
46
47
|
value: () => value,
|
|
47
|
-
onChange: (v) => onChange?.(v),
|
|
48
|
+
onChange: (v) => { if (readonly) return; onChange?.(v) },
|
|
48
49
|
disabled: () => disabled,
|
|
49
50
|
// A visible field `label` also names the group for assistive tech.
|
|
50
51
|
ariaLabel: () => ariaLabel ?? label,
|
|
@@ -60,7 +61,9 @@
|
|
|
60
61
|
<div
|
|
61
62
|
class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
|
|
62
63
|
class:is-invalid={invalid}
|
|
64
|
+
class:is-readonly={readonly}
|
|
63
65
|
{...rg.groupProps()}
|
|
66
|
+
aria-readonly={readonly ? 'true' : undefined}
|
|
64
67
|
>
|
|
65
68
|
{#each options as opt (opt.value)}
|
|
66
69
|
<button
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
.sv-radiogroup--md .sv-radio { font-size: 13px; }
|
|
89
92
|
.sv-radiogroup--lg .sv-radio { font-size: 15px; }
|
|
90
93
|
.sv-radio[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
94
|
+
.sv-radiogroup.is-readonly .sv-radio { cursor: default; }
|
|
91
95
|
.sv-radio__dot {
|
|
92
96
|
width: 17px; height: 17px; flex: none; border-radius: 50%;
|
|
93
97
|
border: 1.5px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvResult - a centered result / status page for the outcome of an operation or
|
|
4
|
+
* a route state: success, error, warning, info, or not-found. An icon (built-in
|
|
5
|
+
* per status, or your own), a title, an optional description, and an actions
|
|
6
|
+
* slot. For empty data tables prefer SvEmptyState; use this for full-page
|
|
7
|
+
* outcomes ("Payment complete", "Something went wrong", "404").
|
|
8
|
+
*
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <SvResult status="success" title="Payment complete" description="A receipt is on its way.">
|
|
11
|
+
* {#snippet actions()}<SvButton>Back to dashboard</SvButton>{/snippet}
|
|
12
|
+
* </SvResult>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import type { Snippet } from 'svelte'
|
|
16
|
+
|
|
17
|
+
type Status = 'success' | 'error' | 'warning' | 'info' | 'notfound'
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
status?: Status
|
|
21
|
+
title: string
|
|
22
|
+
description?: string
|
|
23
|
+
/** Replace the built-in status icon. */
|
|
24
|
+
icon?: Snippet
|
|
25
|
+
/** Action buttons row under the text. */
|
|
26
|
+
actions?: Snippet
|
|
27
|
+
children?: Snippet
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { status = 'info', title, description, icon, actions, children }: Props = $props()
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="sv-result sv-result--{status}" role="status">
|
|
34
|
+
<div class="sv-result__icon" aria-hidden="true">
|
|
35
|
+
{#if icon}{@render icon()}
|
|
36
|
+
{:else if status === 'success'}
|
|
37
|
+
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
|
|
38
|
+
{:else if status === 'error'}
|
|
39
|
+
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12" /></svg>
|
|
40
|
+
{:else if status === 'warning'}
|
|
41
|
+
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" /></svg>
|
|
42
|
+
{:else if status === 'notfound'}
|
|
43
|
+
<span class="sv-result__code">404</span>
|
|
44
|
+
{:else}
|
|
45
|
+
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8h.01M11 12h1v4h1" /></svg>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
<h2 class="sv-result__title">{title}</h2>
|
|
49
|
+
{#if description}<p class="sv-result__desc">{description}</p>{/if}
|
|
50
|
+
{#if children}<div class="sv-result__body">{@render children()}</div>{/if}
|
|
51
|
+
{#if actions}<div class="sv-result__actions">{@render actions()}</div>{/if}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
.sv-result {
|
|
56
|
+
--_c: var(--sg-accent, #2563eb);
|
|
57
|
+
display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px;
|
|
58
|
+
padding: 40px 24px; max-width: 460px; margin: 0 auto;
|
|
59
|
+
}
|
|
60
|
+
.sv-result--success { --_c: var(--sg-success, #16a34a); }
|
|
61
|
+
.sv-result--error { --_c: var(--sg-danger, #dc2626); }
|
|
62
|
+
.sv-result--warning { --_c: var(--sg-warning, #f59e0b); }
|
|
63
|
+
.sv-result--notfound { --_c: var(--sg-muted, #64748b); }
|
|
64
|
+
.sv-result__icon {
|
|
65
|
+
display: grid; place-items: center; width: 60px; height: 60px; border-radius: 50%; margin-bottom: 4px;
|
|
66
|
+
color: var(--_c); background: color-mix(in srgb, var(--_c) 12%, transparent);
|
|
67
|
+
}
|
|
68
|
+
.sv-result__code { font-size: 22px; font-weight: 800; letter-spacing: 0.02em; }
|
|
69
|
+
.sv-result__title { margin: 0; font-size: 19px; font-weight: 700; color: var(--sg-fg, #0f172a); }
|
|
70
|
+
.sv-result__desc { margin: 0; font-size: 14px; line-height: 1.5; color: var(--sg-muted, #64748b); }
|
|
71
|
+
.sv-result__body { margin-top: 6px; }
|
|
72
|
+
.sv-result__actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; justify-content: center; }
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type { RadioOption as SegmentedOption } from './createRadioGroup.svelte'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvSegmented - a segmented control: a compact row of mutually-exclusive
|
|
8
|
+
* options in a shared track (the modern alternative to a small radio group or a
|
|
9
|
+
* tab strip for a setting). Single-select, keyboard-accessible (it reuses the
|
|
10
|
+
* radio-group core: arrow keys roam, Space/Enter select), and themed from the
|
|
11
|
+
* shared `--sg-*` tokens.
|
|
12
|
+
*
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <SvSegmented options={[{value:'list',label:'List'},{value:'grid',label:'Grid'}]} bind:value />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte'
|
|
18
|
+
import SvField from './SvField.svelte'
|
|
19
|
+
import { createRadioGroup, type RadioOption } from './createRadioGroup.svelte'
|
|
20
|
+
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
21
|
+
|
|
22
|
+
type SegOption = RadioOption & { icon?: Snippet }
|
|
23
|
+
|
|
24
|
+
type Props = Pick<
|
|
25
|
+
SvEditorProps,
|
|
26
|
+
'disabled' | 'size' | 'dir' | 'id' | 'ariaLabel' | 'label' | 'hint' | 'error' | 'required' | 'invalid'
|
|
27
|
+
> & {
|
|
28
|
+
options: ReadonlyArray<SegOption>
|
|
29
|
+
value?: string | number | null
|
|
30
|
+
onChange?: (value: string | number) => void
|
|
31
|
+
/** Stretch the control (and split options evenly) to the container width. */
|
|
32
|
+
block?: boolean
|
|
33
|
+
name?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
options,
|
|
38
|
+
value = $bindable<string | number | null>(null),
|
|
39
|
+
onChange,
|
|
40
|
+
disabled = false,
|
|
41
|
+
size = 'md',
|
|
42
|
+
dir,
|
|
43
|
+
id,
|
|
44
|
+
ariaLabel,
|
|
45
|
+
label,
|
|
46
|
+
hint,
|
|
47
|
+
error,
|
|
48
|
+
required = false,
|
|
49
|
+
invalid = false,
|
|
50
|
+
block = false,
|
|
51
|
+
name,
|
|
52
|
+
}: Props = $props()
|
|
53
|
+
|
|
54
|
+
const autoId = nextEditorId('sv-seg')
|
|
55
|
+
const uid = $derived(id ?? autoId)
|
|
56
|
+
|
|
57
|
+
const rg = createRadioGroup({
|
|
58
|
+
options: () => options,
|
|
59
|
+
value: () => value,
|
|
60
|
+
onChange: (v) => { value = v; onChange?.(v) },
|
|
61
|
+
disabled: () => disabled,
|
|
62
|
+
ariaLabel: () => ariaLabel ?? label,
|
|
63
|
+
id: () => uid,
|
|
64
|
+
invalid: () => invalid,
|
|
65
|
+
required: () => required,
|
|
66
|
+
error: () => error,
|
|
67
|
+
})
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
71
|
+
<div class="sv-seg sv-seg--{size}" class:is-block={block} class:is-disabled={disabled} class:is-invalid={invalid} {...rg.groupProps()}>
|
|
72
|
+
{#each options as opt (opt.value)}
|
|
73
|
+
<button
|
|
74
|
+
class="sv-seg__opt"
|
|
75
|
+
class:is-selected={rg.isChecked(opt)}
|
|
76
|
+
{...rg.radioProps(opt)}
|
|
77
|
+
>
|
|
78
|
+
{#if opt.icon}<span class="sv-seg__icon">{@render opt.icon()}</span>{/if}
|
|
79
|
+
<span class="sv-seg__label">{opt.label}</span>
|
|
80
|
+
</button>
|
|
81
|
+
{/each}
|
|
82
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
83
|
+
</div>
|
|
84
|
+
</SvField>
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.sv-seg {
|
|
88
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
89
|
+
display: inline-flex; gap: 2px; padding: 2px; box-sizing: border-box;
|
|
90
|
+
background: var(--sg-row-hover-bg, #f1f5f9); border-radius: calc(var(--sg-radius, 8px) + 2px);
|
|
91
|
+
border: 1px solid var(--sg-border, #e2e8f0);
|
|
92
|
+
}
|
|
93
|
+
.sv-seg.is-block { display: flex; width: 100%; }
|
|
94
|
+
.sv-seg.is-disabled { opacity: 0.6; }
|
|
95
|
+
.sv-seg__opt {
|
|
96
|
+
flex: 1 1 auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
97
|
+
padding: 0 12px; border: 0; border-radius: var(--sg-radius, 8px); background: none; cursor: pointer;
|
|
98
|
+
font: inherit; font-weight: 550; color: var(--sg-muted, #64748b); white-space: nowrap;
|
|
99
|
+
transition: background 0.14s, color 0.14s, box-shadow 0.14s;
|
|
100
|
+
}
|
|
101
|
+
.sv-seg--sm .sv-seg__opt { height: 26px; font-size: 12px; }
|
|
102
|
+
.sv-seg--md .sv-seg__opt { height: 32px; font-size: 13px; }
|
|
103
|
+
.sv-seg--lg .sv-seg__opt { height: 38px; font-size: 14.5px; }
|
|
104
|
+
.sv-seg__opt:hover:not(.is-selected):not([disabled]) { color: var(--sg-fg, #0f172a); }
|
|
105
|
+
.sv-seg__opt.is-selected {
|
|
106
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
107
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12);
|
|
108
|
+
}
|
|
109
|
+
.sv-seg__opt[disabled] { opacity: 0.5; cursor: default; }
|
|
110
|
+
.sv-seg__opt:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: -2px; }
|
|
111
|
+
.sv-seg.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
112
|
+
.sv-seg__icon { display: inline-flex; }
|
|
113
|
+
@media (prefers-reduced-motion: reduce) { .sv-seg__opt { transition: none; } }
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvSimpleGrid - a responsive grid primitive: lays children into `cols` equal
|
|
4
|
+
* columns that auto-fit down to a `minChildWidth` before wrapping, with a
|
|
5
|
+
* consistent `gap`. Use it for card galleries, stat tiles, and dashboards.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <SvSimpleGrid minChildWidth={220} gap={12}>{#each tiles as t}<Card {t} />{/each}</SvSimpleGrid>
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import type { Snippet } from 'svelte'
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
/**
|
|
15
|
+
* Fixed column count. When omitted, columns auto-fit based on
|
|
16
|
+
* `minChildWidth` (responsive by default).
|
|
17
|
+
*/
|
|
18
|
+
cols?: number
|
|
19
|
+
/** Minimum child width (px) before wrapping, for the auto-fit layout. Default 200. */
|
|
20
|
+
minChildWidth?: number
|
|
21
|
+
/** Gap between cells (px, or any CSS length). Default 12. */
|
|
22
|
+
gap?: number | string
|
|
23
|
+
children?: Snippet
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { cols, minChildWidth = 200, gap = 12, children }: Props = $props()
|
|
27
|
+
const g = $derived(typeof gap === 'number' ? `${gap}px` : gap)
|
|
28
|
+
const template = $derived(
|
|
29
|
+
cols != null
|
|
30
|
+
? `repeat(${cols}, minmax(0, 1fr))`
|
|
31
|
+
: `repeat(auto-fit, minmax(min(${minChildWidth}px, 100%), 1fr))`,
|
|
32
|
+
)
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<div class="sv-simplegrid" style:gap={g} style:grid-template-columns={template}>
|
|
36
|
+
{@render children?.()}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.sv-simplegrid { display: grid; min-width: 0; }
|
|
41
|
+
</style>
|