@svgrid/grid 1.2.22 → 1.2.24

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 (263) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +89 -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 +409 -0
  8. package/dist/SvCalendar.svelte.d.ts +52 -0
  9. package/dist/SvCheckBox.svelte +82 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +160 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +108 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +125 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +265 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +38 -0
  19. package/dist/SvDropDownList.svelte +120 -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 +97 -0
  24. package/dist/SvGauge.svelte.d.ts +26 -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 +89 -0
  35. package/dist/SvListBox.svelte.d.ts +22 -0
  36. package/dist/SvMaskedInput.svelte +66 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +116 -0
  39. package/dist/SvNumberInput.svelte.d.ts +18 -0
  40. package/dist/SvPasswordInput.svelte +100 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +18 -0
  42. package/dist/SvPhoneInput.svelte +87 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +85 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +20 -0
  46. package/dist/SvRating.svelte +91 -0
  47. package/dist/SvRating.svelte.d.ts +18 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +155 -0
  51. package/dist/SvSlider.svelte.d.ts +20 -0
  52. package/dist/SvSwitchButton.svelte +87 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +14 -0
  54. package/dist/SvTabs.svelte +109 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +70 -0
  57. package/dist/SvTagsInput.svelte.d.ts +14 -0
  58. package/dist/SvTimePicker.svelte +201 -0
  59. package/dist/SvTimePicker.svelte.d.ts +30 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +131 -0
  63. package/dist/SvTree.svelte.d.ts +27 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +11674 -5907
  66. package/dist/cdn/svgrid.svelte-external.js +10436 -4671
  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/createAutocomplete.svelte.d.ts +77 -0
  76. package/dist/createAutocomplete.svelte.js +100 -0
  77. package/dist/createCalendar.svelte.d.ts +192 -0
  78. package/dist/createCalendar.svelte.js +402 -0
  79. package/dist/createCheckbox.svelte.d.ts +45 -0
  80. package/dist/createCheckbox.svelte.js +26 -0
  81. package/dist/createColorInput.svelte.d.ts +67 -0
  82. package/dist/createColorInput.svelte.js +100 -0
  83. package/dist/createCombobox.svelte.d.ts +102 -0
  84. package/dist/createCombobox.svelte.js +171 -0
  85. package/dist/createCountryInput.svelte.d.ts +86 -0
  86. package/dist/createCountryInput.svelte.js +126 -0
  87. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  88. package/dist/createDateTimePicker.svelte.js +202 -0
  89. package/dist/createDropdownList.svelte.d.ts +78 -0
  90. package/dist/createDropdownList.svelte.js +117 -0
  91. package/dist/createGauge.svelte.d.ts +56 -0
  92. package/dist/createGauge.svelte.js +49 -0
  93. package/dist/createListbox.svelte.d.ts +79 -0
  94. package/dist/createListbox.svelte.js +117 -0
  95. package/dist/createMaskedInput.svelte.d.ts +30 -0
  96. package/dist/createMaskedInput.svelte.js +62 -0
  97. package/dist/createNumberInput.svelte.d.ts +72 -0
  98. package/dist/createNumberInput.svelte.js +167 -0
  99. package/dist/createPasswordInput.svelte.d.ts +60 -0
  100. package/dist/createPasswordInput.svelte.js +72 -0
  101. package/dist/createPhoneInput.svelte.d.ts +46 -0
  102. package/dist/createPhoneInput.svelte.js +105 -0
  103. package/dist/createRadioGroup.svelte.d.ts +64 -0
  104. package/dist/createRadioGroup.svelte.js +73 -0
  105. package/dist/createRating.svelte.d.ts +70 -0
  106. package/dist/createRating.svelte.js +85 -0
  107. package/dist/createSlider.svelte.d.ts +71 -0
  108. package/dist/createSlider.svelte.js +123 -0
  109. package/dist/createSwitch.svelte.d.ts +43 -0
  110. package/dist/createSwitch.svelte.js +41 -0
  111. package/dist/createTabs.svelte.d.ts +78 -0
  112. package/dist/createTabs.svelte.js +92 -0
  113. package/dist/createTagsInput.svelte.d.ts +65 -0
  114. package/dist/createTagsInput.svelte.js +93 -0
  115. package/dist/createTimePicker.svelte.d.ts +106 -0
  116. package/dist/createTimePicker.svelte.js +248 -0
  117. package/dist/createToggle.svelte.d.ts +43 -0
  118. package/dist/createToggle.svelte.js +39 -0
  119. package/dist/createTree.svelte.d.ts +96 -0
  120. package/dist/createTree.svelte.js +194 -0
  121. package/dist/datetime/date-core.d.ts +88 -0
  122. package/dist/datetime/date-core.js +165 -0
  123. package/dist/datetime/date-format.d.ts +39 -0
  124. package/dist/datetime/date-format.js +371 -0
  125. package/dist/datetime/date-restrict.d.ts +23 -0
  126. package/dist/datetime/date-restrict.js +50 -0
  127. package/dist/datetime/date-selection.d.ts +25 -0
  128. package/dist/datetime/date-selection.js +94 -0
  129. package/dist/datetime/mask.d.ts +21 -0
  130. package/dist/datetime/mask.js +90 -0
  131. package/dist/editing.d.ts +1 -1
  132. package/dist/editing.js +7 -0
  133. package/dist/editor-contract.d.ts +48 -0
  134. package/dist/editor-contract.js +26 -0
  135. package/dist/editors/cell-editors.d.ts +2 -2
  136. package/dist/editors/cell-editors.js +3 -1
  137. package/dist/export-format.d.ts +178 -0
  138. package/dist/export-format.js +419 -0
  139. package/dist/hyperformula-adapter.d.ts +4 -0
  140. package/dist/hyperformula-adapter.js +15 -6
  141. package/dist/index.d.ts +60 -0
  142. package/dist/index.js +72 -0
  143. package/dist/list-option.d.ts +10 -0
  144. package/dist/list-option.js +7 -0
  145. package/dist/menus.js +18 -2
  146. package/dist/popover.d.ts +48 -0
  147. package/dist/popover.js +65 -0
  148. package/dist/row-resize.d.ts +4 -4
  149. package/dist/row-resize.js +8 -5
  150. package/dist/selection.d.ts +6 -0
  151. package/dist/selection.js +31 -5
  152. package/dist/server-data-source.d.ts +43 -0
  153. package/dist/server-data-source.js +100 -0
  154. package/dist/sparkline.js +8 -4
  155. package/dist/svgrid-wrapper.types.d.ts +26 -1
  156. package/package.json +6 -1
  157. package/src/GridFooter.svelte +34 -20
  158. package/src/SvAutoComplete.svelte +89 -0
  159. package/src/SvButton.svelte +97 -0
  160. package/src/SvCalendar.svelte +409 -0
  161. package/src/SvCalendar.test.ts +211 -0
  162. package/src/SvCheckBox.svelte +82 -0
  163. package/src/SvColorInput.svelte +160 -0
  164. package/src/SvComboBox.svelte +108 -0
  165. package/src/SvCountryInput.svelte +125 -0
  166. package/src/SvDateTimePicker.svelte +265 -0
  167. package/src/SvDateTimePicker.test.ts +136 -0
  168. package/src/SvDropDownList.svelte +120 -0
  169. package/src/SvForm.svelte +147 -0
  170. package/src/SvGauge.svelte +97 -0
  171. package/src/SvGrid.controller.svelte.ts +74 -8
  172. package/src/SvGrid.css +84 -31
  173. package/src/SvGrid.helpers.ts +41 -12
  174. package/src/SvGrid.svelte +84 -5
  175. package/src/SvGrid.types.ts +33 -0
  176. package/src/SvGridDropdown.svelte +16 -30
  177. package/src/SvListBox.svelte +89 -0
  178. package/src/SvMaskedInput.svelte +66 -0
  179. package/src/SvNumberInput.svelte +116 -0
  180. package/src/SvPasswordInput.svelte +100 -0
  181. package/src/SvPhoneInput.svelte +87 -0
  182. package/src/SvRadioGroup.svelte +85 -0
  183. package/src/SvRating.svelte +91 -0
  184. package/src/SvRepeatButton.svelte +89 -0
  185. package/src/SvSlider.svelte +155 -0
  186. package/src/SvSwitchButton.svelte +87 -0
  187. package/src/SvTabs.svelte +109 -0
  188. package/src/SvTagsInput.svelte +70 -0
  189. package/src/SvTimePicker.svelte +201 -0
  190. package/src/SvTimePicker.test.ts +98 -0
  191. package/src/SvToggleButton.svelte +57 -0
  192. package/src/SvTree.svelte +131 -0
  193. package/src/build-api.ts +115 -0
  194. package/src/cell-render.test.ts +38 -0
  195. package/src/cell-render.ts +43 -0
  196. package/src/clipboard.test.ts +18 -0
  197. package/src/clipboard.ts +24 -1
  198. package/src/collaboration.test.ts +30 -0
  199. package/src/collaboration.ts +6 -0
  200. package/src/core.ts +27 -3
  201. package/src/countries.ts +71 -0
  202. package/src/createAutocomplete.svelte.ts +104 -0
  203. package/src/createCalendar.svelte.ts +488 -0
  204. package/src/createCheckbox.svelte.ts +74 -0
  205. package/src/createColorInput.svelte.ts +115 -0
  206. package/src/createCombobox.svelte.ts +158 -0
  207. package/src/createCountryInput.svelte.ts +126 -0
  208. package/src/createDateTimePicker.svelte.ts +216 -0
  209. package/src/createDropdownList.svelte.ts +125 -0
  210. package/src/createGauge.svelte.ts +86 -0
  211. package/src/createListbox.svelte.ts +186 -0
  212. package/src/createMaskedInput.svelte.ts +81 -0
  213. package/src/createNumberInput.svelte.ts +193 -0
  214. package/src/createPasswordInput.svelte.ts +87 -0
  215. package/src/createPhoneInput.svelte.ts +119 -0
  216. package/src/createRadioGroup.svelte.ts +132 -0
  217. package/src/createRating.svelte.ts +145 -0
  218. package/src/createSlider.svelte.ts +160 -0
  219. package/src/createSwitch.svelte.ts +78 -0
  220. package/src/createTabs.svelte.ts +129 -0
  221. package/src/createTagsInput.svelte.ts +98 -0
  222. package/src/createTimePicker.svelte.ts +289 -0
  223. package/src/createToggle.svelte.ts +69 -0
  224. package/src/createTree.svelte.ts +216 -0
  225. package/src/datetime/date-core.test.ts +217 -0
  226. package/src/datetime/date-core.ts +204 -0
  227. package/src/datetime/date-format.test.ts +121 -0
  228. package/src/datetime/date-format.ts +317 -0
  229. package/src/datetime/date-restrict.ts +60 -0
  230. package/src/datetime/date-selection.test.ts +129 -0
  231. package/src/datetime/date-selection.ts +137 -0
  232. package/src/datetime/mask.test.ts +36 -0
  233. package/src/datetime/mask.ts +84 -0
  234. package/src/editing.test.ts +22 -0
  235. package/src/editing.ts +7 -1
  236. package/src/editor-contract.test.ts +32 -0
  237. package/src/editor-contract.ts +60 -0
  238. package/src/editors/cell-editors.ts +7 -1
  239. package/src/export-data-api.test.ts +126 -0
  240. package/src/export-format.test.ts +107 -0
  241. package/src/export-format.ts +598 -0
  242. package/src/hyperformula-adapter.test.ts +35 -1
  243. package/src/hyperformula-adapter.ts +18 -6
  244. package/src/index.ts +110 -0
  245. package/src/list-option.ts +15 -0
  246. package/src/menus.ts +21 -2
  247. package/src/popover.ts +79 -0
  248. package/src/row-resize.test.ts +25 -0
  249. package/src/row-resize.ts +8 -5
  250. package/src/selection.test.ts +36 -4
  251. package/src/selection.ts +30 -9
  252. package/src/server-data-source.test.ts +201 -0
  253. package/src/server-data-source.ts +148 -0
  254. package/src/sparkline.test.ts +9 -0
  255. package/src/sparkline.ts +9 -4
  256. package/src/svgrid-wrapper.types.ts +28 -1
  257. package/src/svgrid.behavior.test.ts +27 -0
  258. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  259. package/src/ui-buttons.test.ts +105 -0
  260. package/src/ui-composite.test.ts +89 -0
  261. package/src/ui-inputs.test.ts +118 -0
  262. package/src/ui-range.test.ts +61 -0
  263. package/src/ui-selection.test.ts +119 -0
