@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
@@ -39,6 +39,8 @@
39
39
  labels = 'none',
40
40
  showValue = false,
41
41
  disabled = false,
42
+ readonly = false,
43
+ size = 'md',
42
44
  orientation = 'horizontal',
43
45
  name,
44
46
  ariaLabel,
@@ -60,7 +62,7 @@
60
62
  // in the headless `createSlider` core; the component keeps the DOM measuring.
61
63
  const slider = createSlider({
62
64
  value: () => value,
63
- onChange: (v) => onChange?.(v),
65
+ onChange: (v) => { if (readonly) return; onChange?.(v) },
64
66
  min: () => min,
65
67
  max: () => max,
66
68
  step: () => step,
@@ -69,6 +71,11 @@
69
71
  disabled: () => disabled,
70
72
  ariaLabel: () => ariaLabel ?? label,
71
73
  rtl: () => resolvedDir === 'rtl',
74
+ id: () => uid,
75
+ invalid: () => invalid,
76
+ required: () => required,
77
+ error: () => error,
78
+ hint: () => hint,
72
79
  })
73
80
  const lo = $derived(slider.lo)
74
81
  const hi = $derived(slider.hi)
@@ -87,7 +94,7 @@
87
94
  // The rect is measured here (a DOM concern) and passed INTO the core.
88
95
  let trackEl: HTMLDivElement | null = null
89
96
  function onTrackDown(e: PointerEvent) {
90
- if (disabled) return
97
+ if (disabled || readonly) return
91
98
  slider.pointerDown({ x: e.clientX, y: e.clientY }, trackEl!.getBoundingClientRect())
92
99
  trackEl?.setPointerCapture(e.pointerId)
93
100
  }
@@ -102,8 +109,9 @@
102
109
 
103
110
  <SvField id={uid} {label} {hint} {error} {required} {dir}>
104
111
  <div
105
- class="sv-slider sv-slider--{orientation}"
112
+ class="sv-slider sv-slider--{orientation} sv-slider--{size}"
106
113
  class:is-disabled={disabled}
114
+ class:is-readonly={readonly}
107
115
  dir={resolvedDir}
108
116
  >
109
117
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -126,14 +134,14 @@
126
134
  {/each}
127
135
 
