@eifi1/ui-kit 0.21.0 → 0.22.0

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 (278) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/iban-input.d.ts +66 -0
  72. package/dist/components/iban-input.js +125 -0
  73. package/dist/components/iban-input.js.map +1 -0
  74. package/dist/components/icon-picker.d.ts +12 -2
  75. package/dist/components/icon-picker.js +9 -2
  76. package/dist/components/icon-picker.js.map +1 -1
  77. package/dist/components/inline-edit-field.d.ts +167 -0
  78. package/dist/components/inline-edit-field.js +293 -0
  79. package/dist/components/inline-edit-field.js.map +1 -0
  80. package/dist/components/language-select.d.ts +127 -0
  81. package/dist/components/language-select.js +18 -0
  82. package/dist/components/language-select.js.map +1 -0
  83. package/dist/components/line-items.d.ts +25 -3
  84. package/dist/components/line-items.js +55 -46
  85. package/dist/components/line-items.js.map +1 -1
  86. package/dist/components/list.d.ts +25 -0
  87. package/dist/components/list.js +16 -5
  88. package/dist/components/list.js.map +1 -1
  89. package/dist/components/money-field.d.ts +20 -8
  90. package/dist/components/money-field.js.map +1 -1
  91. package/dist/components/month-picker.d.ts +52 -4
  92. package/dist/components/month-picker.js +290 -86
  93. package/dist/components/month-picker.js.map +1 -1
  94. package/dist/components/multi-select.d.ts +8 -1
  95. package/dist/components/multi-select.js +9 -4
  96. package/dist/components/multi-select.js.map +1 -1
  97. package/dist/components/number-field.d.ts +25 -13
  98. package/dist/components/number-field.js +6 -20
  99. package/dist/components/number-field.js.map +1 -1
  100. package/dist/components/number-input.d.ts +70 -13
  101. package/dist/components/number-input.js +90 -38
  102. package/dist/components/number-input.js.map +1 -1
  103. package/dist/components/numpad-sheet.d.ts +10 -8
  104. package/dist/components/one-time-code-input.d.ts +100 -0
  105. package/dist/components/one-time-code-input.js +137 -0
  106. package/dist/components/one-time-code-input.js.map +1 -0
  107. package/dist/components/phone-input.d.ts +66 -0
  108. package/dist/components/phone-input.js +171 -0
  109. package/dist/components/phone-input.js.map +1 -0
  110. package/dist/components/series-chart.d.ts +10 -8
  111. package/dist/components/settings-fields.d.ts +10 -8
  112. package/dist/components/share-card.d.ts +8 -6
  113. package/dist/components/sign-chip.d.ts +66 -0
  114. package/dist/components/sign-chip.js +39 -0
  115. package/dist/components/sign-chip.js.map +1 -0
  116. package/dist/components/swatch-picker.d.ts +27 -2
  117. package/dist/components/swatch-picker.js +73 -31
  118. package/dist/components/swatch-picker.js.map +1 -1
  119. package/dist/components/switch.d.ts +19 -0
  120. package/dist/components/switch.js +24 -6
  121. package/dist/components/switch.js.map +1 -1
  122. package/dist/components/text-link.d.ts +10 -8
  123. package/dist/components/tile-radio.d.ts +68 -13
  124. package/dist/components/tile-radio.js +3 -3
  125. package/dist/components/tile-radio.js.map +1 -1
  126. package/dist/components/time-input.d.ts +15 -9
  127. package/dist/components/time-input.js.map +1 -1
  128. package/dist/components/toggle-group.d.ts +47 -1
  129. package/dist/components/toggle-group.js +110 -69
  130. package/dist/components/toggle-group.js.map +1 -1
  131. package/dist/components/ui.d.ts +10 -8
  132. package/dist/components/ui.js +280 -78
  133. package/dist/components/ui.js.map +1 -1
  134. package/dist/components/use-table-state.d.ts +1 -2
  135. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  136. package/dist/data-table.d.ts +1 -2
  137. package/dist/data-table.js +2 -1
  138. package/dist/data-table.js.map +1 -1
  139. package/dist/feedback/feedback-attachment.d.ts +1 -1
  140. package/dist/feedback/feedback-attachment.js +210 -3
  141. package/dist/feedback/feedback-attachment.js.map +1 -1
  142. package/dist/feedback/feedback-dialog.d.ts +1 -1
  143. package/dist/feedback/feedback-dialog.js.map +1 -1
  144. package/dist/feedback/feedback-inbox.d.ts +1 -1
  145. package/dist/feedback/feedback-thread.d.ts +37 -3
  146. package/dist/feedback/feedback-thread.js +38 -9
  147. package/dist/feedback/feedback-thread.js.map +1 -1
  148. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  149. package/dist/feedback.d.ts +2 -2
  150. package/dist/hooks/use-file-drop.d.ts +10 -8
  151. package/dist/i18n/defaults.d.ts +10 -8
  152. package/dist/i18n/defaults.js +17 -2
  153. package/dist/i18n/defaults.js.map +1 -1
  154. package/dist/i18n/german.d.ts +10 -8
  155. package/dist/i18n/german.js +50 -5
  156. package/dist/i18n/german.js.map +1 -1
  157. package/dist/i18n/kit-labels.d.ts +10 -8
  158. package/dist/i18n/kit-labels.js +8 -2
  159. package/dist/i18n/kit-labels.js.map +1 -1
  160. package/dist/i18n/languages.d.ts +10 -8
  161. package/dist/i18n/locales/de-CH.d.ts +10 -8
  162. package/dist/i18n/locales/en.d.ts +10 -8
  163. package/dist/i18n/locales/en.js +9 -0
  164. package/dist/i18n/locales/en.js.map +1 -1
  165. package/dist/i18n/locales/es.d.ts +10 -8
  166. package/dist/i18n/locales/es.js +48 -5
  167. package/dist/i18n/locales/es.js.map +1 -1
  168. package/dist/i18n/locales/fr.d.ts +10 -8
  169. package/dist/i18n/locales/fr.js +48 -5
  170. package/dist/i18n/locales/fr.js.map +1 -1
  171. package/dist/i18n/locales/hu.d.ts +10 -8
  172. package/dist/i18n/locales/hu.js +49 -5
  173. package/dist/i18n/locales/hu.js.map +1 -1
  174. package/dist/i18n/locales/it.d.ts +10 -8
  175. package/dist/i18n/locales/it.js +49 -5
  176. package/dist/i18n/locales/it.js.map +1 -1
  177. package/dist/i18n/locales/zh.d.ts +10 -8
  178. package/dist/i18n/locales/zh.js +48 -5
  179. package/dist/i18n/locales/zh.js.map +1 -1
  180. package/dist/i18n/review.d.ts +10 -8
  181. package/dist/i18n/review.js +8 -1
  182. package/dist/i18n/review.js.map +1 -1
  183. package/dist/index.d.ts +17 -10
  184. package/dist/index.js +57 -2
  185. package/dist/index.js.map +1 -1
  186. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  187. package/dist/lib/countries.d.ts +43 -0
  188. package/dist/lib/countries.js +39 -0
  189. package/dist/lib/countries.js.map +1 -0
  190. package/dist/lib/decimal-marks.d.ts +46 -0
  191. package/dist/lib/decimal-marks.js +40 -0
  192. package/dist/lib/decimal-marks.js.map +1 -0
  193. package/dist/lib/iban.d.ts +98 -0
  194. package/dist/lib/iban.js +154 -0
  195. package/dist/lib/iban.js.map +1 -0
  196. package/dist/lib/isin.d.ts +30 -0
  197. package/dist/lib/isin.js +36 -0
  198. package/dist/lib/isin.js.map +1 -0
  199. package/dist/lib/phone.d.ts +69 -0
  200. package/dist/lib/phone.js +121 -0
  201. package/dist/lib/phone.js.map +1 -0
  202. package/dist/rhf/fields.d.ts +148 -11
  203. package/dist/rhf/fields.js +332 -3
  204. package/dist/rhf/fields.js.map +1 -1
  205. package/dist/rhf/form.d.ts +10 -8
  206. package/dist/rhf.d.ts +13 -9
  207. package/dist/rhf.js.map +1 -1
  208. package/dist/shell/app-shell.d.ts +10 -8
  209. package/dist/shell/top-bar-brand.d.ts +10 -8
  210. package/dist/shell.d.ts +10 -8
  211. package/dist/tour/tour.js +20 -24
  212. package/dist/tour/tour.js.map +1 -1
  213. package/dist/wizard/stepper-nav.d.ts +10 -8
  214. package/dist/wizard.d.ts +10 -8
  215. package/package.json +1 -1
  216. package/src/components/account-chips.tsx +17 -3
  217. package/src/components/account-settings.tsx +92 -26
  218. package/src/components/amount-input.tsx +49 -56
  219. package/src/components/autocomplete.tsx +133 -110
  220. package/src/components/checkbox-group.tsx +229 -0
  221. package/src/components/checkbox.tsx +72 -8
  222. package/src/components/chip.tsx +80 -10
  223. package/src/components/combobox.tsx +292 -253
  224. package/src/components/country-select.tsx +443 -0
  225. package/src/components/currency-select.tsx +109 -33
  226. package/src/components/danger-confirm.tsx +345 -77
  227. package/src/components/data-table-labels.ts +16 -1
  228. package/src/components/data-table.tsx +175 -6
  229. package/src/components/date-picker.tsx +173 -62
  230. package/src/components/entity-combobox.tsx +107 -87
  231. package/src/components/field-anatomy.tsx +190 -0
  232. package/src/components/field-parts.tsx +184 -0
  233. package/src/components/file-button.tsx +36 -3
  234. package/src/components/file-dropzone.tsx +106 -38
  235. package/src/components/form-actions.tsx +115 -2
  236. package/src/components/iban-input.tsx +231 -0
  237. package/src/components/icon-picker.tsx +26 -4
  238. package/src/components/inline-edit-field.tsx +533 -0
  239. package/src/components/language-select.tsx +78 -0
  240. package/src/components/line-items.tsx +35 -2
  241. package/src/components/list.tsx +55 -6
  242. package/src/components/money-field.tsx +10 -0
  243. package/src/components/month-picker.tsx +399 -63
  244. package/src/components/multi-select.tsx +80 -58
  245. package/src/components/number-field.tsx +28 -46
  246. package/src/components/number-input.tsx +210 -43
  247. package/src/components/one-time-code-input.tsx +288 -0
  248. package/src/components/phone-input.tsx +299 -0
  249. package/src/components/sign-chip.tsx +103 -0
  250. package/src/components/swatch-picker.tsx +118 -30
  251. package/src/components/switch.tsx +58 -7
  252. package/src/components/tile-radio.tsx +71 -16
  253. package/src/components/time-input.tsx +8 -1
  254. package/src/components/toggle-group.tsx +206 -73
  255. package/src/components/ui.tsx +472 -105
  256. package/src/data-table.ts +2 -1
  257. package/src/feedback/feedback-attachment.tsx +313 -9
  258. package/src/feedback/feedback-dialog.tsx +2 -0
  259. package/src/feedback/feedback-thread.tsx +66 -5
  260. package/src/i18n/defaults.ts +16 -1
  261. package/src/i18n/german.ts +47 -0
  262. package/src/i18n/kit-labels.tsx +78 -2
  263. package/src/i18n/locales/en.ts +10 -0
  264. package/src/i18n/locales/es.ts +46 -0
  265. package/src/i18n/locales/fr.ts +45 -0
  266. package/src/i18n/locales/hu.ts +46 -0
  267. package/src/i18n/locales/it.ts +47 -0
  268. package/src/i18n/locales/zh.ts +44 -0
  269. package/src/i18n/review.ts +7 -0
  270. package/src/index.ts +53 -3
  271. package/src/lib/countries.ts +91 -0
  272. package/src/lib/decimal-marks.ts +99 -0
  273. package/src/lib/iban.ts +171 -0
  274. package/src/lib/isin.ts +62 -0
  275. package/src/lib/phone.ts +211 -0
  276. package/src/rhf/fields.tsx +606 -9
  277. package/src/rhf.ts +2 -1
  278. package/src/tour/tour.tsx +66 -28
