@kenlo/sds-design-system 0.9.9

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 (246) hide show
  1. package/README.md +129 -0
  2. package/components/Accordion/Accordion.css +186 -0
  3. package/components/Accordion/accordion-variants.js +17 -0
  4. package/components/Accordion/accordion.marko +264 -0
  5. package/components/AccordionGroup/AccordionGroup.css +35 -0
  6. package/components/AccordionGroup/accordion-group.marko +17 -0
  7. package/components/Alert/Alert.css +104 -0
  8. package/components/Alert/alert-variants.js +35 -0
  9. package/components/Alert/alert.marko +94 -0
  10. package/components/AttachmentList/AttachmentList.css +92 -0
  11. package/components/AttachmentList/attachment-list.marko +51 -0
  12. package/components/AttachmentList/attachment-row.marko +167 -0
  13. package/components/Avatar/Avatar.css +110 -0
  14. package/components/Avatar/avatar-variants.js +12 -0
  15. package/components/Avatar/avatar.marko +71 -0
  16. package/components/Badge/Badge.css +381 -0
  17. package/components/Badge/badge-dismissible.marko +143 -0
  18. package/components/Badge/badge-variants.js +35 -0
  19. package/components/Badge/badge.marko +134 -0
  20. package/components/BottomSheet/BottomSheet.css +300 -0
  21. package/components/BottomSheet/bottom-sheet.marko +340 -0
  22. package/components/BulkActionBar/BulkActionBar.css +88 -0
  23. package/components/BulkActionBar/bulk-action-bar.marko +186 -0
  24. package/components/Button/Button.css +340 -0
  25. package/components/Button/button-variants.js +25 -0
  26. package/components/Button/button.marko +139 -0
  27. package/components/Card/Card.css +391 -0
  28. package/components/Card/card-body.marko +5 -0
  29. package/components/Card/card-collapsible.marko +94 -0
  30. package/components/Card/card-footer.marko +12 -0
  31. package/components/Card/card-header.marko +30 -0
  32. package/components/Card/card-info-pair.marko +17 -0
  33. package/components/Card/card-info-row.marko +12 -0
  34. package/components/Card/card-section.marko +14 -0
  35. package/components/Card/card-variants.js +24 -0
  36. package/components/Card/card.marko +124 -0
  37. package/components/Checkbox/Checkbox.css +249 -0
  38. package/components/Checkbox/checkbox-variants.js +21 -0
  39. package/components/Checkbox/checkbox.marko +128 -0
  40. package/components/ColorPicker/ColorPicker.css +253 -0
  41. package/components/ColorPicker/color-maths.js +104 -0
  42. package/components/ColorPicker/color-picker.marko +474 -0
  43. package/components/DatePicker/DatePicker.css +429 -0
  44. package/components/DatePicker/date-logic.js +225 -0
  45. package/components/DatePicker/date-picker.marko +492 -0
  46. package/components/Drawer/Drawer.css +174 -0
  47. package/components/Drawer/drawer.marko +262 -0
  48. package/components/Floating/Floating.css +28 -0
  49. package/components/Floating/floating-position.js +251 -0
  50. package/components/Floating/floating.marko +291 -0
  51. package/components/Form/Form.css +15 -0
  52. package/components/Form/form-context.js +69 -0
  53. package/components/Form/form.marko +73 -0
  54. package/components/GradientPicker/GradientPicker.css +179 -0
  55. package/components/GradientPicker/gradient-picker.marko +398 -0
  56. package/components/Grid/Grid.css +161 -0
  57. package/components/Grid/col.marko +36 -0
  58. package/components/Grid/grid-variants.js +43 -0
  59. package/components/Grid/grid.marko +25 -0
  60. package/components/Header/Header.css +457 -0
  61. package/components/Header/header.marko +523 -0
  62. package/components/Icon/Icon.css +44 -0
  63. package/components/Icon/icon-colors.js +31 -0
  64. package/components/Icon/icon.marko +56 -0
  65. package/components/IconButton/IconButton.css +85 -0
  66. package/components/IconButton/icon-button-variants.js +18 -0
  67. package/components/IconButton/icon-button.marko +103 -0
  68. package/components/Incrementer/Incrementer.css +94 -0
  69. package/components/Incrementer/incrementer.marko +70 -0
  70. package/components/Input/Input.css +547 -0
  71. package/components/Input/input-countries.js +109 -0
  72. package/components/Input/input-masks.js +218 -0
  73. package/components/Input/input-types.js +194 -0
  74. package/components/Input/input.marko +530 -0
  75. package/components/Menu/Menu.css +651 -0
  76. package/components/Menu/menu-variants.js +89 -0
  77. package/components/Menu/menu.marko +560 -0
  78. package/components/Modal/Modal.css +160 -0
  79. package/components/Modal/modal.marko +296 -0
  80. package/components/Nav/Nav.css +298 -0
  81. package/components/Nav/nav-variants.js +23 -0
  82. package/components/Nav/nav.marko +164 -0
  83. package/components/Notification/Notification.css +78 -0
  84. package/components/Notification/notification-variants.js +30 -0
  85. package/components/Notification/notification.marko +55 -0
  86. package/components/Pagination/Pagination.css +78 -0
  87. package/components/Pagination/pagination-pages.js +66 -0
  88. package/components/Pagination/pagination.marko +141 -0
  89. package/components/Pill/Pill.css +193 -0
  90. package/components/Pill/pill-variants.js +16 -0
  91. package/components/Pill/pill.marko +165 -0
  92. package/components/Popover/Popover.css +121 -0
  93. package/components/Popover/popover.marko +96 -0
  94. package/components/Radio/Radio.css +230 -0
  95. package/components/Radio/radio-variants.js +16 -0
  96. package/components/Radio/radio.marko +93 -0
  97. package/components/RadioGroup/RadioGroup.css +32 -0
  98. package/components/RadioGroup/radio-group.marko +93 -0
  99. package/components/SearchBar/SearchBar.css +181 -0
  100. package/components/SearchBar/search-bar-variants.js +24 -0
  101. package/components/SearchBar/search-bar.marko +157 -0
  102. package/components/Select/Select.css +427 -0
  103. package/components/Select/select-options.js +163 -0
  104. package/components/Select/select.marko +449 -0
  105. package/components/Separator/Separator.css +22 -0
  106. package/components/Separator/separator.marko +14 -0
  107. package/components/Skeleton/Skeleton.css +100 -0
  108. package/components/Skeleton/skeleton-avatar.marko +28 -0
  109. package/components/Skeleton/skeleton-button.marko +22 -0
  110. package/components/Skeleton/skeleton-image.marko +20 -0
  111. package/components/Skeleton/skeleton-input.marko +22 -0
  112. package/components/Skeleton/skeleton-variants.js +16 -0
  113. package/components/Skeleton/skeleton.marko +53 -0
  114. package/components/Spinner/Spinner.css +173 -0
  115. package/components/Spinner/spinner-geometry.js +39 -0
  116. package/components/Spinner/spinner-overlay.marko +97 -0
  117. package/components/Spinner/spinner.marko +88 -0
  118. package/components/StatCard/StatCard.css +90 -0
  119. package/components/StatCard/stat-card-group.marko +5 -0
  120. package/components/StatCard/stat-card.marko +57 -0
  121. package/components/Switch/Switch.css +209 -0
  122. package/components/Switch/switch.marko +99 -0
  123. package/components/Table/Table.css +270 -0
  124. package/components/Table/table-cell.marko +28 -0
  125. package/components/Table/table-header.marko +27 -0
  126. package/components/Table/table-logic.js +129 -0
  127. package/components/Table/table.marko +372 -0
  128. package/components/Text/Text.css +79 -0
  129. package/components/Text/text-variants.js +25 -0
  130. package/components/Text/text.marko +48 -0
  131. package/components/TextEditor/TextEditor.css +78 -0
  132. package/components/TextEditor/safe-url.js +50 -0
  133. package/components/TextEditor/text-editor-commands.js +22 -0
  134. package/components/TextEditor/text-editor.marko +253 -0
  135. package/components/Toast/Toast.css +88 -0
  136. package/components/Toast/toast-api.js +19 -0
  137. package/components/Toast/toast-store.js +118 -0
  138. package/components/Toast/toast-variants.js +16 -0
  139. package/components/Toast/toast.marko +93 -0
  140. package/components/Toast/toaster.marko +70 -0
  141. package/components/Toggle/Toggle.css +169 -0
  142. package/components/Toggle/toggle.marko +93 -0
  143. package/components/Tooltip/Tooltip.css +115 -0
  144. package/components/Tooltip/tooltip.marko +77 -0
  145. package/components/Uploader/Uploader.css +180 -0
  146. package/components/Uploader/crop-geometry.js +161 -0
  147. package/components/Uploader/uploader.marko +269 -0
  148. package/foundations/Scrollbar/Scrollbar.css +26 -0
  149. package/foundations/attrs.js +27 -0
  150. package/foundations/breakpoints.js +27 -0
  151. package/foundations/contrast.js +74 -0
  152. package/foundations/custom-scheme.js +222 -0
  153. package/foundations/overlay.js +108 -0
  154. package/foundations/styles.css +284 -0
  155. package/foundations/text.js +22 -0
  156. package/foundations/theme-server.js +81 -0
  157. package/foundations/theme.js +42 -0
  158. package/foundations/tokens.css +1656 -0
  159. package/icons/custom/custom-video-horizontal-filled.marko +6 -0
  160. package/icons/custom/custom-video-horizontal-regular.marko +6 -0
  161. package/icons/custom/custom-video-tour-3d-filled.marko +7 -0
  162. package/icons/custom/custom-video-tour-3d-regular.marko +9 -0
  163. package/icons/custom/custom-video-vertical-filled.marko +6 -0
  164. package/icons/custom/custom-video-vertical-regular.marko +6 -0
  165. package/icons/generated/fluent-add-regular.marko +3 -0
  166. package/icons/generated/fluent-alert-regular.marko +3 -0
  167. package/icons/generated/fluent-apps-regular.marko +3 -0
  168. package/icons/generated/fluent-arrow-download-regular.marko +3 -0
  169. package/icons/generated/fluent-arrow-left-regular.marko +3 -0
  170. package/icons/generated/fluent-arrow-right-regular.marko +3 -0
  171. package/icons/generated/fluent-arrow-rotate-clockwise-regular.marko +3 -0
  172. package/icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko +3 -0
  173. package/icons/generated/fluent-border-all-regular.marko +3 -0
  174. package/icons/generated/fluent-calendar-regular.marko +3 -0
  175. package/icons/generated/fluent-checkmark-circle-filled.marko +3 -0
  176. package/icons/generated/fluent-checkmark-regular.marko +3 -0
  177. package/icons/generated/fluent-chevron-down-regular.marko +3 -0
  178. package/icons/generated/fluent-chevron-left-regular.marko +3 -0
  179. package/icons/generated/fluent-chevron-right-regular.marko +3 -0
  180. package/icons/generated/fluent-chevron-up-regular.marko +3 -0
  181. package/icons/generated/fluent-circle-half-fill-regular.marko +3 -0
  182. package/icons/generated/fluent-clock-regular.marko +3 -0
  183. package/icons/generated/fluent-color-regular.marko +3 -0
  184. package/icons/generated/fluent-crop-regular.marko +3 -0
  185. package/icons/generated/fluent-dark-theme-regular.marko +3 -0
  186. package/icons/generated/fluent-delete-regular.marko +3 -0
  187. package/icons/generated/fluent-desktop-regular.marko +3 -0
  188. package/icons/generated/fluent-dismiss-circle-filled.marko +3 -0
  189. package/icons/generated/fluent-dismiss-regular.marko +3 -0
  190. package/icons/generated/fluent-document-pdf-regular.marko +3 -0
  191. package/icons/generated/fluent-document-regular.marko +3 -0
  192. package/icons/generated/fluent-eye-off-regular.marko +3 -0
  193. package/icons/generated/fluent-eye-regular.marko +3 -0
  194. package/icons/generated/fluent-eyedropper-regular.marko +3 -0
  195. package/icons/generated/fluent-grid-regular.marko +3 -0
  196. package/icons/generated/fluent-history-regular.marko +3 -0
  197. package/icons/generated/fluent-home-regular.marko +3 -0
  198. package/icons/generated/fluent-image-regular.marko +3 -0
  199. package/icons/generated/fluent-info-regular.marko +3 -0
  200. package/icons/generated/fluent-link-regular.marko +3 -0
  201. package/icons/generated/fluent-more-horizontal-regular.marko +3 -0
  202. package/icons/generated/fluent-navigation-regular.marko +3 -0
  203. package/icons/generated/fluent-paint-brush-regular.marko +3 -0
  204. package/icons/generated/fluent-question-circle-regular.marko +3 -0
  205. package/icons/generated/fluent-search-regular.marko +3 -0
  206. package/icons/generated/fluent-settings-regular.marko +3 -0
  207. package/icons/generated/fluent-square-hint-regular.marko +3 -0
  208. package/icons/generated/fluent-subtract-regular.marko +3 -0
  209. package/icons/generated/fluent-text-align-center-regular.marko +3 -0
  210. package/icons/generated/fluent-text-align-left-regular.marko +3 -0
  211. package/icons/generated/fluent-text-align-right-regular.marko +3 -0
  212. package/icons/generated/fluent-text-bold-regular.marko +3 -0
  213. package/icons/generated/fluent-text-bullet-list-regular.marko +3 -0
  214. package/icons/generated/fluent-text-font-regular.marko +3 -0
  215. package/icons/generated/fluent-text-font-size-regular.marko +3 -0
  216. package/icons/generated/fluent-text-italic-regular.marko +3 -0
  217. package/icons/generated/fluent-text-number-list-ltr-regular.marko +3 -0
  218. package/icons/generated/fluent-video-regular.marko +3 -0
  219. package/icons/generated/fluent-warning-filled.marko +3 -0
  220. package/icons/generated/fluent-weather-moon-regular.marko +3 -0
  221. package/icons/generated/fluent-weather-sunny-regular.marko +3 -0
  222. package/icons/generated/fluent-zoom-in-regular.marko +3 -0
  223. package/icons/generated/fluent-zoom-out-regular.marko +3 -0
  224. package/icons/manifest.json +61 -0
  225. package/icons/social/social-facebook-color.marko +7 -0
  226. package/icons/social/social-facebook.marko +6 -0
  227. package/icons/social/social-google-color.marko +20 -0
  228. package/icons/social/social-google.marko +6 -0
  229. package/icons/social/social-instagram-color.marko +16 -0
  230. package/icons/social/social-instagram.marko +6 -0
  231. package/icons/social/social-linkedin-color.marko +8 -0
  232. package/icons/social/social-linkedin.marko +8 -0
  233. package/icons/social/social-messenger-color.marko +15 -0
  234. package/icons/social/social-messenger.marko +6 -0
  235. package/icons/social/social-pinterest-color.marko +6 -0
  236. package/icons/social/social-pinterest.marko +6 -0
  237. package/icons/social/social-telegram-color.marko +7 -0
  238. package/icons/social/social-telegram.marko +6 -0
  239. package/icons/social/social-whatsapp-color.marko +8 -0
  240. package/icons/social/social-whatsapp.marko +7 -0
  241. package/icons/social/social-x-color.marko +7 -0
  242. package/icons/social/social-x.marko +6 -0
  243. package/icons/social/social-youtube-color.marko +7 -0
  244. package/icons/social/social-youtube.marko +6 -0
  245. package/marko.json +203 -0
  246. package/package.json +55 -0
