@svgrid/grid 1.2.21 → 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.
Files changed (190) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +100 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +669 -0
  8. package/dist/SvCalendar.svelte.d.ts +70 -0
  9. package/dist/SvCheckBox.svelte +81 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +187 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +130 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +123 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +322 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +37 -0
  19. package/dist/SvDropDownList.svelte +143 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +99 -0
  24. package/dist/SvGauge.svelte.d.ts +31 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +122 -0
  35. package/dist/SvListBox.svelte.d.ts +19 -0
  36. package/dist/SvMaskedInput.svelte +0 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +0 -0
  39. package/dist/SvNumberInput.svelte.d.ts +22 -0
  40. package/dist/SvPasswordInput.svelte +110 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +22 -0
  42. package/dist/SvPhoneInput.svelte +0 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +108 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +23 -0
  46. package/dist/SvRating.svelte +111 -0
  47. package/dist/SvRating.svelte.d.ts +20 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +182 -0
  51. package/dist/SvSlider.svelte.d.ts +25 -0
  52. package/dist/SvSwitchButton.svelte +94 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +19 -0
  54. package/dist/SvTabs.svelte +122 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +81 -0
  57. package/dist/SvTagsInput.svelte.d.ts +19 -0
  58. package/dist/SvTimePicker.svelte +359 -0
  59. package/dist/SvTimePicker.svelte.d.ts +27 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +210 -0
  63. package/dist/SvTree.svelte.d.ts +29 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +10022 -5904
  66. package/dist/cdn/svgrid.svelte-external.js +8531 -4429
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/datetime/date-core.d.ts +88 -0
  76. package/dist/datetime/date-core.js +165 -0
  77. package/dist/datetime/date-format.d.ts +39 -0
  78. package/dist/datetime/date-format.js +371 -0
  79. package/dist/datetime/date-restrict.d.ts +23 -0
  80. package/dist/datetime/date-restrict.js +50 -0
  81. package/dist/datetime/date-selection.d.ts +25 -0
  82. package/dist/datetime/date-selection.js +94 -0
  83. package/dist/datetime/mask.d.ts +21 -0
  84. package/dist/datetime/mask.js +90 -0
  85. package/dist/editing.d.ts +1 -1
  86. package/dist/editing.js +7 -0
  87. package/dist/editors/cell-editors.d.ts +2 -2
  88. package/dist/editors/cell-editors.js +3 -1
  89. package/dist/export-format.d.ts +178 -0
  90. package/dist/export-format.js +419 -0
  91. package/dist/hyperformula-adapter.d.ts +4 -0
  92. package/dist/hyperformula-adapter.js +15 -6
  93. package/dist/index.d.ts +36 -0
  94. package/dist/index.js +41 -0
  95. package/dist/list-option.d.ts +10 -0
  96. package/dist/list-option.js +7 -0
  97. package/dist/menus.js +18 -2
  98. package/dist/popover.d.ts +48 -0
  99. package/dist/popover.js +65 -0
  100. package/dist/row-resize.d.ts +4 -4
  101. package/dist/row-resize.js +8 -5
  102. package/dist/selection.d.ts +6 -0
  103. package/dist/selection.js +31 -5
  104. package/dist/server-data-source.d.ts +43 -0
  105. package/dist/server-data-source.js +100 -0
  106. package/dist/sparkline.js +8 -4
  107. package/dist/svgrid-wrapper.types.d.ts +26 -1
  108. package/package.json +6 -1
  109. package/src/GridFooter.svelte +34 -20
  110. package/src/SvAutoComplete.svelte +100 -0
  111. package/src/SvButton.svelte +97 -0
  112. package/src/SvCalendar.svelte +669 -0
  113. package/src/SvCalendar.test.ts +211 -0
  114. package/src/SvCheckBox.svelte +81 -0
  115. package/src/SvColorInput.svelte +187 -0
  116. package/src/SvComboBox.svelte +130 -0
  117. package/src/SvCountryInput.svelte +123 -0
  118. package/src/SvDateTimePicker.svelte +322 -0
  119. package/src/SvDateTimePicker.test.ts +136 -0
  120. package/src/SvDropDownList.svelte +143 -0
  121. package/src/SvForm.svelte +147 -0
  122. package/src/SvGauge.svelte +99 -0
  123. package/src/SvGrid.controller.svelte.ts +74 -8
  124. package/src/SvGrid.css +84 -31
  125. package/src/SvGrid.helpers.ts +41 -12
  126. package/src/SvGrid.svelte +84 -5
  127. package/src/SvGrid.types.ts +33 -0
  128. package/src/SvGridDropdown.svelte +16 -30
  129. package/src/SvListBox.svelte +122 -0
  130. package/src/SvMaskedInput.svelte +0 -0
  131. package/src/SvNumberInput.svelte +0 -0
  132. package/src/SvPasswordInput.svelte +110 -0
  133. package/src/SvPhoneInput.svelte +0 -0
  134. package/src/SvRadioGroup.svelte +108 -0
  135. package/src/SvRating.svelte +111 -0
  136. package/src/SvRepeatButton.svelte +89 -0
  137. package/src/SvSlider.svelte +182 -0
  138. package/src/SvSwitchButton.svelte +94 -0
  139. package/src/SvTabs.svelte +122 -0
  140. package/src/SvTagsInput.svelte +81 -0
  141. package/src/SvTimePicker.svelte +359 -0
  142. package/src/SvTimePicker.test.ts +98 -0
  143. package/src/SvToggleButton.svelte +57 -0
  144. package/src/SvTree.svelte +210 -0
  145. package/src/build-api.ts +115 -0
  146. package/src/cell-render.test.ts +38 -0
  147. package/src/cell-render.ts +43 -0
  148. package/src/clipboard.test.ts +18 -0
  149. package/src/clipboard.ts +24 -1
  150. package/src/collaboration.test.ts +30 -0
  151. package/src/collaboration.ts +6 -0
  152. package/src/core.ts +27 -3
  153. package/src/countries.ts +71 -0
  154. package/src/datetime/date-core.test.ts +217 -0
  155. package/src/datetime/date-core.ts +204 -0
  156. package/src/datetime/date-format.test.ts +121 -0
  157. package/src/datetime/date-format.ts +317 -0
  158. package/src/datetime/date-restrict.ts +60 -0
  159. package/src/datetime/date-selection.test.ts +129 -0
  160. package/src/datetime/date-selection.ts +137 -0
  161. package/src/datetime/mask.test.ts +36 -0
  162. package/src/datetime/mask.ts +84 -0
  163. package/src/editing.test.ts +22 -0
  164. package/src/editing.ts +7 -1
  165. package/src/editors/cell-editors.ts +7 -1
  166. package/src/export-data-api.test.ts +126 -0
  167. package/src/export-format.test.ts +107 -0
  168. package/src/export-format.ts +598 -0
  169. package/src/hyperformula-adapter.test.ts +35 -1
  170. package/src/hyperformula-adapter.ts +18 -6
  171. package/src/index.ts +71 -0
  172. package/src/list-option.ts +15 -0
  173. package/src/menus.ts +21 -2
  174. package/src/popover.ts +79 -0
  175. package/src/row-resize.test.ts +25 -0
  176. package/src/row-resize.ts +8 -5
  177. package/src/selection.test.ts +36 -4
  178. package/src/selection.ts +30 -9
  179. package/src/server-data-source.test.ts +201 -0
  180. package/src/server-data-source.ts +148 -0
  181. package/src/sparkline.test.ts +9 -0
  182. package/src/sparkline.ts +9 -4
  183. package/src/svgrid-wrapper.types.ts +28 -1
  184. package/src/svgrid.behavior.test.ts +27 -0
  185. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  186. package/src/ui-buttons.test.ts +105 -0
  187. package/src/ui-composite.test.ts +89 -0
  188. package/src/ui-inputs.test.ts +92 -0
  189. package/src/ui-range.test.ts +61 -0
  190. 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;