@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,211 @@
1
+ /**
2
+ * Component tests for SvCalendar. Mounts in jsdom and drives real DOM clicks /
3
+ * keydowns, asserting the emitted selection + navigation. Discriminating: each
4
+ * checks the actual selected value(s), not just that something rendered.
5
+ */
6
+ import { describe, expect, it } from 'vitest'
7
+ import { mount, unmount, flushSync } from 'svelte'
8
+ import SvCalendar from './SvCalendar.svelte'
9
+
10
+ type Mounted = { target: HTMLElement; destroy: () => void }
11
+
12
+ function mountCal(props: Record<string, unknown>): Mounted {
13
+ const target = document.createElement('div')
14
+ document.body.appendChild(target)
15
+ const app = mount(SvCalendar, { target, props: props as any })
16
+ flushSync()
17
+ return { target, destroy: () => { unmount(app); target.remove() } }
18
+ }
19
+
20
+ const dayButton = (target: HTMLElement, label: string) =>
21
+ Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__day')).find(
22
+ (b) => b.textContent?.trim() === label && !b.classList.contains('is-outside'),
23
+ )
24
+
25
+ describe('SvCalendar render', () => {
26
+ it('renders the seeded month title and a 6x7 grid', () => {
27
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
28
+ try {
29
+ expect(target.querySelector('.sv-cal__title')?.textContent).toContain('2026')
30
+ const days = target.querySelectorAll('.sv-cal__day')
31
+ expect(days.length).toBe(42) // 6 weeks x 7
32
+ } finally {
33
+ destroy()
34
+ }
35
+ })
36
+
37
+ it('marks the selected day', () => {
38
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
39
+ try {
40
+ const sel = target.querySelector('.sv-cal__day.is-selected')
41
+ expect(sel?.textContent?.trim()).toBe('15')
42
+ } finally {
43
+ destroy()
44
+ }
45
+ })
46
+
47
+ it('shows week numbers when enabled', () => {
48
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), weekNumbers: true, firstDayOfWeek: 1 })
49
+ try {
50
+ expect(target.querySelectorAll('.sv-cal__wk').length).toBe(6)
51
+ } finally {
52
+ destroy()
53
+ }
54
+ })
55
+ })
56
+
57
+ describe('SvCalendar selection', () => {
58
+ it('emits the clicked day (mode one)', () => {
59
+ let got: Date[] = []
60
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), onChange: (d: Date[]) => (got = d) })
61
+ try {
62
+ dayButton(target, '20')!.click()
63
+ flushSync()
64
+ expect(got).toHaveLength(1)
65
+ expect(got[0]!.getDate()).toBe(20)
66
+ expect(got[0]!.getMonth()).toBe(5)
67
+ } finally {
68
+ destroy()
69
+ }
70
+ })
71
+
72
+ it('range mode: two clicks emit the inclusive span', () => {
73
+ let got: Date[] = []
74
+ const { target, destroy } = mountCal({
75
+ value: null,
76
+ selectionMode: 'range',
77
+ displayMode: 'month',
78
+ onChange: (d: Date[]) => (got = d),
79
+ })
80
+ try {
81
+ // Seed view to a known month by selecting from it.
82
+ const cal = mountCal({ value: new Date(2026, 5, 1), selectionMode: 'range', onChange: (d: Date[]) => (got = d) })
83
+ dayButton(cal.target, '3')!.click()
84
+ flushSync()
85
+ dayButton(cal.target, '6')!.click()
86
+ flushSync()
87
+ expect(got.map((d) => d.getDate())).toEqual([3, 4, 5, 6])
88
+ cal.destroy()
89
+ } finally {
90
+ destroy()
91
+ }
92
+ })
93
+
94
+ it('does not select a disabled (out-of-range) day', () => {
95
+ let calls = 0
96
+ const { target, destroy } = mountCal({
97
+ value: new Date(2026, 5, 15),
98
+ min: new Date(2026, 5, 10),
99
+ onChange: () => (calls += 1),
100
+ })
101
+ try {
102
+ const five = dayButton(target, '5')
103
+ expect(five?.disabled).toBe(true)
104
+ five?.click()
105
+ flushSync()
106
+ expect(calls).toBe(0)
107
+ } finally {
108
+ destroy()
109
+ }
110
+ })
111
+ })
112
+
113
+ describe('SvCalendar navigation', () => {
114
+ it('prev/next steps the month', () => {
115
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
116
+ try {
117
+ const [prev, , next] = target.querySelectorAll<HTMLButtonElement>('.sv-cal__nav, .sv-cal__title')
118
+ // next button is the 2nd .sv-cal__nav; grab by aria-label
119
+ target.querySelector<HTMLButtonElement>('[aria-label="Next"]')!.click()
120
+ flushSync()
121
+ expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('july')
122
+ target.querySelector<HTMLButtonElement>('[aria-label="Previous"]')!.click()
123
+ target.querySelector<HTMLButtonElement>('[aria-label="Previous"]')!.click()
124
+ flushSync()
125
+ expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('may')
126
+ void prev; void next
127
+ } finally {
128
+ destroy()
129
+ }
130
+ })
131
+
132
+ it('title drills month -> year -> decade', () => {
133
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
134
+ try {
135
+ const title = () => target.querySelector<HTMLButtonElement>('.sv-cal__title')!
136
+ title().click() // -> year
137
+ flushSync()
138
+ expect(target.querySelectorAll('.sv-cal__cell').length).toBe(12) // 12 months
139
+ title().click() // -> decade
140
+ flushSync()
141
+ expect(title().textContent).toContain('2020 - 2029')
142
+ } finally {
143
+ destroy()
144
+ }
145
+ })
146
+
147
+ it('year mode: clicking a month drills back to that month', () => {
148
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
149
+ try {
150
+ target.querySelector<HTMLButtonElement>('.sv-cal__title')!.click() // year mode
151
+ flushSync()
152
+ const jan = Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__cell'))[0]!
153
+ jan.click()
154
+ flushSync()
155
+ expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('january')
156
+ } finally {
157
+ destroy()
158
+ }
159
+ })
160
+
161
+ it('steps the month with animation enabled (animation never blocks navigation)', () => {
162
+ const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), animate: 'slide' })
163
+ try {
164
+ target.querySelector<HTMLButtonElement>('[aria-label="Next"]')!.click()
165
+ flushSync()
166
+ expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('july')
167
+ } finally {
168
+ destroy()
169
+ }
170
+ })
171
+ })
172
+
173
+ describe('SvCalendar presets + tooltips', () => {
174
+ it('renders preset buttons and applies a range preset', () => {
175
+ let got: Date[] = []
176
+ const { target, destroy } = mountCal({
177
+ value: null,
178
+ selectionMode: 'range',
179
+ presets: [
180
+ { label: 'First five', value: [new Date(2026, 5, 1), new Date(2026, 5, 5)] },
181
+ { label: 'Just the 10th', value: new Date(2026, 5, 10) },
182
+ ],
183
+ onChange: (d: Date[]) => (got = d),
184
+ })
185
+ try {
186
+ const buttons = Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__preset'))
187
+ expect(buttons.map((b) => b.textContent?.trim())).toEqual(['First five', 'Just the 10th'])
188
+ buttons[0]!.click()
189
+ flushSync()
190
+ expect(got.map((d) => d.getDate())).toEqual([1, 2, 3, 4, 5])
191
+ buttons[1]!.click()
192
+ flushSync()
193
+ expect(got.map((d) => d.getDate())).toEqual([10])
194
+ } finally {
195
+ destroy()
196
+ }
197
+ })
198
+
199
+ it('sets a native title tooltip only on matching days', () => {
200
+ const { target, destroy } = mountCal({
201
+ value: new Date(2026, 5, 15),
202
+ dateTooltip: (d: Date) => (d.getDate() === 20 && d.getMonth() === 5 ? 'Release day' : undefined),
203
+ })
204
+ try {
205
+ expect(dayButton(target, '20')?.title).toBe('Release day')
206
+ expect(dayButton(target, '10')?.title).toBe('')
207
+ } finally {
208
+ destroy()
209
+ }
210
+ })
211
+ })
@@ -0,0 +1,82 @@
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
+ import { createCheckbox } from './createCheckbox.svelte'
8
+
9
+ type Props = {
10
+ checked?: boolean
11
+ indeterminate?: boolean
12
+ onChange?: (checked: boolean) => void
13
+ disabled?: boolean
14
+ size?: 'sm' | 'md' | 'lg'
15
+ name?: string
16
+ value?: string
17
+ ariaLabel?: string
18
+ children?: Snippet
19
+ }
20
+
21
+ let {
22
+ checked = false,
23
+ indeterminate = false,
24
+ onChange,
25
+ disabled = false,
26
+ size = 'md',
27
+ name,
28
+ value,
29
+ ariaLabel,
30
+ children,
31
+ }: Props = $props()
32
+
33
+ // The styled checkbox is just a renderer over the headless core. The label
34
+ // fallback depends on `children` (a render concern), so it is computed here.
35
+ const cb = createCheckbox({
36
+ checked: () => checked,
37
+ indeterminate: () => indeterminate,
38
+ onChange: (v) => onChange?.(v),
39
+ disabled: () => disabled,
40
+ ariaLabel: () => ariaLabel ?? (children ? undefined : 'checkbox'),
41
+ })
42
+ </script>
43
+
44
+ <label class="sv-check sv-check--{size}" class:is-disabled={disabled}>
45
+ <button
46
+ class="sv-check__box"
47
+ class:is-checked={checked && !indeterminate}
48
+ class:is-indeterminate={indeterminate}
49
+ {...cb.boxProps()}
50
+ >
51
+ {#if indeterminate}
52
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 8h8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" /></svg>
53
+ {:else if checked}
54
+ <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>
55
+ {/if}
56
+ </button>
57
+ {#if children}<span class="sv-check__label">{@render children()}</span>{/if}
58
+ {#if name}<input type="hidden" {name} value={checked ? (value ?? 'true') : ''} />{/if}
59
+ </label>
60
+
61
+ <style>
62
+ .sv-check {
63
+ --_accent: var(--sg-accent, #2563eb);
64
+ --_sz: 18px;
65
+ display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
66
+ font: inherit; font-size: 13px; color: var(--sg-fg, #0f172a);
67
+ }
68
+ .sv-check--sm { --_sz: 15px; font-size: 12px; }
69
+ .sv-check--lg { --_sz: 22px; font-size: 15px; }
70
+ .sv-check.is-disabled { opacity: 0.55; cursor: not-allowed; }
71
+ .sv-check__box {
72
+ width: var(--_sz); height: var(--_sz); flex: none; padding: 0; cursor: inherit;
73
+ display: grid; place-items: center;
74
+ background: var(--sg-input-bg, #fff); color: var(--sg-on-accent, #fff);
75
+ border: 1.5px solid var(--sg-border, #cbd5e1); border-radius: 5px; transition: background 0.12s, border-color 0.12s;
76
+ }
77
+ .sv-check__box svg { width: 100%; height: 100%; }
78
+ .sv-check__box.is-checked, .sv-check__box.is-indeterminate {
79
+ background: var(--_accent); border-color: var(--_accent);
80
+ }
81
+ .sv-check__box:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
82
+ </style>
@@ -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,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,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>