@svgrid/grid 2.2.25 → 2.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -145
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -728
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +250 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2723 -2719
- package/dist/SvGrid.svelte +3333 -3333
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +287 -287
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +131 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/a11y/dismissable.d.ts +11 -0
- package/dist/a11y/dismissable.js +21 -0
- package/dist/ai.js +3 -3
- package/dist/cdn/GridMenus-BzxEYNtW.js +421 -0
- package/dist/cdn/GridMenus-Dr-nGEXn.js +417 -0
- package/dist/cdn/{SvDateTimePicker-CYC7gRER.js → SvDateTimePicker-B1sDc9Pz.js} +451 -444
- package/dist/cdn/{SvDateTimePicker-DtQKd4Ns.js → SvDateTimePicker-Bh0FTs1E.js} +114 -107
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-BKQsO5fZ.js} +1 -1
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bgk4hDs8.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-DSkHIQDd.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-DkCc2Z7b.js} +1 -1
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-BrbVaELm.js} +1 -1
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-i9rWvSKg.js} +1 -1
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CGxxopQ4.js} +1 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D08sVmAy.js} +1 -1
- package/dist/cdn/{src-DysW9qbk.js → src-Cw8d9U8j.js} +5753 -5753
- package/dist/cdn/{src-bYfr_7aR.js → src-cgs1wcUS.js} +8102 -8102
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +30 -13
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -728
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +250 -250
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3239 -3239
- package/src/SvGrid.css +2723 -2719
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +287 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +131 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -95
- package/src/a11y/dismissable.ts +114 -94
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -712
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-scroll.test.ts +102 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-GfCAHgkZ.js +0 -420
- package/dist/cdn/GridMenus-GiVNSeDU.js +0 -416
package/src/SvContextMenu.svelte
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type { MenuItem } from './SvMenuList.svelte'
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<script lang="ts">
|
|
6
|
-
/**
|
|
7
|
-
* SvContextMenu - wraps a region and opens a menu at the pointer on right-click
|
|
8
|
-
* (or long-press). Reuses SvMenuList (submenus, keyboard, icons, shortcuts),
|
|
9
|
-
* portals to <body>, and closes via the shared dismissable layer stack.
|
|
10
|
-
* Parity: Smart context-menu.
|
|
11
|
-
*
|
|
12
|
-
* ```svelte
|
|
13
|
-
* <SvContextMenu items={items} onSelect={(i) => run(i)}>
|
|
14
|
-
* <div class="drop-zone">Right-click me</div>
|
|
15
|
-
* </SvContextMenu>
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { Snippet } from 'svelte'
|
|
19
|
-
import { portalToBody, popIn } from './popover'
|
|
20
|
-
import { createDismissableLayer } from './a11y/dismissable'
|
|
21
|
-
import { createFocusTrap } from './a11y/focus-trap'
|
|
22
|
-
import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
|
|
23
|
-
|
|
24
|
-
type Props = {
|
|
25
|
-
items: ReadonlyArray<MenuItem>
|
|
26
|
-
onSelect?: (item: MenuItem) => void
|
|
27
|
-
disabled?: boolean
|
|
28
|
-
ariaLabel?: string
|
|
29
|
-
children?: Snippet
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let { items, onSelect, disabled = false, ariaLabel, children }: Props = $props()
|
|
33
|
-
|
|
34
|
-
let open = $state(false)
|
|
35
|
-
let pos = $state({ x: 0, y: 0 })
|
|
36
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
37
|
-
let zoneEl = $state<HTMLDivElement | null>(null)
|
|
38
|
-
|
|
39
|
-
const MENU_W = 200
|
|
40
|
-
function openAt(clientX: number, clientY: number) {
|
|
41
|
-
// Clamp so the menu stays on-screen (estimate height from item count).
|
|
42
|
-
const estH = Math.min(items.length, 12) * 34 + 8
|
|
43
|
-
const x = Math.min(clientX, window.innerWidth - MENU_W - 6)
|
|
44
|
-
const y = Math.min(clientY, window.innerHeight - estH - 6)
|
|
45
|
-
pos = { x: Math.max(6, x), y: Math.max(6, y) }
|
|
46
|
-
open = true
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function onContextMenu(e: MouseEvent) {
|
|
50
|
-
if (disabled) return
|
|
51
|
-
e.preventDefault()
|
|
52
|
-
openAt(e.clientX, e.clientY)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Keyboard equivalent of a right-click: the ContextMenu key, or Shift+F10.
|
|
56
|
-
// Anchors at the focused zone's own position instead of the (nonexistent)
|
|
57
|
-
// pointer coordinates.
|
|
58
|
-
function onZoneKeydown(e: KeyboardEvent) {
|
|
59
|
-
if (disabled) return
|
|
60
|
-
const isMenuKey = e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)
|
|
61
|
-
if (!isMenuKey) return
|
|
62
|
-
e.preventDefault()
|
|
63
|
-
const rect = zoneEl?.getBoundingClientRect()
|
|
64
|
-
openAt(rect ? rect.left : 0, rect ? rect.bottom : 0)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
$effect(() => {
|
|
68
|
-
if (!open || !panelEl) return
|
|
69
|
-
const trap = createFocusTrap(panelEl, {
|
|
70
|
-
initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
|
|
71
|
-
})
|
|
72
|
-
trap.activate()
|
|
73
|
-
const layer = createDismissableLayer({
|
|
74
|
-
element: () => panelEl,
|
|
75
|
-
onDismiss: () => (open = false),
|
|
76
|
-
})
|
|
77
|
-
layer.activate()
|
|
78
|
-
// Reposition-close on scroll (a moved anchor would leave the menu stranded).
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return () => { layer.release(); trap.release();
|
|
82
|
-
})
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
|
86
|
-
<div class="sv-ctx__zone" bind:this={zoneEl} tabindex={disabled ? -1 : 0} oncontextmenu={onContextMenu} onkeydown={onZoneKeydown}>
|
|
87
|
-
{@render children?.()}
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
{#if open}
|
|
91
|
-
<div
|
|
92
|
-
bind:this={panelEl}
|
|
93
|
-
class="sv-ctx"
|
|
94
|
-
use:portalToBody
|
|
95
|
-
use:popIn={{}}
|
|
96
|
-
style:position="fixed"
|
|
97
|
-
style:top={`${pos.y}px`}
|
|
98
|
-
style:left={`${pos.x}px`}
|
|
99
|
-
aria-label={ariaLabel}
|
|
100
|
-
>
|
|
101
|
-
<SvMenuList {items} onclose={() => (open = false)} onselect={(i) => onSelect?.(i)} />
|
|
102
|
-
</div>
|
|
103
|
-
{/if}
|
|
104
|
-
|
|
105
|
-
<style>
|
|
106
|
-
/* display:contents generates no box and per spec cannot receive focus - it
|
|
107
|
-
must be a real box for the keyboard-open path (tabindex) to work at all. */
|
|
108
|
-
.sv-ctx__zone { display: block; }
|
|
109
|
-
.sv-ctx__zone:focus-visible { outline: 2px solid var(--sg-accent, #6366f1); outline-offset: 1px; }
|
|
110
|
-
:global(.sv-ctx) {
|
|
111
|
-
z-index: 2147483646; min-width: 200px;
|
|
112
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
113
|
-
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
114
|
-
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
115
|
-
}
|
|
116
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type { MenuItem } from './SvMenuList.svelte'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvContextMenu - wraps a region and opens a menu at the pointer on right-click
|
|
8
|
+
* (or long-press). Reuses SvMenuList (submenus, keyboard, icons, shortcuts),
|
|
9
|
+
* portals to <body>, and closes via the shared dismissable layer stack.
|
|
10
|
+
* Parity: Smart context-menu.
|
|
11
|
+
*
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <SvContextMenu items={items} onSelect={(i) => run(i)}>
|
|
14
|
+
* <div class="drop-zone">Right-click me</div>
|
|
15
|
+
* </SvContextMenu>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Snippet } from 'svelte'
|
|
19
|
+
import { portalToBody, popIn } from './popover'
|
|
20
|
+
import { createDismissableLayer, onScrollOutside } from './a11y/dismissable'
|
|
21
|
+
import { createFocusTrap } from './a11y/focus-trap'
|
|
22
|
+
import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
items: ReadonlyArray<MenuItem>
|
|
26
|
+
onSelect?: (item: MenuItem) => void
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
ariaLabel?: string
|
|
29
|
+
children?: Snippet
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let { items, onSelect, disabled = false, ariaLabel, children }: Props = $props()
|
|
33
|
+
|
|
34
|
+
let open = $state(false)
|
|
35
|
+
let pos = $state({ x: 0, y: 0 })
|
|
36
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
37
|
+
let zoneEl = $state<HTMLDivElement | null>(null)
|
|
38
|
+
|
|
39
|
+
const MENU_W = 200
|
|
40
|
+
function openAt(clientX: number, clientY: number) {
|
|
41
|
+
// Clamp so the menu stays on-screen (estimate height from item count).
|
|
42
|
+
const estH = Math.min(items.length, 12) * 34 + 8
|
|
43
|
+
const x = Math.min(clientX, window.innerWidth - MENU_W - 6)
|
|
44
|
+
const y = Math.min(clientY, window.innerHeight - estH - 6)
|
|
45
|
+
pos = { x: Math.max(6, x), y: Math.max(6, y) }
|
|
46
|
+
open = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onContextMenu(e: MouseEvent) {
|
|
50
|
+
if (disabled) return
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
openAt(e.clientX, e.clientY)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Keyboard equivalent of a right-click: the ContextMenu key, or Shift+F10.
|
|
56
|
+
// Anchors at the focused zone's own position instead of the (nonexistent)
|
|
57
|
+
// pointer coordinates.
|
|
58
|
+
function onZoneKeydown(e: KeyboardEvent) {
|
|
59
|
+
if (disabled) return
|
|
60
|
+
const isMenuKey = e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)
|
|
61
|
+
if (!isMenuKey) return
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
const rect = zoneEl?.getBoundingClientRect()
|
|
64
|
+
openAt(rect ? rect.left : 0, rect ? rect.bottom : 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (!open || !panelEl) return
|
|
69
|
+
const trap = createFocusTrap(panelEl, {
|
|
70
|
+
initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
|
|
71
|
+
})
|
|
72
|
+
trap.activate()
|
|
73
|
+
const layer = createDismissableLayer({
|
|
74
|
+
element: () => panelEl,
|
|
75
|
+
onDismiss: () => (open = false),
|
|
76
|
+
})
|
|
77
|
+
layer.activate()
|
|
78
|
+
// Reposition-close on scroll (a moved anchor would leave the menu stranded).
|
|
79
|
+
// Scrolling the menu's own item list doesn't count as the anchor moving.
|
|
80
|
+
const offScroll = onScrollOutside(() => panelEl, () => (open = false))
|
|
81
|
+
return () => { layer.release(); trap.release(); offScroll() }
|
|
82
|
+
})
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
|
86
|
+
<div class="sv-ctx__zone" bind:this={zoneEl} tabindex={disabled ? -1 : 0} oncontextmenu={onContextMenu} onkeydown={onZoneKeydown}>
|
|
87
|
+
{@render children?.()}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{#if open}
|
|
91
|
+
<div
|
|
92
|
+
bind:this={panelEl}
|
|
93
|
+
class="sv-ctx"
|
|
94
|
+
use:portalToBody
|
|
95
|
+
use:popIn={{}}
|
|
96
|
+
style:position="fixed"
|
|
97
|
+
style:top={`${pos.y}px`}
|
|
98
|
+
style:left={`${pos.x}px`}
|
|
99
|
+
aria-label={ariaLabel}
|
|
100
|
+
>
|
|
101
|
+
<SvMenuList {items} onclose={() => (open = false)} onselect={(i) => onSelect?.(i)} />
|
|
102
|
+
</div>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
/* display:contents generates no box and per spec cannot receive focus - it
|
|
107
|
+
must be a real box for the keyboard-open path (tabindex) to work at all. */
|
|
108
|
+
.sv-ctx__zone { display: block; }
|
|
109
|
+
.sv-ctx__zone:focus-visible { outline: 2px solid var(--sg-accent, #6366f1); outline-offset: 1px; }
|
|
110
|
+
:global(.sv-ctx) {
|
|
111
|
+
z-index: 2147483646; min-width: 200px;
|
|
112
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
113
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
114
|
+
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvCountryInput - a searchable country picker (flag + name + dial code).
|
|
4
|
-
* Emits the ISO 3166-1 alpha-2 code. Parity: Smart country-input pattern.
|
|
5
|
-
* Data from countries.ts; popover via popover.ts.
|
|
6
|
-
*
|
|
7
|
-
* One styled renderer over the headless `createCountryInput` core (search +
|
|
8
|
-
* roving active index + keyboard); only portal/measure/focus render concerns
|
|
9
|
-
* live here.
|
|
10
|
-
*/
|
|
11
|
-
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
12
|
-
import { flagEmoji } from './countries'
|
|
13
|
-
import SvField from './SvField.svelte'
|
|
14
|
-
import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
|
|
15
|
-
import { createCountryInput } from './createCountryInput.svelte'
|
|
16
|
-
|
|
17
|
-
/** User-facing strings (localizable via `messages`). */
|
|
18
|
-
type CountryMessages = { search: string; noResults: string }
|
|
19
|
-
const DEFAULT_MESSAGES: CountryMessages = { search: 'Search countries', noResults: 'No matches' }
|
|
20
|
-
|
|
21
|
-
type Props = SvEditorProps & {
|
|
22
|
-
value?: string | null
|
|
23
|
-
onChange?: (code: string) => void
|
|
24
|
-
/** Show the dial code beside each country. */
|
|
25
|
-
showDial?: boolean
|
|
26
|
-
placeholder?: string
|
|
27
|
-
/** Override the built-in strings (search + empty-state). */
|
|
28
|
-
messages?: Partial<CountryMessages>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let {
|
|
32
|
-
value = $bindable<string | null>(null),
|
|
33
|
-
onChange,
|
|
34
|
-
showDial = false,
|
|
35
|
-
placeholder = 'Select country…',
|
|
36
|
-
disabled = false,
|
|
37
|
-
name,
|
|
38
|
-
size = 'md',
|
|
39
|
-
ariaLabel,
|
|
40
|
-
invalid = false,
|
|
41
|
-
required = false,
|
|
42
|
-
error,
|
|
43
|
-
label,
|
|
44
|
-
hint,
|
|
45
|
-
dir,
|
|
46
|
-
id,
|
|
47
|
-
messages,
|
|
48
|
-
}: Props = $props()
|
|
49
|
-
|
|
50
|
-
const autoId = nextEditorId('sv-country')
|
|
51
|
-
const uid = $derived(id ?? autoId)
|
|
52
|
-
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
53
|
-
|
|
54
|
-
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
55
|
-
let searchEl = $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
|
-
|
|
59
|
-
const ci = createCountryInput({
|
|
60
|
-
value: () => value,
|
|
61
|
-
onChange: (c) => { value = c; onChange?.(c) },
|
|
62
|
-
disabled: () => disabled,
|
|
63
|
-
ariaLabel: () => ariaLabel,
|
|
64
|
-
searchLabel: () => M.search,
|
|
65
|
-
focusTrigger: () => triggerEl?.focus(),
|
|
66
|
-
id: () => uid,
|
|
67
|
-
invalid: () => invalid,
|
|
68
|
-
required: () => required,
|
|
69
|
-
error: () => error,
|
|
70
|
-
hint: () => hint,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const selected = $derived(ci.selected)
|
|
74
|
-
|
|
75
|
-
function updatePos() { if (triggerEl) rect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 320, minWidth: 240 }) }
|
|
76
|
-
|
|
77
|
-
$effect(() => {
|
|
78
|
-
if (!ci.open) return
|
|
79
|
-
updatePos()
|
|
80
|
-
queueMicrotask(() => searchEl?.focus())
|
|
81
|
-
const rp = () => updatePos()
|
|
82
|
-
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
83
|
-
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; ci.close() }
|
|
84
|
-
document.addEventListener('pointerdown', od, true)
|
|
85
|
-
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
86
|
-
})
|
|
87
|
-
// Keep the active option scrolled into view (render concern).
|
|
88
|
-
$effect(() => {
|
|
89
|
-
if (!ci.open) return
|
|
90
|
-
const i = ci.activeIndex
|
|
91
|
-
queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
92
|
-
})
|
|
93
|
-
</script>
|
|
94
|
-
|
|
95
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
96
|
-
<button bind:this={triggerEl} class="sv-country sv-country--{size}" class:is-open={ci.open} class:is-disabled={disabled} class:is-invalid={invalid} {...ci.triggerProps()}>
|
|
97
|
-
{#if selected}
|
|
98
|
-
<span class="sv-country__flag" aria-hidden="true">{flagEmoji(selected.code)}</span>
|
|
99
|
-
<span class="sv-country__name">{selected.name}</span>
|
|
100
|
-
{#if showDial}<span class="sv-country__dial">{selected.dial}</span>{/if}
|
|
101
|
-
{:else}
|
|
102
|
-
<span class="sv-country__ph">{placeholder}</span>
|
|
103
|
-
{/if}
|
|
104
|
-
<svg class="sv-country__chev" viewBox="0 0 24 24" width="15" height="15" 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>
|
|
105
|
-
</button>
|
|
106
|
-
</SvField>
|
|
107
|
-
|
|
108
|
-
{#if ci.open}
|
|
109
|
-
<div bind:this={panelEl} class="sv-country__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="dialog">
|
|
110
|
-
<input bind:this={searchEl} class="sv-country__search" placeholder={`${M.search}…`} {...ci.searchProps()} />
|
|
111
|
-
<div class="sv-country__list" {...ci.listboxProps()}>
|
|
112
|
-
{#each ci.filtered as c, i (c.code)}
|
|
113
|
-
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
114
|
-
<div class="sv-country__opt" class:is-active={ci.isActive(i)} class:is-selected={ci.isSelected(c.code)} {...ci.optionProps(i)}>
|
|
115
|
-
<span class="sv-country__flag" aria-hidden="true">{flagEmoji(c.code)}</span>
|
|
116
|
-
<span class="sv-country__name">{c.name}</span>
|
|
117
|
-
<span class="sv-country__dial">{c.dial}</span>
|
|
118
|
-
</div>
|
|
119
|
-
{:else}
|
|
120
|
-
<div class="sv-ddl__empty">{M.noResults}</div>
|
|
121
|
-
{/each}
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
{/if}
|
|
125
|
-
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
126
|
-
|
|
127
|
-
<style>
|
|
128
|
-
.sv-country {
|
|
129
|
-
--_accent: var(--sg-accent, #2563eb);
|
|
130
|
-
display: inline-flex; align-items: center; gap: 8px; width: 220px;
|
|
131
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: start;
|
|
132
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); padding: 0 10px; cursor: pointer;
|
|
133
|
-
}
|
|
134
|
-
.sv-country--sm { height: 28px; font-size: 12px; }
|
|
135
|
-
.sv-country--md { height: 34px; font-size: 13px; }
|
|
136
|
-
.sv-country--lg { height: 40px; font-size: 15px; }
|
|
137
|
-
.sv-country.is-open, .sv-country:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); outline: none; }
|
|
138
|
-
.sv-country.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
139
|
-
.sv-country.is-invalid.is-open, .sv-country.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
140
|
-
.sv-country.is-disabled { opacity: 0.6; cursor: not-allowed; }
|
|
141
|
-
.sv-country__flag { font-size: 16px; }
|
|
142
|
-
.sv-country__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
143
|
-
.sv-country__ph { flex: 1; color: var(--sg-muted, #94a3b8); }
|
|
144
|
-
.sv-country__dial { color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
145
|
-
.sv-country__chev { color: var(--sg-muted, #64748b); flex: none; }
|
|
146
|
-
|
|
147
|
-
:global(.sv-country__panel) {
|
|
148
|
-
z-index: 2147483647; display: flex; flex-direction: column; max-height: 340px; padding: 6px;
|
|
149
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
150
|
-
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
|
|
151
|
-
}
|
|
152
|
-
:global(.sv-country__search) {
|
|
153
|
-
height: 32px; margin-bottom: 4px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
154
|
-
border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 7px; background: var(--sg-input-bg, #fff); color: inherit; outline: none;
|
|
155
|
-
}
|
|
156
|
-
:global(.sv-country__list) { overflow-y: auto; }
|
|
157
|
-
:global(.sv-country__opt) { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
158
|
-
:global(.sv-country__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
159
|
-
:global(.sv-country__opt.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
160
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvCountryInput - a searchable country picker (flag + name + dial code).
|
|
4
|
+
* Emits the ISO 3166-1 alpha-2 code. Parity: Smart country-input pattern.
|
|
5
|
+
* Data from countries.ts; popover via popover.ts.
|
|
6
|
+
*
|
|
7
|
+
* One styled renderer over the headless `createCountryInput` core (search +
|
|
8
|
+
* roving active index + keyboard); only portal/measure/focus render concerns
|
|
9
|
+
* live here.
|
|
10
|
+
*/
|
|
11
|
+
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
12
|
+
import { flagEmoji } from './countries'
|
|
13
|
+
import SvField from './SvField.svelte'
|
|
14
|
+
import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
|
|
15
|
+
import { createCountryInput } from './createCountryInput.svelte'
|
|
16
|
+
|
|
17
|
+
/** User-facing strings (localizable via `messages`). */
|
|
18
|
+
type CountryMessages = { search: string; noResults: string }
|
|
19
|
+
const DEFAULT_MESSAGES: CountryMessages = { search: 'Search countries', noResults: 'No matches' }
|
|
20
|
+
|
|
21
|
+
type Props = SvEditorProps & {
|
|
22
|
+
value?: string | null
|
|
23
|
+
onChange?: (code: string) => void
|
|
24
|
+
/** Show the dial code beside each country. */
|
|
25
|
+
showDial?: boolean
|
|
26
|
+
placeholder?: string
|
|
27
|
+
/** Override the built-in strings (search + empty-state). */
|
|
28
|
+
messages?: Partial<CountryMessages>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
value = $bindable<string | null>(null),
|
|
33
|
+
onChange,
|
|
34
|
+
showDial = false,
|
|
35
|
+
placeholder = 'Select country…',
|
|
36
|
+
disabled = false,
|
|
37
|
+
name,
|
|
38
|
+
size = 'md',
|
|
39
|
+
ariaLabel,
|
|
40
|
+
invalid = false,
|
|
41
|
+
required = false,
|
|
42
|
+
error,
|
|
43
|
+
label,
|
|
44
|
+
hint,
|
|
45
|
+
dir,
|
|
46
|
+
id,
|
|
47
|
+
messages,
|
|
48
|
+
}: Props = $props()
|
|
49
|
+
|
|
50
|
+
const autoId = nextEditorId('sv-country')
|
|
51
|
+
const uid = $derived(id ?? autoId)
|
|
52
|
+
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
53
|
+
|
|
54
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
55
|
+
let searchEl = $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
|
+
|
|
59
|
+
const ci = createCountryInput({
|
|
60
|
+
value: () => value,
|
|
61
|
+
onChange: (c) => { value = c; onChange?.(c) },
|
|
62
|
+
disabled: () => disabled,
|
|
63
|
+
ariaLabel: () => ariaLabel,
|
|
64
|
+
searchLabel: () => M.search,
|
|
65
|
+
focusTrigger: () => triggerEl?.focus(),
|
|
66
|
+
id: () => uid,
|
|
67
|
+
invalid: () => invalid,
|
|
68
|
+
required: () => required,
|
|
69
|
+
error: () => error,
|
|
70
|
+
hint: () => hint,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const selected = $derived(ci.selected)
|
|
74
|
+
|
|
75
|
+
function updatePos() { if (triggerEl) rect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 320, minWidth: 240 }) }
|
|
76
|
+
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (!ci.open) return
|
|
79
|
+
updatePos()
|
|
80
|
+
queueMicrotask(() => searchEl?.focus())
|
|
81
|
+
const rp = () => updatePos()
|
|
82
|
+
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
83
|
+
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; ci.close() }
|
|
84
|
+
document.addEventListener('pointerdown', od, true)
|
|
85
|
+
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
86
|
+
})
|
|
87
|
+
// Keep the active option scrolled into view (render concern).
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (!ci.open) return
|
|
90
|
+
const i = ci.activeIndex
|
|
91
|
+
queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
92
|
+
})
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
96
|
+
<button bind:this={triggerEl} class="sv-country sv-country--{size}" class:is-open={ci.open} class:is-disabled={disabled} class:is-invalid={invalid} {...ci.triggerProps()}>
|
|
97
|
+
{#if selected}
|
|
98
|
+
<span class="sv-country__flag" aria-hidden="true">{flagEmoji(selected.code)}</span>
|
|
99
|
+
<span class="sv-country__name">{selected.name}</span>
|
|
100
|
+
{#if showDial}<span class="sv-country__dial">{selected.dial}</span>{/if}
|
|
101
|
+
{:else}
|
|
102
|
+
<span class="sv-country__ph">{placeholder}</span>
|
|
103
|
+
{/if}
|
|
104
|
+
<svg class="sv-country__chev" viewBox="0 0 24 24" width="15" height="15" 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>
|
|
105
|
+
</button>
|
|
106
|
+
</SvField>
|
|
107
|
+
|
|
108
|
+
{#if ci.open}
|
|
109
|
+
<div bind:this={panelEl} class="sv-country__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="dialog">
|
|
110
|
+
<input bind:this={searchEl} class="sv-country__search" placeholder={`${M.search}…`} {...ci.searchProps()} />
|
|
111
|
+
<div class="sv-country__list" {...ci.listboxProps()}>
|
|
112
|
+
{#each ci.filtered as c, i (c.code)}
|
|
113
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
114
|
+
<div class="sv-country__opt" class:is-active={ci.isActive(i)} class:is-selected={ci.isSelected(c.code)} {...ci.optionProps(i)}>
|
|
115
|
+
<span class="sv-country__flag" aria-hidden="true">{flagEmoji(c.code)}</span>
|
|
116
|
+
<span class="sv-country__name">{c.name}</span>
|
|
117
|
+
<span class="sv-country__dial">{c.dial}</span>
|
|
118
|
+
</div>
|
|
119
|
+
{:else}
|
|
120
|
+
<div class="sv-ddl__empty">{M.noResults}</div>
|
|
121
|
+
{/each}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
126
|
+
|
|
127
|
+
<style>
|
|
128
|
+
.sv-country {
|
|
129
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
130
|
+
display: inline-flex; align-items: center; gap: 8px; width: 220px;
|
|
131
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: start;
|
|
132
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); padding: 0 10px; cursor: pointer;
|
|
133
|
+
}
|
|
134
|
+
.sv-country--sm { height: 28px; font-size: 12px; }
|
|
135
|
+
.sv-country--md { height: 34px; font-size: 13px; }
|
|
136
|
+
.sv-country--lg { height: 40px; font-size: 15px; }
|
|
137
|
+
.sv-country.is-open, .sv-country:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); outline: none; }
|
|
138
|
+
.sv-country.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
139
|
+
.sv-country.is-invalid.is-open, .sv-country.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
140
|
+
.sv-country.is-disabled { opacity: 0.6; cursor: not-allowed; }
|
|
141
|
+
.sv-country__flag { font-size: 16px; }
|
|
142
|
+
.sv-country__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
143
|
+
.sv-country__ph { flex: 1; color: var(--sg-muted, #94a3b8); }
|
|
144
|
+
.sv-country__dial { color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
145
|
+
.sv-country__chev { color: var(--sg-muted, #64748b); flex: none; }
|
|
146
|
+
|
|
147
|
+
:global(.sv-country__panel) {
|
|
148
|
+
z-index: 2147483647; display: flex; flex-direction: column; max-height: 340px; padding: 6px;
|
|
149
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
150
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
|
|
151
|
+
}
|
|
152
|
+
:global(.sv-country__search) {
|
|
153
|
+
height: 32px; margin-bottom: 4px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
154
|
+
border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 7px; background: var(--sg-input-bg, #fff); color: inherit; outline: none;
|
|
155
|
+
}
|
|
156
|
+
:global(.sv-country__list) { overflow-y: auto; }
|
|
157
|
+
:global(.sv-country__opt) { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
158
|
+
:global(.sv-country__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
159
|
+
:global(.sv-country__opt.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
160
|
+
</style>
|