@@ -0,0 +1,71 @@
1
+ <!-- sds-avatar — ported from kdls-design-system v2.5.1 (0f3edf8) components/Avatar/Avatar.tsx.
2
+
3
+ No state, which KDLS needed for one thing: an onError handler that swapped a broken image
4
+ for the initials. The initials are rendered underneath the image here and show through
5
+ when it paints nothing — see Avatar.css. That removes a serialised component per avatar,
6
+ and an avatar list is exactly where they repeat.
7
+
8
+ It is a <button> when it has a click handler and a plain <div> otherwise, rather than a
9
+ div wearing role="button" with a hand-written key handler as KDLS did. -->
10
+
11
+ import { assertAvatarSize } from "./avatar-variants.js";
12
+
13
+ $ {
14
+ const {
15
+ src,
16
+ initials,
17
+ alt,
18
+ size = "sm",
19
+ onClick,
20
+ class: extraClass = "",
21
+ } = input;
22
+
23
+ assertAvatarSize(size);
24
+
25
+ const interactive = Boolean(onClick);
26
+ const label = alt || initials || "Avatar";
27
+
28
+ const classes = [
29
+ "sds-avatar",
30
+ `sds-avatar--${size}`,
31
+ interactive ? "sds-avatar--clickable" : "",
32
+ extraClass,
33
+ ].filter(Boolean).join(" ");
34
+ }
35
+
36
+ <macro|{ }| name="content">
37
+ <!-- Underneath. Visible only where the image is not. -->
38
+ <span class="sds-avatar__placeholder" aria-hidden=(src ? "true" : null)>
39
+ ${initials ?? "?"}
40
+ </span>
41
+ <if(src)>
42
+ <!-- `onerror` as an attribute, not a handler this component hydrates for. KDLS keeps a
43
+ `useState(imgFailed)` for exactly this, and one serialised component per avatar is
44
+ the cost an avatar list pays fifty times over. The attribute costs nothing and
45
+ does the same job: the broken image leaves the box and the initials underneath are
46
+ what remains. Under a strict CSP that forbids inline handlers it simply does not
47
+ run, which is the behaviour this had before — it degrades, it does not break.
48
+
49
+ The name: with a photo and no `alt`, nothing in here had an accessible name at all
50
+ — the image was silent, the placeholder hidden, and the root dropped its label. An
51
+ `alt` the caller wrote wins, including an empty one, which is how you say
52
+ "decorative". Only the absence of the prop falls back to the initials. -->
53
+ <img
54
+ class="sds-avatar__image"
55
+ src=src
56
+ alt=(alt === undefined ? (initials ?? "Avatar") : alt)
57
+ onerror="this.remove()"
58
+ />
59
+ </if>
60
+ </macro>
61
+
62
+ <if(interactive)>
63
+ <button type="button" class=classes aria-label=label on-click(onClick)>
64
+ <content/>
65
+ </button>
66
+ </if>
67
+ <else>
68
+ <div class=classes role=(src ? null : "img") aria-label=(src ? null : label)>
69
+ <content/>
70
+ </div>
71
+ </else>
@@ -0,0 +1,381 @@
1
+ /* SDS Badge.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Badge/Badge.css.
3
+
4
+ Divergences:
5
+ - the primary, secondary, tertiary and lya colour blocks are removed with the
6
+ families they read. Fourteen colours remain.
7
+ - text and outline painted from the primary slot read --color-primary-N-solid.
8
+ `background` accepts a gradient; `color` and `box-shadow` do not, and
9
+ applyCustomGradient can put a gradient in that slot. */
10
+
11
+ /* ============================================================
12
+ SDS Badge
13
+ Sizes: 24px | 20px
14
+ Types: neutral | scheme-color |
15
+ success | alert | error |
16
+ leaf | sand | wood | clay | flower | dusk | sky | sea | lake
17
+ Appearance: outline | tone-10..tone-50 (progressive tint, tone-50 = full color)
18
+ Icons: left (optional) | right/dismiss (optional)
19
+ ============================================================ */
20
+
21
+ .sds-badge {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ border-radius: var(--radius-xs);
25
+ /* inset instead of border — the 1px outline never adds to the box size,
26
+ so tone (transparent) and outline (currentColor) close at the same height */
27
+ box-shadow: inset 0 0 0 1px transparent;
28
+ font-family: var(--font-family-base);
29
+ font-weight: var(--font-weight-medium);
30
+ width: fit-content;
31
+ }
32
+
33
+ /* width="fill" — only when explicitly requested.
34
+ Without iconRight: iconLeft+label are centered in the pill.
35
+ With iconRight: iconLeft+label go to the left (.sds-badge__content) and the
36
+ clickable iconRight goes to the right (.sds-badge__icon-right-wrap), one at each
37
+ end of the pill. */
38
+ .sds-badge--fill {
39
+ width: 100%;
40
+ justify-content: center;
41
+ }
42
+
43
+ .sds-badge--fill.sds-badge--has-icon-right {
44
+ justify-content: space-between;
45
+ }
46
+
47
+ .sds-badge__content {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: var(--spacing-1);
51
+ padding: var(--spacing-1) var(--spacing-2);
52
+ }
53
+
54
+ /* When there's an iconRight, the content's padding-right shrinks — the icon's
55
+ wrap already gives its own breathing room to the pill's edge (see
56
+ .sds-badge__icon-right-wrap), so it doesn't need both added together */
57
+ .sds-badge__content--has-icon-right {
58
+ padding-right: var(--spacing-0-5);
59
+ }
60
+
61
+ /* ── Sizes — spacing tweaks at lg ──────────────────────────────────────────────
62
+ The label's typography used to be redeclared here, once per badge size. Those three
63
+ rules were the Text scale written out by hand — label-sm at sm, body-sm at md, body-md
64
+ at lg — so the label now renders through <sds-text> with the matching variant and the
65
+ duplication is gone. It passes color="inherit" so the badge keeps deciding the colour,
66
+ which is where the contrast tokens live. */
67
+
68
+ .sds-badge--lg .sds-badge__content {
69
+ gap: var(--spacing-2);
70
+ }
71
+
72
+ .sds-badge--lg .sds-badge__content--has-icon-right {
73
+ padding-right: var(--spacing-1);
74
+ }
75
+
76
+ /* ── Icons ─────────────────────────────────── */
77
+ .sds-badge__icon {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ font-size: 16px;
81
+ flex-shrink: 0;
82
+ }
83
+
84
+ .sds-badge__icon-right-wrap {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ padding: var(--spacing-0-5) 6px var(--spacing-0-5) 0;
88
+ }
89
+
90
+ .sds-badge--md .sds-badge__icon-right-wrap {
91
+ padding-top: var(--spacing-1);
92
+ padding-bottom: var(--spacing-1);
93
+ }
94
+
95
+ .sds-badge--lg .sds-badge__icon-right-wrap {
96
+ padding-top: var(--spacing-1);
97
+ padding-right: var(--spacing-1);
98
+ padding-bottom: var(--spacing-1);
99
+ }
100
+
101
+ .sds-badge__icon--right--clickable {
102
+ cursor: pointer;
103
+ border-radius: min(4px, var(--radius-xs));
104
+ transition: background-color 120ms ease;
105
+ padding: var(--spacing-0-5);
106
+ }
107
+ .sds-badge__icon--right--clickable:hover { background-color: var(--color-opacity-05); }
108
+ .sds-badge__icon--right--clickable:active { background-color: var(--color-opacity-10); }
109
+
110
+ .sds-badge--lg .sds-badge__icon--right--clickable {
111
+ padding: var(--spacing-1);
112
+ }
113
+
114
+ /* Loading spinner in place of the icon — the Spinner's smallest size (sm) is
115
+ 24px, larger than the badge icon (16px). The internal SVG comes with fixed width/height
116
+ as an HTML attribute (24px, computed in JS from size="sm"), not
117
+ just via the CSS variable — so it needs to override the <svg> itself
118
+ (.sds-spinner__svg), not just the outer <div>, otherwise the graphic keeps
119
+ rendering at a true 24px inside a smaller box. */
120
+ .sds-badge__icon--right--clickable .sds-spinner {
121
+ --sds-spinner-size: 16px;
122
+ }
123
+ .sds-badge__icon--right--clickable .sds-spinner__svg {
124
+ width: 16px;
125
+ height: 16px;
126
+ }
127
+
128
+ .sds-badge__icon--right--disabled {
129
+ cursor: not-allowed;
130
+ }
131
+ .sds-badge__icon--right--disabled:hover,
132
+ .sds-badge__icon--right--disabled:active {
133
+ background-color: transparent;
134
+ }
135
+
136
+ .sds-badge__icon--right--loading {
137
+ cursor: wait;
138
+ }
139
+
140
+ /* ══════════════════════════════════════════════
141
+ COLOR × APPEARANCE MATRIX
142
+ outline = transparent bg, border tied to the color
143
+ tone-10 = lightest tint
144
+ tone-20 = light tint
145
+ tone-30 = medium tint (default)
146
+ tone-40 = medium-strong tint
147
+ tone-50 = full color / full-saturation
148
+ ══════════════════════════════════════════════ */
149
+
150
+ /* ── NEUTRAL ──────────────────────────────── */
151
+ .sds-badge--neutral.sds-badge--outline { background: transparent; color: var(--color-neutral-60); }
152
+ .sds-badge--neutral.sds-badge--tone-10 { background: var(--color-neutral-10); color: var(--color-neutral-10-on); }
153
+ .sds-badge--neutral.sds-badge--tone-20 { background: var(--color-neutral-20); color: var(--color-neutral-20-on); }
154
+ .sds-badge--neutral.sds-badge--tone-30 { background: var(--color-neutral-30); color: var(--color-neutral-30-on); }
155
+ .sds-badge--neutral.sds-badge--tone-40 { background: var(--color-neutral-40); color: var(--color-neutral-40-on); }
156
+ .sds-badge--neutral.sds-badge--tone-50 { background: var(--color-neutral-50); color: var(--color-neutral-50-on); }
157
+
158
+ /* ── SCHEME-COLOR — follows data-color-scheme; tone-50 label auto-adjusts for
159
+ contrast via --color-on-primary, same mechanism as Button/Toggle/Pill ── */
160
+ /* ── scheme-color outline — the gradient-capable ring ─────────────────────────
161
+ The primary slot may hold a gradient (applyCustomGradient), and box-shadow cannot
162
+ paint one. KDLS solved this for its Lya variant with a masked ::before, and that
163
+ technique moves here, to the one variant that can now receive a gradient.
164
+
165
+ No real `border`: a fit-content box grows by the border's width regardless of
166
+ box-sizing, which would make outline the only appearance measuring 2px taller than
167
+ its siblings. The ring is an absolutely positioned ::before instead, masked down to
168
+ its own 1px padding, so it paints over the box without taking layout space.
169
+
170
+ Divergence from KDLS, deliberate: KDLS put background-clip:text on the whole badge,
171
+ which sets color:transparent on everything inside — including any icon, which then
172
+ disappears. Here the clip is scoped to the label, and the badge keeps a solid
173
+ currentColor from the -solid companion so icons still paint.
174
+
175
+ In solid mode all of this renders exactly like a plain outline. */
176
+ .sds-badge--scheme-color.sds-badge--outline {
177
+ position: relative;
178
+ background: transparent;
179
+ color: var(--color-primary-50-solid);
180
+ box-shadow: none;
181
+ }
182
+
183
+ .sds-badge--scheme-color.sds-badge--outline .sds-badge__label {
184
+ background: var(--color-primary-50);
185
+ background-clip: text;
186
+ -webkit-background-clip: text;
187
+ color: transparent;
188
+ }
189
+
190
+ .sds-badge--scheme-color.sds-badge--outline::before {
191
+ content: "";
192
+ position: absolute;
193
+ inset: 0;
194
+ border-radius: inherit;
195
+ padding: 1px;
196
+ background: var(--color-primary-50);
197
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
198
+ -webkit-mask-composite: xor;
199
+ mask-composite: exclude;
200
+ pointer-events: none;
201
+ }
202
+ .sds-badge--scheme-color.sds-badge--tone-10 { background: var(--color-primary-10); color: var(--color-primary-10-on); }
203
+ .sds-badge--scheme-color.sds-badge--tone-20 { background: var(--color-primary-20); color: var(--color-primary-20-on); }
204
+ .sds-badge--scheme-color.sds-badge--tone-30 { background: var(--color-primary-30); color: var(--color-primary-30-on); }
205
+ .sds-badge--scheme-color.sds-badge--tone-40 { background: var(--color-primary-40); color: var(--color-primary-40-on); }
206
+ .sds-badge--scheme-color.sds-badge--tone-50 { background: var(--color-primary-50); color: var(--color-primary-50-on); }
207
+
208
+ /* ── Label colour ──────────────────────────────────────────────────────────────
209
+ Labels are not painted by hand. Each surface reads --color-<family>-<tone>-on,
210
+ generated by scripts/gen-contrast-tokens.mjs from the palette itself: the step of
211
+ the family that reads best on that surface, in that theme, falling back to white or
212
+ black only where the family cannot reach WCAG AA.
213
+
214
+ Of the 130 combinations, 113 are satisfied by the family itself and 17 need the
215
+ fallback. None is left below AA.
216
+
217
+ This replaces one hand-written declaration per family per tone per theme, plus a
218
+ block of dark-theme corrections. KDLS wrote those by eye and several were wrong in
219
+ ways only visible by squinting — tone-40 in the light theme read the -10 step across
220
+ every family, at ratios from 1.38 to 3.20 against a 4.5 minimum. */
221
+
222
+ /* ── SUCCESS ──────────────────────────────── */
223
+ .sds-badge--success.sds-badge--outline { background: transparent; color: var(--color-success-50); }
224
+ .sds-badge--success.sds-badge--tone-10 { background: var(--color-success-10); color: var(--color-success-10-on); }
225
+ .sds-badge--success.sds-badge--tone-20 { background: var(--color-success-20); color: var(--color-success-20-on); }
226
+ .sds-badge--success.sds-badge--tone-30 { background: var(--color-success-30); color: var(--color-success-30-on); }
227
+ .sds-badge--success.sds-badge--tone-40 { background: var(--color-success-40); color: var(--color-success-40-on); }
228
+ .sds-badge--success.sds-badge--tone-50 { background: var(--color-success-50); color: var(--color-success-50-on); }
229
+
230
+ /* ── ALERT ────────────────────────────────── */
231
+ .sds-badge--alert.sds-badge--outline { background: transparent; color: var(--color-alert-50); }
232
+ .sds-badge--alert.sds-badge--tone-10 { background: var(--color-alert-10); color: var(--color-alert-10-on); }
233
+ .sds-badge--alert.sds-badge--tone-20 { background: var(--color-alert-20); color: var(--color-alert-20-on); }
234
+ .sds-badge--alert.sds-badge--tone-30 { background: var(--color-alert-30); color: var(--color-alert-30-on); }
235
+ .sds-badge--alert.sds-badge--tone-40 { background: var(--color-alert-40); color: var(--color-alert-40-on); }
236
+ .sds-badge--alert.sds-badge--tone-50 { background: var(--color-alert-50); color: var(--color-alert-50-on); }
237
+
238
+ /* ── ERROR ────────────────────────────────── */
239
+ .sds-badge--error.sds-badge--outline { background: transparent; color: var(--color-error-50); }
240
+ .sds-badge--error.sds-badge--tone-10 { background: var(--color-error-10); color: var(--color-error-10-on); }
241
+ .sds-badge--error.sds-badge--tone-20 { background: var(--color-error-20); color: var(--color-error-20-on); }
242
+ .sds-badge--error.sds-badge--tone-30 { background: var(--color-error-30); color: var(--color-error-30-on); }
243
+ .sds-badge--error.sds-badge--tone-40 { background: var(--color-error-40); color: var(--color-error-40-on); }
244
+ .sds-badge--error.sds-badge--tone-50 { background: var(--color-error-50); color: var(--color-error-50-on); }
245
+
246
+ /* ── LEAF ── */
247
+ .sds-badge--leaf.sds-badge--outline { background: transparent; color: var(--color-leaf-70); }
248
+ .sds-badge--leaf.sds-badge--tone-10 { background: var(--color-leaf-10); color: var(--color-leaf-10-on); }
249
+ .sds-badge--leaf.sds-badge--tone-20 { background: var(--color-leaf-20); color: var(--color-leaf-20-on); }
250
+ .sds-badge--leaf.sds-badge--tone-30 { background: var(--color-leaf-30); color: var(--color-leaf-30-on); }
251
+ .sds-badge--leaf.sds-badge--tone-40 { background: var(--color-leaf-40); color: var(--color-leaf-40-on); }
252
+ /* tone-50 gets a dark-mode override below (§ Dark-mode text overrides) pinning it to the
253
+ same leaf-90 look as light — tone-40 needs none, its natural auto-invert already lands
254
+ on leaf-10 in dark, which is exactly what's wanted there. */
255
+ .sds-badge--leaf.sds-badge--tone-50 { background: var(--color-leaf-50); color: var(--color-leaf-50-on); }
256
+
257
+ /* ── SAND ── */
258
+ .sds-badge--sand.sds-badge--outline { background: transparent; color: var(--color-sand-70); }
259
+ .sds-badge--sand.sds-badge--tone-10 { background: var(--color-sand-10); color: var(--color-sand-10-on); }
260
+ .sds-badge--sand.sds-badge--tone-20 { background: var(--color-sand-20); color: var(--color-sand-20-on); }
261
+ .sds-badge--sand.sds-badge--tone-30 { background: var(--color-sand-30); color: var(--color-sand-30-on); }
262
+ .sds-badge--sand.sds-badge--tone-40 { background: var(--color-sand-40); color: var(--color-sand-40-on); }
263
+ /* Same reasoning as Leaf's tone-50 above — dark override lives in § Dark-mode text overrides. */
264
+ .sds-badge--sand.sds-badge--tone-50 { background: var(--color-sand-50); color: var(--color-sand-50-on); }
265
+
266
+ /* ── WOOD ── */
267
+ .sds-badge--wood.sds-badge--outline { background: transparent; color: var(--color-wood-50); }
268
+ .sds-badge--wood.sds-badge--tone-10 { background: var(--color-wood-10); color: var(--color-wood-10-on); }
269
+ .sds-badge--wood.sds-badge--tone-20 { background: var(--color-wood-20); color: var(--color-wood-20-on); }
270
+ .sds-badge--wood.sds-badge--tone-30 { background: var(--color-wood-30); color: var(--color-wood-30-on); }
271
+ .sds-badge--wood.sds-badge--tone-40 { background: var(--color-wood-40); color: var(--color-wood-40-on); }
272
+ .sds-badge--wood.sds-badge--tone-50 { background: var(--color-wood-50); color: var(--color-wood-50-on); }
273
+
274
+ /* ── CLAY ── */
275
+ .sds-badge--clay.sds-badge--outline { background: transparent; color: var(--color-clay-50); }
276
+ .sds-badge--clay.sds-badge--tone-10 { background: var(--color-clay-10); color: var(--color-clay-10-on); }
277
+ .sds-badge--clay.sds-badge--tone-20 { background: var(--color-clay-20); color: var(--color-clay-20-on); }
278
+ .sds-badge--clay.sds-badge--tone-30 { background: var(--color-clay-30); color: var(--color-clay-30-on); }
279
+ .sds-badge--clay.sds-badge--tone-40 { background: var(--color-clay-40); color: var(--color-clay-40-on); }
280
+ .sds-badge--clay.sds-badge--tone-50 { background: var(--color-clay-50); color: var(--color-clay-50-on); }
281
+
282
+ /* ── FLOWER ── */
283
+ .sds-badge--flower.sds-badge--outline { background: transparent; color: var(--color-flower-50); }
284
+ .sds-badge--flower.sds-badge--tone-10 { background: var(--color-flower-10); color: var(--color-flower-10-on); }
285
+ .sds-badge--flower.sds-badge--tone-20 { background: var(--color-flower-20); color: var(--color-flower-20-on); }
286
+ .sds-badge--flower.sds-badge--tone-30 { background: var(--color-flower-30); color: var(--color-flower-30-on); }
287
+ .sds-badge--flower.sds-badge--tone-40 { background: var(--color-flower-40); color: var(--color-flower-40-on); }
288
+ .sds-badge--flower.sds-badge--tone-50 { background: var(--color-flower-50); color: var(--color-flower-50-on); }
289
+
290
+ /* ── DUSK ── */
291
+ .sds-badge--dusk.sds-badge--outline { background: transparent; color: var(--color-dusk-50); }
292
+ .sds-badge--dusk.sds-badge--tone-10 { background: var(--color-dusk-10); color: var(--color-dusk-10-on); }
293
+ .sds-badge--dusk.sds-badge--tone-20 { background: var(--color-dusk-20); color: var(--color-dusk-20-on); }
294
+ .sds-badge--dusk.sds-badge--tone-30 { background: var(--color-dusk-30); color: var(--color-dusk-30-on); }
295
+ .sds-badge--dusk.sds-badge--tone-40 { background: var(--color-dusk-40); color: var(--color-dusk-40-on); }
296
+ .sds-badge--dusk.sds-badge--tone-50 { background: var(--color-dusk-50); color: var(--color-dusk-50-on); }
297
+
298
+ /* ── SKY ────────────────────────────────── */
299
+ .sds-badge--sky.sds-badge--outline { background: transparent; color: var(--color-sky-50); }
300
+ .sds-badge--sky.sds-badge--tone-10 { background: var(--color-sky-10); color: var(--color-sky-10-on); }
301
+ .sds-badge--sky.sds-badge--tone-20 { background: var(--color-sky-20); color: var(--color-sky-20-on); }
302
+ .sds-badge--sky.sds-badge--tone-30 { background: var(--color-sky-30); color: var(--color-sky-30-on); }
303
+ .sds-badge--sky.sds-badge--tone-40 { background: var(--color-sky-40); color: var(--color-sky-40-on); }
304
+ .sds-badge--sky.sds-badge--tone-50 { background: var(--color-sky-50); color: var(--color-sky-50-on); }
305
+
306
+ /* ── SEA ── */
307
+ .sds-badge--sea.sds-badge--outline { background: transparent; color: var(--color-sea-50); }
308
+ .sds-badge--sea.sds-badge--tone-10 { background: var(--color-sea-10); color: var(--color-sea-10-on); }
309
+ .sds-badge--sea.sds-badge--tone-20 { background: var(--color-sea-20); color: var(--color-sea-20-on); }
310
+ .sds-badge--sea.sds-badge--tone-30 { background: var(--color-sea-30); color: var(--color-sea-30-on); }
311
+ .sds-badge--sea.sds-badge--tone-40 { background: var(--color-sea-40); color: var(--color-sea-40-on); }
312
+ .sds-badge--sea.sds-badge--tone-50 { background: var(--color-sea-50); color: var(--color-sea-50-on); }
313
+
314
+ /* ── LAKE ── */
315
+ .sds-badge--lake.sds-badge--outline { background: transparent; color: var(--color-lake-50); }
316
+ .sds-badge--lake.sds-badge--tone-10 { background: var(--color-lake-10); color: var(--color-lake-10-on); }
317
+ .sds-badge--lake.sds-badge--tone-20 { background: var(--color-lake-20); color: var(--color-lake-20-on); }
318
+ .sds-badge--lake.sds-badge--tone-30 { background: var(--color-lake-30); color: var(--color-lake-30-on); }
319
+ .sds-badge--lake.sds-badge--tone-40 { background: var(--color-lake-40); color: var(--color-lake-40-on); }
320
+ .sds-badge--lake.sds-badge--tone-50 { background: var(--color-lake-50); color: var(--color-lake-50-on); }
321
+
322
+ /* ── Border radius variants ────────────── */
323
+ [data-radius="rounded"] .sds-badge { border-radius: var(--radius-full); }
324
+
325
+ /* ── Dark-mode text overrides — see tokens-reference.md § Mirror-Pin Technique
326
+ and component-rules.md § Badge text color rules for the full table ── */
327
+
328
+ /* ══════════════════════════════════════════════
329
+ BORDER — appearance="outline" always shows a border
330
+ (text color, via currentColor). tone-10..tone-50
331
+ keep the base's transparent border (reserves
332
+ space, avoids layout shift when switching appearance).
333
+ ══════════════════════════════════════════════ */
334
+
335
+ .sds-badge--outline { box-shadow: inset 0 0 0 1px currentColor; }
336
+
337
+ /* The badge decides its own label weight, and needs to win over the Text variant's.
338
+ sds-text--labelRegular carries --font-weight-regular; a badge label is medium. Two
339
+ classes beat one, which is the whole trick. */
340
+ .sds-badge .sds-badge__label {
341
+ font-weight: var(--font-weight-medium);
342
+ }
343
+
344
+ /* ── The dismiss control ───────────────────────────────────────────────────────
345
+ It is a real <button> rather than a span with role="button", which buys keyboard
346
+ handling, focus and disabled from the browser. It is NOT an <sds-icon-button>: that
347
+ component is a fixed 32x32 at its smallest, and the badge's target is a 16px glyph
348
+ with 2px of padding inside a pill about 24px tall. Importing it would blow the badge
349
+ apart — which is why KDLS hand-rolled this too.
350
+
351
+ These reset the native chrome so the button carries no appearance of its own. */
352
+ .sds-badge__icon--right--clickable {
353
+ appearance: none;
354
+ background: none;
355
+ border: 0;
356
+ font: inherit;
357
+ color: inherit;
358
+ }
359
+
360
+
361
+ /* The dismiss control takes focus, so it has to show it.
362
+ KDLS styles no focus state for it at all: a keyboard user tabbing through a row of
363
+ dismissible badges has no idea which one they are on, which is the difference between
364
+ removing the right filter and removing a random one.
365
+
366
+ The ring is inset rather than outside, because the button sits inside the badge's own
367
+ rounded box and an outline would be clipped by it. */
368
+ .sds-badge__icon:focus-visible {
369
+ outline: none;
370
+ box-shadow: var(--focus-ring-inset);
371
+ border-radius: var(--radius-xs);
372
+ }
373
+
374
+ /* Same reasoning as Text: a clipped background paints nothing once forced colours have
375
+ discarded backgrounds, and the label would vanish instead of changing colour. */
376
+ @media (forced-colors: active) {
377
+ .sds-badge--scheme-color.sds-badge--outline .sds-badge__label {
378
+ background: none;
379
+ color: CanvasText;
380
+ }
381
+ }
@@ -0,0 +1,143 @@
1
+ <!-- sds-badge-dismissible — the interactive half of the Badge.
2
+
3
+ <sds-badge> is markup and costs nothing. This one hydrates: it listens for a click
4
+ and a keypress on the right icon, and it tracks its own loading state. Marko 5
5
+ serialises the entire input of a stateful component into the HTML, once per
6
+ instance, with no way to opt out — so this tag is the deliberate, visible choice to
7
+ pay that, and the name is what makes the choice visible at the call site.
8
+
9
+ Use it for a filter chip a person removes. Do not use it inside a repeated card:
10
+ twenty instances write twenty copies of their input into the page.
11
+
12
+ The dismiss control is a real <button>, not a span with role="button": keyboard handling,
13
+ focus and disabled come from the browser, so there is no key handler here to get wrong.
14
+ It is deliberately NOT an <sds-icon-button> — that component is a fixed 32x32 at its
15
+ smallest, and this target is a 16px glyph inside a pill about 24px tall.
16
+
17
+ Because it is already paying for hydration, the full KDLS behaviour comes back —
18
+ including the automatic spinner when onClickIconRight returns a Promise, which
19
+ <sds-badge> cannot offer and which the controlled `iconRightLoading` attribute only
20
+ approximates.
21
+
22
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Badge/Badge.tsx. -->
23
+
24
+ import { assertVariant, assertAppearance, assertSize, assertWidth } from "./badge-variants.js";
25
+
26
+ class {
27
+ onCreate() {
28
+ this.state = { loading: false };
29
+ }
30
+
31
+ async handleClick(event) {
32
+ event.stopPropagation();
33
+ const { onClickIconRight, iconRightDisabled, iconRightLoading } = this.input;
34
+ // The guard has to read the same thing the render does. It read only the internal
35
+ // flag, so a caller driving `iconRightLoading` from outside showed a spinner and still
36
+ // took the click — the one moment the control exists to refuse. KDLS mirrors these two
37
+ // the same way.
38
+ if (iconRightDisabled || this.state.loading || iconRightLoading) return;
39
+
40
+ const result = onClickIconRight?.(event);
41
+ if (result instanceof Promise) {
42
+ this.state.loading = true;
43
+ try {
44
+ await result;
45
+ } finally {
46
+ this.state.loading = false;
47
+ }
48
+ }
49
+ }
50
+
51
+ }
52
+
53
+ $ {
54
+ const {
55
+ label,
56
+ variant = "neutral",
57
+ appearance = "tone-30",
58
+ size = "sm",
59
+ width = "hug",
60
+ iconLeft,
61
+ iconRight,
62
+ iconRightLoading = false,
63
+ iconRightDisabled = false,
64
+ iconRightLabel = "Remover",
65
+ class: extraClass = "",
66
+ } = input;
67
+
68
+ assertVariant(variant);
69
+ assertAppearance(appearance);
70
+ assertSize(size);
71
+ assertWidth(width);
72
+
73
+
74
+ // An icon may arrive two ways: as an attribute tag (<@icon-left>…</@icon-left>, which
75
+ // is the Marko idiom and what a page writes) or as a template value (which is what a
76
+ // test or a loop passes). Normalising here means callers never have to care.
77
+ const renderable = (value) => (value && value.renderBody) || value;
78
+
79
+ const showSpinner = (state.loading || iconRightLoading) && !iconRightDisabled;
80
+
81
+ // The label's typography is the Text scale: label-sm at sm, body-sm at md, body-md at
82
+ // lg. It renders through <sds-text> so that scale lives in one place, and passes
83
+ // color="inherit" because the badge decides the colour — that is where the contrast
84
+ // tokens are applied.
85
+ const labelVariant = { sm: "labelRegular", md: "bodySm", lg: "bodyMd" }[size];
86
+
87
+ const badgeClass = [
88
+ "sds-badge",
89
+ `sds-badge--${variant}`,
90
+ `sds-badge--${appearance}`,
91
+ `sds-badge--${size}`,
92
+ width === "fill" ? "sds-badge--fill" : "",
93
+ "sds-badge--has-icon-right",
94
+ extraClass,
95
+ ].filter(Boolean).join(" ");
96
+
97
+ const iconRightClass = [
98
+ "sds-badge__icon",
99
+ "sds-badge__icon--right",
100
+ "sds-badge__icon--right--clickable",
101
+ iconRightDisabled ? "sds-badge__icon--right--disabled" : "",
102
+ showSpinner ? "sds-badge__icon--right--loading" : "",
103
+ ].filter(Boolean).join(" ");
104
+ }
105
+
106
+ <span class=badgeClass>
107
+ <span class="sds-badge__content sds-badge__content--has-icon-right">
108
+ <if(iconLeft)>
109
+ <span class="sds-badge__icon sds-badge__icon--left" aria-hidden="true">
110
+ <sds-icon size="sm">
111
+ <${renderable(iconLeft)}/>
112
+ </sds-icon>
113
+ </span>
114
+ </if>
115
+ <sds-text as="span" variant=labelVariant color="inherit" class="sds-badge__label">
116
+ ${label}
117
+ </sds-text>
118
+ </span>
119
+
120
+ <span class="sds-badge__icon-right-wrap">
121
+ <button
122
+ type="button"
123
+ class=iconRightClass
124
+ disabled=(iconRightDisabled || null)
125
+ aria-label=iconRightLabel
126
+ aria-busy=(showSpinner ? "true" : null)
127
+ aria-disabled=(iconRightDisabled ? "true" : null)
128
+ on-click("handleClick")
129
+ >
130
+ <if(showSpinner)>
131
+ // No label. It was passed `iconRightLabel`, so the live region announced "Remover"
132
+ // — the button's own name repeated, never saying that anything was happening. The
133
+ // spinner's own default says "Carregando", which is the news.
134
+ <sds-spinner size="sm"/>
135
+ </if>
136
+ <else>
137
+ <sds-icon size="sm">
138
+ <${renderable(iconRight)}/>
139
+ </sds-icon>
140
+ </else>
141
+ </button>
142
+ </span>
143
+ </span>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The Badge's vocabulary.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/Badge/Badge.tsx.
5
+ * Divergence: `primary`, `secondary`, `tertiary` and `lya` are removed — the first
6
+ * three read Kenlo brand families SDS does not carry, and `lya` was the gradient
7
+ * special case, which now reaches every component through applyCustomGradient.
8
+ *
9
+ * `scheme-color` is the one to reach for when a badge should follow the site's own
10
+ * colour: it reads the primary slot, so it picks up applyCustomColor and
11
+ * applyCustomGradient alike.
12
+ */
13
+ export const BADGE_VARIANTS = [
14
+ 'neutral', 'scheme-color',
15
+ 'success', 'alert', 'error',
16
+ 'leaf', 'sand', 'wood', 'clay', 'flower', 'dusk', 'sky', 'sea', 'lake',
17
+ ];
18
+
19
+ export const BADGE_APPEARANCES = ['outline', 'tone-10', 'tone-20', 'tone-30', 'tone-40', 'tone-50'];
20
+
21
+ export const BADGE_SIZES = ['sm', 'md', 'lg'];
22
+
23
+ export const BADGE_WIDTHS = ['hug', 'fill'];
24
+
25
+ function assertOneOf(label, value, allowed) {
26
+ if (!allowed.includes(value)) {
27
+ throw new Error(`Unknown badge ${label}: "${value}". Allowed: ${allowed.join(', ')}`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ export const assertVariant = (v) => assertOneOf('variant', v, BADGE_VARIANTS);
33
+ export const assertAppearance = (v) => assertOneOf('appearance', v, BADGE_APPEARANCES);
34
+ export const assertSize = (v) => assertOneOf('size', v, BADGE_SIZES);
35
+ export const assertWidth = (v) => assertOneOf('width', v, BADGE_WIDTHS);