@svgrid/grid 2.2.16 → 2.2.17

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 (290) hide show
  1. package/README.md +1 -1
  2. package/dist/GridFooter.svelte +12 -11
  3. package/dist/SvAnchor.svelte +65 -0
  4. package/dist/SvAnchor.svelte.d.ts +24 -0
  5. package/dist/SvAspectRatio.svelte +38 -0
  6. package/dist/SvAspectRatio.svelte.d.ts +16 -0
  7. package/dist/SvAutoComplete.svelte +52 -7
  8. package/dist/SvAutoComplete.svelte.d.ts +4 -1
  9. package/dist/SvBlockquote.svelte +43 -0
  10. package/dist/SvBlockquote.svelte.d.ts +16 -0
  11. package/dist/SvCalendar.svelte +33 -2
  12. package/dist/SvCalendar.svelte.d.ts +8 -1
  13. package/dist/SvCheckBox.svelte +6 -2
  14. package/dist/SvCode.svelte +52 -0
  15. package/dist/SvCode.svelte.d.ts +19 -0
  16. package/dist/SvCollapsible.svelte +75 -0
  17. package/dist/SvCollapsible.svelte.d.ts +27 -0
  18. package/dist/SvColorInput.svelte +11 -17
  19. package/dist/SvColorInput.svelte.d.ts +2 -0
  20. package/dist/SvComboBox.svelte +85 -8
  21. package/dist/SvComboBox.svelte.d.ts +14 -0
  22. package/dist/SvCountryInput.svelte +2 -2
  23. package/dist/SvCountryInput.svelte.d.ts +1 -1
  24. package/dist/SvDropDownList.svelte +154 -51
  25. package/dist/SvDropDownList.svelte.d.ts +17 -3
  26. package/dist/SvDurationInput.svelte +59 -41
  27. package/dist/SvDurationInput.svelte.d.ts +18 -0
  28. package/dist/SvField.svelte +203 -12
  29. package/dist/SvField.svelte.d.ts +45 -8
  30. package/dist/SvForm.svelte +195 -21
  31. package/dist/SvForm.svelte.d.ts +23 -0
  32. package/dist/SvGrid.controller.svelte.d.ts +12 -1
  33. package/dist/SvGrid.controller.svelte.js +87 -7
  34. package/dist/SvGrid.svelte +383 -181
  35. package/dist/SvGrid.types.d.ts +123 -2
  36. package/dist/SvGridChartView.svelte +70 -0
  37. package/dist/SvGridChartView.svelte.d.ts +11 -0
  38. package/dist/SvGridDropdown.svelte +9 -2
  39. package/dist/SvGridSelect.svelte +5 -2
  40. package/dist/SvGridSelect.svelte.d.ts +1 -1
  41. package/dist/SvGroup.svelte +41 -0
  42. package/dist/SvGroup.svelte.d.ts +27 -0
  43. package/dist/SvHoverCard.svelte +70 -0
  44. package/dist/SvHoverCard.svelte.d.ts +41 -0
  45. package/dist/SvKbd.svelte +51 -0
  46. package/dist/SvKbd.svelte.d.ts +18 -0
  47. package/dist/SvList.svelte +55 -0
  48. package/dist/SvList.svelte.d.ts +23 -0
  49. package/dist/SvListBox.svelte +166 -77
  50. package/dist/SvListBox.svelte.d.ts +17 -4
  51. package/dist/SvLoadingOverlay.svelte +53 -0
  52. package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
  53. package/dist/SvMark.svelte +36 -0
  54. package/dist/SvMark.svelte.d.ts +14 -0
  55. package/dist/SvMaskedInput.svelte +50 -35
  56. package/dist/SvMaskedInput.svelte.d.ts +17 -2
  57. package/dist/SvMenuList.svelte +32 -3
  58. package/dist/SvMenubar.svelte +200 -0
  59. package/dist/SvMenubar.svelte.d.ts +18 -0
  60. package/dist/SvMultiSelect.svelte +7 -4
  61. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  62. package/dist/SvNumberInput.svelte +79 -42
  63. package/dist/SvNumberInput.svelte.d.ts +25 -0
  64. package/dist/SvOtpInput.svelte +2 -2
  65. package/dist/SvPasswordInput.svelte +51 -31
  66. package/dist/SvPasswordInput.svelte.d.ts +17 -0
  67. package/dist/SvPhoneInput.svelte +39 -28
  68. package/dist/SvPhoneInput.svelte.d.ts +4 -0
  69. package/dist/SvPopconfirm.svelte +79 -0
  70. package/dist/SvPopconfirm.svelte.d.ts +42 -0
  71. package/dist/SvPopover.svelte +87 -29
  72. package/dist/SvPopover.svelte.d.ts +18 -4
  73. package/dist/SvRadioGroup.svelte +5 -1
  74. package/dist/SvResult.svelte +73 -0
  75. package/dist/SvResult.svelte.d.ts +28 -0
  76. package/dist/SvSegmented.svelte +114 -0
  77. package/dist/SvSegmented.svelte.d.ts +29 -0
  78. package/dist/SvSimpleGrid.svelte +41 -0
  79. package/dist/SvSimpleGrid.svelte.d.ts +25 -0
  80. package/dist/SvSlider.svelte +23 -10
  81. package/dist/SvSpinner.svelte +43 -0
  82. package/dist/SvSpinner.svelte.d.ts +20 -0
  83. package/dist/SvStack.svelte +37 -0
  84. package/dist/SvStack.svelte.d.ts +24 -0
  85. package/dist/SvSwitchButton.svelte +6 -1
  86. package/dist/SvTagsInput.svelte +9 -5
  87. package/dist/SvText.svelte +76 -0
  88. package/dist/SvText.svelte.d.ts +31 -0
  89. package/dist/SvTextInput.svelte +75 -43
  90. package/dist/SvTextInput.svelte.d.ts +28 -1
  91. package/dist/SvTimePicker.svelte +32 -2
  92. package/dist/SvTimePicker.svelte.d.ts +8 -9
  93. package/dist/SvTitle.svelte +57 -0
  94. package/dist/SvTitle.svelte.d.ts +24 -0
  95. package/dist/SvToaster.svelte +41 -6
  96. package/dist/SvToggleButton.svelte +4 -1
  97. package/dist/SvTooltip.svelte +68 -34
  98. package/dist/SvTooltip.svelte.d.ts +14 -2
  99. package/dist/SvTree.svelte +81 -21
  100. package/dist/SvTree.svelte.d.ts +3 -2
  101. package/dist/SvTreeSelect.svelte +5 -2
  102. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  103. package/dist/SvVisuallyHidden.svelte +49 -0
  104. package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
  105. package/dist/ai.d.ts +289 -0
  106. package/dist/ai.js +1013 -0
  107. package/dist/board-view.svelte.d.ts +22 -0
  108. package/dist/board-view.svelte.js +13 -0
  109. package/dist/build-api.js +36 -6
  110. package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
  111. package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
  112. package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
  113. package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
  114. package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
  115. package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
  116. package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
  117. package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
  118. package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
  119. package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
  120. package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
  121. package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
  122. package/dist/cdn/chart-T1usDN0o.js +1212 -0
  123. package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
  124. package/dist/cdn/export-format-JT5pWVT8.js +561 -0
  125. package/dist/cdn/popover-P-9P2YYn.js +69 -0
  126. package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
  127. package/dist/cdn/src-BLhdLmoN.js +25707 -0
  128. package/dist/cdn/src-BwnUVj3T.js +25623 -0
  129. package/dist/cdn/svgrid.js +9 -32897
  130. package/dist/cdn/svgrid.svelte-external.js +9 -30052
  131. package/dist/cell-formatting.d.ts +2 -2
  132. package/dist/cell-render.js +4 -4
  133. package/dist/chart-view.svelte.d.ts +25 -0
  134. package/dist/chart-view.svelte.js +14 -0
  135. package/dist/createCombobox.svelte.d.ts +5 -0
  136. package/dist/createCombobox.svelte.js +21 -5
  137. package/dist/createDropdownList.svelte.d.ts +3 -0
  138. package/dist/createDropdownList.svelte.js +8 -2
  139. package/dist/createForm.svelte.d.ts +18 -0
  140. package/dist/createForm.svelte.js +51 -6
  141. package/dist/createMenu.svelte.js +27 -0
  142. package/dist/createPopoverSelect.svelte.d.ts +3 -0
  143. package/dist/createPopoverSelect.svelte.js +9 -1
  144. package/dist/createSlider.svelte.d.ts +8 -21
  145. package/dist/createSlider.svelte.js +34 -0
  146. package/dist/createTooltip.svelte.d.ts +12 -0
  147. package/dist/createTooltip.svelte.js +72 -9
  148. package/dist/editor-contract.d.ts +15 -0
  149. package/dist/export-provider.d.ts +27 -0
  150. package/dist/export-provider.js +9 -0
  151. package/dist/filter-operators.d.ts +6 -0
  152. package/dist/filter-operators.js +33 -0
  153. package/dist/form-field.d.ts +45 -2
  154. package/dist/grid-messages.d.ts +61 -0
  155. package/dist/grid-messages.js +72 -0
  156. package/dist/group-display.d.ts +62 -0
  157. package/dist/group-display.js +78 -0
  158. package/dist/index.d.ts +36 -4
  159. package/dist/index.js +43 -2
  160. package/dist/js-scroller.svelte.d.ts +35 -0
  161. package/dist/js-scroller.svelte.js +154 -0
  162. package/dist/list-option.d.ts +46 -0
  163. package/dist/list-option.js +53 -0
  164. package/dist/menus.js +25 -11
  165. package/dist/pivot-view.svelte.d.ts +72 -0
  166. package/dist/pivot-view.svelte.js +13 -0
  167. package/dist/positioning.d.ts +111 -0
  168. package/dist/positioning.js +191 -0
  169. package/dist/scheduler-model.d.ts +3 -0
  170. package/dist/scheduler-model.js +22 -5
  171. package/dist/svgrid-wrapper.types.d.ts +18 -0
  172. package/dist/toast-store.svelte.d.ts +54 -0
  173. package/dist/toast-store.svelte.js +65 -15
  174. package/package.json +9 -1
  175. package/src/GridFooter.svelte +12 -11
  176. package/src/SvAnchor.svelte +65 -0
  177. package/src/SvAspectRatio.svelte +38 -0
  178. package/src/SvAutoComplete.svelte +52 -7
  179. package/src/SvBlockquote.svelte +43 -0
  180. package/src/SvCalendar.svelte +33 -2
  181. package/src/SvCalendar.test.ts +15 -0
  182. package/src/SvCheckBox.svelte +6 -2
  183. package/src/SvCode.svelte +52 -0
  184. package/src/SvCollapsible.svelte +75 -0
  185. package/src/SvColorInput.svelte +11 -17
  186. package/src/SvComboBox.svelte +85 -8
  187. package/src/SvCountryInput.svelte +2 -2
  188. package/src/SvDropDownList.svelte +154 -51
  189. package/src/SvDurationInput.svelte +59 -41
  190. package/src/SvField.svelte +203 -12
  191. package/src/SvForm.svelte +195 -21
  192. package/src/SvForm.test.ts +112 -0
  193. package/src/SvGrid.controller.svelte.ts +88 -7
  194. package/src/SvGrid.svelte +383 -181
  195. package/src/SvGrid.types.ts +125 -1
  196. package/src/SvGridChartView.svelte +70 -0
  197. package/src/SvGridDropdown.svelte +9 -2
  198. package/src/SvGridSelect.svelte +5 -2
  199. package/src/SvGroup.svelte +41 -0
  200. package/src/SvHoverCard.svelte +70 -0
  201. package/src/SvKbd.svelte +51 -0
  202. package/src/SvList.svelte +55 -0
  203. package/src/SvListBox.svelte +166 -77
  204. package/src/SvLoadingOverlay.svelte +53 -0
  205. package/src/SvMark.svelte +36 -0
  206. package/src/SvMaskedInput.svelte +50 -35
  207. package/src/SvMenuList.svelte +32 -3
  208. package/src/SvMenubar.svelte +200 -0
  209. package/src/SvMultiSelect.svelte +7 -4
  210. package/src/SvNumberInput.svelte +79 -42
  211. package/src/SvOtpInput.svelte +2 -2
  212. package/src/SvPasswordInput.svelte +51 -31
  213. package/src/SvPhoneInput.svelte +39 -28
  214. package/src/SvPopconfirm.svelte +79 -0
  215. package/src/SvPopover.svelte +87 -29
  216. package/src/SvRadioGroup.svelte +5 -1
  217. package/src/SvResult.svelte +73 -0
  218. package/src/SvSegmented.svelte +114 -0
  219. package/src/SvSimpleGrid.svelte +41 -0
  220. package/src/SvSlider.svelte +23 -10
  221. package/src/SvSpinner.svelte +43 -0
  222. package/src/SvStack.svelte +37 -0
  223. package/src/SvSwitchButton.svelte +6 -1
  224. package/src/SvTagsInput.svelte +9 -5
  225. package/src/SvText.svelte +76 -0
  226. package/src/SvTextInput.svelte +75 -43
  227. package/src/SvTimePicker.svelte +32 -2
  228. package/src/SvTitle.svelte +57 -0
  229. package/src/SvToaster.svelte +41 -6
  230. package/src/SvToaster.test.ts +32 -0
  231. package/src/SvToggleButton.svelte +4 -1
  232. package/src/SvTooltip.svelte +68 -34
  233. package/src/SvTree.svelte +81 -21
  234. package/src/SvTreeSelect.svelte +5 -2
  235. package/src/SvVisuallyHidden.svelte +49 -0
  236. package/src/ai.test.ts +502 -0
  237. package/src/ai.ts +1391 -0
  238. package/src/board-view.svelte.ts +33 -0
  239. package/src/build-api.coverage.test.ts +101 -0
  240. package/src/build-api.ts +43 -18
  241. package/src/builtin-editors.grid.test.ts +1 -1
  242. package/src/cell-formatting.ts +5 -5
  243. package/src/cell-render.ts +4 -4
  244. package/src/chart-view.svelte.ts +36 -0
  245. package/src/createCombobox.svelte.ts +17 -5
  246. package/src/createDropdownList.svelte.ts +8 -2
  247. package/src/createForm.svelte.ts +59 -6
  248. package/src/createMenu.svelte.ts +23 -0
  249. package/src/createPopoverSelect.svelte.ts +8 -1
  250. package/src/createSlider.svelte.ts +20 -0
  251. package/src/createTooltip.svelte.ts +67 -9
  252. package/src/editor-contract.ts +16 -0
  253. package/src/export-provider.ts +40 -0
  254. package/src/filter-operators.ts +37 -0
  255. package/src/form-field.ts +42 -2
  256. package/src/grid-messages.test.ts +24 -0
  257. package/src/grid-messages.ts +137 -0
  258. package/src/group-display.test.ts +77 -0
  259. package/src/group-display.ts +117 -0
  260. package/src/i18n.grid.test.ts +73 -0
  261. package/src/index.ts +91 -2
  262. package/src/js-scroller.svelte.ts +173 -0
  263. package/src/list-option.test.ts +56 -0
  264. package/src/list-option.ts +81 -0
  265. package/src/menus.test.ts +32 -0
  266. package/src/menus.ts +25 -12
  267. package/src/pivot-view.svelte.ts +90 -0
  268. package/src/pivot-view.test.ts +38 -0
  269. package/src/pivot.grid.test.ts +102 -0
  270. package/src/positioning.test.ts +141 -0
  271. package/src/positioning.ts +266 -0
  272. package/src/scheduler-model.test.ts +16 -0
  273. package/src/scheduler-model.ts +24 -5
  274. package/src/svgrid-wrapper.types.ts +23 -0
  275. package/src/svgrid.charting.test.ts +8 -1
  276. package/src/svgrid.comments-autocomplete.test.ts +7 -2
  277. package/src/svgrid.context-menu.test.ts +8 -3
  278. package/src/toast-store.svelte.ts +98 -0
  279. package/src/toast-store.test.ts +62 -1
  280. package/src/ui-breadth.test.ts +138 -0
  281. package/src/ui-buttons.test.ts +39 -0
  282. package/src/ui-inputs.test.ts +2 -2
  283. package/src/ui-range.test.ts +9 -0
  284. package/src/ui-selection.test.ts +36 -0
  285. package/src/ui-tier1.test.ts +1 -1
  286. package/src/virtual.test.ts +20 -6
  287. package/dist/SvGridBoard.svelte +0 -2339
  288. package/dist/SvGridBoard.svelte.d.ts +0 -31
  289. package/src/SvGridBoard.svelte +0 -2339
  290. package/src/board.test.ts +0 -927