128
136
  {#if range}
129
- <div class="sv-slider__thumb" style:--p={`${pct(lo)}%`} {...slider.thumbProps(0)}>
137
+ <div class="sv-slider__thumb" style:--p={`${pct(lo)}%`} {...slider.thumbProps(0)} aria-readonly={readonly ? 'true' : undefined}>
130
138
  {#if showValue}<span class="sv-slider__bubble">{fmt(lo)}</span>{/if}
131
139
  </div>
132
- <div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} {...slider.thumbProps(1)}>
140
+ <div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} {...slider.thumbProps(1)} aria-readonly={readonly ? 'true' : undefined}>
133
141
  {#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
134
142
  </div>
135
143
  {:else}
136
- <div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} {...slider.thumbProps(0)}>
144
+ <div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} {...slider.thumbProps(0)} aria-readonly={readonly ? 'true' : undefined}>
137
145
  {#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
138
146
  </div>
139
147
  {/if}
@@ -152,15 +160,20 @@
152
160
  </SvField>
153
161
 
154
162
  <style>
155
- .sv-slider { --_accent: var(--sg-accent, #2563eb); --_track: var(--sg-border, #e2e8f0); padding: 10px; }
163
+ .sv-slider { --_accent: var(--sg-accent, #2563eb); --_track: var(--sg-border, #e2e8f0); --_th: 16px; --_trk: 6px; padding: 10px; }
164
+ .sv-slider--sm { --_th: 13px; --_trk: 4px; }
165
+ .sv-slider--lg { --_th: 20px; --_trk: 8px; }
156
166
  .sv-slider--horizontal { width: 240px; }
167
+ .sv-slider--horizontal.sv-slider--sm { width: 180px; }
168
+ .sv-slider--horizontal.sv-slider--lg { width: 300px; }
157
169
  .sv-slider--vertical { height: 200px; width: 40px; }
158
170
  .sv-slider.is-disabled { opacity: 0.5; pointer-events: none; }
171
+ .sv-slider.is-readonly .sv-slider__track, .sv-slider.is-readonly .sv-slider__thumb { cursor: default; }
159
172
  .sv-slider__track {
160
173
  position: relative; background: var(--_track); border-radius: 999px; cursor: pointer; touch-action: none;
161
174
  }
162
- .sv-slider--horizontal .sv-slider__track { height: 6px; }
163
- .sv-slider--vertical .sv-slider__track { width: 6px; height: 100%; margin: 0 auto; }
175
+ .sv-slider--horizontal .sv-slider__track { height: var(--_trk); }
176
+ .sv-slider--vertical .sv-slider__track { width: var(--_trk); height: 100%; margin: 0 auto; }
164
177
  .sv-slider__scale { position: relative; height: 15px; margin-top: 8px; font-size: 11px; color: var(--sg-muted, #64748b); font-variant-numeric: tabular-nums; }
165
178
  .sv-slider__scale--ends { display: flex; justify-content: space-between; }
166
179
  .sv-slider__scale-lbl { position: absolute; inset-inline-start: var(--p); transform: translateX(-50%); white-space: nowrap; }
@@ -168,7 +181,7 @@
168
181
  .sv-slider--horizontal .sv-slider__fill { inset-inline-start: var(--a); inset-inline-end: calc(100% - var(--b)); top: 0; bottom: 0; }
169
182
  .sv-slider--vertical .sv-slider__fill { bottom: var(--a); top: calc(100% - var(--b)); left: 0; right: 0; }
170
183
  .sv-slider__thumb {
171
- position: absolute; width: 16px; height: 16px; border-radius: 50%;
184
+ position: absolute; width: var(--_th); height: var(--_th); border-radius: 50%;
172
185
  background: #fff; border: 2px solid var(--_accent); box-shadow: 0 1px 3px rgba(0,0,0,0.25);
173
186
  cursor: grab; touch-action: none;
174
187
  }
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvSpinner - a dedicated indeterminate loading spinner (distinct from the
4
+ * determinate SvProgress bars). Sizeable, colorable, and it respects
5
+ * `prefers-reduced-motion`. Give it a `label` for screen readers.
6
+ *
7
+ * ```svelte
8
+ * <SvSpinner size="lg" label="Loading results" />
9
+ * ```
10
+ */
11
+ type Props = {
12
+ /** Diameter in px, or a named size. Default `md` (20px). */
13
+ size?: 'sm' | 'md' | 'lg' | number
14
+ /** Stroke color (defaults to the accent token). */
15
+ color?: string
16
+ /** Accessible label; when omitted the spinner is `aria-hidden`. */
17
+ label?: string
18
+ }
19
+
20
+ let { size = 'md', color, label }: Props = $props()
21
+ const px = $derived(typeof size === 'number' ? size : size === 'sm' ? 15 : size === 'lg' ? 28 : 20)
22
+ </script>
23
+
24
+ <span
25
+ class="sv-spinner"
26
+ role={label ? 'status' : undefined}
27
+ aria-label={label}
28
+ aria-hidden={label ? undefined : 'true'}
29
+ style:width={`${px}px`}
30
+ style:height={`${px}px`}
31
+ style:color={color}
32
+ ></span>
33
+
34
+ <style>
35
+ .sv-spinner {
36
+ display: inline-block; box-sizing: border-box; flex: none; border-radius: 50%;
37
+ border: 2px solid color-mix(in srgb, currentcolor 25%, transparent);
38
+ border-top-color: currentcolor; color: var(--sg-accent, #2563eb);
39
+ animation: sv-spinner-rot 0.6s linear infinite;
40
+ }
41
+ @keyframes sv-spinner-rot { to { transform: rotate(360deg); } }
42
+ @media (prefers-reduced-motion: reduce) { .sv-spinner { animation-duration: 1.4s; } }
43
+ </style>
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvStack - a vertical flex layout primitive: stacks children in a column with a
4
+ * consistent `gap` and optional cross-axis `align`. The building block for form
5
+ * rows, card bodies, and sidebars. (For a horizontal row use SvGroup.)
6
+ *
7
+ * ```svelte
8
+ * <SvStack gap={12}><A /><B /><C /></SvStack>
9
+ * ```
10
+ */
11
+ import type { Snippet } from 'svelte'
12
+
13
+ type Props = {
14
+ /** Gap between children (px, or any CSS length). Default 12. */
15
+ gap?: number | string
16
+ /** Cross-axis alignment. */
17
+ align?: 'start' | 'center' | 'end' | 'stretch'
18
+ /** Main-axis distribution. */
19
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around'
20
+ /** Wrapping element tag. Default `div`. */
21
+ as?: string
22
+ children?: Snippet
23
+ }
24
+
25
+ let { gap = 12, align = 'stretch', justify = 'start', as = 'div', children }: Props = $props()
26
+ const g = $derived(typeof gap === 'number' ? `${gap}px` : gap)
27
+ const alignMap = { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch' } as const
28
+ const justifyMap = { start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between', around: 'space-around' } as const
29
+ </script>
30
+
31
+ <svelte:element this={as} class="sv-stack" style:gap={g} style:align-items={alignMap[align]} style:justify-content={justifyMap[justify]}>
32
+ {@render children?.()}
33
+ </svelte:element>
34
+
35
+ <style>
36
+ .sv-stack { display: flex; flex-direction: column; min-width: 0; }
37
+ </style>
@@ -20,6 +20,7 @@
20
20
  checked = false,
21
21
  onChange,
22
22
  disabled = false,
23
+ readonly = false,
23
24
  size = 'md',
24
25
  name,
25
26
  ariaLabel,
@@ -40,7 +41,7 @@
40
41
  // The styled switch is just a renderer over the headless core.
41
42
  const sw = createSwitch({
42
43
  checked: () => checked,
43
- onChange: (v) => onChange?.(v),
44
+ onChange: (v) => { if (readonly) return; onChange?.(v) },
44
45
  disabled: () => disabled,
45
46
  ariaLabel: () => ariaLabel,
46
47
  id: () => uid,
@@ -53,9 +54,12 @@
53
54
 
54
55
  <SvField id={uid} {label} {hint} {error} {required} {dir}>
55
56
  <button
57
+ role="switch"
56
58
  class="sv-switch sv-switch--{size}"
57
59
  class:is-on={checked}
60
+ class:is-readonly={readonly}
58
61
  {...sw.switchProps()}
62
+ aria-readonly={readonly ? 'true' : undefined}
59
63
  >
60
64
  <span class="sv-switch__track">
61
65
  {#if onLabel || offLabel}
@@ -77,6 +81,7 @@
77
81
  .sv-switch--sm { --_h: 18px; --_w: 32px; }
78
82
  .sv-switch--lg { --_h: 28px; --_w: 52px; }
79
83
  .sv-switch[disabled] { opacity: 0.5; cursor: not-allowed; }
84
+ .sv-switch.is-readonly { cursor: default; }
80
85
  .sv-switch__track {
81
86
  position: relative; width: var(--_w); height: var(--_h); border-radius: 999px;
82
87
  background: var(--sg-border, #cbd5e1); transition: background 0.16s;
@@ -33,6 +33,7 @@
33
33
  disabled = false,
34
34
  unique = true,
35
35
  max = Infinity,
36
+ size = 'md',
36
37
  name,
37
38
  ariaLabel,
38
39
  invalid = false,
@@ -68,7 +69,7 @@
68
69
  </script>
69
70
 
70
71
  <SvField id={uid} {label} {hint} {error} {required} {dir}>
71
- <div class="sv-tags" class:is-disabled={disabled} class:is-invalid={invalid} {...ti.rootProps()}>
72
+ <div class="sv-tags sv-tags--{size}" class:is-disabled={disabled} class:is-invalid={invalid} {...ti.rootProps()}>
72
73
  {#each value as tag, i (tag + i)}
73
74
  <span class="sv-tags__chip" {...ti.tagProps(i)}>
74
75
  <span class="sv-tags__label">{tag}</span>
@@ -89,20 +90,23 @@
89
90
  <style>
90
91
  .sv-tags {
91
92
  --_accent: var(--sg-accent, #2563eb);
92
- display: flex; flex-wrap: wrap; gap: 5px; align-items: center; width: 280px; min-height: 34px; padding: 4px 6px;
93
+ --_minh: 34px; --_chip-h: 22px; --_fs: 13px;
94
+ display: flex; flex-wrap: wrap; gap: 5px; align-items: center; width: 280px; min-height: var(--_minh); padding: 4px 6px;
93
95
  background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
94
96
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
95
97
  }
98
+ .sv-tags--sm { --_minh: 28px; --_chip-h: 19px; --_fs: 12px; }
99
+ .sv-tags--lg { --_minh: 40px; --_chip-h: 26px; --_fs: 15px; }
96
100
  .sv-tags:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
97
101
  .sv-tags.is-invalid { border-color: var(--sg-danger, #dc2626); }
98
102
  .sv-tags.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
99
103
  .sv-tags.is-disabled { opacity: 0.6; }
100
104
  .sv-tags__chip {
101
- display: inline-flex; align-items: center; gap: 4px; height: 22px; padding-block: 0; padding-inline: 8px 4px;
105
+ display: inline-flex; align-items: center; gap: 4px; height: var(--_chip-h); padding-block: 0; padding-inline: 8px 4px;
102
106
  background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent);
103
- border-radius: 5px; font-size: 12px; font-weight: 600;
107
+ border-radius: 5px; font-size: calc(var(--_fs) - 1px); font-weight: 600;
104
108
  }
105
109
  .sv-tags__x { background: none; border: 0; color: inherit; cursor: pointer; font-size: 15px; line-height: 1; padding: 0 2px; opacity: 0.7; }
106
110
  .sv-tags__x:hover { opacity: 1; }
107
- .sv-tags__input { flex: 1; min-width: 80px; border: 0; background: none; outline: none; color: inherit; font: inherit; font-size: 13px; height: 24px; }
111
+ .sv-tags__input { flex: 1; min-width: 80px; border: 0; background: none; outline: none; color: inherit; font: inherit; font-size: var(--_fs); height: 24px; }
108
112
  </style>
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvText - the workhorse text primitive: a paragraph or inline span with
4
+ * consistent size / weight / colour tokens, optional truncation and line
5
+ * clamping. Themes from the shared `--sg-*` tokens.
6
+ *
7
+ * <SvText size="sm" tone="muted">Last synced 2 minutes ago</SvText>
8
+ * <SvText clamp={2}>A long description that wraps then ellipsises...</SvText>
9
+ */
10
+ import type { Snippet } from 'svelte'
11
+
12
+ type Props = {
13
+ /** Font size step. */
14
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
15
+ /** Font weight. */
16
+ weight?: 'normal' | 'medium' | 'semibold' | 'bold'
17
+ /** Semantic colour. */
18
+ tone?: 'default' | 'muted' | 'accent' | 'success' | 'warning' | 'error'
19
+ /** Text alignment. */
20
+ align?: 'start' | 'center' | 'end'
21
+ /** Truncate to a single line with an ellipsis. */
22
+ truncate?: boolean
23
+ /** Clamp to N lines with an ellipsis (overrides truncate). */
24
+ clamp?: number
25
+ /** Render inline (`span`) instead of a block (`p`). */
26
+ inline?: boolean
27
+ /** Override the element/tag. */
28
+ as?: string
29
+ children?: Snippet
30
+ }
31
+
32
+ let {
33
+ size = 'md', weight = 'normal', tone = 'default', align,
34
+ truncate = false, clamp, inline = false, as, children,
35
+ }: Props = $props()
36
+
37
+ const sizeMap = { xs: '11px', sm: '13px', md: '14px', lg: '16px', xl: '20px' } as const
38
+ const weightMap = { normal: 400, medium: 500, semibold: 600, bold: 700 } as const
39
+ const toneMap = {
40
+ default: 'var(--sg-fg, #0f172a)',
41
+ muted: 'var(--sg-muted, #64748b)',
42
+ accent: 'var(--sg-accent, #2563eb)',
43
+ success: 'var(--sg-success, #16a34a)',
44
+ warning: 'var(--sg-warning, #d97706)',
45
+ error: 'var(--sg-danger, #dc2626)',
46
+ } as const
47
+ const alignMap = { start: 'left', center: 'center', end: 'right' } as const
48
+
49
+ const tag = $derived(as ?? (inline ? 'span' : 'p'))
50
+ </script>
51
+
52
+ <svelte:element
53
+ this={tag}
54
+ class="sv-text"
55
+ class:is-truncate={truncate && !clamp}
56
+ class:is-clamp={clamp != null}
57
+ style:font-size={sizeMap[size]}
58
+ style:font-weight={weightMap[weight]}
59
+ style:color={toneMap[tone]}
60
+ style:text-align={align ? alignMap[align] : undefined}
61
+ style:--sv-text-clamp={clamp}
62
+ >
63
+ {@render children?.()}
64
+ </svelte:element>
65
+
66
+ <style>
67
+ .sv-text { margin: 0; font-family: var(--sg-font, inherit); line-height: 1.5; }
68
+ .sv-text.is-truncate { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
69
+ .sv-text.is-clamp {
70
+ display: -webkit-box;
71
+ -webkit-box-orient: vertical;
72
+ -webkit-line-clamp: var(--sv-text-clamp, 2);
73
+ line-clamp: var(--sv-text-clamp, 2);
74
+ overflow: hidden;
75
+ }
76
+ </style>
@@ -4,9 +4,14 @@
4
4
  * (label / hint / error / RTL / a11y via SvField). Handles `text`, `email`,
5
5
  * `url`, `tel` and `search`. As a grid cell editor it honours the interaction
6
6
  * contract: Enter commits, Escape cancels. Parity: Smart `smart-input`.
7
+ *
8
+ * The control box, size, invalid state, clear button and leading/trailing
9
+ * adornments are all owned by SvField's `frame` mode, so they behave identically
10
+ * across the text-input family.
7
11
  */
12
+ import type { Snippet } from 'svelte'
8
13
  import SvField from './SvField.svelte'
9
- import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
14
+ import { editorAria, nextEditorId, type EditorAction, type SvEditorProps } from './editor-contract'
10
15
 
11
16
  type Props = SvEditorProps & {
12
17
  value?: string
@@ -23,6 +28,22 @@
23
28
  autocomplete?: AutoFill
24
29
  /** Autofocus + select on mount (used when mounted as a cell editor). */
25
30
  autofocus?: boolean
31
+ /** Select the whole value whenever the field is focused. */
32
+ selectOnFocus?: boolean
33
+ /** Leading adornment (icon/button) inside the field. */
34
+ leading?: Snippet
35
+ /** Trailing adornment (icon/button) inside the field. */
36
+ trailing?: Snippet
37
+ /** Plain-text affix at the start (e.g. a `$` or protocol). */
38
+ prefix?: string
39
+ /** Plain-text affix at the end (e.g. a unit). */
40
+ suffix?: string
41
+ /** `floating` animates the label up on focus/value. Default `static`. */
42
+ labelMode?: 'static' | 'floating'
43
+ /** Compact in-field action buttons (lookup/generate/copy...). */
44
+ actions?: EditorAction[]
45
+ /** Stretch to the container width. */
46
+ block?: boolean
26
47
  }
27
48
 
28
49
  let {
@@ -46,8 +67,17 @@
46
67
  hint,
47
68
  dir,
48
69
  id,
70
+ loading = false,
49
71
  autocomplete,
50
72
  autofocus = false,
73
+ selectOnFocus = false,
74
+ leading,
75
+ trailing,
76
+ prefix,
77
+ suffix,
78
+ labelMode = 'static',
79
+ actions,
80
+ block = false,
51
81
  }: Props = $props()
52
82
 
53
83
  const autoId = nextEditorId('sv-text')
@@ -68,48 +98,50 @@
68
98
  function mountFocus(node: HTMLInputElement) {
69
99
  if (autofocus) { node.focus(); node.select() }
70
100
  }
101
+ function onFocus(e: FocusEvent) {
102
+ if (selectOnFocus) (e.currentTarget as HTMLInputElement).select()
103
+ }
71
104
  </script>
72
105
 
73
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
74
- <div class="sv-text sv-text--{size}" class:is-disabled={disabled}>
75
- <div class="sv-text__field" class:is-invalid={invalid}>
76
- <input
77
- use:mountFocus
78
- class="sv-text__input"
79
- {type}
80
- {value}
81
- {placeholder}
82
- {disabled}
83
- {readonly}
84
- {maxlength}
85
- {autocomplete}
86
- oninput={onInput}
87
- onkeydown={onKeydown}
88
- {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
89
- />
90
- {#if clearable && value && !disabled && !readonly}
91
- <button class="sv-text__clear" type="button" tabindex="-1" aria-label="Clear" onclick={() => set('')}>&times;</button>
92
- {/if}
93
- </div>
94
- {#if name}<input type="hidden" {name} value={value} />{/if}
95
- </div>
106
+ <SvField
107
+ frame
108
+ id={uid}
109
+ {label}
110
+ {hint}
111
+ {error}
112
+ {required}
113
+ {dir}
114
+ {size}
115
+ {invalid}
116
+ {disabled}
117
+ {readonly}
118
+ {loading}
119
+ {block}
120
+ {leading}
121
+ {trailing}
122
+ {prefix}
123
+ {suffix}
124
+ {labelMode}
125
+ filled={!!value}
126
+ {actions}
127
+ clearable={clearable}
128
+ showClear={!!value}
129
+ onclear={() => set('')}
130
+ >
131
+ <input
132
+ use:mountFocus
133
+ class="sv-text__input"
134
+ {type}
135
+ {value}
136
+ placeholder={labelMode === 'floating' ? undefined : placeholder}
137
+ {disabled}
138
+ {readonly}
139
+ {maxlength}
140
+ {autocomplete}
141
+ oninput={onInput}
142
+ onkeydown={onKeydown}
143
+ onfocus={onFocus}
144
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
145
+ />
146
+ {#if name}<input type="hidden" {name} value={value} />{/if}
96
147
  </SvField>
97
-
98
- <style>
99
- .sv-text { --_accent: var(--sg-accent, #2563eb); display: inline-flex; flex-direction: column; width: 220px; }
100
- .sv-text.is-disabled { opacity: 0.6; }
101
- .sv-text__field {
102
- display: flex; align-items: center;
103
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
104
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
105
- }
106
- .sv-text__field:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
107
- .sv-text__field.is-invalid { border-color: var(--sg-danger, #dc2626); }
108
- .sv-text__field.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
109
- .sv-text__input { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 10px; }
110
- .sv-text--sm .sv-text__field { height: 28px; font-size: 12px; }
111
- .sv-text--md .sv-text__field { height: 34px; font-size: 13px; }
112
- .sv-text--lg .sv-text__field { height: 40px; font-size: 15px; }
113
- .sv-text__clear { display: grid; place-items: center; width: 28px; align-self: stretch; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; font-size: 17px; line-height: 1; }
114
- .sv-text__clear:hover { color: var(--sg-fg, #0f172a); }
115
- </style>
@@ -11,7 +11,8 @@
11
11
  * and ARIA; this component keeps the DOM-bound pointer capture + hit-testing.
12
12
  * All visuals come from `--sg-*` tokens so every theme applies for free.
13
13
  */
14
- import { resolveMessages, type EditorDir } from './editor-contract'
14
+ import SvField from './SvField.svelte'
15
+ import { nextEditorId, resolveMessages, type EditorDir, type EditorSize } from './editor-contract'
15
16
  import { createTimePicker, type TimeValue, type TimeFormat, type TimeSelection } from './createTimePicker.svelte'
16
17
 
17
18
  /** User-facing strings (localizable via `messages`). */
@@ -38,6 +39,16 @@
38
39
  dir?: EditorDir
39
40
  /** Override the built-in strings (group label + AM/PM/Now). */
40
41
  messages?: Partial<TimeMessages>
42
+ // Optional field chrome: when any of label/hint/error is set, the dial is
43
+ // wrapped in <SvField> so it gains a label/hint/error line; otherwise it
44
+ // renders bare (as embedded inside SvDateTimePicker's popover).
45
+ label?: string
46
+ hint?: string
47
+ error?: string
48
+ required?: boolean
49
+ invalid?: boolean
50
+ size?: EditorSize
51
+ id?: string
41
52
  }
42
53
 
43
54
  let {
@@ -53,8 +64,18 @@
53
64
  selection = 'hour',
54
65
  dir,
55
66
  messages,
67
+ label,
68
+ hint,
69
+ error,
70
+ required = false,
71
+ invalid = false,
72
+ id,
56
73
  }: Props = $props()
57
74
 
75
+ const autoId = nextEditorId('sv-tp')
76
+ const uid = $derived(id ?? autoId)
77
+ const hasChrome = $derived(!!(label || hint || error))
78
+
58
79
  const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
59
80
  const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
60
81
 
@@ -107,7 +128,8 @@
107
128
  }
108
129
  </script>
109
130
 
110
- <div class="sv-tp" class:sv-tp--disabled={disabled} role="group" aria-label={M.label} dir={resolvedDir}>
131
+ {#snippet dialBody()}
132
+ <div class="sv-tp" class:sv-tp--disabled={disabled} class:sv-tp--invalid={invalid} id={uid} role="group" aria-label={label ?? M.label} dir={resolvedDir}>
111
133
  <div class="sv-tp__head">
112
134
  <button class="sv-tp__seg" class:is-active={tp.selection === 'hour'} {...tp.segProps('hour')}>
113
135
  {String(tp.displayHour).padStart(2, '0')}
@@ -159,6 +181,13 @@
159
181
 
160
182
  {#if name}<input type="hidden" {name} value={`${String(tp.hours).padStart(2, '0')}:${tp.mm}`} />{/if}
161
183
  </div>
184
+ {/snippet}
185
+
186
+ {#if hasChrome}
187
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>{@render dialBody()}</SvField>
188
+ {:else}
189
+ {@render dialBody()}
190
+ {/if}
162
191
 
163
192
  <style>
164
193
  .sv-tp {
@@ -175,6 +204,7 @@
175
204
  border: 1px solid var(--_border); border-radius: calc(var(--_radius) + 4px);
176
205
  user-select: none;
177
206
  }
207
+ .sv-tp--invalid { border-color: var(--sg-danger, #dc2626); }
178
208
  .sv-tp--disabled { opacity: 0.55; pointer-events: none; }
179
209
 
180
210
  .sv-tp__head { display: flex; align-items: center; gap: 2px; font-variant-numeric: tabular-nums; }
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvTitle - a heading primitive. `order` sets the semantic level (h1-h6) and,
4
+ * by default, the visual size; pass `size` to decouple the two (e.g. an h2 that
5
+ * looks like an h4). Themes from the shared `--sg-*` tokens.
6
+ *
7
+ * <SvTitle order={1}>Dashboard</SvTitle>
8
+ * <SvTitle order={3} size={5}>Small but semantic h3</SvTitle>
9
+ */
10
+ import type { Snippet } from 'svelte'
11
+
12
+ type Level = 1 | 2 | 3 | 4 | 5 | 6
13
+
14
+ type Props = {
15
+ /** Semantic heading level (h1-h6). */
16
+ order?: Level
17
+ /** Visual size step, defaults to `order`. */
18
+ size?: Level
19
+ /** Text alignment. */
20
+ align?: 'start' | 'center' | 'end'
21
+ /** Truncate to a single line with an ellipsis. */
22
+ truncate?: boolean
23
+ children?: Snippet
24
+ }
25
+
26
+ let { order = 2, size, align, truncate = false, children }: Props = $props()
27
+
28
+ // px + weight per visual step - a compact, balanced type scale.
29
+ const sizeMap = { 1: '30px', 2: '24px', 3: '20px', 4: '17px', 5: '15px', 6: '13px' } as const
30
+ const weightMap = { 1: 700, 2: 700, 3: 600, 4: 600, 5: 600, 6: 600 } as const
31
+ const alignMap = { start: 'left', center: 'center', end: 'right' } as const
32
+
33
+ const step = $derived(size ?? order)
34
+ const tag = $derived(`h${order}`)
35
+ </script>
36
+
37
+ <svelte:element
38
+ this={tag}
39
+ class="sv-title"
40
+ class:is-truncate={truncate}
41
+ style:font-size={sizeMap[step]}
42
+ style:font-weight={weightMap[step]}
43
+ style:text-align={align ? alignMap[align] : undefined}
44
+ >
45
+ {@render children?.()}
46
+ </svelte:element>
47
+
48
+ <style>
49
+ .sv-title {
50
+ margin: 0;
51
+ font-family: var(--sg-font, inherit);
52
+ color: var(--sg-fg, #0f172a);
53
+ line-height: 1.25;
54
+ letter-spacing: -0.01em;
55
+ }
56
+ .sv-title.is-truncate { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
57
+ </style>