@@ -1,8 +1,10 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { aQ as SelectProps } from '../kit-labels-D1J2_jyD.js';
3
+ import { bg as SelectProps } from '../kit-labels-R8mIAc9N.js';
4
4
  import { ThemePreference } from '../theme/theme-store.js';
5
- import './data-table-labels.js';
5
+ import '../data-table-labels-B7OdnM0S.js';
6
+ import './data-table-sort.js';
7
+ import './swipeable-row.js';
6
8
  import './mini-calendar.js';
7
9
  import './calendar-heatmap.js';
8
10
  import './popover.js';
@@ -25,7 +27,6 @@ import './sparkline.js';
25
27
  import './stat-tile.js';
26
28
  import './signature-pad.js';
27
29
  import './password-strength.js';
28
- import './danger-confirm.js';
29
30
  import './swatch-picker.js';
30
31
  import './tile-radio.js';
31
32
  import './icon-picker.js';
@@ -33,11 +34,10 @@ import './dialog-frame.js';
33
34
  import './modal.js';
34
35
  import '../hooks/use-search-param-state.js';
35
36
  import './measured-grid.js';
36
- import '../feedback-attachment-WFiXJ8We.js';
37
+ import '../feedback-attachment-NhUm7Zga.js';
37
38
  import '../feedback/feedback-thread.js';
38
39
  import '../feedback/feedback-inbox.js';
39
40
  import './account-settings-labels.js';
40
- import './confirm-dialog.js';
41
41
  import './floating-panel.js';
42
42
  import './bulk-action-bar.js';
43
43
  import './list.js';
@@ -56,14 +56,16 @@ import './write-lock.js';
56
56
  import './legal.js';
57
57
  import './account-chips.js';
58
58
  import '../lib/format.js';
59
- import '../data-table-3mi2R3so.js';
60
- import './data-table-sort.js';
61
- import './swipeable-row.js';
62
59
  import './reauth-dialog.js';
63
60
  import './server-wake.js';
64
61
  import '../lib/server-wake.js';
65
62
  import './translation-review-labels.js';
66
63
  import '../lib/translation-review.js';
64
+ import './country-select.js';
65
+ import './inline-edit-field.js';
66
+ import '../lib/iban.js';
67
+ import '../lib/phone.js';
68
+ import './sign-chip.js';
67
69
  import 'zustand/middleware';
68
70
  import 'zustand';
69
71
 
@@ -1,14 +1,13 @@
1
1
  import 'react';
2
2
  import './account-chips.js';
3
3
  import './dialog-frame.js';
4
- export { U as DEFAULT_SHARE_CARD_LABELS, aR as ShareAddRequest, aS as ShareCandidate, aT as ShareCard, aU as ShareCardLabels, aV as ShareCardProps, aW as ShareDialog, aX as ShareDialogProps, aY as ShareGrantee, aZ as SharePanel, a_ as SharePanelProps, a$ as SharePendingGrant, b0 as ShareRole } from '../kit-labels-D1J2_jyD.js';
4
+ export { a7 as DEFAULT_SHARE_CARD_LABELS, bh as ShareAddRequest, bi as ShareCandidate, bj as ShareCard, bk as ShareCardLabels, bl as ShareCardProps, bm as ShareDialog, bn as ShareDialogProps, bo as ShareGrantee, bp as SharePanel, bq as SharePanelProps, br as SharePendingGrant, bs as ShareRole } from '../kit-labels-R8mIAc9N.js';
5
5
  import 'lucide-react';
6
6
  import '../lib/format.js';
7
7
  import './chip.js';
8
- import '../data-table-3mi2R3so.js';
8
+ import '../data-table-labels-B7OdnM0S.js';
9
9
  import './data-table-sort.js';
10
10
  import './swipeable-row.js';
11
- import './data-table-labels.js';
12
11
  import './modal.js';
13
12
  import '../hooks/use-search-param-state.js';
14
13
  import './mini-calendar.js';
@@ -31,16 +30,14 @@ import './sparkline.js';
31
30
  import './stat-tile.js';
32
31
  import './signature-pad.js';
33
32
  import './password-strength.js';
34
- import './danger-confirm.js';
35
33
  import './swatch-picker.js';
36
34
  import './tile-radio.js';
37
35
  import './icon-picker.js';
38
36
  import './measured-grid.js';
39
- import '../feedback-attachment-WFiXJ8We.js';
37
+ import '../feedback-attachment-NhUm7Zga.js';
40
38
  import '../feedback/feedback-thread.js';
41
39
  import '../feedback/feedback-inbox.js';
42
40
  import './account-settings-labels.js';
43
- import './confirm-dialog.js';
44
41
  import './floating-panel.js';
45
42
  import './bulk-action-bar.js';
46
43
  import './list.js';
@@ -62,3 +59,8 @@ import './server-wake.js';
62
59
  import '../lib/server-wake.js';
63
60
  import './translation-review-labels.js';
64
61
  import '../lib/translation-review.js';