@@ -0,0 +1,160 @@
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
+ * The styled renderer over the headless `createColorInput` core: hex
8
+ * normalization, palette, draft field and open/close state come from the core;
9
+ * the portal + anchored measurement stay here (render concerns).
10
+ */
11
+ import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
12
+ import { createColorInput } from './createColorInput.svelte'
13
+
14
+ type Props = {
15
+ value?: string
16
+ onChange?: (hex: string) => void
17
+ disabled?: boolean
18
+ readonly?: boolean
19
+ /** Preset swatches. */
20
+ palette?: string[]
21
+ name?: string
22
+ size?: 'sm' | 'md' | 'lg'
23
+ ariaLabel?: string
24
+ autoOpen?: boolean
25
+ }
26
+
27
+ let {
28
+ value = '#3b82f6',
29
+ onChange,
30
+ disabled = false,
31
+ readonly = false,
32
+ palette,
33
+ name,
34
+ size = 'md',
35
+ ariaLabel,
36
+ autoOpen = false,
37
+ }: Props = $props()
38
+
39
+ const col = createColorInput({
40
+ value: () => value,
41
+ onChange: (hex) => onChange?.(hex),
42
+ disabled: () => disabled,
43
+ readonly: () => readonly,
44
+ palette: () => palette as string[],
45
+ ariaLabel: () => ariaLabel,
46
+ })
47
+
48
+ let triggerEl = $state<HTMLButtonElement | null>(null)
49
+ let panelEl = $state<HTMLDivElement | null>(null)
50
+ let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
51
+
52
+ function updatePos() {
53
+ if (!triggerEl) return
54
+ panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 210, minWidth: 200 })
55
+ }
56
+
57
+ $effect(() => {
58
+ if (!col.popover.open) return
59
+ updatePos()
60
+ const reposition = () => updatePos()
61
+ window.addEventListener('scroll', reposition, true)
62
+ window.addEventListener('resize', reposition)
63
+ const onDown = (e: PointerEvent) => {
64
+ const t = e.target as Node | null
65
+ if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return
66
+ col.popover.close()
67
+ }
68
+ document.addEventListener('pointerdown', onDown, true)
69
+ return () => {
70
+ window.removeEventListener('scroll', reposition, true)
71
+ window.removeEventListener('resize', reposition)
72
+ document.removeEventListener('pointerdown', onDown, true)
73
+ }
74
+ })
75
+
76
+ function focusOpen(node: HTMLButtonElement) {
77
+ if (!autoOpen) return
78
+ node.focus(); col.popover.show()
79
+ }
80
+ </script>
81
+
82
+ <button
83
+ bind:this={triggerEl}
84
+ class="sv-color sv-color--{size}"
85
+ class:is-disabled={disabled}
86
+ {...col.swatchProps()}
87
+ use:focusOpen
88
+ >
89
+ <span class="sv-color__swatch" style:background={col.normalized}></span>
90
+ <span class="sv-color__hex">{col.normalized}</span>
91
+ </button>
92
+
93
+ {#if col.popover.open}
94
+ <div
95
+ class="sv-color__panel"
96
+ bind:this={panelEl}
97
+ use:portalToBody
98
+ style:position="fixed"
99
+ style:top={`${panelRect.top}px`}
100
+ style:left={`${panelRect.left}px`}
101
+ role="dialog"
102
+ aria-label="Choose color"
103
+ >
104
+ <div class="sv-color__top">
105
+ <input class="sv-color__native" type="color" value={col.normalized} oninput={(e) => col.pick((e.currentTarget as HTMLInputElement).value)} aria-label="Color picker" />
106
+ <input
107
+ class="sv-color__field"
108
+ type="text"
109
+ bind:value={col.hexDraft}
110
+ spellcheck="false"
111
+ aria-label="Hex value"
112
+ onblur={col.commitHex}
113
+ onkeydown={(e) => { if (e.key === 'Enter') col.commitHex() }}
114
+ />
115
+ </div>
116
+ <div class="sv-color__palette">
117
+ {#each col.palette as c (c)}
118
+ <button
119
+ class="sv-color__chip"
120
+ class:is-active={col.isActive(c)}
121
+ style:background={c}
122
+ {...col.chipProps(c)}
123
+ ></button>
124
+ {/each}
125
+ </div>
126
+ </div>
127
+ {/if}
128
+ {#if name}<input type="hidden" {name} value={col.normalized} />{/if}
129
+
130
+ <style>
131
+ .sv-color {
132
+ --_accent: var(--sg-accent, #2563eb);
133
+ display: inline-flex; align-items: center; gap: 8px;
134
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
135
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
136
+ padding: 0 10px; cursor: pointer; font: inherit;
137
+ }
138
+ .sv-color--sm { height: 28px; font-size: 12px; }
139
+ .sv-color--md { height: 34px; font-size: 13px; }
140
+ .sv-color--lg { height: 40px; font-size: 15px; }
141
+ .sv-color.is-disabled { opacity: 0.6; cursor: not-allowed; }
142
+ .sv-color:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
143
+ .sv-color__swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.15); flex: none; }
144
+ .sv-color__hex { font-variant-numeric: tabular-nums; text-transform: lowercase; }
145
+
146
+ :global(.sv-color__panel) {
147
+ z-index: 2147483647; width: 208px; padding: 12px;
148
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
149
+ border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
150
+ }
151
+ :global(.sv-color__top) { display: flex; gap: 8px; margin-bottom: 10px; }
152
+ :global(.sv-color__native) { width: 36px; height: 34px; padding: 0; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; background: none; cursor: pointer; }
153
+ :global(.sv-color__field) {
154
+ flex: 1; min-width: 0; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
155
+ border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 8px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
156
+ }
157
+ :global(.sv-color__palette) { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
158
+ :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; }
159
+ :global(.sv-color__chip.is-active) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
160
+ </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,108 @@
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
+ * This is one styled renderer over the headless `createCombobox` core (state +
8
+ * keyboard + ARIA); only portal/measure/scroll render concerns live here.
9
+ */
10
+ import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
11
+ import { type ListOption } from './list-option'
12
+ import { createCombobox } from './createCombobox.svelte'
13
+
14
+ type Props = {
15
+ options: ReadonlyArray<ListOption>
16
+ value?: string | number | null
17
+ onChange?: (value: string | number | null) => void
18
+ placeholder?: string
19
+ disabled?: boolean
20
+ name?: string
21
+ size?: 'sm' | 'md' | 'lg'
22
+ ariaLabel?: string
23
+ autoOpen?: boolean
24
+ }
25
+
26
+ let { options, value = null, onChange, placeholder = 'Select…', disabled = false, name, size = 'md', ariaLabel, autoOpen = false }: Props = $props()
27
+
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 combo = createCombobox({
34
+ options: () => options,
35
+ value: () => value,
36
+ onChange: (v) => onChange?.(v),
37
+ disabled: () => disabled,
38
+ ariaLabel: () => ariaLabel,
39
+ focusInput: () => inputEl?.focus(),
40
+ blurInput: () => inputEl?.blur(),
41
+ })
42
+
43
+ function updatePos() {
44
+ if (!fieldEl) return
45
+ rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(combo.filtered.length, 8) * 34 + 8 })
46
+ }
47
+
48
+ // Position + reposition + outside-click close are render concerns (need the DOM).
49
+ $effect(() => {
50
+ if (!combo.open) return
51
+ updatePos()
52
+ const rp = () => updatePos()
53
+ window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
54
+ const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (fieldEl?.contains(t) || panelEl?.contains(t))) return; combo.close() }
55
+ document.addEventListener('pointerdown', od, true)
56
+ return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
57
+ })
58
+ // Keep the active option scrolled into view (render concern).
59
+ $effect(() => {
60
+ if (!combo.open) return
61
+ const i = combo.activeIndex
62
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
63
+ })
64
+ function focusOpen(node: HTMLInputElement) { if (autoOpen) { node.focus() } }
65
+ </script>
66
+
67
+ <div bind:this={fieldEl} class="sv-combo sv-combo--{size}" class:is-open={combo.open} class:is-disabled={disabled}>
68
+ <input
69
+ bind:this={inputEl}
70
+ class="sv-combo__input"
71
+ type="text"
72
+ {placeholder}
73
+ {...combo.inputProps()}
74
+ use:focusOpen
75
+ />
76
+ <button class="sv-combo__chev" {...combo.triggerProps()}>
77
+ <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>
78
+ </button>
79
+ </div>
80
+
81
+ {#if combo.open}
82
+ <div bind:this={panelEl} 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`} {...combo.listboxProps()}>
83
+ {#each combo.filtered as opt, i (opt.value)}
84
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
85
+ <div class="sv-ddl__opt" class:is-active={combo.isActive(i)} class:is-selected={combo.isSelected(opt)} class:is-disabled={opt.disabled} {...combo.optionProps(i)}>{opt.label}</div>
86
+ {:else}
87
+ <div class="sv-ddl__empty">No matches</div>
88
+ {/each}
89
+ </div>
90
+ {/if}
91
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
92
+
93
+ <style>
94
+ .sv-combo {
95
+ --_accent: var(--sg-accent, #2563eb);
96
+ display: inline-flex; align-items: center; width: 200px;
97
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
98
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
99
+ }
100
+ .sv-combo--sm { height: 28px; font-size: 12px; }
101
+ .sv-combo--md { height: 34px; font-size: 13px; }
102
+ .sv-combo--lg { height: 40px; font-size: 15px; }
103
+ .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); }
104
+ .sv-combo.is-disabled { opacity: 0.6; }
105
+ .sv-combo__input { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 4px 0 10px; }
106
+ .sv-combo__chev { display: grid; place-items: center; width: 28px; align-self: stretch; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; }
107
+ :global(.sv-ddl__empty) { padding: 8px 10px; color: var(--sg-muted, #94a3b8); font-size: 13px; }
108
+ </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,125 @@
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, type AnchoredRect } from './popover'
12
+ import { flagEmoji } from './countries'
13
+ import { createCountryInput } from './createCountryInput.svelte'
14
+
15
+ type Props = {
16
+ value?: string | null
17
+ onChange?: (code: string) => void
18
+ /** Show the dial code beside each country. */
19
+ showDial?: boolean
20
+ placeholder?: string
21
+ disabled?: boolean
22
+ name?: string
23
+ size?: 'sm' | 'md' | 'lg'
24
+ ariaLabel?: string
25
+ }
26
+
27
+ let { value = null, onChange, showDial = false, placeholder = 'Select country…', disabled = false, name, size = 'md', ariaLabel }: Props = $props()
28
+
29
+ let triggerEl = $state<HTMLButtonElement | null>(null)
30
+ let searchEl = $state<HTMLInputElement | null>(null)
31
+ let panelEl = $state<HTMLDivElement | null>(null)
32
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
33
+
34
+ const ci = createCountryInput({
35
+ value: () => value,
36
+ onChange: (c) => onChange?.(c),
37
+ disabled: () => disabled,
38
+ ariaLabel: () => ariaLabel,
39
+ focusTrigger: () => triggerEl?.focus(),
40
+ })
41
+
42
+ const selected = $derived(ci.selected)
43
+
44
+ function updatePos() { if (triggerEl) rect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 320, minWidth: 240 }) }
45
+
46
+ $effect(() => {
47
+ if (!ci.open) return
48
+ updatePos()
49
+ queueMicrotask(() => searchEl?.focus())
50
+ const rp = () => updatePos()
51
+ window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
52
+ const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; ci.close() }
53
+ document.addEventListener('pointerdown', od, true)
54
+ return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
55
+ })
56
+ // Keep the active option scrolled into view (render concern).
57
+ $effect(() => {
58
+ if (!ci.open) return
59
+ const i = ci.activeIndex
60
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
61
+ })
62
+ </script>
63
+
64
+ <button bind:this={triggerEl} class="sv-country sv-country--{size}" class:is-open={ci.open} class:is-disabled={disabled} {...ci.triggerProps()}>
65
+ {#if selected}
66
+ <span class="sv-country__flag" aria-hidden="true">{flagEmoji(selected.code)}</span>
67
+ <span class="sv-country__name">{selected.name}</span>
68
+ {#if showDial}<span class="sv-country__dial">{selected.dial}</span>{/if}
69
+ {:else}
70
+ <span class="sv-country__ph">{placeholder}</span>
71
+ {/if}
72
+ <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>
73
+ </button>
74
+
75
+ {#if ci.open}
76
+ <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">
77
+ <input bind:this={searchEl} class="sv-country__search" placeholder="Search countries…" {...ci.searchProps()} />
78
+ <div class="sv-country__list" {...ci.listboxProps()}>
79
+ {#each ci.filtered as c, i (c.code)}
80
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
81
+ <div class="sv-country__opt" class:is-active={ci.isActive(i)} class:is-selected={ci.isSelected(c.code)} {...ci.optionProps(i)}>
82
+ <span class="sv-country__flag" aria-hidden="true">{flagEmoji(c.code)}</span>
83
+ <span class="sv-country__name">{c.name}</span>
84
+ <span class="sv-country__dial">{c.dial}</span>
85
+ </div>
86
+ {:else}
87
+ <div class="sv-ddl__empty">No matches</div>
88
+ {/each}
89
+ </div>
90
+ </div>
91
+ {/if}
92
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
93
+
94
+ <style>
95
+ .sv-country {
96
+ --_accent: var(--sg-accent, #2563eb);
97
+ display: inline-flex; align-items: center; gap: 8px; width: 220px;
98
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: left;
99
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); padding: 0 10px; cursor: pointer;
100
+ }
101
+ .sv-country--sm { height: 28px; font-size: 12px; }
102
+ .sv-country--md { height: 34px; font-size: 13px; }
103
+ .sv-country--lg { height: 40px; font-size: 15px; }
104
+ .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; }
105
+ .sv-country.is-disabled { opacity: 0.6; cursor: not-allowed; }
106
+ .sv-country__flag { font-size: 16px; }
107
+ .sv-country__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
108
+ .sv-country__ph { flex: 1; color: var(--sg-muted, #94a3b8); }
109
+ .sv-country__dial { color: var(--sg-muted, #64748b); font-size: 12px; }
110
+ .sv-country__chev { color: var(--sg-muted, #64748b); flex: none; }
111
+
112
+ :global(.sv-country__panel) {
113
+ z-index: 2147483647; display: flex; flex-direction: column; max-height: 340px; padding: 6px;
114
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
115
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
116
+ }
117
+ :global(.sv-country__search) {
118
+ height: 32px; margin-bottom: 4px; padding: 0 10px; font: inherit; font-size: 13px;
119
+ border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 7px; background: var(--sg-input-bg, #fff); color: inherit; outline: none;
120
+ }
121
+ :global(.sv-country__list) { overflow-y: auto; }
122
+ :global(.sv-country__opt) { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; }
123
+ :global(.sv-country__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
124
+ :global(.sv-country__opt.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
125
+ </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;