@@ -77,6 +77,7 @@
77
77
  const changePage = $derived(ctrl.changePage);
78
78
  const goToPage = $derived(ctrl.goToPage);
79
79
  const setPageSize = $derived(ctrl.setPageSize);
80
+ const messages = $derived(ctrl.messages);
80
81
  </script>
81
82
 
82
83
  {#if showStatus && statusBarEnabled && statusBarStats}
@@ -85,7 +86,7 @@
85
86
  {#each statusBarAggregates as agg (agg)}
86
87
  {#if agg === "count"}
87
88
  <span class="sv-grid-status-item"
88
- ><span class="sv-grid-status-label">Count</span>{fmtStat(s.count)}</span
89
+ ><span class="sv-grid-status-label">{messages.statCount}</span>{fmtStat(s.count)}</span
89
90
  >
90
91
  {:else if agg === "numericCount"}
91
92
  <span class="sv-grid-status-item"
@@ -93,19 +94,19 @@
93
94
  >
94
95
  {:else if s.numericCount > 0 && agg === "sum"}
95
96
  <span class="sv-grid-status-item"
96
- ><span class="sv-grid-status-label">Sum</span>{fmtStat(s.sum)}</span
97
+ ><span class="sv-grid-status-label">{messages.statSum}</span>{fmtStat(s.sum)}</span
97
98
  >
98
99
  {:else if s.numericCount > 0 && agg === "avg"}
99
100
  <span class="sv-grid-status-item"
100
- ><span class="sv-grid-status-label">Avg</span>{fmtStat(s.avg)}</span
101
+ ><span class="sv-grid-status-label">{messages.statAvg}</span>{fmtStat(s.avg)}</span
101
102
  >
102
103
  {:else if s.numericCount > 0 && agg === "min"}
103
104
  <span class="sv-grid-status-item"
104
- ><span class="sv-grid-status-label">Min</span>{fmtStat(s.min)}</span
105
+ ><span class="sv-grid-status-label">{messages.statMin}</span>{fmtStat(s.min)}</span
105
106
  >
106
107
  {:else if s.numericCount > 0 && agg === "max"}
107
108
  <span class="sv-grid-status-item"
108
- ><span class="sv-grid-status-label">Max</span>{fmtStat(s.max)}</span
109
+ ><span class="sv-grid-status-label">{messages.statMax}</span>{fmtStat(s.max)}</span
109
110
  >
110
111
  {/if}
111
112
  {/each}
@@ -125,7 +126,7 @@
125
126
  {@const sizes = pageSizeOptions && pageSizeOptions.length ? pageSizeOptions : [10, 25, 50, 100]}
126
127
  <div class="sv-grid-pagination" class:sv-grid-pagination-top={top} role="navigation" aria-label="Pagination">
127
128
  <div class="sv-grid-pagination-pagesize">
128
- <span>Page Size:</span>
129
+ <span>{messages.pageSize}</span>
129
130
  <div class="sv-grid-pagination-pagesize-dd">
130
131
  <SvGridDropdown
131
132
  options={sizes.map((n) => ({ value: n, label: String(n) }))}
@@ -146,17 +147,17 @@
146
147
  class="sv-grid-pagination-btn"
147
148
  disabled={onFirst}
148
149
  onclick={() => goToPage(0)}
149
- aria-label="First page">⇤</button
150
+ aria-label={messages.firstPage}>⇤</button
150
151
  >
151
152
  <button
152
153
  type="button"
153
154
  class="sv-grid-pagination-btn"
154
155
  disabled={onFirst}
155
156
  onclick={() => changePage(-1)}
156
- aria-label="Previous page">‹</button
157
+ aria-label={messages.prevPage}>‹</button
157
158
  >
158
159
  <span class="sv-grid-pagination-label">
159
- Page <strong>{currentPage.toLocaleString()}</strong> of
160
+ {messages.page} <strong>{currentPage.toLocaleString()}</strong> {messages.of}
160
161
  <strong>{pageCount.toLocaleString()}</strong>
161
162
  </span>
162
163
  <button
@@ -164,14 +165,14 @@
164
165
  class="sv-grid-pagination-btn"
165
166
  disabled={onLast}
166
167
  onclick={() => changePage(1)}
167
- aria-label="Next page">›</button
168
+ aria-label={messages.nextPage}>›</button
168
169
  >
169
170
  <button
170
171
  type="button"
171
172
  class="sv-grid-pagination-btn"
172
173
  disabled={onLast}
173
174
  onclick={() => goToPage(pageCount - 1)}
174
- aria-label="Last page">⇥</button
175
+ aria-label={messages.lastPage}>⇥</button
175
176
  >
176
177
  </div>
177
178
  </div>
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvAnchor - a themed link. Underline on hover by default; `external` adds
4
+ * `target="_blank"` + safe `rel` and an offscreen "opens in new tab" hint.
5
+ *
6
+ * <SvAnchor href="/docs">Read the docs</SvAnchor>
7
+ * <SvAnchor href="https://svelte.dev" external>Svelte</SvAnchor>
8
+ */
9
+ import type { Snippet } from 'svelte'
10
+
11
+ type Props = {
12
+ href?: string
13
+ /** Open in a new tab with safe `rel`. */
14
+ external?: boolean
15
+ /** Underline behaviour. */
16
+ underline?: 'hover' | 'always' | 'none'
17
+ /** Semantic colour. */
18
+ tone?: 'accent' | 'default' | 'muted'
19
+ /** Font size step. */
20
+ size?: 'sm' | 'md' | 'lg'
21
+ onclick?: (e: MouseEvent) => void
22
+ children?: Snippet
23
+ }
24
+
25
+ let { href, external = false, underline = 'hover', tone = 'accent', size = 'md', onclick, children }: Props = $props()
26
+
27
+ const sizeMap = { sm: '13px', md: '14px', lg: '16px' } as const
28
+ const toneMap = {
29
+ accent: 'var(--sg-accent, #2563eb)',
30
+ default: 'var(--sg-fg, #0f172a)',
31
+ muted: 'var(--sg-muted, #64748b)',
32
+ } as const
33
+ </script>
34
+
35
+ <a
36
+ {href}
37
+ {onclick}
38
+ class="sv-anchor sv-anchor--u-{underline}"
39
+ style:font-size={sizeMap[size]}
40
+ style:color={toneMap[tone]}
41
+ target={external ? '_blank' : undefined}
42
+ rel={external ? 'noopener noreferrer' : undefined}
43
+ >
44
+ {@render children?.()}{#if external}<span class="sv-anchor__ext" aria-hidden="true">↗</span><span class="sv-anchor__sr">(opens in a new tab)</span>{/if}
45
+ </a>
46
+
47
+ <style>
48
+ .sv-anchor {
49
+ font-family: var(--sg-font, inherit);
50
+ text-decoration: none;
51
+ border-radius: 2px;
52
+ cursor: pointer;
53
+ }
54
+ .sv-anchor--u-always { text-decoration: underline; text-underline-offset: 2px; }
55
+ .sv-anchor--u-hover:hover { text-decoration: underline; text-underline-offset: 2px; }
56
+ .sv-anchor:focus-visible {
57
+ outline: 2px solid var(--sg-focus-ring, #2563eb);
58
+ outline-offset: 2px;
59
+ }
60
+ .sv-anchor__ext { margin-left: 2px; font-size: 0.85em; }
61
+ .sv-anchor__sr {
62
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
63
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
64
+ }
65
+ </style>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvAspectRatio - hold a fixed width:height ratio for its content (media,
4
+ * embeds, maps, skeletons) so it never causes layout shift. The child fills
5
+ * the box.
6
+ *
7
+ * <SvAspectRatio ratio={16 / 9}><img src={src} alt="" /></SvAspectRatio>
8
+ */
9
+ import type { Snippet } from 'svelte'
10
+
11
+ type Props = {
12
+ /** Width / height ratio (e.g. `16 / 9`, `1`, `4 / 3`). */
13
+ ratio?: number
14
+ children?: Snippet
15
+ }
16
+
17
+ let { ratio = 16 / 9, children }: Props = $props()
18
+ </script>
19
+
20
+ <div class="sv-aspect" style:aspect-ratio={ratio}>
21
+ {@render children?.()}
22
+ </div>
23
+
24
+ <style>
25
+ .sv-aspect {
26
+ position: relative;
27
+ width: 100%;
28
+ overflow: hidden;
29
+ }
30
+ /* Make the single child fill the ratio box by default. */
31
+ .sv-aspect > :global(*) {
32
+ position: absolute;
33
+ inset: 0;
34
+ width: 100%;
35
+ height: 100%;
36
+ object-fit: cover;
37
+ }
38
+ </style>
@@ -8,6 +8,7 @@
8
8
  * portal/measure render concerns live here.
9
9
  */
10
10
  import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
11
+ import { startPanelResize } from './panel-resize'
11
12
  import { createDismissableLayer } from './a11y/dismissable'
12
13
  import { type ListOption } from './list-option'
13
14
  import SvField from './SvField.svelte'
@@ -21,14 +22,18 @@
21
22
  suggestions?: ReadonlyArray<string | ListOption>
22
23
  minChars?: number
23
24
  placeholder?: string
25
+ /** Show a bottom drag grip so the user can resize the open suggestion panel's
26
+ * height (hidden when the panel flips upward, where there is no room to grow). */
27
+ resizable?: boolean
24
28
  }
25
29
 
26
30
  let {
27
- value = '',
31
+ value = $bindable(''),
28
32
  onChange,
29
33
  suggestions = [],
30
34
  minChars = 1,
31
35
  placeholder,
36
+ resizable = false,
32
37
  disabled = false,
33
38
  name,
34
39
  size = 'md',
@@ -40,6 +45,7 @@
40
45
  hint,
41
46
  dir,
42
47
  id,
48
+ loading = false,
43
49
  }: Props = $props()
44
50
 
45
51
  const autoId = nextEditorId('sv-ac')
@@ -48,10 +54,17 @@
48
54
  let fieldEl = $state<HTMLInputElement | null>(null)
49
55
  let panelEl = $state<HTMLDivElement | null>(null)
50
56
  let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
57
+ // User-chosen panel height (px) once they drag the resize grip; null = auto.
58
+ let userHeight = $state<number | null>(null)
59
+ const DEFAULT_CAP = 288
60
+ const panelMaxH = $derived(
61
+ resizable && userHeight != null ? Math.min(userHeight, rect.availHeight) : Math.min(rect.availHeight, DEFAULT_CAP),
62
+ )
63
+ const showGrip = $derived(resizable && !rect.openUpward)
51
64
 
52
65
  const ac = createAutocomplete({
53
66
  value: () => value,
54
- onChange: (v) => onChange?.(v),
67
+ onChange: (v) => { value = v; onChange?.(v) },
55
68
  suggestions: () => suggestions,
56
69
  minChars: () => minChars,
57
70
  disabled: () => disabled,
@@ -67,8 +80,20 @@
67
80
  function updatePos() {
68
81
  if (!fieldEl) return
69
82
  // estimatedHeight only picks the flip direction; real max-height is availHeight
70
- // below, so the panel takes its natural content height. See SvComboBox.
71
- rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(ac.filtered.length, 8) * 34 + 12 })
83
+ // below, so the panel takes its natural content height. See SvComboBox. When
84
+ // the user has dragged the grip, anchor the flip decision to THAT height.
85
+ const est = resizable && userHeight != null ? userHeight : Math.min(ac.filtered.length, 8) * 34 + 12
86
+ rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: est })
87
+ }
88
+
89
+ function startResize(e: PointerEvent) {
90
+ if (!panelEl) return
91
+ startPanelResize(e, {
92
+ startHeight: panelEl.clientHeight,
93
+ min: 96,
94
+ max: () => rect.availHeight,
95
+ onHeight: (h) => { userHeight = h; updatePos() },
96
+ })
72
97
  }
73
98
 
74
99
  $effect(() => {
@@ -82,7 +107,7 @@
82
107
  })
83
108
  </script>
84
109
 
85
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
110
+ <SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
86
111
  <input
87
112
  bind:this={fieldEl}
88
113
  class="sv-ac sv-ac--{size}"
@@ -94,11 +119,17 @@
94
119
  </SvField>
95
120
 
96
121
  {#if ac.open}
97
- <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${Math.min(rect.availHeight, 288)}px`} {...ac.listboxProps()}>
98
- {#each ac.filtered as opt, i (opt.value)}
122
+ <div bind:this={panelEl} class="sv-ddl__panel" class:is-resizable={showGrip} use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${panelMaxH}px`} style:height={resizable && userHeight != null && !rect.openUpward ? `${panelMaxH}px` : undefined} {...ac.listboxProps()}>
123
+ {#each ac.filtered as opt, i (i)}
99
124
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
100
125
  <div class="sv-ddl__opt" class:is-active={ac.isActive(i)} {...ac.optionProps(i)}>{opt.label}</div>
101
126
  {/each}
127
+ {#if showGrip}
128
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
129
+ <div class="sv-ddl__grip" onpointerdown={startResize} title="Drag to resize" aria-hidden="true">
130
+ <span class="sv-ddl__grip-dots"></span>
131
+ </div>
132
+ {/if}
102
133
  </div>
103
134
  {/if}
104
135
  {#if name}<input type="hidden" {name} value={value} />{/if}
@@ -117,4 +148,18 @@
117
148
  .sv-ac:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
118
149
  .sv-ac.is-invalid { border-color: var(--sg-danger, #dc2626); }
119
150
  .sv-ac.is-invalid:focus { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
151
+ /* Bottom resize grip (shared "····" handle - see SvDropDownList / panel-resize). */
152
+ :global(.sv-ddl__panel.is-resizable) { padding-bottom: 0; }
153
+ :global(.sv-ddl__grip) {
154
+ position: sticky; bottom: 0; z-index: 1; margin: 2px -4px -4px; height: 15px;
155
+ display: flex; align-items: center; justify-content: center; cursor: ns-resize;
156
+ background: var(--sg-bg, #fff); border-top: 1px solid var(--sg-border, #e2e8f0);
157
+ border-radius: 0 0 10px 10px; touch-action: none;
158
+ }
159
+ :global(.sv-ddl__grip-dots) {
160
+ width: 26px; height: 4px; color: var(--sg-muted, #94a3b8); opacity: 0.6;
161
+ background-image: radial-gradient(currentColor 1px, transparent 1.6px);
162
+ background-size: 6px 4px; background-position: center; background-repeat: repeat-x;
163
+ }
164
+ :global(.sv-ddl__grip:hover .sv-ddl__grip-dots) { opacity: 1; }
120
165
  </style>
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvBlockquote - a quotation block with an accent rule and an optional citation.
4
+ *
5
+ * <SvBlockquote cite="Ada Lovelace">The Analytical Engine weaves algebraic patterns.</SvBlockquote>
6
+ */
7
+ import type { Snippet } from 'svelte'
8
+
9
+ type Props = {
10
+ /** Attribution shown under the quote. */
11
+ cite?: string
12
+ /** Accent colour of the rule. */
13
+ tone?: 'accent' | 'muted' | 'success' | 'warning' | 'error'
14
+ children?: Snippet
15
+ }
16
+
17
+ let { cite, tone = 'accent', children }: Props = $props()
18
+
19
+ const toneMap = {
20
+ accent: 'var(--sg-accent, #2563eb)',
21
+ muted: 'var(--sg-muted, #64748b)',
22
+ success: 'var(--sg-success, #16a34a)',
23
+ warning: 'var(--sg-warning, #d97706)',
24
+ error: 'var(--sg-danger, #dc2626)',
25
+ } as const
26
+ </script>
27
+
28
+ <blockquote class="sv-blockquote" style:--sv-bq-rule={toneMap[tone]}>
29
+ <div class="sv-blockquote__body">{@render children?.()}</div>
30
+ {#if cite}<footer class="sv-blockquote__cite">- {cite}</footer>{/if}
31
+ </blockquote>
32
+
33
+ <style>
34
+ .sv-blockquote {
35
+ margin: 0;
36
+ padding: 4px 0 4px 16px;
37
+ border-left: 3px solid var(--sv-bq-rule, #2563eb);
38
+ font-family: var(--sg-font, inherit);
39
+ color: var(--sg-fg, #0f172a);
40
+ }
41
+ .sv-blockquote__body { font-size: 15px; line-height: 1.55; }
42
+ .sv-blockquote__cite { margin-top: 8px; font-size: 13px; color: var(--sg-muted, #64748b); }
43
+ </style>
@@ -17,7 +17,8 @@
17
17
  * important dates, week numbers, firstDayOfWeek, drill navigation, keyboard).
18
18
  */
19
19
  import { untrack, type Snippet } from 'svelte'
20
- import { resolveMessages, type EditorDir } from './editor-contract'
20
+ import SvField from './SvField.svelte'
21
+ import { nextEditorId, resolveMessages, type EditorDir, type EditorSize } from './editor-contract'
21
22
  import {
22
23
  createCalendar,
23
24
  type CalendarValue,
@@ -91,6 +92,16 @@
91
92
  * the date + its day-state. Switches the month grid to a taller,
92
93
  * top-aligned layout so there's room for the content. */
93
94
  day?: Snippet<[Date, CalendarDayState]>
95
+ // Optional field chrome: when any of label/hint/error is set, the calendar is
96
+ // wrapped in <SvField> so it gains a label/hint/error line; otherwise it
97
+ // renders bare (as embedded inside SvDateTimePicker's popover).
98
+ label?: string
99
+ hint?: string
100
+ error?: string
101
+ required?: boolean
102
+ invalid?: boolean
103
+ size?: EditorSize
104
+ id?: string
94
105
  }
95
106
 
96
107
  let {
@@ -124,8 +135,17 @@
124
135
  messages,
125
136
  recurrence = null,
126
137
  day,
138
+ label,
139
+ hint,
140
+ error,
141
+ required = false,
142
+ invalid = false,
143
+ id,
127
144
  }: Props = $props()
128
145
 
146
+ const calAutoId = nextEditorId('sv-cal')
147
+ const uid = $derived(id ?? calAutoId)
148
+ const hasChrome = $derived(!!(label || hint || error))
129
149
  const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
130
150
  const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
131
151
  // With several month panels side by side, leading/trailing days of adjacent
@@ -206,14 +226,17 @@
206
226
  }
207
227
  </script>
208
228
 
229
+ {#snippet calBody()}
209
230
  <div
210
231
  class="sv-cal"
211
232
  class:sv-cal--disabled={disabled}
233
+ class:sv-cal--invalid={invalid}
212
234
  class:sv-cal--with-presets={presets && presets.length}
213
235
  class:sv-cal--band={selectionMode === 'range' || selectionMode === 'week'}
214
236
  class:sv-cal--rich={!!day}
237
+ id={uid}
215
238
  role="group"
216
- aria-label={M.label}
239
+ aria-label={label ?? M.label}
217
240
  aria-disabled={disabled}
218
241
  dir={resolvedDir}
219
242
  >
@@ -314,6 +337,13 @@
314
337
 
315
338
  {#if name}<input type="hidden" {name} value={cal.selectedDates.map((d) => d.toISOString()).join(',')} />{/if}
316
339
  </div>
340
+ {/snippet}
341
+
342
+ {#if hasChrome}
343
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>{@render calBody()}</SvField>
344
+ {:else}
345
+ {@render calBody()}
346
+ {/if}
317
347
 
318
348
  <style>
319
349
  .sv-cal {
@@ -336,6 +366,7 @@
336
366
  user-select: none;
337
367
  }
338
368
  .sv-cal--disabled { opacity: 0.55; pointer-events: none; }
369
+ .sv-cal--invalid { border-color: var(--sg-danger, #dc2626); }
339
370
  .sv-cal__main { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
340
371
  /* Clip so animated content slides in from the edge (not past the border). Day
341
372
  focus outlines use outline-offset:-2px, so they stay inside and aren't clipped. */
@@ -34,6 +34,21 @@ describe('SvCalendar render', () => {
34
34
  }
35
35
  })
36
36
 
37
+ it('renders bare by default, but wraps in SvField chrome when label/error given', () => {
38
+ const bare = mountCal({ value: new Date(2026, 5, 15) })
39
+ try {
40
+ expect(bare.target.querySelector('.sv-field')).toBeNull() // bare
41
+ expect(bare.target.querySelector('.sv-cal')).not.toBeNull()
42
+ } finally { bare.destroy() }
43
+ const chrome = mountCal({ value: new Date(2026, 5, 15), label: 'Start date', error: 'Required', required: true })
44
+ try {
45
+ expect(chrome.target.querySelector('.sv-field')).not.toBeNull()
46
+ expect(chrome.target.querySelector('.sv-field__label')?.textContent).toContain('Start date')
47
+ expect(chrome.target.querySelector('.sv-field__error')?.textContent).toContain('Required')
48
+ expect(chrome.target.querySelector('.sv-cal.sv-cal--invalid')).toBeNull() // invalid not set
49
+ } finally { chrome.destroy() }
50
+ })
51
+
37
52
  it('marks the selected day', () => {
38
53
  const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
39
54
  try {
@@ -21,6 +21,7 @@
21
21
  indeterminate = false,
22
22
  onChange,
23
23
  disabled = false,
24
+ readonly = false,
24
25
  size = 'md',
25
26
  name,
26
27
  value,
@@ -42,7 +43,7 @@
42
43
  const cb = createCheckbox({
43
44
  checked: () => checked,
44
45
  indeterminate: () => indeterminate,
45
- onChange: (v) => onChange?.(v),
46
+ onChange: (v) => { if (readonly) return; onChange?.(v) },
46
47
  disabled: () => disabled,
47
48
  ariaLabel: () => ariaLabel ?? (children ? undefined : 'checkbox'),
48
49
  id: () => uid,
@@ -54,12 +55,14 @@
54
55
  </script>
55
56
 
56
57
  <SvField id={uid} {hint} {error} {required} {dir}>
57
- <label class="sv-check sv-check--{size}" class:is-disabled={disabled} class:is-invalid={invalid}>
58
+ <label class="sv-check sv-check--{size}" class:is-disabled={disabled} class:is-readonly={readonly} class:is-invalid={invalid}>
58
59
  <button
60
+ role="checkbox"
59
61
  class="sv-check__box"
60
62
  class:is-checked={checked && !indeterminate}
61
63
  class:is-indeterminate={indeterminate}
62
64
  {...cb.boxProps()}
65
+ aria-readonly={readonly ? 'true' : undefined}
63
66
  >
64
67
  {#if indeterminate}
65
68
  <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 8h8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" /></svg>
@@ -82,6 +85,7 @@
82
85
  .sv-check--sm { --_sz: 15px; font-size: 12px; }
83
86
  .sv-check--lg { --_sz: 22px; font-size: 15px; }
84
87
  .sv-check.is-disabled { opacity: 0.55; cursor: not-allowed; }
88
+ .sv-check.is-readonly { cursor: default; }
85
89
  .sv-check__box {
86
90
  width: var(--_sz); height: var(--_sz); flex: none; padding: 0; cursor: inherit;
87
91
  display: grid; place-items: center;
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvCode - monospace code. Inline by default; set `block` for a padded,
4
+ * horizontally-scrolling code block. Pass `code` as a string or use the
5
+ * `children` snippet. No syntax highlighting (bring your own).
6
+ *
7
+ * <SvCode>npm i @svgrid/grid</SvCode>
8
+ * <SvCode block code={snippet} />
9
+ */
10
+ import type { Snippet } from 'svelte'
11
+
12
+ type Props = {
13
+ /** Render as a padded block instead of inline. */
14
+ block?: boolean
15
+ /** Code as a string (skip the snippet). */
16
+ code?: string
17
+ children?: Snippet
18
+ }
19
+
20
+ let { block = false, code, children }: Props = $props()
21
+ </script>
22
+
23
+ {#if block}
24
+ <pre class="sv-code sv-code--block"><code>{#if code != null}{code}{:else}{@render children?.()}{/if}</code></pre>
25
+ {:else}
26
+ <code class="sv-code sv-code--inline">{#if code != null}{code}{:else}{@render children?.()}{/if}</code>
27
+ {/if}
28
+
29
+ <style>
30
+ .sv-code {
31
+ font-family: var(--sg-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
32
+ font-size: 0.9em;
33
+ color: var(--sg-fg, #0f172a);
34
+ }
35
+ .sv-code--inline {
36
+ padding: 1px 5px;
37
+ background: var(--sg-muted-bg, color-mix(in srgb, var(--sg-fg, #0f172a) 7%, var(--sg-bg, #fff)));
38
+ border: 1px solid var(--sg-border, #e2e8f0);
39
+ border-radius: 4px;
40
+ white-space: nowrap;
41
+ }
42
+ .sv-code--block {
43
+ margin: 0;
44
+ padding: 12px 14px;
45
+ background: var(--sg-muted-bg, color-mix(in srgb, var(--sg-fg, #0f172a) 7%, var(--sg-bg, #fff)));
46
+ border: 1px solid var(--sg-border, #e2e8f0);
47
+ border-radius: 8px;
48
+ overflow-x: auto;
49
+ line-height: 1.55;
50
+ }
51
+ .sv-code--block code { font-size: 13px; white-space: pre; }
52
+ </style>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvCollapsible - a single show/hide region with an accessible trigger (the
4
+ * standalone form of one accordion item). The header toggles the panel with a
5
+ * rotating chevron; the panel animates its height open/closed and respects
6
+ * `prefers-reduced-motion`.
7
+ *
8
+ * ```svelte
9
+ * <SvCollapsible title="Advanced options" bind:open>
10
+ * ...content...
11
+ * </SvCollapsible>
12
+ * ```
13
+ */
14
+ import type { Snippet } from 'svelte'
15
+ import { nextEditorId } from './editor-contract'
16
+
17
+ type Props = {
18
+ /** Header text (ignored when a `header` snippet is given). */
19
+ title?: string
20
+ /** Open state (bindable). */
21
+ open?: boolean
22
+ onOpenChange?: (open: boolean) => void
23
+ disabled?: boolean
24
+ /** Custom header content (replaces `title`). */
25
+ header?: Snippet
26
+ children?: Snippet
27
+ }
28
+
29
+ let { title, open = $bindable(false), onOpenChange, disabled = false, header, children }: Props = $props()
30
+
31
+ const uid = nextEditorId('sv-collapse')
32
+ function toggle() {
33
+ if (disabled) return
34
+ open = !open
35
+ onOpenChange?.(open)
36
+ }
37
+ </script>
38
+
39
+ <div class="sv-collapse" class:is-open={open} class:is-disabled={disabled}>
40
+ <button
41
+ class="sv-collapse__trigger"
42
+ type="button"
43
+ {disabled}
44
+ aria-expanded={open}
45
+ aria-controls={`${uid}-panel`}
46
+ id={`${uid}-trigger`}
47
+ onclick={toggle}
48
+ >
49
+ <span class="sv-collapse__label">{#if header}{@render header()}{:else}{title}{/if}</span>
50
+ <svg class="sv-collapse__chev" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
51
+ </button>
52
+ <div class="sv-collapse__region" id={`${uid}-panel`} role="region" aria-labelledby={`${uid}-trigger`} inert={!open}>
53
+ <div class="sv-collapse__content">{@render children?.()}</div>
54
+ </div>
55
+ </div>
56
+
57
+ <style>
58
+ .sv-collapse { border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; background: var(--sg-bg, #fff); }
59
+ .sv-collapse__trigger {
60
+ display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; box-sizing: border-box;
61
+ padding: 11px 14px; background: none; border: 0; cursor: pointer; font: inherit; font-size: 13.5px; font-weight: 600;
62
+ color: var(--sg-fg, #0f172a); text-align: start;
63
+ }
64
+ .sv-collapse__trigger:hover:not(:disabled) { background: var(--sg-row-hover-bg, #f1f5f9); border-radius: 10px; }
65
+ .sv-collapse__trigger:disabled { opacity: 0.55; cursor: not-allowed; }
66
+ .sv-collapse__trigger:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: -2px; border-radius: 10px; }
67
+ .sv-collapse__label { min-width: 0; }
68
+ .sv-collapse__chev { flex: none; color: var(--sg-muted, #64748b); transition: transform 0.16s ease; }
69
+ .sv-collapse.is-open .sv-collapse__chev { transform: rotate(180deg); }
70
+ .sv-collapse__region { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.18s ease; }
71
+ .sv-collapse.is-open .sv-collapse__region { grid-template-rows: 1fr; }
72
+ .sv-collapse__content { overflow: hidden; padding: 0 14px; min-height: 0; }
73
+ .sv-collapse.is-open .sv-collapse__content { padding-bottom: 13px; }
74
+ @media (prefers-reduced-motion: reduce) { .sv-collapse__region, .sv-collapse__chev { transition: none; } }
75
+ </style>