62
+ import './country-select.js';
63
+ import './inline-edit-field.js';
64
+ import '../lib/iban.js';
65
+ import '../lib/phone.js';
66
+ import './sign-chip.js';
@@ -0,0 +1,66 @@
1
+ import * as react from 'react';
2
+
3
+ /** The words a {@link SignChip} says — the `signChip` namespace of
4
+ * `<UiKitProvider labels>`, and per chip through its `labels` prop. */
5
+ interface SignChipLabels {
6
+ /** The chip's text while the figure is negative — money going out. */
7
+ outflow: string;
8
+ /** The chip's text while the figure is positive — money coming in. */
9
+ inflow: string;
10
+ /** The chip's accessible name and its tooltip: the state it is in, and what a press
11
+ * turns it into — `current` and `next` are the two words above. The other option is
12
+ * nowhere on screen to be read, so the name has to say both. */
13
+ direction: (current: string, next: string) => string;
14
+ }
15
+ declare const DEFAULT_SIGN_CHIP_LABELS: SignChipLabels;
16
+ interface SignChipProps {
17
+ /** The figure is negative (an outflow) — the same flag {@link AmountInput}'s
18
+ * `negative` is, so one state feeds both: `<AmountInput negative={neg}
19
+ * onNegativeChange={setNeg} />` and `<SignChip negative={neg}
20
+ * onNegativeChange={setNeg} />`. */
21
+ negative: boolean;
22
+ /** A press: the other direction. */
23
+ onNegativeChange: (negative: boolean) => void;
24
+ disabled?: boolean;
25
+ /** Per-chip overrides of the `signChip` namespace. */
26
+ labels?: Partial<SignChipLabels>;
27
+ /** On the chip. Place it in its row from here — keksdose's form passes `self-end`
28
+ * to sit it on the display figure's baseline strip. */
29
+ className?: string;
30
+ }
31
+ /**
32
+ * The SIGN of an amount, as one chip that shows only the state it is in — keksdose
33
+ * K15, its `DirectionToggle` (feedback #417, #430, #431, dev#434) made a kit piece,
34
+ * so the figure and its direction come from one package and wear one palette.
35
+ *
36
+ * WHY A CHIP, NOT A SEGMENTED CONTROL. A two-option toggle spends half its width
37
+ * telling you what the figure is NOT, and at a phone's width it was the widest thing on
38
+ * the form after the figure itself (#417). This sits beside the amount and reads as one
39
+ * phrase with it — "−42,50 Outflow" — and a press flips it. Hiding the alternative costs
40
+ * discoverability, so two things pay it back: the ± glyph and the swap arrows say "this
41
+ * is a control", not a badge; and the name and tooltip spell out both the current state
42
+ * and what a press will do.
43
+ *
44
+ * NOT A PRESSED/UNPRESSED TOGGLE. A {@link Chip} toggle has a FIXED label naming its
45
+ * on-state, with `aria-pressed` carrying the state. Here the word IS the state, so it
46
+ * is an action button with no `aria-pressed`: "Direction: Inflow, not pressed" would
47
+ * name no on-state for "not pressed" to be the absence of. The name changing under
48
+ * focus is the echo.
49
+ *
50
+ * THE COLOUR is the money pairing — the chip's `expense` / `income` tones, the same
51
+ * `--money-expense` / `--money-income` that {@link AmountInput}'s `tone` paints the
52
+ * figure with (dev#434): only the text and the border carry the tint, on a neutral
53
+ * surface, because a filled pill next to a tinted figure reads as one smear.
54
+ *
55
+ * SIZE. `lg` on a phone — the 44px touch target beside a display figure that has no
56
+ * field height to match — and from `md` up exactly a labelled field's 42px, so it never
57
+ * stands taller than the fields in its grid row (#431).
58
+ *
59
+ * An exported chip rather than an `AmountInput signControl` slot: the slot would have
60
+ * to own the row the two share, and every form lays that row out differently (beside
61
+ * the figure on a phone, in a grid column on a desktop). The chip is the part that was
62
+ * being re-written; the row stays the form's.
63
+ */
64
+ declare function SignChip({ negative, onNegativeChange, disabled, labels: labelsProp, className }: SignChipProps): react.JSX.Element;
65
+
66
+ export { DEFAULT_SIGN_CHIP_LABELS, SignChip, type SignChipLabels, type SignChipProps };
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { ArrowUpDown, Minus, Plus } from "lucide-react";
4
+ import { Chip } from "./chip.js";
5
+ import { Tooltip } from "./tooltip.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import { useKitLabels } from "../i18n/kit-labels.js";
8
+ const DEFAULT_SIGN_CHIP_LABELS = {
9
+ outflow: "Outflow",
10
+ inflow: "Inflow",
11
+ direction: (current, next) => `Direction: ${current} \u2014 switch to ${next}`
12
+ };
13
+ function SignChip({ negative, onNegativeChange, disabled, labels: labelsProp, className }) {
14
+ const labels = useKitLabels("signChip", DEFAULT_SIGN_CHIP_LABELS, labelsProp);
15
+ const current = negative ? labels.outflow : labels.inflow;
16
+ const next = negative ? labels.inflow : labels.outflow;
17
+ const name = labels.direction(current, next);
18
+ return /* @__PURE__ */ jsx(Tooltip, { label: name, lazy: true, children: /* @__PURE__ */ jsxs(
19
+ Chip,
20
+ {
21
+ size: "lg",
22
+ tone: negative ? "expense" : "income",
23
+ icon: negative ? Minus : Plus,
24
+ onClick: () => onNegativeChange(!negative),
25
+ disabled,
26
+ "aria-label": name,
27
+ className: cn("shrink-0 gap-1.5 whitespace-nowrap px-3 font-medium md:min-h-0 md:py-2.5", className),
28
+ children: [
29
+ current,
30
+ /* @__PURE__ */ jsx(ArrowUpDown, { className: "ms-1.5 inline size-3.5 align-[-0.2em] opacity-60", "aria-hidden": true })
31
+ ]
32
+ }
33
+ ) });
34
+ }
35
+ export {
36
+ DEFAULT_SIGN_CHIP_LABELS,
37
+ SignChip
38
+ };
39
+ //# sourceMappingURL=sign-chip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/sign-chip.tsx"],"sourcesContent":["import { ArrowUpDown, Minus, Plus } from \"lucide-react\";\nimport { Chip } from \"./chip\";\nimport { Tooltip } from \"./tooltip\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words a {@link SignChip} says — the `signChip` namespace of\n * `<UiKitProvider labels>`, and per chip through its `labels` prop. */\nexport interface SignChipLabels {\n /** The chip's text while the figure is negative — money going out. */\n outflow: string;\n /** The chip's text while the figure is positive — money coming in. */\n inflow: string;\n /** The chip's accessible name and its tooltip: the state it is in, and what a press\n * turns it into — `current` and `next` are the two words above. The other option is\n * nowhere on screen to be read, so the name has to say both. */\n direction: (current: string, next: string) => string;\n}\n\nexport const DEFAULT_SIGN_CHIP_LABELS: SignChipLabels = {\n outflow: \"Outflow\",\n inflow: \"Inflow\",\n direction: (current, next) => `Direction: ${current} — switch to ${next}`,\n};\n\nexport interface SignChipProps {\n /** The figure is negative (an outflow) — the same flag {@link AmountInput}'s\n * `negative` is, so one state feeds both: `<AmountInput negative={neg}\n * onNegativeChange={setNeg} />` and `<SignChip negative={neg}\n * onNegativeChange={setNeg} />`. */\n negative: boolean;\n /** A press: the other direction. */\n onNegativeChange: (negative: boolean) => void;\n disabled?: boolean;\n /** Per-chip overrides of the `signChip` namespace. */\n labels?: Partial<SignChipLabels>;\n /** On the chip. Place it in its row from here — keksdose's form passes `self-end`\n * to sit it on the display figure's baseline strip. */\n className?: string;\n}\n\n/**\n * The SIGN of an amount, as one chip that shows only the state it is in — keksdose\n * K15, its `DirectionToggle` (feedback #417, #430, #431, dev#434) made a kit piece,\n * so the figure and its direction come from one package and wear one palette.\n *\n * WHY A CHIP, NOT A SEGMENTED CONTROL. A two-option toggle spends half its width\n * telling you what the figure is NOT, and at a phone's width it was the widest thing on\n * the form after the figure itself (#417). This sits beside the amount and reads as one\n * phrase with it — \"−42,50 Outflow\" — and a press flips it. Hiding the alternative costs\n * discoverability, so two things pay it back: the ± glyph and the swap arrows say \"this\n * is a control\", not a badge; and the name and tooltip spell out both the current state\n * and what a press will do.\n *\n * NOT A PRESSED/UNPRESSED TOGGLE. A {@link Chip} toggle has a FIXED label naming its\n * on-state, with `aria-pressed` carrying the state. Here the word IS the state, so it\n * is an action button with no `aria-pressed`: \"Direction: Inflow, not pressed\" would\n * name no on-state for \"not pressed\" to be the absence of. The name changing under\n * focus is the echo.\n *\n * THE COLOUR is the money pairing — the chip's `expense` / `income` tones, the same\n * `--money-expense` / `--money-income` that {@link AmountInput}'s `tone` paints the\n * figure with (dev#434): only the text and the border carry the tint, on a neutral\n * surface, because a filled pill next to a tinted figure reads as one smear.\n *\n * SIZE. `lg` on a phone — the 44px touch target beside a display figure that has no\n * field height to match — and from `md` up exactly a labelled field's 42px, so it never\n * stands taller than the fields in its grid row (#431).\n *\n * An exported chip rather than an `AmountInput signControl` slot: the slot would have\n * to own the row the two share, and every form lays that row out differently (beside\n * the figure on a phone, in a grid column on a desktop). The chip is the part that was\n * being re-written; the row stays the form's.\n */\nexport function SignChip({ negative, onNegativeChange, disabled, labels: labelsProp, className }: SignChipProps) {\n const labels = useKitLabels(\"signChip\", DEFAULT_SIGN_CHIP_LABELS, labelsProp);\n const current = negative ? labels.outflow : labels.inflow;\n const next = negative ? labels.inflow : labels.outflow;\n const name = labels.direction(current, next);\n return (\n <Tooltip label={name} lazy>\n <Chip\n size=\"lg\"\n tone={negative ? \"expense\" : \"income\"}\n icon={negative ? Minus : Plus}\n onClick={() => onNegativeChange(!negative)}\n disabled={disabled}\n aria-label={name}\n // `gap-1.5 px-3`: the chip's narrower body, because on a 375px screen every\n // pixel of it is taken from the figure beside it (#430). `md:py-2.5` with the\n // hairline border is the 42px a labelled field measures (#431).\n className={cn(\"shrink-0 gap-1.5 whitespace-nowrap px-3 font-medium md:min-h-0 md:py-2.5\", className)}\n >\n {current}\n {/* The swap arrows after the word, in the label's own row: decoration (the\n name already says what a press does), set a little apart from the word. */}\n <ArrowUpDown className=\"ms-1.5 inline size-3.5 align-[-0.2em] opacity-60\" aria-hidden />\n </Chip>\n </Tooltip>\n );\n}\n"],"mappings":";AAmFM,SAeE,KAfF;AAnFN,SAAS,aAAa,OAAO,YAAY;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAiBtB,MAAM,2BAA2C;AAAA,EACtD,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,WAAW,CAAC,SAAS,SAAS,cAAc,OAAO,qBAAgB,IAAI;AACzE;AAmDO,SAAS,SAAS,EAAE,UAAU,kBAAkB,UAAU,QAAQ,YAAY,UAAU,GAAkB;AAC/G,QAAM,SAAS,aAAa,YAAY,0BAA0B,UAAU;AAC5E,QAAM,UAAU,WAAW,OAAO,UAAU,OAAO;AACnD,QAAM,OAAO,WAAW,OAAO,SAAS,OAAO;AAC/C,QAAM,OAAO,OAAO,UAAU,SAAS,IAAI;AAC3C,SACE,oBAAC,WAAQ,OAAO,MAAM,MAAI,MACxB;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,MAAM,WAAW,YAAY;AAAA,MAC7B,MAAM,WAAW,QAAQ;AAAA,MACzB,SAAS,MAAM,iBAAiB,CAAC,QAAQ;AAAA,MACzC;AAAA,MACA,cAAY;AAAA,MAIZ,WAAW,GAAG,4EAA4E,SAAS;AAAA,MAElG;AAAA;AAAA,QAGD,oBAAC,eAAY,WAAU,oDAAmD,eAAW,MAAC;AAAA;AAAA;AAAA,EACxF,GACF;AAEJ;","names":[]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef } from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  import { TileSize } from './tile-radio.js';
4
4
 
