@svgrid/grid 1.2.22 → 1.2.23
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/dist/GridFooter.svelte +34 -20
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/SvAutoComplete.svelte +100 -0
- package/dist/SvAutoComplete.svelte.d.ts +16 -0
- package/dist/SvButton.svelte +97 -0
- package/dist/SvButton.svelte.d.ts +23 -0
- package/dist/SvCalendar.svelte +669 -0
- package/dist/SvCalendar.svelte.d.ts +70 -0
- package/dist/SvCheckBox.svelte +81 -0
- package/dist/SvCheckBox.svelte.d.ts +19 -0
- package/dist/SvColorInput.svelte +187 -0
- package/dist/SvColorInput.svelte.d.ts +15 -0
- package/dist/SvComboBox.svelte +130 -0
- package/dist/SvComboBox.svelte.d.ts +15 -0
- package/dist/SvCountryInput.svelte +123 -0
- package/dist/SvCountryInput.svelte.d.ts +14 -0
- package/dist/SvDateTimePicker.svelte +322 -0
- package/dist/SvDateTimePicker.svelte.d.ts +37 -0
- package/dist/SvDropDownList.svelte +143 -0
- package/dist/SvDropDownList.svelte.d.ts +15 -0
- package/dist/SvForm.svelte +147 -0
- package/dist/SvForm.svelte.d.ts +29 -0
- package/dist/SvGauge.svelte +99 -0
- package/dist/SvGauge.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +16 -2
- package/dist/SvGrid.controller.svelte.js +63 -8
- package/dist/SvGrid.css +84 -31
- package/dist/SvGrid.helpers.d.ts +4 -0
- package/dist/SvGrid.helpers.js +40 -12
- package/dist/SvGrid.svelte +84 -5
- package/dist/SvGrid.types.d.ts +36 -0
- package/dist/SvGridDropdown.svelte +16 -30
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvListBox.svelte +122 -0
- package/dist/SvListBox.svelte.d.ts +19 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvMaskedInput.svelte.d.ts +15 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +22 -0
- package/dist/SvPasswordInput.svelte +110 -0
- package/dist/SvPasswordInput.svelte.d.ts +22 -0
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvPhoneInput.svelte.d.ts +19 -0
- package/dist/SvRadioGroup.svelte +108 -0
- package/dist/SvRadioGroup.svelte.d.ts +23 -0
- package/dist/SvRating.svelte +111 -0
- package/dist/SvRating.svelte.d.ts +20 -0
- package/dist/SvRepeatButton.svelte +89 -0
- package/dist/SvRepeatButton.svelte.d.ts +21 -0
- package/dist/SvSlider.svelte +182 -0
- package/dist/SvSlider.svelte.d.ts +25 -0
- package/dist/SvSwitchButton.svelte +94 -0
- package/dist/SvSwitchButton.svelte.d.ts +19 -0
- package/dist/SvTabs.svelte +122 -0
- package/dist/SvTabs.svelte.d.ts +26 -0
- package/dist/SvTagsInput.svelte +81 -0
- package/dist/SvTagsInput.svelte.d.ts +19 -0
- package/dist/SvTimePicker.svelte +359 -0
- package/dist/SvTimePicker.svelte.d.ts +27 -0
- package/dist/SvToggleButton.svelte +57 -0
- package/dist/SvToggleButton.svelte.d.ts +16 -0
- package/dist/SvTree.svelte +210 -0
- package/dist/SvTree.svelte.d.ts +29 -0
- package/dist/build-api.js +78 -1
- package/dist/cdn/svgrid.js +10022 -5904
- package/dist/cdn/svgrid.svelte-external.js +8531 -4429
- package/dist/cell-render.d.ts +4 -0
- package/dist/cell-render.js +35 -1
- package/dist/clipboard.js +19 -1
- package/dist/collaboration.d.ts +2 -0
- package/dist/collaboration.js +4 -0
- package/dist/core.d.ts +22 -1
- package/dist/countries.d.ts +15 -0
- package/dist/countries.js +61 -0
- package/dist/datetime/date-core.d.ts +88 -0
- package/dist/datetime/date-core.js +165 -0
- package/dist/datetime/date-format.d.ts +39 -0
- package/dist/datetime/date-format.js +371 -0
- package/dist/datetime/date-restrict.d.ts +23 -0
- package/dist/datetime/date-restrict.js +50 -0
- package/dist/datetime/date-selection.d.ts +25 -0
- package/dist/datetime/date-selection.js +94 -0
- package/dist/datetime/mask.d.ts +21 -0
- package/dist/datetime/mask.js +90 -0
- package/dist/editing.d.ts +1 -1
- package/dist/editing.js +7 -0
- package/dist/editors/cell-editors.d.ts +2 -2
- package/dist/editors/cell-editors.js +3 -1
- package/dist/export-format.d.ts +178 -0
- package/dist/export-format.js +419 -0
- package/dist/hyperformula-adapter.d.ts +4 -0
- package/dist/hyperformula-adapter.js +15 -6
- package/dist/index.d.ts +36 -0
- package/dist/index.js +41 -0
- package/dist/list-option.d.ts +10 -0
- package/dist/list-option.js +7 -0
- package/dist/menus.js +18 -2
- package/dist/popover.d.ts +48 -0
- package/dist/popover.js +65 -0
- package/dist/row-resize.d.ts +4 -4
- package/dist/row-resize.js +8 -5
- package/dist/selection.d.ts +6 -0
- package/dist/selection.js +31 -5
- package/dist/server-data-source.d.ts +43 -0
- package/dist/server-data-source.js +100 -0
- package/dist/sparkline.js +8 -4
- package/dist/svgrid-wrapper.types.d.ts +26 -1
- package/package.json +6 -1
- package/src/GridFooter.svelte +34 -20
- package/src/SvAutoComplete.svelte +100 -0
- package/src/SvButton.svelte +97 -0
- package/src/SvCalendar.svelte +669 -0
- package/src/SvCalendar.test.ts +211 -0
- package/src/SvCheckBox.svelte +81 -0
- package/src/SvColorInput.svelte +187 -0
- package/src/SvComboBox.svelte +130 -0
- package/src/SvCountryInput.svelte +123 -0
- package/src/SvDateTimePicker.svelte +322 -0
- package/src/SvDateTimePicker.test.ts +136 -0
- package/src/SvDropDownList.svelte +143 -0
- package/src/SvForm.svelte +147 -0
- package/src/SvGauge.svelte +99 -0
- package/src/SvGrid.controller.svelte.ts +74 -8
- package/src/SvGrid.css +84 -31
- package/src/SvGrid.helpers.ts +41 -12
- package/src/SvGrid.svelte +84 -5
- package/src/SvGrid.types.ts +33 -0
- package/src/SvGridDropdown.svelte +16 -30
- package/src/SvListBox.svelte +122 -0
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +110 -0
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +108 -0
- package/src/SvRating.svelte +111 -0
- package/src/SvRepeatButton.svelte +89 -0
- package/src/SvSlider.svelte +182 -0
- package/src/SvSwitchButton.svelte +94 -0
- package/src/SvTabs.svelte +122 -0
- package/src/SvTagsInput.svelte +81 -0
- package/src/SvTimePicker.svelte +359 -0
- package/src/SvTimePicker.test.ts +98 -0
- package/src/SvToggleButton.svelte +57 -0
- package/src/SvTree.svelte +210 -0
- package/src/build-api.ts +115 -0
- package/src/cell-render.test.ts +38 -0
- package/src/cell-render.ts +43 -0
- package/src/clipboard.test.ts +18 -0
- package/src/clipboard.ts +24 -1
- package/src/collaboration.test.ts +30 -0
- package/src/collaboration.ts +6 -0
- package/src/core.ts +27 -3
- package/src/countries.ts +71 -0
- package/src/datetime/date-core.test.ts +217 -0
- package/src/datetime/date-core.ts +204 -0
- package/src/datetime/date-format.test.ts +121 -0
- package/src/datetime/date-format.ts +317 -0
- package/src/datetime/date-restrict.ts +60 -0
- package/src/datetime/date-selection.test.ts +129 -0
- package/src/datetime/date-selection.ts +137 -0
- package/src/datetime/mask.test.ts +36 -0
- package/src/datetime/mask.ts +84 -0
- package/src/editing.test.ts +22 -0
- package/src/editing.ts +7 -1
- package/src/editors/cell-editors.ts +7 -1
- package/src/export-data-api.test.ts +126 -0
- package/src/export-format.test.ts +107 -0
- package/src/export-format.ts +598 -0
- package/src/hyperformula-adapter.test.ts +35 -1
- package/src/hyperformula-adapter.ts +18 -6
- package/src/index.ts +71 -0
- package/src/list-option.ts +15 -0
- package/src/menus.ts +21 -2
- package/src/popover.ts +79 -0
- package/src/row-resize.test.ts +25 -0
- package/src/row-resize.ts +8 -5
- package/src/selection.test.ts +36 -4
- package/src/selection.ts +30 -9
- package/src/server-data-source.test.ts +201 -0
- package/src/server-data-source.ts +148 -0
- package/src/sparkline.test.ts +9 -0
- package/src/sparkline.ts +9 -4
- package/src/svgrid-wrapper.types.ts +28 -1
- package/src/svgrid.behavior.test.ts +27 -0
- package/src/svgrid.new-features.wrapper.test.ts +27 -1
- package/src/ui-buttons.test.ts +105 -0
- package/src/ui-composite.test.ts +89 -0
- package/src/ui-inputs.test.ts +92 -0
- package/src/ui-range.test.ts +61 -0
- package/src/ui-selection.test.ts +106 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type CalendarValue = Date | number | string | ReadonlyArray<Date | number | string> | null;
|
|
2
|
+
/** A one-click shortcut shown in the presets rail. `value` is a single date
|
|
3
|
+
* (or an [start, end] range for range mode); pass a function for "today"-
|
|
4
|
+
* relative shortcuts that resolve at click time. */
|
|
5
|
+
export type CalendarPreset = {
|
|
6
|
+
label: string;
|
|
7
|
+
value: Date | number | string | readonly [Date | number | string, Date | number | string] | (() => Date | number | string | readonly [Date | number | string, Date | number | string]);
|
|
8
|
+
};
|
|
9
|
+
/** How the calendar animates a navigation / drill. */
|
|
10
|
+
export type CalendarAnimation = 'slide' | 'fade' | 'none';
|
|
11
|
+
/**
|
|
12
|
+
* SvCalendar - a themeable, accessible month/year/decade calendar built on the
|
|
13
|
+
* framework-free date engine in ./datetime. It works standalone AND is the
|
|
14
|
+
* date cell editor for SvGrid (rich-by-default). All visuals come from the
|
|
15
|
+
* grid's `--sg-*` theme tokens, so every preset theme styles it for free.
|
|
16
|
+
*
|
|
17
|
+
* Parity target: Smart `smart-calendar` (selectionModes, min/max, restricted /
|
|
18
|
+
* important dates, week numbers, firstDayOfWeek, drill navigation, keyboard).
|
|
19
|
+
*/
|
|
20
|
+
import { type DateLike } from './datetime/date-core';
|
|
21
|
+
import { type SelectionMode } from './datetime/date-selection';
|
|
22
|
+
import { type RestrictOptions } from './datetime/date-restrict';
|
|
23
|
+
type NameFormat = 'narrow' | 'short' | 'long';
|
|
24
|
+
type DisplayMode = 'month' | 'year' | 'decade';
|
|
25
|
+
type Props = {
|
|
26
|
+
/** Selected value(s). Single Date for one/zeroOrOne, array for multi modes. */
|
|
27
|
+
value?: CalendarValue;
|
|
28
|
+
/** Fires with the full selected-day list on every change. */
|
|
29
|
+
onChange?: (dates: Date[]) => void;
|
|
30
|
+
/** Fires when the visible month/year/decade page changes. */
|
|
31
|
+
onNavigate?: (viewDate: Date, displayMode: DisplayMode) => void;
|
|
32
|
+
selectionMode?: SelectionMode;
|
|
33
|
+
min?: DateLike | null;
|
|
34
|
+
max?: DateLike | null;
|
|
35
|
+
/** Non-selectable dates (list or predicate). */
|
|
36
|
+
restrictedDates?: RestrictOptions['restrictedDates'];
|
|
37
|
+
/** Highlighted (but still selectable) dates. */
|
|
38
|
+
importantDates?: ReadonlyArray<DateLike> | ((d: Date) => boolean) | null;
|
|
39
|
+
/** 0 = Sunday .. 6 = Saturday. */
|
|
40
|
+
firstDayOfWeek?: number;
|
|
41
|
+
weeks?: number;
|
|
42
|
+
weekNumbers?: boolean;
|
|
43
|
+
/** Number of month panels side by side. */
|
|
44
|
+
months?: number;
|
|
45
|
+
hideDayNames?: boolean;
|
|
46
|
+
hideOtherMonthDays?: boolean;
|
|
47
|
+
dayNameFormat?: NameFormat;
|
|
48
|
+
monthNameFormat?: NameFormat;
|
|
49
|
+
/** Show the Today / Clear footer. */
|
|
50
|
+
footer?: boolean;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
readonly?: boolean;
|
|
53
|
+
locale?: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
/** Which drill level to open on. */
|
|
56
|
+
displayMode?: DisplayMode;
|
|
57
|
+
/** Animate month/year navigation + drill (opt-in). `true` = 'slide'. This is
|
|
58
|
+
* an explicit opt-in, so it plays regardless of the OS "reduce motion"
|
|
59
|
+
* setting - pass 'none' (or gate it yourself) to suppress it. */
|
|
60
|
+
animate?: boolean | CalendarAnimation;
|
|
61
|
+
/** Change the visible month/year/decade with the mouse wheel over the grid. */
|
|
62
|
+
wheelNavigation?: boolean;
|
|
63
|
+
/** Per-day tooltip text (shown as the native title on the day button). */
|
|
64
|
+
dateTooltip?: (date: Date) => string | null | undefined;
|
|
65
|
+
/** One-click shortcuts (e.g. Today, Last 7 days) shown in a side rail. */
|
|
66
|
+
presets?: ReadonlyArray<CalendarPreset>;
|
|
67
|
+
};
|
|
68
|
+
declare const SvCalendar: import("svelte").Component<Props, {}, "">;
|
|
69
|
+
type SvCalendar = ReturnType<typeof SvCalendar>;
|
|
70
|
+
export default SvCalendar;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvCheckBox - a themed checkbox with an indeterminate state and optional
|
|
4
|
+
* label. Parity: Smart `smart-check-box`. Controlled via `checked` + `onChange`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte'
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
checked?: boolean
|
|
10
|
+
indeterminate?: boolean
|
|
11
|
+
onChange?: (checked: boolean) => void
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
size?: 'sm' | 'md' | 'lg'
|
|
14
|
+
name?: string
|
|
15
|
+
value?: string
|
|
16
|
+
ariaLabel?: string
|
|
17
|
+
children?: Snippet
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
checked = false,
|
|
22
|
+
indeterminate = false,
|
|
23
|
+
onChange,
|
|
24
|
+
disabled = false,
|
|
25
|
+
size = 'md',
|
|
26
|
+
name,
|
|
27
|
+
value,
|
|
28
|
+
ariaLabel,
|
|
29
|
+
children,
|
|
30
|
+
}: Props = $props()
|
|
31
|
+
|
|
32
|
+
function toggle() {
|
|
33
|
+
if (disabled) return
|
|
34
|
+
onChange?.(indeterminate ? true : !checked)
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<label class="sv-check sv-check--{size}" class:is-disabled={disabled}>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
role="checkbox"
|
|
42
|
+
class="sv-check__box"
|
|
43
|
+
class:is-checked={checked && !indeterminate}
|
|
44
|
+
class:is-indeterminate={indeterminate}
|
|
45
|
+
aria-checked={indeterminate ? 'mixed' : checked}
|
|
46
|
+
aria-label={ariaLabel ?? (children ? undefined : 'checkbox')}
|
|
47
|
+
{disabled}
|
|
48
|
+
onclick={toggle}
|
|
49
|
+
>
|
|
50
|
+
{#if indeterminate}
|
|
51
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 8h8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" /></svg>
|
|
52
|
+
{:else if checked}
|
|
53
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-6.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
54
|
+
{/if}
|
|
55
|
+
</button>
|
|
56
|
+
{#if children}<span class="sv-check__label">{@render children()}</span>{/if}
|
|
57
|
+
{#if name}<input type="hidden" {name} value={checked ? (value ?? 'true') : ''} />{/if}
|
|
58
|
+
</label>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.sv-check {
|
|
62
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
63
|
+
--_sz: 18px;
|
|
64
|
+
display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
|
|
65
|
+
font: inherit; font-size: 13px; color: var(--sg-fg, #0f172a);
|
|
66
|
+
}
|
|
67
|
+
.sv-check--sm { --_sz: 15px; font-size: 12px; }
|
|
68
|
+
.sv-check--lg { --_sz: 22px; font-size: 15px; }
|
|
69
|
+
.sv-check.is-disabled { opacity: 0.55; cursor: not-allowed; }
|
|
70
|
+
.sv-check__box {
|
|
71
|
+
width: var(--_sz); height: var(--_sz); flex: none; padding: 0; cursor: inherit;
|
|
72
|
+
display: grid; place-items: center;
|
|
73
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-on-accent, #fff);
|
|
74
|
+
border: 1.5px solid var(--sg-border, #cbd5e1); border-radius: 5px; transition: background 0.12s, border-color 0.12s;
|
|
75
|
+
}
|
|
76
|
+
.sv-check__box svg { width: 100%; height: 100%; }
|
|
77
|
+
.sv-check__box.is-checked, .sv-check__box.is-indeterminate {
|
|
78
|
+
background: var(--_accent); border-color: var(--_accent);
|
|
79
|
+
}
|
|
80
|
+
.sv-check__box:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvCheckBox - a themed checkbox with an indeterminate state and optional
|
|
3
|
+
* label. Parity: Smart `smart-check-box`. Controlled via `checked` + `onChange`.
|
|
4
|
+
*/
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
type Props = {
|
|
7
|
+
checked?: boolean;
|
|
8
|
+
indeterminate?: boolean;
|
|
9
|
+
onChange?: (checked: boolean) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
name?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
};
|
|
17
|
+
declare const SvCheckBox: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type SvCheckBox = ReturnType<typeof SvCheckBox>;
|
|
19
|
+
export default SvCheckBox;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvColorInput - a color swatch that opens a portalled popover with a hex
|
|
4
|
+
* field, a native picker, and a preset palette. Parity: Smart `smart-color-input`.
|
|
5
|
+
* Emits a hex string. Popover escapes the grid scroll container via popover.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
value?: string
|
|
11
|
+
onChange?: (hex: string) => void
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
readonly?: boolean
|
|
14
|
+
/** Preset swatches. */
|
|
15
|
+
palette?: string[]
|
|
16
|
+
name?: string
|
|
17
|
+
size?: 'sm' | 'md' | 'lg'
|
|
18
|
+
ariaLabel?: string
|
|
19
|
+
autoOpen?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_PALETTE = [
|
|
23
|
+
'#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981', '#14b8a6',
|
|
24
|
+
'#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899',
|
|
25
|
+
'#f43f5e', '#64748b', '#0f172a', '#ffffff',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
value = '#3b82f6',
|
|
30
|
+
onChange,
|
|
31
|
+
disabled = false,
|
|
32
|
+
readonly = false,
|
|
33
|
+
palette = DEFAULT_PALETTE,
|
|
34
|
+
name,
|
|
35
|
+
size = 'md',
|
|
36
|
+
ariaLabel,
|
|
37
|
+
autoOpen = false,
|
|
38
|
+
}: Props = $props()
|
|
39
|
+
|
|
40
|
+
const isInteractive = $derived(!disabled && !readonly)
|
|
41
|
+
let open = $state(false)
|
|
42
|
+
let hexDraft = $state('')
|
|
43
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
44
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
45
|
+
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
46
|
+
|
|
47
|
+
const normalized = $derived(normalizeHex(value) ?? '#000000')
|
|
48
|
+
|
|
49
|
+
function normalizeHex(h: string): string | null {
|
|
50
|
+
let s = h.trim()
|
|
51
|
+
if (!s.startsWith('#')) s = '#' + s
|
|
52
|
+
if (/^#[0-9a-fA-F]{3}$/.test(s)) s = '#' + s.slice(1).split('').map((c) => c + c).join('')
|
|
53
|
+
return /^#[0-9a-fA-F]{6}$/.test(s) ? s.toLowerCase() : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function updatePos() {
|
|
57
|
+
if (!triggerEl) return
|
|
58
|
+
panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 210, minWidth: 200 })
|
|
59
|
+
}
|
|
60
|
+
function openPanel() {
|
|
61
|
+
if (!isInteractive || open) return
|
|
62
|
+
hexDraft = normalized
|
|
63
|
+
open = true
|
|
64
|
+
updatePos()
|
|
65
|
+
}
|
|
66
|
+
function toggle() { open ? (open = false) : openPanel() }
|
|
67
|
+
|
|
68
|
+
function pick(hex: string) {
|
|
69
|
+
const n = normalizeHex(hex)
|
|
70
|
+
if (n) onChange?.(n)
|
|
71
|
+
}
|
|
72
|
+
function commitHex() {
|
|
73
|
+
const n = normalizeHex(hexDraft)
|
|
74
|
+
if (n) onChange?.(n)
|
|
75
|
+
else hexDraft = normalized
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (!open) return
|
|
80
|
+
const reposition = () => updatePos()
|
|
81
|
+
window.addEventListener('scroll', reposition, true)
|
|
82
|
+
window.addEventListener('resize', reposition)
|
|
83
|
+
const onDown = (e: PointerEvent) => {
|
|
84
|
+
const t = e.target as Node | null
|
|
85
|
+
if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return
|
|
86
|
+
open = false
|
|
87
|
+
}
|
|
88
|
+
document.addEventListener('pointerdown', onDown, true)
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener('scroll', reposition, true)
|
|
91
|
+
window.removeEventListener('resize', reposition)
|
|
92
|
+
document.removeEventListener('pointerdown', onDown, true)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
function focusOpen(node: HTMLButtonElement) {
|
|
97
|
+
if (!autoOpen) return
|
|
98
|
+
node.focus(); openPanel()
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
bind:this={triggerEl}
|
|
105
|
+
class="sv-color sv-color--{size}"
|
|
106
|
+
class:is-disabled={disabled}
|
|
107
|
+
aria-haspopup="dialog"
|
|
108
|
+
aria-expanded={open}
|
|
109
|
+
aria-label={ariaLabel ?? `Color ${normalized}`}
|
|
110
|
+
disabled={!isInteractive}
|
|
111
|
+
onclick={toggle}
|
|
112
|
+
use:focusOpen
|
|
113
|
+
>
|
|
114
|
+
<span class="sv-color__swatch" style:background={normalized}></span>
|
|
115
|
+
<span class="sv-color__hex">{normalized}</span>
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
{#if open}
|
|
119
|
+
<div
|
|
120
|
+
class="sv-color__panel"
|
|
121
|
+
bind:this={panelEl}
|
|
122
|
+
use:portalToBody
|
|
123
|
+
style:position="fixed"
|
|
124
|
+
style:top={`${panelRect.top}px`}
|
|
125
|
+
style:left={`${panelRect.left}px`}
|
|
126
|
+
role="dialog"
|
|
127
|
+
aria-label="Choose color"
|
|
128
|
+
>
|
|
129
|
+
<div class="sv-color__top">
|
|
130
|
+
<input class="sv-color__native" type="color" value={normalized} oninput={(e) => pick((e.currentTarget as HTMLInputElement).value)} aria-label="Color picker" />
|
|
131
|
+
<input
|
|
132
|
+
class="sv-color__field"
|
|
133
|
+
type="text"
|
|
134
|
+
bind:value={hexDraft}
|
|
135
|
+
spellcheck="false"
|
|
136
|
+
aria-label="Hex value"
|
|
137
|
+
onblur={commitHex}
|
|
138
|
+
onkeydown={(e) => { if (e.key === 'Enter') commitHex() }}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="sv-color__palette">
|
|
142
|
+
{#each palette as c (c)}
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
class="sv-color__chip"
|
|
146
|
+
class:is-active={normalizeHex(c) === normalized}
|
|
147
|
+
style:background={c}
|
|
148
|
+
aria-label={c}
|
|
149
|
+
onclick={() => { pick(c); open = false }}
|
|
150
|
+
></button>
|
|
151
|
+
{/each}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
{#if name}<input type="hidden" {name} value={normalized} />{/if}
|
|
156
|
+
|
|
157
|
+
<style>
|
|
158
|
+
.sv-color {
|
|
159
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
160
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
161
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
162
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
163
|
+
padding: 0 10px; cursor: pointer; font: inherit;
|
|
164
|
+
}
|
|
165
|
+
.sv-color--sm { height: 28px; font-size: 12px; }
|
|
166
|
+
.sv-color--md { height: 34px; font-size: 13px; }
|
|
167
|
+
.sv-color--lg { height: 40px; font-size: 15px; }
|
|
168
|
+
.sv-color.is-disabled { opacity: 0.6; cursor: not-allowed; }
|
|
169
|
+
.sv-color:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
170
|
+
.sv-color__swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.15); flex: none; }
|
|
171
|
+
.sv-color__hex { font-variant-numeric: tabular-nums; text-transform: lowercase; }
|
|
172
|
+
|
|
173
|
+
:global(.sv-color__panel) {
|
|
174
|
+
z-index: 2147483647; width: 208px; padding: 12px;
|
|
175
|
+
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
|
176
|
+
border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
|
|
177
|
+
}
|
|
178
|
+
:global(.sv-color__top) { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
179
|
+
:global(.sv-color__native) { width: 36px; height: 34px; padding: 0; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; background: none; cursor: pointer; }
|
|
180
|
+
:global(.sv-color__field) {
|
|
181
|
+
flex: 1; min-width: 0; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
182
|
+
border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 8px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
|
|
183
|
+
}
|
|
184
|
+
:global(.sv-color__palette) { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
|
|
185
|
+
:global(.sv-color__chip) { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid rgba(0,0,0,0.12); cursor: pointer; padding: 0; }
|
|
186
|
+
:global(.sv-color__chip.is-active) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
value?: string;
|
|
3
|
+
onChange?: (hex: string) => void;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
readonly?: boolean;
|
|
6
|
+
/** Preset swatches. */
|
|
7
|
+
palette?: string[];
|
|
8
|
+
name?: string;
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
autoOpen?: boolean;
|
|
12
|
+
};
|
|
13
|
+
declare const SvColorInput: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type SvColorInput = ReturnType<typeof SvColorInput>;
|
|
15
|
+
export default SvColorInput;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvComboBox - an editable combobox: type to filter a portalled list, pick an
|
|
4
|
+
* option (value must come from the list). Parity: Smart `smart-combo-box`.
|
|
5
|
+
* Controlled via `value` + `onChange`. On blur, unmatched text reverts.
|
|
6
|
+
*/
|
|
7
|
+
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
8
|
+
import { filterOptions, type ListOption } from './list-option'
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
options: ReadonlyArray<ListOption>
|
|
12
|
+
value?: string | number | null
|
|
13
|
+
onChange?: (value: string | number | null) => void
|
|
14
|
+
placeholder?: string
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
name?: string
|
|
17
|
+
size?: 'sm' | 'md' | 'lg'
|
|
18
|
+
ariaLabel?: string
|
|
19
|
+
autoOpen?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { options, value = null, onChange, placeholder = 'Select…', disabled = false, name, size = 'md', ariaLabel, autoOpen = false }: Props = $props()
|
|
23
|
+
|
|
24
|
+
let open = $state(false)
|
|
25
|
+
let query = $state('')
|
|
26
|
+
let active = $state(0)
|
|
27
|
+
let editing = $state(false)
|
|
28
|
+
let inputEl = $state<HTMLInputElement | null>(null)
|
|
29
|
+
let fieldEl = $state<HTMLDivElement | null>(null)
|
|
30
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
31
|
+
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
32
|
+
|
|
33
|
+
const selected = $derived(options.find((o) => o.value === value) ?? null)
|
|
34
|
+
const filtered = $derived(editing ? filterOptions(options, query) : [...options])
|
|
35
|
+
const shownText = $derived(editing ? query : selected?.label ?? '')
|
|
36
|
+
|
|
37
|
+
function updatePos() {
|
|
38
|
+
if (!fieldEl) return
|
|
39
|
+
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(filtered.length, 8) * 34 + 8 })
|
|
40
|
+
}
|
|
41
|
+
function openPanel() { if (disabled || open) return; open = true; active = 0; updatePos() }
|
|
42
|
+
function close(revert = true) {
|
|
43
|
+
open = false; editing = false
|
|
44
|
+
if (revert) query = selected?.label ?? ''
|
|
45
|
+
}
|
|
46
|
+
function pick(o: ListOption | undefined) {
|
|
47
|
+
if (!o || o.disabled) return
|
|
48
|
+
onChange?.(o.value); query = o.label; editing = false; open = false; inputEl?.blur()
|
|
49
|
+
}
|
|
50
|
+
function onInput(e: Event) {
|
|
51
|
+
query = (e.currentTarget as HTMLInputElement).value
|
|
52
|
+
editing = true; active = 0
|
|
53
|
+
if (!open) openPanel(); else updatePos()
|
|
54
|
+
}
|
|
55
|
+
function onKeydown(e: KeyboardEvent) {
|
|
56
|
+
if (disabled) return
|
|
57
|
+
if (!open && (e.key === 'ArrowDown')) { e.preventDefault(); editing = true; openPanel(); return }
|
|
58
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
59
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
60
|
+
else if (e.key === 'Enter') { e.preventDefault(); pick(filtered[active]) }
|
|
61
|
+
else if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
62
|
+
queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
63
|
+
}
|
|
64
|
+
function onFocus() { editing = true; query = selected?.label ?? ''; openPanel() }
|
|
65
|
+
function onBlur() { /* outside-click effect handles close+revert */ }
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (!open) return
|
|
69
|
+
const rp = () => updatePos()
|
|
70
|
+
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
71
|
+
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (fieldEl?.contains(t) || panelEl?.contains(t))) return; close() }
|
|
72
|
+
document.addEventListener('pointerdown', od, true)
|
|
73
|
+
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
74
|
+
})
|
|
75
|
+
$effect(() => { if (!editing) query = selected?.label ?? '' })
|
|
76
|
+
function focusOpen(node: HTMLInputElement) { if (autoOpen) { node.focus() } }
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<div bind:this={fieldEl} class="sv-combo sv-combo--{size}" class:is-open={open} class:is-disabled={disabled}>
|
|
80
|
+
<input
|
|
81
|
+
bind:this={inputEl}
|
|
82
|
+
class="sv-combo__input"
|
|
83
|
+
type="text"
|
|
84
|
+
role="combobox"
|
|
85
|
+
aria-expanded={open}
|
|
86
|
+
aria-controls="combo-list"
|
|
87
|
+
aria-autocomplete="list"
|
|
88
|
+
value={shownText}
|
|
89
|
+
{placeholder}
|
|
90
|
+
{disabled}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
oninput={onInput}
|
|
93
|
+
onfocus={onFocus}
|
|
94
|
+
onblur={onBlur}
|
|
95
|
+
onkeydown={onKeydown}
|
|
96
|
+
use:focusOpen
|
|
97
|
+
/>
|
|
98
|
+
<button type="button" class="sv-combo__chev" tabindex="-1" aria-label="Toggle" onclick={() => (open ? close() : (inputEl?.focus(), openPanel()))} {disabled}>
|
|
99
|
+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{#if open}
|
|
104
|
+
<div bind:this={panelEl} id="combo-list" class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="listbox">
|
|
105
|
+
{#each filtered as opt, i (opt.value)}
|
|
106
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
107
|
+
<div class="sv-ddl__opt" class:is-active={i === active} class:is-selected={opt.value === value} class:is-disabled={opt.disabled} role="option" tabindex="-1" aria-selected={opt.value === value} data-idx={i} onclick={() => pick(opt)} onpointermove={() => { if (!opt.disabled) active = i }}>{opt.label}</div>
|
|
108
|
+
{:else}
|
|
109
|
+
<div class="sv-ddl__empty">No matches</div>
|
|
110
|
+
{/each}
|
|
111
|
+
</div>
|
|
112
|
+
{/if}
|
|
113
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
114
|
+
|
|
115
|
+
<style>
|
|
116
|
+
.sv-combo {
|
|
117
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
118
|
+
display: inline-flex; align-items: center; width: 200px;
|
|
119
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
120
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
121
|
+
}
|
|
122
|
+
.sv-combo--sm { height: 28px; font-size: 12px; }
|
|
123
|
+
.sv-combo--md { height: 34px; font-size: 13px; }
|
|
124
|
+
.sv-combo--lg { height: 40px; font-size: 15px; }
|
|
125
|
+
.sv-combo.is-open, .sv-combo:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
126
|
+
.sv-combo.is-disabled { opacity: 0.6; }
|
|
127
|
+
.sv-combo__input { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 4px 0 10px; }
|
|
128
|
+
.sv-combo__chev { display: grid; place-items: center; width: 28px; align-self: stretch; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; }
|
|
129
|
+
:global(.sv-ddl__empty) { padding: 8px 10px; color: var(--sg-muted, #94a3b8); font-size: 13px; }
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ListOption } from './list-option';
|
|
2
|
+
type Props = {
|
|
3
|
+
options: ReadonlyArray<ListOption>;
|
|
4
|
+
value?: string | number | null;
|
|
5
|
+
onChange?: (value: string | number | null) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
name?: string;
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
autoOpen?: boolean;
|
|
12
|
+
};
|
|
13
|
+
declare const SvComboBox: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type SvComboBox = ReturnType<typeof SvComboBox>;
|
|
15
|
+
export default SvComboBox;
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
8
|
+
import { COUNTRIES, COUNTRY_BY_CODE, flagEmoji } from './countries'
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
value?: string | null
|
|
12
|
+
onChange?: (code: string) => void
|
|
13
|
+
/** Show the dial code beside each country. */
|
|
14
|
+
showDial?: boolean
|
|
15
|
+
placeholder?: string
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
name?: string
|
|
18
|
+
size?: 'sm' | 'md' | 'lg'
|
|
19
|
+
ariaLabel?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { value = null, onChange, showDial = false, placeholder = 'Select country…', disabled = false, name, size = 'md', ariaLabel }: Props = $props()
|
|
23
|
+
|
|
24
|
+
let open = $state(false)
|
|
25
|
+
let query = $state('')
|
|
26
|
+
let active = $state(0)
|
|
27
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
28
|
+
let searchEl = $state<HTMLInputElement | null>(null)
|
|
29
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
30
|
+
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
31
|
+
|
|
32
|
+
const selected = $derived(value ? COUNTRY_BY_CODE.get(value) ?? null : null)
|
|
33
|
+
const filtered = $derived(
|
|
34
|
+
query.trim() === ''
|
|
35
|
+
? COUNTRIES
|
|
36
|
+
: COUNTRIES.filter((c) => c.name.toLowerCase().includes(query.toLowerCase()) || c.dial.includes(query) || c.code.toLowerCase() === query.toLowerCase()),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function updatePos() { if (triggerEl) rect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 320, minWidth: 240 }) }
|
|
40
|
+
function openPanel() { if (disabled || open) return; open = true; query = ''; active = 0; updatePos(); queueMicrotask(() => searchEl?.focus()) }
|
|
41
|
+
function close() { open = false; triggerEl?.focus() }
|
|
42
|
+
function pick(code: string) { onChange?.(code); open = false; triggerEl?.focus() }
|
|
43
|
+
function onKeydown(e: KeyboardEvent) {
|
|
44
|
+
if (!open) return
|
|
45
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
46
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
47
|
+
else if (e.key === 'Enter') { e.preventDefault(); const c = filtered[active]; if (c) pick(c.code) }
|
|
48
|
+
else if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
49
|
+
queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!open) return
|
|
54
|
+
const rp = () => updatePos()
|
|
55
|
+
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
56
|
+
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; open = false }
|
|
57
|
+
document.addEventListener('pointerdown', od, true)
|
|
58
|
+
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
59
|
+
})
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<button bind:this={triggerEl} type="button" class="sv-country sv-country--{size}" class:is-open={open} class:is-disabled={disabled} aria-haspopup="listbox" aria-expanded={open} aria-label={ariaLabel} {disabled} onclick={() => (open ? (open = false) : openPanel())}>
|
|
63
|
+
{#if selected}
|
|
64
|
+
<span class="sv-country__flag" aria-hidden="true">{flagEmoji(selected.code)}</span>
|
|
65
|
+
<span class="sv-country__name">{selected.name}</span>
|
|
66
|
+
{#if showDial}<span class="sv-country__dial">{selected.dial}</span>{/if}
|
|
67
|
+
{:else}
|
|
68
|
+
<span class="sv-country__ph">{placeholder}</span>
|
|
69
|
+
{/if}
|
|
70
|
+
<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>
|
|
71
|
+
</button>
|
|
72
|
+
|
|
73
|
+
{#if open}
|
|
74
|
+
<div bind:this={panelEl} class="sv-country__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="dialog">
|
|
75
|
+
<input bind:this={searchEl} class="sv-country__search" type="text" bind:value={query} placeholder="Search countries…" aria-label="Search countries" onkeydown={onKeydown} oninput={() => { active = 0; updatePos() }} />
|
|
76
|
+
<div class="sv-country__list" role="listbox">
|
|
77
|
+
{#each filtered as c, i (c.code)}
|
|
78
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
79
|
+
<div class="sv-country__opt" class:is-active={i === active} class:is-selected={c.code === value} role="option" tabindex="-1" aria-selected={c.code === value} data-idx={i} onclick={() => pick(c.code)} onpointermove={() => (active = i)}>
|
|
80
|
+
<span class="sv-country__flag" aria-hidden="true">{flagEmoji(c.code)}</span>
|
|
81
|
+
<span class="sv-country__name">{c.name}</span>
|
|
82
|
+
<span class="sv-country__dial">{c.dial}</span>
|
|
83
|
+
</div>
|
|
84
|
+
{:else}
|
|
85
|
+
<div class="sv-ddl__empty">No matches</div>
|
|
86
|
+
{/each}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.sv-country {
|
|
94
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
95
|
+
display: inline-flex; align-items: center; gap: 8px; width: 220px;
|
|
96
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: left;
|
|
97
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); padding: 0 10px; cursor: pointer;
|
|
98
|
+
}
|
|
99
|
+
.sv-country--sm { height: 28px; font-size: 12px; }
|
|
100
|
+
.sv-country--md { height: 34px; font-size: 13px; }
|
|
101
|
+
.sv-country--lg { height: 40px; font-size: 15px; }
|
|
102
|
+
.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; }
|
|
103
|
+
.sv-country.is-disabled { opacity: 0.6; cursor: not-allowed; }
|
|
104
|
+
.sv-country__flag { font-size: 16px; }
|
|
105
|
+
.sv-country__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
106
|
+
.sv-country__ph { flex: 1; color: var(--sg-muted, #94a3b8); }
|
|
107
|
+
.sv-country__dial { color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
108
|
+
.sv-country__chev { color: var(--sg-muted, #64748b); flex: none; }
|
|
109
|
+
|
|
110
|
+
:global(.sv-country__panel) {
|
|
111
|
+
z-index: 2147483647; display: flex; flex-direction: column; max-height: 340px; padding: 6px;
|
|
112
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
113
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
|
|
114
|
+
}
|
|
115
|
+
:global(.sv-country__search) {
|
|
116
|
+
height: 32px; margin-bottom: 4px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
117
|
+
border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 7px; background: var(--sg-input-bg, #fff); color: inherit; outline: none;
|
|
118
|
+
}
|
|
119
|
+
:global(.sv-country__list) { overflow-y: auto; }
|
|
120
|
+
:global(.sv-country__opt) { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
121
|
+
:global(.sv-country__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
122
|
+
:global(.sv-country__opt.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
value?: string | null;
|
|
3
|
+
onChange?: (code: string) => void;
|
|
4
|
+
/** Show the dial code beside each country. */
|
|
5
|
+
showDial?: boolean;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
name?: string;
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const SvCountryInput: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type SvCountryInput = ReturnType<typeof SvCountryInput>;
|
|
14
|
+
export default SvCountryInput;
|