@svgrid/grid 2.2.27 → 2.2.29
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 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +782 -734
- 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 +254 -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.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- 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 +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- 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 +133 -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/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- 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/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- 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 +254 -250
- package/src/SvDrawer.test.ts +18 -1
- 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 +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -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 +326 -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/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -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 -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 +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/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -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/createListbox.svelte.ts +39 -7
- 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 +949 -859
- package/src/editing.ts +709 -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/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- 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 +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- 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.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -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-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
package/src/SvPopover.svelte
CHANGED
|
@@ -1,197 +1,197 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvPopover - a floating panel anchored to a trigger, portalled to <body> so it
|
|
4
|
-
* escapes any `overflow:hidden` ancestor. Open on click (default), hover, or
|
|
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.
|
|
10
|
-
*
|
|
11
|
-
* ```svelte
|
|
12
|
-
* <SvPopover placement="bottom" arrow>
|
|
13
|
-
* {#snippet anchor()}<button>Open</button>{/snippet}
|
|
14
|
-
* <p>Panel content</p>
|
|
15
|
-
* </SvPopover>
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { Snippet } from 'svelte'
|
|
19
|
-
import { portalToBody, popIn } from './popover'
|
|
20
|
-
import { computePosition, autoUpdate, type Placement, type ComputePositionResult } from './positioning'
|
|
21
|
-
import { createDismissableLayer } from './a11y/dismissable'
|
|
22
|
-
|
|
23
|
-
type Props = {
|
|
24
|
-
/** Controlled open state (bindable). */
|
|
25
|
-
open?: boolean
|
|
26
|
-
onOpenChange?: (open: boolean) => void
|
|
27
|
-
/** How the anchor opens the panel. `manual` = only `open` drives it. */
|
|
28
|
-
trigger?: 'click' | 'hover' | 'manual'
|
|
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. */
|
|
40
|
-
estimatedHeight?: number
|
|
41
|
-
/** Force a minimum panel width (else it hugs its content). */
|
|
42
|
-
minWidth?: number
|
|
43
|
-
/** Close when clicking outside. Default true. */
|
|
44
|
-
closeOnOutsideClick?: boolean
|
|
45
|
-
ariaLabel?: string
|
|
46
|
-
/** The anchor element(s) that open the panel. */
|
|
47
|
-
anchor?: Snippet
|
|
48
|
-
/** Panel content. */
|
|
49
|
-
children?: Snippet
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let {
|
|
53
|
-
open = $bindable(false),
|
|
54
|
-
onOpenChange,
|
|
55
|
-
trigger = 'click',
|
|
56
|
-
placement = 'bottom-start',
|
|
57
|
-
offset = 8,
|
|
58
|
-
arrow = true,
|
|
59
|
-
openDelay = 0,
|
|
60
|
-
closeDelay = 120,
|
|
61
|
-
estimatedHeight = 220,
|
|
62
|
-
minWidth,
|
|
63
|
-
closeOnOutsideClick = true,
|
|
64
|
-
ariaLabel,
|
|
65
|
-
anchor,
|
|
66
|
-
children,
|
|
67
|
-
}: Props = $props()
|
|
68
|
-
|
|
69
|
-
const ARROW = 10
|
|
70
|
-
|
|
71
|
-
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
72
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
73
|
-
let pos = $state<ComputePositionResult>({
|
|
74
|
-
x: 0, y: 0, placement, side: 'bottom', align: 'start', maxWidth: 0, maxHeight: 0,
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
|
|
78
|
-
function toggle() { setOpen(!open) }
|
|
79
|
-
|
|
80
|
-
function updatePos() {
|
|
81
|
-
if (!anchorEl) return
|
|
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
|
-
)
|
|
93
|
-
}
|
|
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.
|
|
98
|
-
$effect(() => {
|
|
99
|
-
if (!open || !anchorEl || !panelEl) return
|
|
100
|
-
const stop = autoUpdate(anchorEl, panelEl, updatePos)
|
|
101
|
-
const layer = createDismissableLayer({
|
|
102
|
-
element: () => [anchorEl, panelEl],
|
|
103
|
-
onDismiss: () => setOpen(false),
|
|
104
|
-
closeOnOutside: closeOnOutsideClick,
|
|
105
|
-
})
|
|
106
|
-
layer.activate()
|
|
107
|
-
return () => { stop(); layer.release() }
|
|
108
|
-
})
|
|
109
|
-
|
|
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
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Fallback accessible name for the panel when `ariaLabel` isn't given: the
|
|
125
|
-
* anchor's own aria-label, else its trimmed text content. */
|
|
126
|
-
function anchorFallbackLabel(): string | undefined {
|
|
127
|
-
const label = anchorEl?.getAttribute('aria-label')
|
|
128
|
-
if (label) return label
|
|
129
|
-
return anchorEl?.textContent?.trim() || undefined
|
|
130
|
-
}
|
|
131
|
-
</script>
|
|
132
|
-
|
|
133
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
134
|
-
<span
|
|
135
|
-
bind:this={anchorEl}
|
|
136
|
-
class="sv-pop__anchor"
|
|
137
|
-
onclick={() => trigger === 'click' && toggle()}
|
|
138
|
-
onpointerenter={onEnter}
|
|
139
|
-
onpointerleave={onLeave}
|
|
140
|
-
onfocusin={onEnter}
|
|
141
|
-
onfocusout={onLeave}
|
|
142
|
-
>
|
|
143
|
-
{@render anchor?.()}
|
|
144
|
-
</span>
|
|
145
|
-
|
|
146
|
-
{#if open}
|
|
147
|
-
<div
|
|
148
|
-
bind:this={panelEl}
|
|
149
|
-
class="sv-pop__panel"
|
|
150
|
-
use:portalToBody
|
|
151
|
-
use:popIn={{ up: pos.side === 'top' }}
|
|
152
|
-
style:position="fixed"
|
|
153
|
-
style:top={`${pos.y}px`}
|
|
154
|
-
style:left={`${pos.x}px`}
|
|
155
|
-
style:min-width={minWidth ? `${minWidth}px` : undefined}
|
|
156
|
-
role="dialog"
|
|
157
|
-
aria-label={ariaLabel ?? anchorFallbackLabel()}
|
|
158
|
-
onpointerenter={onEnter}
|
|
159
|
-
onpointerleave={onLeave}
|
|
160
|
-
onfocusin={onEnter}
|
|
161
|
-
onfocusout={onLeave}
|
|
162
|
-
>
|
|
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>
|
|
173
|
-
</div>
|
|
174
|
-
{/if}
|
|
175
|
-
|
|
176
|
-
<style>
|
|
177
|
-
.sv-pop__anchor { display: inline-flex; }
|
|
178
|
-
:global(.sv-pop__panel) {
|
|
179
|
-
z-index: 2147483646; max-width: min(92vw, 360px);
|
|
180
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
181
|
-
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
182
|
-
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px; line-height: 1.55;
|
|
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; }
|
|
197
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvPopover - a floating panel anchored to a trigger, portalled to <body> so it
|
|
4
|
+
* escapes any `overflow:hidden` ancestor. Open on click (default), hover, or
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <SvPopover placement="bottom" arrow>
|
|
13
|
+
* {#snippet anchor()}<button>Open</button>{/snippet}
|
|
14
|
+
* <p>Panel content</p>
|
|
15
|
+
* </SvPopover>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Snippet } from 'svelte'
|
|
19
|
+
import { portalToBody, popIn } from './popover'
|
|
20
|
+
import { computePosition, autoUpdate, type Placement, type ComputePositionResult } from './positioning'
|
|
21
|
+
import { createDismissableLayer } from './a11y/dismissable'
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
/** Controlled open state (bindable). */
|
|
25
|
+
open?: boolean
|
|
26
|
+
onOpenChange?: (open: boolean) => void
|
|
27
|
+
/** How the anchor opens the panel. `manual` = only `open` drives it. */
|
|
28
|
+
trigger?: 'click' | 'hover' | 'manual'
|
|
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. */
|
|
40
|
+
estimatedHeight?: number
|
|
41
|
+
/** Force a minimum panel width (else it hugs its content). */
|
|
42
|
+
minWidth?: number
|
|
43
|
+
/** Close when clicking outside. Default true. */
|
|
44
|
+
closeOnOutsideClick?: boolean
|
|
45
|
+
ariaLabel?: string
|
|
46
|
+
/** The anchor element(s) that open the panel. */
|
|
47
|
+
anchor?: Snippet
|
|
48
|
+
/** Panel content. */
|
|
49
|
+
children?: Snippet
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let {
|
|
53
|
+
open = $bindable(false),
|
|
54
|
+
onOpenChange,
|
|
55
|
+
trigger = 'click',
|
|
56
|
+
placement = 'bottom-start',
|
|
57
|
+
offset = 8,
|
|
58
|
+
arrow = true,
|
|
59
|
+
openDelay = 0,
|
|
60
|
+
closeDelay = 120,
|
|
61
|
+
estimatedHeight = 220,
|
|
62
|
+
minWidth,
|
|
63
|
+
closeOnOutsideClick = true,
|
|
64
|
+
ariaLabel,
|
|
65
|
+
anchor,
|
|
66
|
+
children,
|
|
67
|
+
}: Props = $props()
|
|
68
|
+
|
|
69
|
+
const ARROW = 10
|
|
70
|
+
|
|
71
|
+
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
72
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
73
|
+
let pos = $state<ComputePositionResult>({
|
|
74
|
+
x: 0, y: 0, placement, side: 'bottom', align: 'start', maxWidth: 0, maxHeight: 0,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
|
|
78
|
+
function toggle() { setOpen(!open) }
|
|
79
|
+
|
|
80
|
+
function updatePos() {
|
|
81
|
+
if (!anchorEl) return
|
|
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
|
+
)
|
|
93
|
+
}
|
|
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.
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (!open || !anchorEl || !panelEl) return
|
|
100
|
+
const stop = autoUpdate(anchorEl, panelEl, updatePos)
|
|
101
|
+
const layer = createDismissableLayer({
|
|
102
|
+
element: () => [anchorEl, panelEl],
|
|
103
|
+
onDismiss: () => setOpen(false),
|
|
104
|
+
closeOnOutside: closeOnOutsideClick,
|
|
105
|
+
})
|
|
106
|
+
layer.activate()
|
|
107
|
+
return () => { stop(); layer.release() }
|
|
108
|
+
})
|
|
109
|
+
|
|
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
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Fallback accessible name for the panel when `ariaLabel` isn't given: the
|
|
125
|
+
* anchor's own aria-label, else its trimmed text content. */
|
|
126
|
+
function anchorFallbackLabel(): string | undefined {
|
|
127
|
+
const label = anchorEl?.getAttribute('aria-label')
|
|
128
|
+
if (label) return label
|
|
129
|
+
return anchorEl?.textContent?.trim() || undefined
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
134
|
+
<span
|
|
135
|
+
bind:this={anchorEl}
|
|
136
|
+
class="sv-pop__anchor"
|
|
137
|
+
onclick={() => trigger === 'click' && toggle()}
|
|
138
|
+
onpointerenter={onEnter}
|
|
139
|
+
onpointerleave={onLeave}
|
|
140
|
+
onfocusin={onEnter}
|
|
141
|
+
onfocusout={onLeave}
|
|
142
|
+
>
|
|
143
|
+
{@render anchor?.()}
|
|
144
|
+
</span>
|
|
145
|
+
|
|
146
|
+
{#if open}
|
|
147
|
+
<div
|
|
148
|
+
bind:this={panelEl}
|
|
149
|
+
class="sv-pop__panel"
|
|
150
|
+
use:portalToBody
|
|
151
|
+
use:popIn={{ up: pos.side === 'top' }}
|
|
152
|
+
style:position="fixed"
|
|
153
|
+
style:top={`${pos.y}px`}
|
|
154
|
+
style:left={`${pos.x}px`}
|
|
155
|
+
style:min-width={minWidth ? `${minWidth}px` : undefined}
|
|
156
|
+
role="dialog"
|
|
157
|
+
aria-label={ariaLabel ?? anchorFallbackLabel()}
|
|
158
|
+
onpointerenter={onEnter}
|
|
159
|
+
onpointerleave={onLeave}
|
|
160
|
+
onfocusin={onEnter}
|
|
161
|
+
onfocusout={onLeave}
|
|
162
|
+
>
|
|
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>
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
<style>
|
|
177
|
+
.sv-pop__anchor { display: inline-flex; }
|
|
178
|
+
:global(.sv-pop__panel) {
|
|
179
|
+
z-index: 2147483646; max-width: min(92vw, 360px);
|
|
180
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
181
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
182
|
+
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px; line-height: 1.55;
|
|
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; }
|
|
197
|
+
</style>
|
package/src/SvRadioGroup.svelte
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type { RadioOption } from './createRadioGroup.svelte'
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<script lang="ts">
|
|
6
|
-
/**
|
|
7
|
-
* SvRadioGroup - an accessible radio group (WAI-ARIA radiogroup pattern) with
|
|
8
|
-
* roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
|
|
9
|
-
* (as a managed group). Controlled via `value` + `onChange`.
|
|
10
|
-
*/
|
|
11
|
-
import SvField from './SvField.svelte'
|
|
12
|
-
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
13
|
-
import { createRadioGroup, type RadioOption } from './createRadioGroup.svelte'
|
|
14
|
-
|
|
15
|
-
type Props = SvEditorProps & {
|
|
16
|
-
options: ReadonlyArray<RadioOption>
|
|
17
|
-
value?: string | number | null
|
|
18
|
-
onChange?: (value: string | number) => void
|
|
19
|
-
orientation?: 'vertical' | 'horizontal'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let {
|
|
23
|
-
options,
|
|
24
|
-
value = null,
|
|
25
|
-
onChange,
|
|
26
|
-
name,
|
|
27
|
-
disabled = false,
|
|
28
|
-
readonly = false,
|
|
29
|
-
orientation = 'vertical',
|
|
30
|
-
size = 'md',
|
|
31
|
-
ariaLabel,
|
|
32
|
-
invalid = false,
|
|
33
|
-
required = false,
|
|
34
|
-
error,
|
|
35
|
-
label,
|
|
36
|
-
hint,
|
|
37
|
-
dir,
|
|
38
|
-
id,
|
|
39
|
-
}: Props = $props()
|
|
40
|
-
|
|
41
|
-
const autoId = nextEditorId('sv-radio')
|
|
42
|
-
const uid = $derived(id ?? autoId)
|
|
43
|
-
|
|
44
|
-
// The styled radio group is just a renderer over the headless core.
|
|
45
|
-
const rg = createRadioGroup({
|
|
46
|
-
options: () => options,
|
|
47
|
-
value: () => value,
|
|
48
|
-
onChange: (v) => { if (readonly) return; onChange?.(v) },
|
|
49
|
-
disabled: () => disabled,
|
|
50
|
-
// A visible field `label` also names the group for assistive tech.
|
|
51
|
-
ariaLabel: () => ariaLabel ?? label,
|
|
52
|
-
id: () => uid,
|
|
53
|
-
invalid: () => invalid,
|
|
54
|
-
required: () => required,
|
|
55
|
-
error: () => error,
|
|
56
|
-
hint: () => hint,
|
|
57
|
-
})
|
|
58
|
-
</script>
|
|
59
|
-
|
|
60
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
61
|
-
<div
|
|
62
|
-
class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
|
|
63
|
-
class:is-invalid={invalid}
|
|
64
|
-
class:is-readonly={readonly}
|
|
65
|
-
{...rg.groupProps()}
|
|
66
|
-
aria-readonly={readonly ? 'true' : undefined}
|
|
67
|
-
>
|
|
68
|
-
{#each options as opt (opt.value)}
|
|
69
|
-
<button
|
|
70
|
-
class="sv-radio"
|
|
71
|
-
class:is-checked={opt.value === value}
|
|
72
|
-
{...rg.radioProps(opt)}
|
|
73
|
-
>
|
|
74
|
-
<span class="sv-radio__dot"></span>
|
|
75
|
-
<span class="sv-radio__label">{opt.label}</span>
|
|
76
|
-
</button>
|
|
77
|
-
{/each}
|
|
78
|
-
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
79
|
-
</div>
|
|
80
|
-
</SvField>
|
|
81
|
-
|
|
82
|
-
<style>
|
|
83
|
-
.sv-radiogroup { --_accent: var(--sg-accent, #2563eb); display: flex; gap: 10px; }
|
|
84
|
-
.sv-radiogroup--vertical { flex-direction: column; }
|
|
85
|
-
.sv-radiogroup--horizontal { flex-direction: row; flex-wrap: wrap; }
|
|
86
|
-
.sv-radio {
|
|
87
|
-
display: inline-flex; align-items: center; gap: 8px; background: none; border: 0; padding: 2px;
|
|
88
|
-
font: inherit; color: var(--sg-fg, #0f172a); cursor: pointer; user-select: none;
|
|
89
|
-
}
|
|
90
|
-
.sv-radiogroup--sm .sv-radio { font-size: 12px; }
|
|
91
|
-
.sv-radiogroup--md .sv-radio { font-size: 13px; }
|
|
92
|
-
.sv-radiogroup--lg .sv-radio { font-size: 15px; }
|
|
93
|
-
.sv-radio[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
94
|
-
.sv-radiogroup.is-readonly .sv-radio { cursor: default; }
|
|
95
|
-
.sv-radio__dot {
|
|
96
|
-
width: 17px; height: 17px; flex: none; border-radius: 50%;
|
|
97
|
-
border: 1.5px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
|
|
98
|
-
display: grid; place-items: center; transition: border-color 0.12s;
|
|
99
|
-
}
|
|
100
|
-
.sv-radiogroup.is-invalid .sv-radio__dot { border-color: var(--sg-danger, #dc2626); }
|
|
101
|
-
.sv-radio.is-checked .sv-radio__dot { border-color: var(--_accent); }
|
|
102
|
-
.sv-radio.is-checked .sv-radio__dot::after {
|
|
103
|
-
content: ''; width: 9px; height: 9px; border-radius: 50%; background: var(--_accent);
|
|
104
|
-
}
|
|
105
|
-
.sv-radio:focus-visible { outline: none; }
|
|
106
|
-
.sv-radio:focus-visible .sv-radio__dot { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
107
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type { RadioOption } from './createRadioGroup.svelte'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvRadioGroup - an accessible radio group (WAI-ARIA radiogroup pattern) with
|
|
8
|
+
* roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
|
|
9
|
+
* (as a managed group). Controlled via `value` + `onChange`.
|
|
10
|
+
*/
|
|
11
|
+
import SvField from './SvField.svelte'
|
|
12
|
+
import { nextEditorId, type SvEditorProps } from './editor-contract'
|
|
13
|
+
import { createRadioGroup, type RadioOption } from './createRadioGroup.svelte'
|
|
14
|
+
|
|
15
|
+
type Props = SvEditorProps & {
|
|
16
|
+
options: ReadonlyArray<RadioOption>
|
|
17
|
+
value?: string | number | null
|
|
18
|
+
onChange?: (value: string | number) => void
|
|
19
|
+
orientation?: 'vertical' | 'horizontal'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
options,
|
|
24
|
+
value = null,
|
|
25
|
+
onChange,
|
|
26
|
+
name,
|
|
27
|
+
disabled = false,
|
|
28
|
+
readonly = false,
|
|
29
|
+
orientation = 'vertical',
|
|
30
|
+
size = 'md',
|
|
31
|
+
ariaLabel,
|
|
32
|
+
invalid = false,
|
|
33
|
+
required = false,
|
|
34
|
+
error,
|
|
35
|
+
label,
|
|
36
|
+
hint,
|
|
37
|
+
dir,
|
|
38
|
+
id,
|
|
39
|
+
}: Props = $props()
|
|
40
|
+
|
|
41
|
+
const autoId = nextEditorId('sv-radio')
|
|
42
|
+
const uid = $derived(id ?? autoId)
|
|
43
|
+
|
|
44
|
+
// The styled radio group is just a renderer over the headless core.
|
|
45
|
+
const rg = createRadioGroup({
|
|
46
|
+
options: () => options,
|
|
47
|
+
value: () => value,
|
|
48
|
+
onChange: (v) => { if (readonly) return; onChange?.(v) },
|
|
49
|
+
disabled: () => disabled,
|
|
50
|
+
// A visible field `label` also names the group for assistive tech.
|
|
51
|
+
ariaLabel: () => ariaLabel ?? label,
|
|
52
|
+
id: () => uid,
|
|
53
|
+
invalid: () => invalid,
|
|
54
|
+
required: () => required,
|
|
55
|
+
error: () => error,
|
|
56
|
+
hint: () => hint,
|
|
57
|
+
})
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
61
|
+
<div
|
|
62
|
+
class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
|
|
63
|
+
class:is-invalid={invalid}
|
|
64
|
+
class:is-readonly={readonly}
|
|
65
|
+
{...rg.groupProps()}
|
|
66
|
+
aria-readonly={readonly ? 'true' : undefined}
|
|
67
|
+
>
|
|
68
|
+
{#each options as opt (opt.value)}
|
|
69
|
+
<button
|
|
70
|
+
class="sv-radio"
|
|
71
|
+
class:is-checked={opt.value === value}
|
|
72
|
+
{...rg.radioProps(opt)}
|
|
73
|
+
>
|
|
74
|
+
<span class="sv-radio__dot"></span>
|
|
75
|
+
<span class="sv-radio__label">{opt.label}</span>
|
|
76
|
+
</button>
|
|
77
|
+
{/each}
|
|
78
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
79
|
+
</div>
|
|
80
|
+
</SvField>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.sv-radiogroup { --_accent: var(--sg-accent, #2563eb); display: flex; gap: 10px; }
|
|
84
|
+
.sv-radiogroup--vertical { flex-direction: column; }
|
|
85
|
+
.sv-radiogroup--horizontal { flex-direction: row; flex-wrap: wrap; }
|
|
86
|
+
.sv-radio {
|
|
87
|
+
display: inline-flex; align-items: center; gap: 8px; background: none; border: 0; padding: 2px;
|
|
88
|
+
font: inherit; color: var(--sg-fg, #0f172a); cursor: pointer; user-select: none;
|
|
89
|
+
}
|
|
90
|
+
.sv-radiogroup--sm .sv-radio { font-size: 12px; }
|
|
91
|
+
.sv-radiogroup--md .sv-radio { font-size: 13px; }
|
|
92
|
+
.sv-radiogroup--lg .sv-radio { font-size: 15px; }
|
|
93
|
+
.sv-radio[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
94
|
+
.sv-radiogroup.is-readonly .sv-radio { cursor: default; }
|
|
95
|
+
.sv-radio__dot {
|
|
96
|
+
width: 17px; height: 17px; flex: none; border-radius: 50%;
|
|
97
|
+
border: 1.5px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
|
|
98
|
+
display: grid; place-items: center; transition: border-color 0.12s;
|
|
99
|
+
}
|
|
100
|
+
.sv-radiogroup.is-invalid .sv-radio__dot { border-color: var(--sg-danger, #dc2626); }
|
|
101
|
+
.sv-radio.is-checked .sv-radio__dot { border-color: var(--_accent); }
|
|
102
|
+
.sv-radio.is-checked .sv-radio__dot::after {
|
|
103
|
+
content: ''; width: 9px; height: 9px; border-radius: 50%; background: var(--_accent);
|
|
104
|
+
}
|
|
105
|
+
.sv-radio:focus-visible { outline: none; }
|
|
106
|
+
.sv-radio:focus-visible .sv-radio__dot { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
107
|
+
</style>
|