5
5
  interface SwatchPickerLabels {
@@ -62,6 +62,31 @@ interface SwatchPickerProps<T extends string> extends Omit<ComponentPropsWithout
62
62
  tileClassName?: string;
63
63
  disabled?: boolean;
64
64
  labels?: Partial<SwatchPickerLabels>;
65
+ /**
66
+ * An 11px static label in a strip over the swatches, named by it (keksdose K2): the
67
+ * transaction row's flag picker stands between labelled fields and hand-builds this
68
+ * (`FieldLabel` + `pt-5`, transaction-flags.tsx:155) because the picker had no label.
69
+ * The label sits on the same line as a field's own (`top-1`, `inset-x-3`); the
70
+ * swatches start 20px down, below its whole line box — see `LABEL_STRIP_PAD`. With a
71
+ * label, `className` styles the wrapper, as on a labelled {@link ToggleGroup}; an
72
+ * explicit `aria-label` / `aria-labelledby` still names the group instead.
73
+ */
74
+ label?: ReactNode;
75
+ /** A {@link FieldHint} beside the label. Only with `label`. */
76
+ hint?: ReactNode;
77
+ /**
78
+ * Why the colour cannot be changed right now — {@link Button}'s `disabledReason`, for
79
+ * a picker that saves itself on change (keksdose K3, the flag picker in a row a
80
+ * read-only viewer may see). The swatches stay reachable and the arrow keys still
81
+ * walk them, but nothing reaches `onChange`; the reason is shown in the kit
82
+ * {@link Tooltip} under the row (each swatch's own name bubble sits above it) and
83
+ * describes the group. Wins over `disabled`.
84
+ */
85
+ disabledReason?: ReactNode;
86
+ /** This picker COMMITS — choosing a colour saves it. Under a locked
87
+ * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
88
+ * reason, as {@link Button}'s `commit` does. */
89
+ commit?: boolean;
65
90
  }
66
91
  /**
67
92
  * A row of colour swatches that is one radio group.
@@ -75,6 +100,6 @@ interface SwatchPickerProps<T extends string> extends Omit<ComponentPropsWithout
75
100
  * choose as they go; Home and End jump to the ends. Left and right follow the page's
76
101
  * direction.
77
102
  */
78
- declare function SwatchPicker<T extends string>({ options, value, onChange, allowNone, mixed, activation, size, tileClassName, disabled, labels, className, ...rest }: SwatchPickerProps<T>): react.JSX.Element;
103
+ declare function SwatchPicker<T extends string>({ options, value, onChange, allowNone, mixed, activation, size, tileClassName, disabled, labels, label, hint, disabledReason: ownDisabledReason, commit, className, ...rest }: SwatchPickerProps<T>): react.JSX.Element;
79
104
 
80
105
  export { DEFAULT_SWATCH_PICKER_LABELS, type SwatchOption, SwatchPicker, type SwatchPickerLabels, type SwatchPickerProps };
@@ -1,9 +1,12 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId, useMemo } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { useKitLabels } from "../i18n/kit-labels.js";
6
6
  import { TILE_SIZE, TileRadioGroup } from "./tile-radio.js";
7
+ import { Tooltip } from "./tooltip.js";
8
+ import { useCommitReason } from "./write-lock.js";
9
+ import { hasContent, LabelStrip } from "./field-anatomy.js";
7
10
  const DEFAULT_SWATCH_PICKER_LABELS = {
8
11
  none: "No colour",
9
12
  mixed: "Mixed: the selected items have different colours"
@@ -19,56 +22,95 @@ function SwatchPicker({
19
22
  tileClassName,
20
23
  disabled = false,
21
24
  labels,
25
+ label,
26
+ hint,
27
+ disabledReason: ownDisabledReason,
28
+ commit,
22
29
  className,
23
30
  ...rest
24
31
  }) {
25
32
  const text = useKitLabels("swatchPicker", DEFAULT_SWATCH_PICKER_LABELS, labels);
26
33
  const mixedId = useId();
34
+ const labelId = useId();
35
+ const reasonId = useId();
36
+ const disabledReason = useCommitReason(commit, ownDisabledReason);
37
+ const locked = hasContent(disabledReason);
38
+ const labelled = hasContent(label);
27
39
  const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
28
40
  const items = [
29
41
  ...allowNone ? [{ key: null, label: text.none }] : [],
30
42
  ...options.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled }))
31
43
  ];
