@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,449 @@
1
+ <!-- sds-select — ported from kdls-design-system v2.5.1 (0f3edf8) components/Select/Select.tsx.
2
+
3
+ The component Rafael's build order was aiming at: it mirrors Input's field, the Menu's
4
+ trigger and the Menu's floating list, which is why those three came first.
5
+
6
+ Divergences, each with its reason:
7
+
8
+ No Floating. KDLS positions the panel by measuring the trigger in JavaScript. On a
9
+ server-rendered page that means the panel only learns where to sit after hydration, and a
10
+ panel that learns late is a panel that jumps. The list sits below the field, which CSS can
11
+ do — the same choice already made for Menu, and the same one that grows into Popover's
12
+ collision detection later.
13
+
14
+ No BottomSheet. On a narrow screen KDLS swaps the dropdown for a sheet sliding up from the
15
+ bottom. That is a real improvement on a phone and it needs BottomSheet, which is not
16
+ ported. Until it is, a phone gets the same dropdown a desktop gets — worse, but honest,
17
+ and recorded rather than quietly dropped.
18
+
19
+ Options arrive four ways and leave as one: plain strings, label/value objects, objects
20
+ under caller-chosen key names, or attribute tags. A select built from an API response and
21
+ one written out by hand are both real, and KDLS supported both.
22
+
23
+ Search ignores case AND accents, so "sao" finds "São Paulo". KDLS compared lowercased
24
+ strings only, which fails on exactly the words a Brazilian visitor types without accents. -->
25
+
26
+ import { normaliseOptions, filterOptions, toArray, toggleValue, isSelected,
27
+ displayLabel, pillLabel, hiddenChoiceCount, widestLabel, selectableValues, selectAllState,
28
+ assertTrigger, assertWidth }
29
+ from "./select-options.js";
30
+ import { assertFeedback as assertInputFeedback } from "../Input/input-types.js";
31
+ import chevronIcon from "../../icons/generated/fluent-chevron-down-regular.marko";
32
+ import clearIcon from "../../icons/generated/fluent-dismiss-regular.marko";
33
+
34
+ class {
35
+ onCreate(input) {
36
+ this.state = {
37
+ open: false,
38
+ query: "",
39
+ value: input.value ?? input.defaultValue ?? (input.multiple ? [] : undefined),
40
+ };
41
+ }
42
+
43
+ /** sds-floating owns opening, closing and closing-on-outside-click; this follows it. */
44
+ setOpen(next) {
45
+ if (this.input.disabled) return;
46
+ this.state.open = next;
47
+ if (!next) this.state.query = "";
48
+ if (next) this.wantsFocus = true;
49
+ }
50
+
51
+ /**
52
+ * An opened list puts the caret where the next keystroke belongs: the search box when
53
+ * there is one, the list itself otherwise, so the arrow keys work without a click.
54
+ *
55
+ * On update rather than on open, because the panel does not exist yet at the moment the
56
+ * state changes.
57
+ */
58
+ onUpdate() {
59
+ if (!this.wantsFocus || !this.state.open) return;
60
+ this.wantsFocus = false;
61
+
62
+ const panel = this.getEl("panel");
63
+ if (!panel) return;
64
+
65
+ (panel.querySelector("input") ?? panel.querySelector("[role='listbox']"))?.focus();
66
+ }
67
+
68
+ get currentValue() {
69
+ return this.input.value !== undefined ? this.input.value : this.state.value;
70
+ }
71
+
72
+ get options() {
73
+ return normaliseOptions(this.input);
74
+ }
75
+
76
+ /** The rows the reader can actually see — the same filter the render applies. */
77
+ get visibleOptions() {
78
+ const all = this.options;
79
+ return this.input.filterOption === false ? all : filterOptions(all, this.state.query);
80
+ }
81
+
82
+ togglePanel() {
83
+ this.setOpen(!this.state.open);
84
+ }
85
+
86
+ closePanel() {
87
+ this.state.open = false;
88
+ this.state.query = "";
89
+ }
90
+
91
+ commit(next) {
92
+ if (this.input.value === undefined) this.state.value = next;
93
+ this.input.onChange?.(next);
94
+ }
95
+
96
+ choose(option, event) {
97
+ if (option.disabled) {
98
+ event?.preventDefault();
99
+ return;
100
+ }
101
+
102
+ if (this.input.multiple) {
103
+ this.commit(toggleValue(this.currentValue, option.value));
104
+ // A multiple select stays open: choosing one thing usually means choosing another.
105
+ return;
106
+ }
107
+
108
+ this.commit(option.value);
109
+ this.closePanel();
110
+ }
111
+
112
+ /**
113
+ * "Selecionar todos" — todos of what is on screen.
114
+ *
115
+ * It read every option and replaced the whole selection with them, so with a search
116
+ * active it selected rows the reader could not see and threw away the ones they had
117
+ * already chosen outside the filter. Ticking a box should never remove a choice.
118
+ *
119
+ * So: the visible rows are what it acts on, and it merges rather than replaces. Ticking
120
+ * adds them to what is already chosen; unticking removes only them.
121
+ */
122
+ toggleAll() {
123
+ const visible = selectableValues(this.visibleOptions);
124
+ const current = Array.isArray(this.currentValue) ? this.currentValue : [];
125
+ const { checked } = selectAllState(this.visibleOptions, this.currentValue);
126
+ this.commit(checked
127
+ ? current.filter((value) => !visible.includes(value))
128
+ : [...current, ...visible.filter((value) => !current.includes(value))]);
129
+ }
130
+
131
+ /**
132
+ * Empty the selection.
133
+ *
134
+ * Three things were wrong here. It took an `event` and called `stopPropagation` on it,
135
+ * while one of its two call sites passes none — clicking the pill's ✕ threw a TypeError
136
+ * instead of clearing. It reported `undefined` for a single select, which is "no opinion"
137
+ * rather than "cleared", and a controlled caller could not tell the two apart; KDLS
138
+ * reports `null`. And it left the panel open, so clearing from the trigger dropped you
139
+ * into a list you had not asked to see.
140
+ */
141
+ clear(event) {
142
+ event?.stopPropagation();
143
+ this.commit(this.input.multiple ? [] : null);
144
+ this.closePanel();
145
+ }
146
+
147
+ handleSearch(event) {
148
+ this.state.query = event.target.value;
149
+ this.input.onSearch?.(event.target.value);
150
+ }
151
+
152
+ handleKey(event) {
153
+ if (event.key === "Escape") {
154
+ event.preventDefault();
155
+ this.closePanel();
156
+ this.getEl("trigger")?.focus();
157
+ return;
158
+ }
159
+ if (!this.state.open && (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ")) {
160
+ event.preventDefault();
161
+ this.state.open = true;
162
+ }
163
+ }
164
+ }
165
+
166
+ $ {
167
+ const {
168
+ label,
169
+ placeholder = "Selecione",
170
+ trigger = "pill",
171
+ width = "hug",
172
+ multiple = false,
173
+ searchable = false,
174
+ searchPlaceholder = "Digite para buscar...",
175
+ clearable = true,
176
+ showSelectAll = false,
177
+ selectAllLabel = "Selecionar todos",
178
+ loading = false,
179
+ disabled = false,
180
+ required = false,
181
+ feedback = "none",
182
+ hint,
183
+ emptyMessage = "Nenhuma opção",
184
+ name,
185
+ // `component.id`, not a random string. Marko gives every stateful component an id that
186
+ // is the same on the server and in the browser; `Math.random()` inside the render block
187
+ // produced a different one on each side, so `aria-controls`, `aria-labelledby` and
188
+ // `aria-describedby` all pointed at elements that did not exist after hydration — and
189
+ // it changed again on every re-render.
190
+ id = `sds-select-${component.id}`,
191
+ class: extraClass = "",
192
+ } = input;
193
+
194
+ if (!label) throw new Error("sds-select needs a `label`.");
195
+ assertTrigger(trigger);
196
+ assertWidth(width);
197
+ // The feedback reaches the Input underneath, so it is the Input's vocabulary.
198
+ assertInputFeedback(feedback);
199
+
200
+ const open = state.open;
201
+ const value = component.currentValue;
202
+ const allOptions = component.options;
203
+ // `filterOption: false` hands filtering to the caller, for a search that hits a server.
204
+ const visible = input.filterOption === false ? allOptions : filterOptions(allOptions, state.query);
205
+
206
+ const selectedCount = toArray(value).length;
207
+ const text = displayLabel(allOptions, value, { placeholder });
208
+ // Reads the visible rows, because that is what ticking it will act on. Reading the whole
209
+ // list made the box say "all chosen" while the filtered rows in front of you were not.
210
+ const allState = selectAllState(visible, value);
211
+
212
+ const chipLabel = pillLabel(allOptions, value, { label });
213
+ // Reserved so the trigger never changes width when the choice changes. A control that
214
+ // resizes as you use it makes everything beside it in the toolbar jump.
215
+ const widest = widestLabel(allOptions, trigger === "pill" ? label : placeholder);
216
+ const hiddenChoices = multiple ? hiddenChoiceCount(value) : 0;
217
+ const hasValue = selectedCount > 0;
218
+
219
+ const listboxId = `${id}-listbox`;
220
+ const hintId = hint ? `${id}-hint` : null;
221
+
222
+ const wrapperClass = [
223
+ "sds-select-wrapper",
224
+ `sds-select-wrapper--${width}`,
225
+ open ? "sds-select-wrapper--open" : "",
226
+ disabled ? "sds-select-wrapper--disabled" : "",
227
+ loading ? "sds-select-wrapper--loading" : "",
228
+ trigger === "input" ? "sds-select-wrapper--trigger-input" : "",
229
+ extraClass,
230
+ ].filter(Boolean).join(" ");
231
+ }
232
+
233
+ <div key="root" class=wrapperClass>
234
+ <!-- The panel floats; it does not sit in the flow. A dropdown that takes space pushes
235
+ everything below it down when it opens, which is the one thing a dropdown must never
236
+ do — KDLS wraps this in Floating for exactly that reason, and the rule for
237
+ .sds-select-wrapper--fill .sds-floating was carried over here before the component
238
+ existed to match it.
239
+
240
+ Controlled: sds-floating decides when to open and reports it, the Select keeps the
241
+ state because the rest of the component — the query, the keyboard, the value — is
242
+ already hung off it. -->
243
+ <sds-floating
244
+ placement="bottomLeft"
245
+ trigger="click"
246
+ gap=4
247
+ matchTriggerWidth=(trigger === "input")
248
+ open=open
249
+ onOpenChange=(next) => component.setOpen(next)
250
+ >
251
+ <if(trigger === "input")>
252
+ <div class="sds-select-input-wrap">
253
+ <sds-input
254
+ role="combobox"
255
+ aria-haspopup="listbox"
256
+ aria-expanded=(open ? "true" : "false")
257
+ aria-controls=(open ? listboxId : null)
258
+ label=label
259
+ value=text
260
+ placeholder=placeholder
261
+ disabled=disabled
262
+ required=required
263
+ feedback=feedback
264
+ hint=hint
265
+ isLoading=loading
266
+ width=width
267
+ readOnly
268
+ name=name
269
+ id=id
270
+ >
271
+ <@icon-right><fluent-chevron-down-regular/></@icon-right>
272
+ </sds-input>
273
+ </div>
274
+ </if>
275
+ <else-if(trigger === "pill")>
276
+ <!-- A chip, not a field: the filter row of a listing rather than a form. The pill is a
277
+ real <button>, and telling it that it opens a listbox is what stops it announcing
278
+ itself as a pressed switch — KDLS renders this trigger with no aria-haspopup and
279
+ no aria-expanded at all, so a screen reader is told it is a toggle. -->
280
+ <!-- No `active`. A pill has two jobs and they colour differently: as a filter you
281
+ toggle, being on is a state of the data and `--on` fills it with the theme's
282
+ colour; as the trigger of a list, being open or holding a value is a state of
283
+ the control, and filling it would say the filter is applied when it is only the
284
+ thing you press. KDLS passes `active` to a Pill in Pagination — where the pill
285
+ IS the current page — and nowhere else. -->
286
+ <sds-pill
287
+ key="trigger"
288
+ label=chipLabel
289
+ width=width
290
+ iconLeft=input.iconLeft
291
+ iconRight=(hasValue && clearable ? clearIcon : chevronIcon)
292
+ onClickIconRight=(hasValue && clearable ? () => component.clear() : undefined)
293
+ iconRightLabel=(hasValue && clearable ? "Limpar seleção" : undefined)
294
+ showNotification=(hiddenChoices > 0)
295
+ notificationCount=(hiddenChoices > 0 ? hiddenChoices : undefined)
296
+ isLoading=loading
297
+ disabled=disabled
298
+ aria-haspopup="listbox"
299
+ aria-expanded=(open ? "true" : "false")
300
+ aria-controls=(open ? listboxId : null)
301
+ aria-describedby=hintId
302
+ />
303
+ </else-if>
304
+ <else>
305
+ <button
306
+ key="trigger"
307
+ type="button"
308
+ class=`sds-select__trigger--nav ${open ? "sds-select-wrapper--open" : ""}`.trim()
309
+ disabled=(disabled || null)
310
+ aria-expanded=(open ? "true" : "false")
311
+ aria-haspopup="listbox"
312
+ aria-controls=(open ? listboxId : null)
313
+ aria-describedby=hintId
314
+ aria-labelledby=`${id}-name ${id}-value`
315
+ on-keydown("handleKey")
316
+ >
317
+ <!-- The visible content of this trigger is the *value*, not the name: a screen
318
+ reader met "20, collapsed button" and had no way to know what 20 was. An
319
+ aria-label would fix the name and lose the value, so both are pointed at. -->
320
+ <span id=`${id}-name` class="sds-visually-hidden">${label}</span>
321
+ <span id=`${id}-value` class="sds-select__trigger-nav-label" data-widest=widest>${text}</span>
322
+ <span class="sds-select__trigger-nav-chevron" aria-hidden="true">
323
+ <sds-icon size="sm"><fluent-chevron-down-regular/></sds-icon>
324
+ </span>
325
+ </button>
326
+ </else>
327
+
328
+ <!-- Not for the pill: that trigger carries its own clear affordance in its right icon,
329
+ and two controls with the same name is a screen reader reading "Limpar seleção,
330
+ button" twice for one action. -->
331
+ <if(trigger !== "pill" && clearable && selectedCount > 0 && !disabled)>
332
+ <button type="button" class="sds-select__clear" aria-label="Limpar seleção" on-click("clear")>
333
+ <sds-icon size="sm"><fluent-dismiss-regular/></sds-icon>
334
+ </button>
335
+ </if>
336
+
337
+ <@content>
338
+ <div key="panel" class="sds-select__menu" on-keydown("handleKey")>
339
+ <if(searchable)>
340
+ <div class="sds-select__header">
341
+ <div class="sds-select__search-field">
342
+ <span class="sds-select__search-icon" aria-hidden="true">
343
+ <sds-icon size="sm"><fluent-search-regular/></sds-icon>
344
+ </span>
345
+ <input
346
+ class="sds-select__search-input"
347
+ type="search"
348
+ value=state.query
349
+ placeholder=searchPlaceholder
350
+ aria-label=searchPlaceholder
351
+ aria-controls=listboxId
352
+ on-input("handleSearch")
353
+ />
354
+ </div>
355
+ </div>
356
+ </if>
357
+
358
+ <if(multiple && showSelectAll && visible.length)>
359
+ // The listener is on the wrapper, not on the checkbox. `sds-checkbox` is markup —
360
+ // it does not hydrate and emits nothing, so `on-click` on the tag itself was
361
+ // waiting for a component event that is never sent: the box ticked natively and
362
+ // nothing happened. The native `change` bubbles, so the wrapper hears it.
363
+ <div class="sds-select__header" on-change("toggleAll")>
364
+ <sds-checkbox
365
+ label=selectAllLabel
366
+ size="sm"
367
+ checked=allState.checked
368
+ indeterminate=allState.indeterminate
369
+ />
370
+ </div>
371
+ </if>
372
+
373
+ <ul
374
+ class="sds-select__list sds-scroll sds-scroll--thin"
375
+ id=listboxId
376
+ role="listbox"
377
+ aria-label=label
378
+ aria-multiselectable=(multiple ? "true" : null)
379
+ // Focusable, not tabbable: an opened list with no search box takes the caret so
380
+ // the arrow keys work without a click, and the tab order is unchanged.
381
+ tabindex="-1"
382
+ >
383
+ <if(loading)>
384
+ <li class="sds-select__loading"><sds-spinner size="sm" label="Carregando opções"/></li>
385
+ </if>
386
+ <else-if(!visible.length)>
387
+ <li class="sds-select__empty">${emptyMessage}</li>
388
+ </else-if>
389
+
390
+ <for|option| of=(loading ? [] : visible)>
391
+ <if(option.section)>
392
+ <li class="sds-select__section-header" role="presentation">${option.label}</li>
393
+ </if>
394
+ <else>
395
+ <li role="none">
396
+ <button
397
+ type="button"
398
+ class=`sds-select__option ${isSelected(value, option.value) ? "sds-select__option--selected" : ""}`.trim()
399
+ role="option"
400
+ aria-selected=(isSelected(value, option.value) ? "true" : "false")
401
+ disabled=(option.disabled || null)
402
+ on-click("choose", option)
403
+ >
404
+ <if(multiple)>
405
+ <sds-checkbox size="sm" checked=isSelected(value, option.value)/>
406
+ </if>
407
+ <if(option.icon)>
408
+ <span class="sds-select__option-icon" aria-hidden="true">
409
+ <!-- `iconProps` because Marko has no pre-instantiated element. KDLS
410
+ passes `icon={<RadiusCornerIcon r={r} />}` — a React element that
411
+ already carries its props — and the nearest thing here is the
412
+ template plus the props to render it with. Same shape of
413
+ translation as children becoming attribute tags. -->
414
+ <sds-icon size="sm">
415
+ <${(option.icon && option.icon.renderBody) || option.icon} ...(option.iconProps ?? {})/>
416
+ </sds-icon>
417
+ </span>
418
+ </if>
419
+ <span>
420
+ <if(option.renderBody)><${option.renderBody}/></if>
421
+ <else>${option.label}</else>
422
+ </span>
423
+ </button>
424
+ </li>
425
+ </else>
426
+ </for>
427
+ </ul>
428
+ </div>
429
+ </@content>
430
+ </sds-floating>
431
+
432
+ <if(hint && trigger !== "input")>
433
+ <!-- The pill and the nav button point `aria-describedby` at this. They were pointing at
434
+ it before it existed: only the input trigger renders a hint, through sds-input, so
435
+ on the other two the reference dangled and the reader was told nothing. A dangling
436
+ reference is worse than none — it says there is a description and then has none. -->
437
+ <sds-text as="span" id=hintId variant="labelRegular" class="sds-select__hint">
438
+ ${hint}
439
+ </sds-text>
440
+ </if>
441
+
442
+ <if(name)>
443
+ <!-- The value a form submits. The trigger is a button or a read-only field, neither of
444
+ which posts anything on its own. -->
445
+ <for|chosen| of=toArray(value)>
446
+ <input type="hidden" name=(multiple ? `${name}[]` : name) value=chosen/>
447
+ </for>
448
+ </if>
449
+ </div>
@@ -0,0 +1,22 @@
1
+ /* SDS Separator.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Separator/Separator.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ *//* ============================================================
5
+ SDS Separator
6
+ Orientations: horizontal | vertical
7
+ ============================================================ */
8
+
9
+ .sds-separator {
10
+ border: none;
11
+ margin: 0;
12
+ background-color: var(--color-neutral-20);
13
+ flex-shrink: 0;
14
+ }
15
+ .sds-separator--horizontal {
16
+ width: 100%;
17
+ height: 1px;
18
+ }
19
+ .sds-separator--vertical {
20
+ width: 1px;
21
+ height: 100%;
22
+ }
@@ -0,0 +1,14 @@
1
+ <!-- sds-separator — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/Separator/Separator.tsx.
3
+
4
+ An <hr>, which already means "thematic break" to a screen reader. No state, no behaviour. -->
5
+
6
+ $ {
7
+ const { orientation = "horizontal", class: extraClass = "" } = input;
8
+ if (orientation !== "horizontal" && orientation !== "vertical") {
9
+ throw new Error(`Unknown separator orientation: "${orientation}". Allowed: horizontal, vertical`);
10
+ }
11
+ }
12
+
13
+ <hr class=`sds-separator sds-separator--${orientation} ${extraClass}`.trim()
14
+ aria-orientation=orientation/>
@@ -0,0 +1,100 @@
1
+ /* SDS Skeleton.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Skeleton/Skeleton.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ *//* ── Skeleton ──────────────────────────────────────────────────────────
5
+ Loading placeholder — title/paragraph block, with optional avatar,
6
+ plus the standalone blocks Skeleton.Avatar/Button/Input/Image.
7
+ BEM root: .sds-skeleton
8
+ ─────────────────────────────────────────────────────────────────── */
9
+
10
+ .sds-skeleton {
11
+ display: flex;
12
+ align-items: flex-start;
13
+ gap: var(--spacing-4);
14
+ }
15
+ .sds-skeleton__content {
16
+ flex: 1;
17
+ min-width: 0;
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: var(--spacing-2);
21
+ }
22
+ /* ── Blocks — base color, overridden by --active (see end of file) ── */
23
+ .sds-skeleton__avatar,
24
+ .sds-skeleton__button,
25
+ .sds-skeleton__input,
26
+ .sds-skeleton__image,
27
+ .sds-skeleton__line {
28
+ background: var(--color-neutral-10);
29
+ }
30
+ /* ── Avatar — always round, no shape prop ── */
31
+ .sds-skeleton__avatar {
32
+ border-radius: var(--radius-full);
33
+ flex-shrink: 0;
34
+ }
35
+ .sds-skeleton__avatar--sm { width: var(--spacing-10); height: var(--spacing-10); }
36
+ .sds-skeleton__avatar--md { width: var(--spacing-12); height: var(--spacing-12); }
37
+ .sds-skeleton__avatar--lg { width: var(--spacing-14); height: var(--spacing-14); }
38
+ /* ── Title/paragraph lines ── */
39
+ .sds-skeleton__line {
40
+ width: 100%;
41
+ height: var(--spacing-4);
42
+ border-radius: var(--radius-sm);
43
+ }
44
+ .sds-skeleton__line--title {
45
+ width: 40%;
46
+ height: var(--spacing-5);
47
+ }
48
+ .sds-skeleton__line--last {
49
+ width: 60%;
50
+ }
51
+ /* ── Button — fixed width (there's no label to measure), project radius.
52
+ Heights match the real Button: sm = variant="pill" (40px), md = default (48px),
53
+ lg = variant="lg" (56px). 120px is hardcoded — the spacing scale stops at
54
+ --spacing-16 (64px), no token reaches that far. ── */
55
+ .sds-skeleton__button {
56
+ width: 120px;
57
+ border-radius: var(--radius-md);
58
+ }
59
+ .sds-skeleton__button--sm { height: var(--spacing-10); }
60
+ .sds-skeleton__button--md { height: var(--spacing-12); }
61
+ .sds-skeleton__button--lg { height: var(--spacing-14); }
62
+ /* ── Input — fluid width (form field), project radius.
63
+ lg (56px) is the default — matches the real Input's fixed height, which has no
64
+ size prop. sm/md are available to represent more compact fields. ── */
65
+ .sds-skeleton__input {
66
+ width: 100%;
67
+ border-radius: var(--radius-md);
68
+ }
69
+ .sds-skeleton__input--sm { height: var(--spacing-10); }
70
+ .sds-skeleton__input--md { height: var(--spacing-12); }
71
+ .sds-skeleton__input--lg { height: var(--spacing-14); }
72
+ /* ── Image — fixed block with a centered icon, never round, project radius.
73
+ fill=true swaps the fixed size for 100%/100% — use when the real container
74
+ (e.g. a Card's cover) has a variable size. ── */
75
+ .sds-skeleton__image {
76
+ width: var(--spacing-16);
77
+ height: var(--spacing-16);
78
+ flex-shrink: 0;
79
+ border-radius: var(--radius-md);
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ color: var(--color-neutral-30);
84
+ }
85
+ .sds-skeleton__image--fill {
86
+ width: 100%;
87
+ height: 100%;
88
+ }
89
+ /* ── active — shimmer sweeping across the block. Same specificity as the base color;
90
+ needs to come later in the file to win the cascade (same weight, same
91
+ source order). ── */
92
+ @keyframes sds-skeleton-shimmer {
93
+ 0% { background-position: 100% 50%; }
94
+ 100% { background-position: 0 50%; }
95
+ }
96
+ .sds-skeleton--active {
97
+ background: linear-gradient(90deg, var(--color-neutral-10) 25%, var(--color-neutral-20) 37%, var(--color-neutral-10) 63%);
98
+ background-size: 400% 100%;
99
+ animation: sds-skeleton-shimmer 1.4s ease infinite;
100
+ }
@@ -0,0 +1,28 @@
1
+ <!-- sds-skeleton-avatar — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/Skeleton/Skeleton.tsx, where this was Skeleton.Avatar.
3
+
4
+ A sibling tag rather than a nested one: it shares nothing with sds-skeleton but a
5
+ stylesheet, so nesting would be a namespace, not a relationship.
6
+
7
+ The circle already existed here — `sds-skeleton` draws one when `avatar` is set, and the
8
+ stylesheet has carried `.sds-skeleton__avatar` since the port. What was missing was the
9
+ standalone tag, so a caller who wanted the circle beside something that is not a
10
+ title-and-paragraph block had no way to ask for it. Always round, like the real Avatar,
11
+ and the only one of these with no shape of its own to choose. -->
12
+
13
+ import { assertSkeletonSize } from "./skeleton-variants.js";
14
+
15
+ $ {
16
+ const { size = "md", active = true, class: extraClass = "" } = input;
17
+ assertSkeletonSize(size);
18
+
19
+ const classes = [
20
+ "sds-skeleton",
21
+ active ? "sds-skeleton--active" : "",
22
+ "sds-skeleton__avatar",
23
+ `sds-skeleton__avatar--${size}`,
24
+ extraClass,
25
+ ].filter(Boolean).join(" ");
26
+ }
27
+
28
+ <span class=classes aria-hidden="true"/>
@@ -0,0 +1,22 @@
1
+ <!-- sds-skeleton-button — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/Skeleton/Skeleton.tsx, where this was Skeleton.Button.
3
+
4
+ A sibling tag rather than a nested one: it shares nothing with sds-skeleton but a
5
+ stylesheet, so nesting would be a namespace, not a relationship. -->
6
+
7
+ import { assertSkeletonSize } from "./skeleton-variants.js";
8
+
9
+ $ {
10
+ const { size = "md", active = true, class: extraClass = "" } = input;
11
+ assertSkeletonSize(size);
12
+
13
+ const classes = [
14
+ "sds-skeleton",
15
+ active ? "sds-skeleton--active" : "",
16
+ "sds-skeleton__button",
17
+ `sds-skeleton__button--${size}`,
18
+ extraClass,
19
+ ].filter(Boolean).join(" ");
20
+ }
21
+
22
+ <span class=classes aria-hidden="true"/>
@@ -0,0 +1,20 @@
1
+ <!-- sds-skeleton-image — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/Skeleton/Skeleton.tsx, where this was Skeleton.Image.
3
+
4
+ A sibling tag rather than a nested one: it shares nothing with sds-skeleton but a
5
+ stylesheet, so nesting would be a namespace, not a relationship. -->
6
+
7
+ $ {
8
+ // No `size`: an image skeleton is either its fixed block or the whole of its parent.
9
+ const { fill = false, active = true, class: extraClass = "" } = input;
10
+
11
+ const classes = [
12
+ "sds-skeleton",
13
+ active ? "sds-skeleton--active" : "",
14
+ "sds-skeleton__image",
15
+ fill ? "sds-skeleton__image--fill" : "",
16
+ extraClass,
17
+ ].filter(Boolean).join(" ");
18
+ }
19
+
20
+ <span class=classes aria-hidden="true"/>
@@ -0,0 +1,22 @@
1
+ <!-- sds-skeleton-input — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/Skeleton/Skeleton.tsx, where this was Skeleton.Input.
3
+
4
+ A sibling tag rather than a nested one: it shares nothing with sds-skeleton but a
5
+ stylesheet, so nesting would be a namespace, not a relationship. -->
6
+
7
+ import { assertSkeletonSize } from "./skeleton-variants.js";
8
+
9
+ $ {
10
+ const { size = "md", active = true, class: extraClass = "" } = input;
11
+ assertSkeletonSize(size);
12
+
13
+ const classes = [
14
+ "sds-skeleton",
15
+ active ? "sds-skeleton--active" : "",
16
+ "sds-skeleton__input",
17
+ `sds-skeleton__input--${size}`,
18
+ extraClass,
19
+ ].filter(Boolean).join(" ");
20
+ }
21
+
22
+ <span class=classes aria-hidden="true"/>