32
- return /* @__PURE__ */ jsxs(
44
+ const tiles = /* @__PURE__ */ jsx(
45
+ TileRadioGroup,
46
+ {
47
+ items,
48
+ checked: mixed ? void 0 : value,
49
+ onSelect: (next) => {
50
+ if (!locked) onChange(next);
51
+ },
52
+ activation,
53
+ disabled: disabled && !locked,
54
+ size,
55
+ tileClassName: tileClassName || locked ? (_item, selected) => cn(
56
+ // The disabled tile's look, without its `disabled`: dimmed, a
57
+ // not-allowed cursor, and no hover offer on a tile that cannot be
58
+ // chosen. Not on the selected tile, whose frame IS its state.
59
+ locked && "cursor-not-allowed opacity-50",
60
+ locked && !selected && "hover:border-[var(--border)] hover:bg-[var(--bg-surface)]",
61
+ tileClassName
62
+ ) : void 0,
63
+ renderTile: (item) => {
64
+ const opt = item.key === null ? void 0 : byValue.get(item.key);
65
+ return /* @__PURE__ */ jsx(
66
+ "span",
67
+ {
68
+ "aria-hidden": true,
69
+ className: cn(
70
+ "rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15",
71
+ TILE_SIZE[size].swatch,
72
+ opt?.swatchClassName
73
+ ),
74
+ style: { background: opt?.color }
75
+ }
76
+ );
77
+ }
78
+ }
79
+ );
80
+ const group = /* @__PURE__ */ jsxs(
33
81
  "div",
34
82
  {
35
83
  ...rest,
36
84
  role: "radiogroup",
37
- "aria-disabled": disabled || void 0,
38
- "aria-describedby": [rest["aria-describedby"], mixed && mixedId].filter(Boolean).join(" ") || void 0,
39
- className: cn("flex flex-wrap items-center gap-1.5", className),
85
+ "aria-labelledby": labelled && rest["aria-label"] === void 0 && rest["aria-labelledby"] === void 0 ? labelId : rest["aria-labelledby"],
86
+ "aria-disabled": disabled || locked || void 0,
87
+ "aria-describedby": [rest["aria-describedby"], mixed && mixedId, locked && reasonId].filter(Boolean).join(" ") || void 0,
88
+ className: cn("flex flex-wrap items-center gap-1.5", !labelled && className),
40
89
  children: [
41
- /* @__PURE__ */ jsx(
42
- TileRadioGroup,
43
- {
44
- items,
45
- checked: mixed ? void 0 : value,
46
- onSelect: onChange,
47
- activation,
48
- disabled,
49
- size,
50
- tileClassName: tileClassName ? () => tileClassName : void 0,
51
- renderTile: (item) => {
52
- const opt = item.key === null ? void 0 : byValue.get(item.key);
53
- return /* @__PURE__ */ jsx(
54
- "span",
55
- {
56
- "aria-hidden": true,
57
- className: cn(
58
- "rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15",
59
- TILE_SIZE[size].swatch,
60
- opt?.swatchClassName
61
- ),
62
- style: { background: opt?.color }
63
- }
64
- );
90
+ locked ? (
91
+ // Inside the group, round the tiles — not round the group, whose box is the
92
+ // caller's row item. This row inherits the group's gap, wrap and alignment, so
93
+ // the tiles sit exactly where they sit unlocked. Below the row: each tile's own
94
+ // bubble (its colour's name) is above it.
95
+ /* @__PURE__ */ jsx(
96
+ Tooltip,
97
+ {
98
+ label: disabledReason,
99
+ side: "bottom",
100
+ className: "flex min-w-0 flex-1 gap-[inherit] [align-items:inherit] [flex-wrap:inherit] [justify-content:inherit]",
101
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ tiles,
103
+ /* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
104
+ ] })
65
105
  }
66
- }
67
- ),
106
+ )
107
+ ) : tiles,
68
108
  mixed && /* @__PURE__ */ jsx("span", { id: mixedId, hidden: true, children: text.mixed })
69
109
  ]
70
110
  }
71
111
  );
112
+ if (!labelled) return group;
113
+ return /* @__PURE__ */ jsx(LabelStrip, { labelId, label, hint, disabled: disabled || locked, pad: "tiles", className, children: group });
72
114
  }
73
115
  export {
74
116
  DEFAULT_SWATCH_PICKER_LABELS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/swatch-picker.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\n\nexport interface SwatchPickerLabels {\n /** The \"no colour\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n}\n\nexport const DEFAULT_SWATCH_PICKER_LABELS: SwatchPickerLabels = {\n none: \"No colour\",\n mixed: \"Mixed: the selected items have different colours\",\n};\n\nexport interface SwatchOption<T extends string> {\n value: T;\n /** Any CSS colour — a token (`var(--chart-3)`), a hex, an `oklch()`. Painted as an\n * inline background, so it does not have to be a class the kit's CSS knows.\n * Leave it out when `swatchClassName` paints the dot instead. For a colour that\n * must match a `StatusDot` / `Chip` hue elsewhere, `statusDotColor(\"teal\")` rather\n * than `\"var(--hue-teal)\"` written out — one table, so the two cannot drift. */\n color?: string;\n /** The colour's name, in the user's language. Shown in the bubble and read out;\n * required, because a swatch with no name is a colour only some people can read. */\n label: string;\n /** Classes for the swatch dot, for a palette that lives in classes rather than\n * values (a light/dark pair). An inline `color` would win over a class, so pass\n * one or the other. */\n swatchClassName?: string;\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Groceries\"). The tile wears a dot while it has one. */\n note?: string;\n disabled?: boolean;\n}\n\n/**\n * `onChange` is the picker's own — the chosen VALUE, not a DOM event — so the div's\n * is omitted rather than shadowed, as on {@link ToggleGroup}.\n */\nexport interface SwatchPickerProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\"> {\n options: SwatchOption<T>[];\n /** The chosen colour, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no colour\" tile whose value is `null`. Clearing a colour has to be\n * as reachable as setting one, and a radio cannot be unchecked by clicking it. */\n allowNone?: boolean;\n /**\n * \"Some of each\" — a bulk edit over rows whose colours differ. Nothing is checked\n * (not even \"none\", whatever `value` says), and the group is described as mixed.\n * Distinct from `value={null}`, which is an answer: \"no colour\".\n */\n mixed?: boolean;\n /** See {@link IconPickerProps.activation}. Default \"automatic\". */\n activation?: \"automatic\" | \"manual\";\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: Partial<SwatchPickerLabels>;\n}\n\n/**\n * A row of colour swatches that is one radio group.\n *\n * Name the group with `aria-label` or `aria-labelledby` — usually the visible heading\n * above it. Each swatch is named by its `label` and shows it in a bubble on hover and\n * focus, so the name is not the screen reader's alone.\n *\n * Keyboard (the APG radio group): Tab reaches the checked swatch, or the first one\n * while nothing is checked; the arrow keys move between swatches and, by default,\n * choose as they go; Home and End jump to the ends. Left and right follow the page's\n * direction.\n */\nexport function SwatchPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n className,\n ...rest\n}: SwatchPickerProps<T>) {\n const text = useKitLabels(\"swatchPicker\", DEFAULT_SWATCH_PICKER_LABELS, labels);\n const mixedId = useId();\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...options.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled })),\n ];\n return (\n <div\n {...rest}\n role=\"radiogroup\"\n aria-disabled={disabled || undefined}\n aria-describedby={\n [rest[\"aria-describedby\"], mixed && mixedId].filter(Boolean).join(\" \") || undefined\n }\n className={cn(\"flex flex-wrap items-center gap-1.5\", className)}\n >\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n onSelect={onChange}\n activation={activation}\n disabled={disabled}\n size={size}\n tileClassName={tileClassName ? () => tileClassName : undefined}\n renderTile={(item) => {\n const opt = item.key === null ? undefined : byValue.get(item.key);\n return (\n <span\n aria-hidden\n // A hairline round the dot, drawn over whatever colour it is: a pale\n // yellow swatch on a white surface is otherwise a hole in the row.\n className={cn(\n \"rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15\",\n TILE_SIZE[size].swatch,\n opt?.swatchClassName,\n )}\n style={{ background: opt?.color }}\n />\n );\n }}\n />\n {/* `hidden`, not `sr-only`: a description is read from hidden text, and this\n way it takes no room in the row. */}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n </div>\n );\n}\n"],"mappings":";AA8GI,SAoBQ,KApBR;AA9GJ,SAAS,OAAO,eAAe;AAE/B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,WAAW,sBAAsB;AAUnC,MAAM,+BAAmD;AAAA,EAC9D,MAAM;AAAA,EACN,OAAO;AACT;AAuEO,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO;AAAA,EACP;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,OAAO,aAAa,gBAAgB,8BAA8B,MAAM;AAC9E,QAAM,UAAU,MAAM;AACtB,QAAM,UAAU,QAAQ,MAAM,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC;AAClF,QAAM,QAAuB;AAAA,IAC3B,GAAI,YAAY,CAAC,EAAE,KAAK,MAAM,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC;AAAA,IACrD,GAAG,QAAQ,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,EAAE;AAAA,EAC9F;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,iBAAe,YAAY;AAAA,MAC3B,oBACE,CAAC,KAAK,kBAAkB,GAAG,SAAS,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAAA,MAE5E,WAAW,GAAG,uCAAuC,SAAS;AAAA,MAE9D;AAAA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,SAAS,QAAQ,SAAY;AAAA,YAC7B,UAAU;AAAA,YACV;AAAA,YACA;AAAA,YACA;AAAA,YACA,eAAe,gBAAgB,MAAM,gBAAgB;AAAA,YACrD,YAAY,CAAC,SAAS;AACpB,oBAAM,MAAM,KAAK,QAAQ,OAAO,SAAY,QAAQ,IAAI,KAAK,GAAG;AAChE,qBACE;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAW;AAAA,kBAGX,WAAW;AAAA,oBACT;AAAA,oBACA,UAAU,IAAI,EAAE;AAAA,oBAChB,KAAK;AAAA,kBACP;AAAA,kBACA,OAAO,EAAE,YAAY,KAAK,MAAM;AAAA;AAAA,cAClC;AAAA,YAEJ;AAAA;AAAA,QACF;AAAA,QAGC,SACC,oBAAC,UAAK,IAAI,SAAS,QAAM,MACtB,eAAK,OACR;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/swatch-picker.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason } from \"./write-lock\";\nimport { hasContent, LabelStrip } from \"./field-anatomy\";\n\nexport interface SwatchPickerLabels {\n /** The \"no colour\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n}\n\nexport const DEFAULT_SWATCH_PICKER_LABELS: SwatchPickerLabels = {\n none: \"No colour\",\n mixed: \"Mixed: the selected items have different colours\",\n};\n\nexport interface SwatchOption<T extends string> {\n value: T;\n /** Any CSS colour — a token (`var(--chart-3)`), a hex, an `oklch()`. Painted as an\n * inline background, so it does not have to be a class the kit's CSS knows.\n * Leave it out when `swatchClassName` paints the dot instead. For a colour that\n * must match a `StatusDot` / `Chip` hue elsewhere, `statusDotColor(\"teal\")` rather\n * than `\"var(--hue-teal)\"` written out — one table, so the two cannot drift. */\n color?: string;\n /** The colour's name, in the user's language. Shown in the bubble and read out;\n * required, because a swatch with no name is a colour only some people can read. */\n label: string;\n /** Classes for the swatch dot, for a palette that lives in classes rather than\n * values (a light/dark pair). An inline `color` would win over a class, so pass\n * one or the other. */\n swatchClassName?: string;\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Groceries\"). The tile wears a dot while it has one. */\n note?: string;\n disabled?: boolean;\n}\n\n/**\n * `onChange` is the picker's own — the chosen VALUE, not a DOM event — so the div's\n * is omitted rather than shadowed, as on {@link ToggleGroup}.\n */\nexport interface SwatchPickerProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\"> {\n options: SwatchOption<T>[];\n /** The chosen colour, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no colour\" tile whose value is `null`. Clearing a colour has to be\n * as reachable as setting one, and a radio cannot be unchecked by clicking it. */\n allowNone?: boolean;\n /**\n * \"Some of each\" — a bulk edit over rows whose colours differ. Nothing is checked\n * (not even \"none\", whatever `value` says), and the group is described as mixed.\n * Distinct from `value={null}`, which is an answer: \"no colour\".\n */\n mixed?: boolean;\n /** See {@link IconPickerProps.activation}. Default \"automatic\". */\n activation?: \"automatic\" | \"manual\";\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: Partial<SwatchPickerLabels>;\n /**\n * An 11px static label in a strip over the swatches, named by it (keksdose K2): the\n * transaction row's flag picker stands between labelled fields and hand-builds this\n * (`FieldLabel` + `pt-5`, transaction-flags.tsx:155) because the picker had no label.\n * The label sits on the same line as a field's own (`top-1`, `inset-x-3`); the\n * swatches start 20px down, below its whole line box — see `LABEL_STRIP_PAD`. With a\n * label, `className` styles the wrapper, as on a labelled {@link ToggleGroup}; an\n * explicit `aria-label` / `aria-labelledby` still names the group instead.\n */\n label?: ReactNode;\n /** A {@link FieldHint} beside the label. Only with `label`. */\n hint?: ReactNode;\n /**\n * Why the colour cannot be changed right now — {@link Button}'s `disabledReason`, for\n * a picker that saves itself on change (keksdose K3, the flag picker in a row a\n * read-only viewer may see). The swatches stay reachable and the arrow keys still\n * walk them, but nothing reaches `onChange`; the reason is shown in the kit\n * {@link Tooltip} under the row (each swatch's own name bubble sits above it) and\n * describes the group. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** This picker COMMITS — choosing a colour saves it. Under a locked\n * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's\n * reason, as {@link Button}'s `commit` does. */\n commit?: boolean;\n}\n\n/**\n * A row of colour swatches that is one radio group.\n *\n * Name the group with `aria-label` or `aria-labelledby` — usually the visible heading\n * above it. Each swatch is named by its `label` and shows it in a bubble on hover and\n * focus, so the name is not the screen reader's alone.\n *\n * Keyboard (the APG radio group): Tab reaches the checked swatch, or the first one\n * while nothing is checked; the arrow keys move between swatches and, by default,\n * choose as they go; Home and End jump to the ends. Left and right follow the page's\n * direction.\n */\nexport function SwatchPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n label,\n hint,\n disabledReason: ownDisabledReason,\n commit,\n className,\n ...rest\n}: SwatchPickerProps<T>) {\n const text = useKitLabels(\"swatchPicker\", DEFAULT_SWATCH_PICKER_LABELS, labels);\n const mixedId = useId();\n const labelId = useId();\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const labelled = hasContent(label);\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...options.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled })),\n ];\n const tiles = (\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n // A locked picker saves on change, so the change is swallowed — the arrow keys\n // still move the focus (and, automatic, would choose), and nothing is chosen.\n onSelect={(next) => {\n if (!locked) onChange(next);\n }}\n activation={activation}\n // A reason wins over `disabled`: the tiles stay in the tab order.\n disabled={disabled && !locked}\n size={size}\n tileClassName={\n tileClassName || locked\n ? (_item, selected) =>\n cn(\n // The disabled tile's look, without its `disabled`: dimmed, a\n // not-allowed cursor, and no hover offer on a tile that cannot be\n // chosen. Not on the selected tile, whose frame IS its state.\n locked && \"cursor-not-allowed opacity-50\",\n locked && !selected && \"hover:border-[var(--border)] hover:bg-[var(--bg-surface)]\",\n tileClassName,\n )\n : undefined\n }\n renderTile={(item) => {\n const opt = item.key === null ? undefined : byValue.get(item.key);\n return (\n <span\n aria-hidden\n // A hairline round the dot, drawn over whatever colour it is: a pale\n // yellow swatch on a white surface is otherwise a hole in the row.\n className={cn(\n \"rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15\",\n TILE_SIZE[size].swatch,\n opt?.swatchClassName,\n )}\n style={{ background: opt?.color }}\n />\n );\n }}\n />\n );\n const group = (\n <div\n {...rest}\n role=\"radiogroup\"\n aria-labelledby={\n labelled && rest[\"aria-label\"] === undefined && rest[\"aria-labelledby\"] === undefined\n ? labelId\n : rest[\"aria-labelledby\"]\n }\n aria-disabled={disabled || locked || undefined}\n aria-describedby={\n [rest[\"aria-describedby\"], mixed && mixedId, locked && reasonId].filter(Boolean).join(\" \") ||\n undefined\n }\n className={cn(\"flex flex-wrap items-center gap-1.5\", !labelled && className)}\n >\n {locked ? (\n // Inside the group, round the tiles — not round the group, whose box is the\n // caller's row item. This row inherits the group's gap, wrap and alignment, so\n // the tiles sit exactly where they sit unlocked. Below the row: each tile's own\n // bubble (its colour's name) is above it.\n <Tooltip\n label={disabledReason}\n side=\"bottom\"\n className=\"flex min-w-0 flex-1 gap-[inherit] [align-items:inherit] [flex-wrap:inherit] [justify-content:inherit]\"\n >\n <>\n {tiles}\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n </>\n </Tooltip>\n ) : (\n tiles\n )}\n {/* `hidden`, not `sr-only`: a description is read from hidden text, and this\n way it takes no room in the row. */}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n </div>\n );\n if (!labelled) return group;\n return (\n <LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled || locked} pad=\"tiles\" className={className}>\n {group}\n </LabelStrip>\n );\n}\n"],"mappings":";AA+KU,SAyCA,UAzCA,KAyCA,YAzCA;AA/KV,SAAS,OAAO,eAAe;AAE/B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,WAAW,sBAAsB;AAE1C,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,YAAY,kBAAkB;AAShC,MAAM,+BAAmD;AAAA,EAC9D,MAAM;AAAA,EACN,OAAO;AACT;AAgGO,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO;AAAA,EACP;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,OAAO,aAAa,gBAAgB,8BAA8B,MAAM;AAC9E,QAAM,UAAU,MAAM;AACtB,QAAM,UAAU,MAAM;AACtB,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,gBAAgB,QAAQ,iBAAiB;AAChE,QAAM,SAAS,WAAW,cAAc;AACxC,QAAM,WAAW,WAAW,KAAK;AACjC,QAAM,UAAU,QAAQ,MAAM,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC;AAClF,QAAM,QAAuB;AAAA,IAC3B,GAAI,YAAY,CAAC,EAAE,KAAK,MAAM,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC;AAAA,IACrD,GAAG,QAAQ,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,EAAE;AAAA,EAC9F;AACA,QAAM,QACJ;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,QAAQ,SAAY;AAAA,MAG7B,UAAU,CAAC,SAAS;AAClB,YAAI,CAAC,OAAQ,UAAS,IAAI;AAAA,MAC5B;AAAA,MACA;AAAA,MAEA,UAAU,YAAY,CAAC;AAAA,MACvB;AAAA,MACA,eACE,iBAAiB,SACb,CAAC,OAAO,aACN;AAAA;AAAA;AAAA;AAAA,QAIE,UAAU;AAAA,QACV,UAAU,CAAC,YAAY;AAAA,QACvB;AAAA,MACF,IACF;AAAA,MAEN,YAAY,CAAC,SAAS;AACpB,cAAM,MAAM,KAAK,QAAQ,OAAO,SAAY,QAAQ,IAAI,KAAK,GAAG;AAChE,eACE;AAAA,UAAC;AAAA;AAAA,YACC,eAAW;AAAA,YAGX,WAAW;AAAA,cACT;AAAA,cACA,UAAU,IAAI,EAAE;AAAA,cAChB,KAAK;AAAA,YACP;AAAA,YACA,OAAO,EAAE,YAAY,KAAK,MAAM;AAAA;AAAA,QAClC;AAAA,MAEJ;AAAA;AAAA,EACF;AAEF,QAAM,QACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,mBACE,YAAY,KAAK,YAAY,MAAM,UAAa,KAAK,iBAAiB,MAAM,SACxE,UACA,KAAK,iBAAiB;AAAA,MAE5B,iBAAe,YAAY,UAAU;AAAA,MACrC,oBACE,CAAC,KAAK,kBAAkB,GAAG,SAAS,SAAS,UAAU,QAAQ,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KACzF;AAAA,MAEF,WAAW,GAAG,uCAAuC,CAAC,YAAY,SAAS;AAAA,MAE1E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAKC;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAK;AAAA,cACL,WAAU;AAAA,cAEV,2CACG;AAAA;AAAA,gBACD,oBAAC,UAAK,IAAI,UAAU,QAAM,MACvB,0BACH;AAAA,iBACF;AAAA;AAAA,UACF;AAAA,YAEA;AAAA,QAID,SACC,oBAAC,UAAK,IAAI,SAAS,QAAM,MACtB,eAAK,OACR;AAAA;AAAA;AAAA,EAEJ;AAEF,MAAI,CAAC,SAAU,QAAO;AACtB,SACE,oBAAC,cAAW,SAAkB,OAAc,MAAY,UAAU,YAAY,QAAQ,KAAI,SAAQ,WAC/F,iBACH;AAEJ;","names":[]}
@@ -56,6 +56,25 @@ interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"
56
56
  * label WITHOUT a literal "*". No mark without a label.
57
57
  */
58
58
  required?: boolean;
59
+ /**
60
+ * Why the switch cannot be flipped — {@link Button}'s `disabledReason`. A switch is
61
+ * the control that "takes effect the moment it is flipped", so it is the first one
62
+ * keksdose K3 names: its settings rows sat behind a `SaveGuard` that forced a native
63
+ * `disabled`, out of the tab order, so the reason never reached a keyboard.
64
+ *
65
+ * With a reason the switch is `aria-disabled` instead: still focusable, a click or
66
+ * Space flips nothing and fires no `onChange` / `onCheckedChange`, the reason is in
67
+ * the kit {@link Tooltip} on the track and on its `aria-describedby`, and the row
68
+ * fades as a disabled one does. It wins over `disabled`. Changing the lock mounts the
69
+ * switch anew (it moves in or out of the Tooltip), as a locked Button is.
70
+ */
71
+ disabledReason?: ReactNode;
72
+ /**
73
+ * This switch COMMITS — flipping it saves. Under a locked {@link WriteLockProvider} it
74
+ * is locked the `disabledReason` way with the lock's reason (which wins over its
75
+ * own). No provider, or an unlocked one: no effect. Button's `commit`, for K3.
76
+ */
77
+ commit?: boolean;
59
78
  }
60
79
  /**
61
80
  * A native checkbox drawn as a track and thumb, announced as a switch.
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useId } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
6
+ import { LockedReason, useLockReason } from "./field-parts.js";
6
7
  const SIZES = {
7
8
  // 28×16 track, 12px thumb: the dense setting rows in Kastlan's tables.
8
9
  sm: { track: "h-4 w-7", thumb: "size-3 peer-checked:start-[14px]" },
@@ -23,35 +24,52 @@ const Switch = forwardRef(function Switch2({
23
24
  id,
24
25
  disabled,
25
26
  required,
27
+ disabledReason,
28
+ commit,
29
+ onClick,
26
30
  ...rest
27
31
  }, ref) {
28
32
  const generated = useId();
29
33
  const inputId = id ?? generated;
34
+ const lock = useLockReason(commit, disabledReason);
35
+ const locked = lock.locked;
36
+ const looksDisabled = Boolean(disabled) || locked;
30
37
  const descriptionId = `${inputId}-description`;
31
38
  const showDescription = hasMessage(description);
32
39
  const bare = label === void 0 && !showDescription;
33
40
  const geometry = SIZES[size];
34
- const control = /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex shrink-0", bare && className), children: [
41
+ const track = /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex shrink-0", bare && !locked && className), children: [
35
42
  /* @__PURE__ */ jsx(
36
43
  "input",
37
44
  {
38
45
  ref,
39
46
  id: bare ? id : inputId,
40
- disabled,
47
+ disabled: locked ? void 0 : disabled,
41
48
  required,
42
49
  ...rest,
43
50
  type: "checkbox",
44
51
  role: "switch",
45
- "aria-describedby": mergeDescribedBy(rest["aria-describedby"], showDescription && descriptionId),
52
+ "aria-disabled": locked || rest["aria-disabled"] || void 0,
53
+ "aria-describedby": mergeDescribedBy(
54
+ rest["aria-describedby"],
55
+ showDescription && descriptionId,
56
+ locked && lock.reasonId
57
+ ),
58
+ onClick: locked ? void 0 : onClick,
46
59
  onChange: (e) => {
60
+ if (locked) {
61
+ e.currentTarget.checked = !e.currentTarget.checked;
62
+ return;
63
+ }
47
64
  onChange?.(e);
48
65
  onCheckedChange?.(e.target.checked);
49
66
  },
50
- className: cn(TRACK, geometry.track, inputClassName)
67
+ className: cn(TRACK, geometry.track, locked && "cursor-not-allowed", inputClassName)
51
68
  }
52
69
  ),
53
70
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn(THUMB, geometry.thumb) })
54
71
  ] });
72
+ const control = locked ? /* @__PURE__ */ jsx(LockedReason, { lock, className: cn("shrink-0", bare && className), children: track }) : track;
55
73
  if (bare) return control;
56
74
  const cell = /* @__PURE__ */ jsx("span", { className: "flex h-5 shrink-0 items-center", children: control });
57
75
  const words = /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
@@ -61,7 +79,7 @@ const Switch = forwardRef(function Switch2({
61
79
  htmlFor: inputId,
62
80
  className: cn(
63
81
  "block text-sm leading-5 text-[var(--text-primary)]",
64
- disabled ? "cursor-not-allowed" : "cursor-pointer select-none"
82
+ looksDisabled ? "cursor-not-allowed" : "cursor-pointer select-none"
65
83
  ),
66
84
  children: [
67
85
  label,
@@ -71,7 +89,7 @@ const Switch = forwardRef(function Switch2({
71
89
  ),
72
90
  showDescription && /* @__PURE__ */ jsx("span", { id: descriptionId, className: "mt-0.5 block text-xs text-[var(--text-muted)]", children: description })
73
91
  ] });
74
- return /* @__PURE__ */ jsx("div", { className: cn("flex items-start gap-3", disabled && "opacity-60", className), children: switchPosition === "end" ? /* @__PURE__ */ jsxs(Fragment, { children: [
92
+ return /* @__PURE__ */ jsx("div", { className: cn("flex items-start gap-3", looksDisabled && "opacity-60", className), children: switchPosition === "end" ? /* @__PURE__ */ jsxs(Fragment, { children: [
75
93
  words,
76
94
  cell
77
95
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * An on/off switch that is still `<input type=\"checkbox\">`, with `role=\"switch\"`.\n *\n * Kastlan's was a Radix `<button>`; this keeps the native element for the reasons\n * `Checkbox` gives (form submission, `required`, reset, `<label for>`), and adds the\n * one thing a checkbox is not: `role=\"switch\"`, which screen readers announce as\n * \"on/off\" rather than \"checked/not checked\". Use it for a setting that takes effect\n * the moment it is flipped; a choice that waits for a Save button is a checkbox.\n */\n\n/** Geometry per size. The thumb sits `2px` in from each end of the track, so its\n * checked offset is `track width − thumb − 2px`. `start-*`, never `left-*` or a\n * `translate-x`: an inline-start offset follows the writing direction, so in a\n * right-to-left form the thumb travels right-to-left with no `rtl:` override to\n * keep in step. */\nconst SIZES = {\n // 28×16 track, 12px thumb: the dense setting rows in Kastlan's tables.\n sm: { track: \"h-4 w-7\", thumb: \"size-3 peer-checked:start-[14px]\" },\n // 36×20 track, 16px thumb.\n md: { track: \"h-5 w-9\", thumb: \"size-4 peer-checked:start-[18px]\" },\n} as const;\n\nexport type SwitchSize = keyof typeof SIZES;\n\n// The track IS the input — `appearance-none` and a pill — so its box is the pointer\n// target and the focus ring lands around the whole control, not the thumb alone.\n//\n// Unchecked it takes `--border-strong`, not `--border`: a hairline-coloured track on\n// a surface-coloured card is a switch you have to look for. Checked it takes the\n// brand fill, which is the only difference in COLOUR; the thumb's position is the\n// difference in SHAPE, so the state never rests on colour alone.\nconst TRACK =\n \"peer block shrink-0 cursor-pointer appearance-none rounded-full bg-[var(--border-strong)] transition-colors \" +\n \"checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// The thumb, laid over the input and ignoring the pointer so every click still reaches\n// the input. It swaps to `--brand-contrast` on the brand fill: the surface colour would\n// match the fill in some presets, and the contrast token is the one guaranteed to read\n// against --brand.\nconst THUMB =\n \"pointer-events-none absolute start-0.5 top-1/2 -translate-y-1/2 rounded-full bg-[var(--bg-surface)] shadow-sm \" +\n \"transition-[inset-inline-start,background-color] motion-reduce:transition-none \" +\n \"peer-checked:bg-[var(--brand-contrast)]\";\n\nexport interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"size\" | \"role\"> {\n /** `\"md\"` (36×20, default) or `\"sm\"` (28×16) — Kastlan's `default` and `sm`. */\n size?: SwitchSize;\n /** The words beside the switch, as a `<label for>` — see {@link Checkbox}'s\n * `label` for why it does not wrap the input. Without it, give the switch an\n * `aria-label`. */\n label?: ReactNode;\n /** Secondary text under the label, attached with `aria-describedby` (merged with\n * any the caller passed). */\n description?: ReactNode;\n /** Which end of the row the switch sits at when there is a label. `\"end\"` (the\n * default) is the settings-list shape Kastlan's profile page uses — the words at\n * the start, the control at the far end; `\"start\"` puts it before the words, like\n * a checkbox. Logical, so it follows the writing direction. */\n switchPosition?: \"start\" | \"end\";\n /** The new state as a boolean — Radix's `onCheckedChange`, so Kastlan's call sites\n * move without unwrapping the event. Fires alongside `onChange`. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` (the track). `className` styles the outermost element:\n * the row when there is a label, the switch's own wrapper when there is not. */\n inputClassName?: string;\n /**\n * Must be ticked to submit — a consent, an acceptance before paying. Reaches the\n * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a\n * screen reader announces \"required\"), and draws the kit's required mark after the\n * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from\n * the control, so a star inside the name would only be read out as noise. Write the\n * label WITHOUT a literal \"*\". No mark without a label.\n */\n required?: boolean;\n}\n\n/**\n * A native checkbox drawn as a track and thumb, announced as a switch.\n *\n * Every other prop reaches the `<input>`, so it works controlled (`checked` +\n * `onChange`/`onCheckedChange`), uncontrolled (`defaultChecked`) and in a plain\n * `<form>` (`name`, `value`).\n */\nexport const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(\n {\n size = \"md\",\n label,\n description,\n switchPosition = \"end\",\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const descriptionId = `${inputId}-description`;\n const showDescription = hasMessage(description);\n const bare = label === undefined && !showDescription;\n const geometry = SIZES[size];\n\n const control = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && className)}>\n <input\n ref={ref}\n id={bare ? id : inputId}\n disabled={disabled}\n required={required}\n {...rest}\n // After the spread: a caller's props object must not be able to turn this\n // back into a plain checkbox, or into a text field.\n type=\"checkbox\"\n role=\"switch\"\n aria-describedby={mergeDescribedBy(rest[\"aria-describedby\"], showDescription && descriptionId)}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(TRACK, geometry.track, inputClassName)}\n />\n <span aria-hidden className={cn(THUMB, geometry.thumb)} />\n </span>\n );\n\n if (bare) return control;\n\n // The switch's cell is one label line tall (text-sm, 20px) so it centres on the\n // FIRST line of a label that wraps, and a description underneath does not drag it\n // down to the middle of the paragraph.\n const cell = <span className=\"flex h-5 shrink-0 items-center\">{control}</span>;\n const words = (\n <span className=\"min-w-0 flex-1\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\n </label>\n )}\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n </span>\n );\n\n return (\n <div className={cn(\"flex items-start gap-3\", disabled && \"opacity-60\", className)}>\n {switchPosition === \"end\" ? (\n <>\n {words}\n {cell}\n </>\n ) : (\n <>\n {cell}\n {words}\n </>\n )}\n </div>\n );\n});\nSwitch.displayName = \"Switch\";\n"],"mappings":";AAmHI,SAyDI,UAxDF,KADF;AAnHJ,SAAS,YAAY,aAAa;AAElC,SAAS,UAAU;AACnB,SAAS,YAAY,wBAAwB;AAiB7C,MAAM,QAAQ;AAAA;AAAA,EAEZ,IAAI,EAAE,OAAO,WAAW,OAAO,mCAAmC;AAAA;AAAA,EAElE,IAAI,EAAE,OAAO,WAAW,OAAO,mCAAmC;AACpE;AAWA,MAAM,QACJ;AASF,MAAM,QACJ;AA2CK,MAAM,SAAS,WAA0C,SAASA,QACvE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,MAAM;AACtB,QAAM,gBAAgB,GAAG,OAAO;AAChC,QAAM,kBAAkB,WAAW,WAAW;AAC9C,QAAM,OAAO,UAAU,UAAa,CAAC;AACrC,QAAM,WAAW,MAAM,IAAI;AAE3B,QAAM,UACJ,qBAAC,UAAK,WAAW,GAAG,iCAAiC,QAAQ,SAAS,GACpE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,IAAI,OAAO,KAAK;AAAA,QAChB;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QAGJ,MAAK;AAAA,QACL,MAAK;AAAA,QACL,oBAAkB,iBAAiB,KAAK,kBAAkB,GAAG,mBAAmB,aAAa;AAAA,QAC7F,UAAU,CAAC,MAAqC;AAC9C,qBAAW,CAAC;AACZ,4BAAkB,EAAE,OAAO,OAAO;AAAA,QACpC;AAAA,QACA,WAAW,GAAG,OAAO,SAAS,OAAO,cAAc;AAAA;AAAA,IACrD;AAAA,IACA,oBAAC,UAAK,eAAW,MAAC,WAAW,GAAG,OAAO,SAAS,KAAK,GAAG;AAAA,KAC1D;AAGF,MAAI,KAAM,QAAO;AAKjB,QAAM,OAAO,oBAAC,UAAK,WAAU,kCAAkC,mBAAQ;AACvE,QAAM,QACJ,qBAAC,UAAK,WAAU,kBACb;AAAA,cAAU,UACT;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA,WAAW,uBAAuB;AAAA,QACpC;AAAA,QAEC;AAAA;AAAA,UACA,YACC,oBAAC,UAAK,eAAW,MAAC,WAAU,+BAA8B,eAE1D;AAAA;AAAA;AAAA,IAEJ;AAAA,IAED,mBACC,oBAAC,UAAK,IAAI,eAAe,WAAU,iDAChC,uBACH;AAAA,KAEJ;AAGF,SACE,oBAAC,SAAI,WAAW,GAAG,0BAA0B,YAAY,cAAc,SAAS,GAC7E,6BAAmB,QAClB,iCACG;AAAA;AAAA,IACA;AAAA,KACH,IAEA,iCACG;AAAA;AAAA,IACA;AAAA,KACH,GAEJ;AAEJ,CAAC;AACD,OAAO,cAAc;","names":["Switch"]}
1
+ {"version":3,"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { LockedReason, useLockReason } from \"./field-parts\";\n\n/**\n * An on/off switch that is still `<input type=\"checkbox\">`, with `role=\"switch\"`.\n *\n * Kastlan's was a Radix `<button>`; this keeps the native element for the reasons\n * `Checkbox` gives (form submission, `required`, reset, `<label for>`), and adds the\n * one thing a checkbox is not: `role=\"switch\"`, which screen readers announce as\n * \"on/off\" rather than \"checked/not checked\". Use it for a setting that takes effect\n * the moment it is flipped; a choice that waits for a Save button is a checkbox.\n */\n\n/** Geometry per size. The thumb sits `2px` in from each end of the track, so its\n * checked offset is `track width − thumb − 2px`. `start-*`, never `left-*` or a\n * `translate-x`: an inline-start offset follows the writing direction, so in a\n * right-to-left form the thumb travels right-to-left with no `rtl:` override to\n * keep in step. */\nconst SIZES = {\n // 28×16 track, 12px thumb: the dense setting rows in Kastlan's tables.\n sm: { track: \"h-4 w-7\", thumb: \"size-3 peer-checked:start-[14px]\" },\n // 36×20 track, 16px thumb.\n md: { track: \"h-5 w-9\", thumb: \"size-4 peer-checked:start-[18px]\" },\n} as const;\n\nexport type SwitchSize = keyof typeof SIZES;\n\n// The track IS the input — `appearance-none` and a pill — so its box is the pointer\n// target and the focus ring lands around the whole control, not the thumb alone.\n//\n// Unchecked it takes `--border-strong`, not `--border`: a hairline-coloured track on\n// a surface-coloured card is a switch you have to look for. Checked it takes the\n// brand fill, which is the only difference in COLOUR; the thumb's position is the\n// difference in SHAPE, so the state never rests on colour alone.\nconst TRACK =\n \"peer block shrink-0 cursor-pointer appearance-none rounded-full bg-[var(--border-strong)] transition-colors \" +\n \"checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// The thumb, laid over the input and ignoring the pointer so every click still reaches\n// the input. It swaps to `--brand-contrast` on the brand fill: the surface colour would\n// match the fill in some presets, and the contrast token is the one guaranteed to read\n// against --brand.\nconst THUMB =\n \"pointer-events-none absolute start-0.5 top-1/2 -translate-y-1/2 rounded-full bg-[var(--bg-surface)] shadow-sm \" +\n \"transition-[inset-inline-start,background-color] motion-reduce:transition-none \" +\n \"peer-checked:bg-[var(--brand-contrast)]\";\n\nexport interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"size\" | \"role\"> {\n /** `\"md\"` (36×20, default) or `\"sm\"` (28×16) — Kastlan's `default` and `sm`. */\n size?: SwitchSize;\n /** The words beside the switch, as a `<label for>` — see {@link Checkbox}'s\n * `label` for why it does not wrap the input. Without it, give the switch an\n * `aria-label`. */\n label?: ReactNode;\n /** Secondary text under the label, attached with `aria-describedby` (merged with\n * any the caller passed). */\n description?: ReactNode;\n /** Which end of the row the switch sits at when there is a label. `\"end\"` (the\n * default) is the settings-list shape Kastlan's profile page uses — the words at\n * the start, the control at the far end; `\"start\"` puts it before the words, like\n * a checkbox. Logical, so it follows the writing direction. */\n switchPosition?: \"start\" | \"end\";\n /** The new state as a boolean — Radix's `onCheckedChange`, so Kastlan's call sites\n * move without unwrapping the event. Fires alongside `onChange`. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` (the track). `className` styles the outermost element:\n * the row when there is a label, the switch's own wrapper when there is not. */\n inputClassName?: string;\n /**\n * Must be ticked to submit — a consent, an acceptance before paying. Reaches the\n * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a\n * screen reader announces \"required\"), and draws the kit's required mark after the\n * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from\n * the control, so a star inside the name would only be read out as noise. Write the\n * label WITHOUT a literal \"*\". No mark without a label.\n */\n required?: boolean;\n /**\n * Why the switch cannot be flipped — {@link Button}'s `disabledReason`. A switch is\n * the control that \"takes effect the moment it is flipped\", so it is the first one\n * keksdose K3 names: its settings rows sat behind a `SaveGuard` that forced a native\n * `disabled`, out of the tab order, so the reason never reached a keyboard.\n *\n * With a reason the switch is `aria-disabled` instead: still focusable, a click or\n * Space flips nothing and fires no `onChange` / `onCheckedChange`, the reason is in\n * the kit {@link Tooltip} on the track and on its `aria-describedby`, and the row\n * fades as a disabled one does. It wins over `disabled`. Changing the lock mounts the\n * switch anew (it moves in or out of the Tooltip), as a locked Button is.\n */\n disabledReason?: ReactNode;\n /**\n * This switch COMMITS — flipping it saves. Under a locked {@link WriteLockProvider} it\n * is locked the `disabledReason` way with the lock's reason (which wins over its\n * own). No provider, or an unlocked one: no effect. Button's `commit`, for K3.\n */\n commit?: boolean;\n}\n\n/**\n * A native checkbox drawn as a track and thumb, announced as a switch.\n *\n * Every other prop reaches the `<input>`, so it works controlled (`checked` +\n * `onChange`/`onCheckedChange`), uncontrolled (`defaultChecked`) and in a plain\n * `<form>` (`name`, `value`).\n */\nexport const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(\n {\n size = \"md\",\n label,\n description,\n switchPosition = \"end\",\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n disabledReason,\n commit,\n onClick,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n // Locked is disabled the focusable way — the row fades and the words stop inviting a\n // click, as for `disabled`.\n const looksDisabled = Boolean(disabled) || locked;\n const descriptionId = `${inputId}-description`;\n const showDescription = hasMessage(description);\n const bare = label === undefined && !showDescription;\n const geometry = SIZES[size];\n\n const track = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && !locked && className)}>\n <input\n ref={ref}\n id={bare ? id : inputId}\n // A reason wins over `disabled`, as on Button: the switch stays reachable.\n disabled={locked ? undefined : disabled}\n required={required}\n {...rest}\n // After the spread: a caller's props object must not be able to turn this\n // back into a plain checkbox, or into a text field.\n type=\"checkbox\"\n role=\"switch\"\n aria-disabled={locked || rest[\"aria-disabled\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n locked && lock.reasonId,\n )}\n // A locked switch's click is swallowed, as a locked Button's is.\n onClick={locked ? undefined : onClick}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n if (locked) {\n // Put the thumb back and report nothing — see Checkbox for why here and not\n // by cancelling the click. Label clicks and Space arrive as clicks too.\n e.currentTarget.checked = !e.currentTarget.checked;\n return;\n }\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(TRACK, geometry.track, locked && \"cursor-not-allowed\", inputClassName)}\n />\n <span aria-hidden className={cn(THUMB, geometry.thumb)} />\n </span>\n );\n // The Tooltip goes round the track, not the row — the row is the caller's layout. A\n // bare switch hands its `className` to the wrapper, then the outermost element.\n const control = locked ? (\n <LockedReason lock={lock} className={cn(\"shrink-0\", bare && className)}>\n {track}\n </LockedReason>\n ) : (\n track\n );\n\n if (bare) return control;\n\n // The switch's cell is one label line tall (text-sm, 20px) so it centres on the\n // FIRST line of a label that wraps, and a description underneath does not drag it\n // down to the middle of the paragraph.\n const cell = <span className=\"flex h-5 shrink-0 items-center\">{control}</span>;\n const words = (\n <span className=\"min-w-0 flex-1\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n looksDisabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\n </label>\n )}\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n </span>\n );\n\n return (\n <div className={cn(\"flex items-start gap-3\", looksDisabled && \"opacity-60\", className)}>\n {switchPosition === \"end\" ? (\n <>\n {words}\n {cell}\n </>\n ) : (\n <>\n {cell}\n {words}\n </>\n )}\n </div>\n );\n});\nSwitch.displayName = \"Switch\";\n"],"mappings":";AA+II,SAgFI,UA/EF,KADF;AA/IJ,SAAS,YAAY,aAAa;AAElC,SAAS,UAAU;AACnB,SAAS,YAAY,wBAAwB;AAC7C,SAAS,cAAc,qBAAqB;AAiB5C,MAAM,QAAQ;AAAA;AAAA,EAEZ,IAAI,EAAE,OAAO,WAAW,OAAO,mCAAmC;AAAA;AAAA,EAElE,IAAI,EAAE,OAAO,WAAW,OAAO,mCAAmC;AACpE;AAWA,MAAM,QACJ;AASF,MAAM,QACJ;AA8DK,MAAM,SAAS,WAA0C,SAASA,QACvE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,MAAM;AACtB,QAAM,OAAO,cAAc,QAAQ,cAAc;AACjD,QAAM,SAAS,KAAK;AAGpB,QAAM,gBAAgB,QAAQ,QAAQ,KAAK;AAC3C,QAAM,gBAAgB,GAAG,OAAO;AAChC,QAAM,kBAAkB,WAAW,WAAW;AAC9C,QAAM,OAAO,UAAU,UAAa,CAAC;AACrC,QAAM,WAAW,MAAM,IAAI;AAE3B,QAAM,QACJ,qBAAC,UAAK,WAAW,GAAG,iCAAiC,QAAQ,CAAC,UAAU,SAAS,GAC/E;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,IAAI,OAAO,KAAK;AAAA,QAEhB,UAAU,SAAS,SAAY;AAAA,QAC/B;AAAA,QACC,GAAG;AAAA,QAGJ,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe,UAAU,KAAK,eAAe,KAAK;AAAA,QAClD,oBAAkB;AAAA,UAChB,KAAK,kBAAkB;AAAA,UACvB,mBAAmB;AAAA,UACnB,UAAU,KAAK;AAAA,QACjB;AAAA,QAEA,SAAS,SAAS,SAAY;AAAA,QAC9B,UAAU,CAAC,MAAqC;AAC9C,cAAI,QAAQ;AAGV,cAAE,cAAc,UAAU,CAAC,EAAE,cAAc;AAC3C;AAAA,UACF;AACA,qBAAW,CAAC;AACZ,4BAAkB,EAAE,OAAO,OAAO;AAAA,QACpC;AAAA,QACA,WAAW,GAAG,OAAO,SAAS,OAAO,UAAU,sBAAsB,cAAc;AAAA;AAAA,IACrF;AAAA,IACA,oBAAC,UAAK,eAAW,MAAC,WAAW,GAAG,OAAO,SAAS,KAAK,GAAG;AAAA,KAC1D;AAIF,QAAM,UAAU,SACd,oBAAC,gBAAa,MAAY,WAAW,GAAG,YAAY,QAAQ,SAAS,GAClE,iBACH,IAEA;AAGF,MAAI,KAAM,QAAO;AAKjB,QAAM,OAAO,oBAAC,UAAK,WAAU,kCAAkC,mBAAQ;AACvE,QAAM,QACJ,qBAAC,UAAK,WAAU,kBACb;AAAA,cAAU,UACT;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA,gBAAgB,uBAAuB;AAAA,QACzC;AAAA,QAEC;AAAA;AAAA,UACA,YACC,oBAAC,UAAK,eAAW,MAAC,WAAU,+BAA8B,eAE1D;AAAA;AAAA;AAAA,IAEJ;AAAA,IAED,mBACC,oBAAC,UAAK,IAAI,eAAe,WAAU,iDAChC,uBACH;AAAA,KAEJ;AAGF,SACE,oBAAC,SAAI,WAAW,GAAG,0BAA0B,iBAAiB,cAAc,SAAS,GAClF,6BAAmB,QAClB,iCACG;AAAA;AAAA,IACA;AAAA,KACH,IAEA,iCACG;AAAA;AAAA,IACA;AAAA,KACH,GAEJ;AAEJ,CAAC;AACD,OAAO,cAAc;","names":["Switch"]}