@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,340 @@
1
+ <!-- sds-bottom-sheet — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/BottomSheet/BottomSheet.tsx.
3
+
4
+ A panel that rises from the bottom of the screen with a list of choices in it. It is
5
+ the mobile shape of a Select, and on a phone it is the better one: a dropdown anchored
6
+ to a control near the bottom of a 812px screen has nowhere to open, and a list of
7
+ twenty options in a floating panel is a list nobody can scroll without closing it.
8
+
9
+ It shares foundations/overlay.js with Modal and Drawer — the scroll lock, the focus
10
+ trap, Escape — and the option vocabulary with Select, so `options` means the same
11
+ thing in both and a value moves between them without translation.
12
+
13
+ Divergences:
14
+
15
+ KDLS draws each option as an <li> with a mousedown handler. That is not focusable, not
16
+ activatable by keyboard, and invisible to anything that goes looking for controls —
17
+ on a list whose whole purpose is choosing. Here each row is a real <button> inside its
18
+ <li>, with the browser's chrome taken off in CSS.
19
+
20
+ KDLS closes on any mousedown on the overlay, including the mousedown that ends a drag
21
+ started inside the sheet. This closes on a press that both starts and ends outside.
22
+
23
+ No portal, for the reason in components/Floating/floating.marko: Marko 5 has none, and
24
+ a node moved to <body> returns on the next re-render. The sheet is position:fixed and
25
+ covers the viewport, so the only thing lost is escaping a transformed ancestor. -->
26
+
27
+ import { normaliseOptions, filterOptions, toArray, toggleValue, isSelected,
28
+ selectableValues, selectAllState } from "../Select/select-options.js";
29
+ import { lockScroll, unlockScroll, trapFocus } from "../../foundations/overlay.js";
30
+ import dismissIcon from "../../icons/generated/fluent-dismiss-regular.marko";
31
+ import searchIcon from "../../icons/generated/fluent-search-regular.marko";
32
+ import checkIcon from "../../icons/generated/fluent-checkmark-regular.marko";
33
+
34
+ class {
35
+ onCreate(input) {
36
+ this.state = {
37
+ query: "",
38
+ value: input.value ?? input.defaultValue ?? (input.multiple ? [] : null),
39
+ };
40
+ this.release = null;
41
+ }
42
+
43
+ onMount() {
44
+ this.onKeyDown = (event) => {
45
+ if (event.key !== "Escape" || !this.input.isOpen) return;
46
+ event.stopPropagation();
47
+ this.input.onClose?.();
48
+ };
49
+ document.addEventListener("keydown", this.onKeyDown);
50
+ if (this.input.isOpen) this.open();
51
+ }
52
+
53
+ onUpdate() {
54
+ if (this.input.isOpen && !this.locked) this.open();
55
+ else if (!this.input.isOpen && this.locked) this.shut();
56
+ }
57
+
58
+ onDestroy() {
59
+ document.removeEventListener("keydown", this.onKeyDown);
60
+ this.shut();
61
+ }
62
+
63
+ open() {
64
+ this.locked = true;
65
+ lockScroll();
66
+ requestAnimationFrame(() => {
67
+ if (!this.input.isOpen) return;
68
+ this.release = trapFocus(this.getEl("panel"));
69
+ });
70
+ }
71
+
72
+ shut() {
73
+ if (!this.locked) return;
74
+ this.locked = false;
75
+ unlockScroll();
76
+ this.release?.();
77
+ this.release = null;
78
+ this.state.query = "";
79
+ }
80
+
81
+ get currentValue() {
82
+ return this.input.value !== undefined ? this.input.value : this.state.value;
83
+ }
84
+
85
+ get options() {
86
+ return normaliseOptions(this.input);
87
+ }
88
+
89
+ commit(next) {
90
+ if (this.input.value === undefined) this.state.value = next;
91
+ this.input.onChange?.(next);
92
+ }
93
+
94
+ choose(option) {
95
+ if (option.disabled) return;
96
+ if (this.input.multiple) {
97
+ this.commit(toggleValue(toArray(this.currentValue), option.value));
98
+ return;
99
+ }
100
+ this.commit(option.value);
101
+ // A single choice is the whole interaction: staying open asks the visitor to close a
102
+ // sheet whose job is done.
103
+ this.input.onClose?.();
104
+ }
105
+
106
+ /** The rows on screen — what a search has left, which is what the reader can act on. */
107
+ get visibleOptions() {
108
+ const all = this.options;
109
+ return this.input.filterOption === false ? all : filterOptions(all, this.state.query);
110
+ }
111
+
112
+ /**
113
+ * "Selecionar todos" — todos of what is on screen, merged rather than replacing.
114
+ *
115
+ * It read every option and replaced the whole selection, so under a search it selected
116
+ * rows the reader could not see and threw away the ones chosen outside the filter.
117
+ * Ticking a box must never remove a choice. Same fix the Select needed.
118
+ */
119
+ toggleAll() {
120
+ const visible = selectableValues(this.visibleOptions);
121
+ const current = toArray(this.currentValue);
122
+ const { checked } = selectAllState(this.visibleOptions, this.currentValue);
123
+ this.commit(checked
124
+ ? current.filter((v) => !visible.includes(v))
125
+ : [...current, ...visible.filter((v) => !current.includes(v))]);
126
+ }
127
+
128
+ clear() {
129
+ this.commit(this.input.multiple ? [] : null);
130
+ // And close. KDLS's own prop doc says so — "resets the value to empty and closes the
131
+ // sheet" — and leaving it open puts the reader back in a list they have just emptied,
132
+ // with no indication that anything happened.
133
+ this.input.onClose?.();
134
+ }
135
+
136
+ handleSearch(event) {
137
+ this.state.query = event.target.value;
138
+ this.input.onSearch?.(event.target.value);
139
+ }
140
+
141
+ /** Closes only on a press that both starts and ends outside the sheet. */
142
+ handleOverlayDown(event) {
143
+ if (this.getEl("panel")?.contains(event.target)) return;
144
+ this.input.onClose?.();
145
+ }
146
+ }
147
+
148
+ $ {
149
+ const {
150
+ isOpen,
151
+ title,
152
+ searchable = false,
153
+ searchPlaceholder = "Digite para buscar...",
154
+ multiple = false,
155
+ showSelectAll = false,
156
+ selectAllLabel = "Selecionar todos",
157
+ clearable = false,
158
+ emptyMessage = "Nenhuma opção",
159
+ closeLabel = "Fechar",
160
+ applyLabel = "Aplicar",
161
+ clearLabel = "Limpar",
162
+ class: extraClass = "",
163
+ } = input;
164
+
165
+ const value = component.currentValue;
166
+ const all = component.options;
167
+ const visible = input.filterOption === false ? all : filterOptions(all, state.query);
168
+ // Reads the visible rows, because that is what ticking it acts on. Reading the whole
169
+ // list made the box claim "all chosen" while the filtered rows in front of you were not.
170
+ const allState = selectAllState(visible, value);
171
+ const hasValue = toArray(value).length > 0;
172
+
173
+ // A sheet with a list needs a name for that list; a sheet holding arbitrary body content
174
+ // is named by its title alone.
175
+ // `component.id`, not a random string: Marko's is the same on the server and in the
176
+ // browser, and a random one drawn in the render block is neither.
177
+ const listId = `sds-bottom-sheet-${component.id}`;
178
+ const showList = all.length > 0 || !input.renderBody;
179
+ // `(clearable || true)` was always true, so this read as `multiple` alone: a
180
+ // single-select sheet asked to be `clearable` got no footer at all and the "Limpar" it
181
+ // was promised never appeared. A constant comparison hiding inside a condition, the same
182
+ // shape the Skeleton shapes had.
183
+ const showFooter = Boolean(input.footer) || multiple || clearable;
184
+ }
185
+
186
+ <if(isOpen)>
187
+ <div class="sds-bottomsheet__overlay" on-mousedown("handleOverlayDown")>
188
+ <div
189
+ key="panel"
190
+ class=`sds-bottomsheet ${extraClass}`.trim()
191
+ role="dialog"
192
+ aria-modal="true"
193
+ aria-label=title
194
+ >
195
+ <div class="sds-bottomsheet__title-row">
196
+ <if(title)>
197
+ <sds-text as="span" variant="labelSemiBold" class="sds-bottomsheet__title">${title}</sds-text>
198
+ </if>
199
+ <sds-icon-button
200
+ class="sds-bottomsheet__close"
201
+ variant="neutral"
202
+ aria-label=closeLabel
203
+ onClick=() => input.onClose?.()
204
+ >
205
+ <@icon><${dismissIcon}/></@icon>
206
+ </sds-icon-button>
207
+ </div>
208
+
209
+ <if(searchable)>
210
+ <div class="sds-bottomsheet__search">
211
+ <div class="sds-bottomsheet__searchable-field">
212
+ <span class="sds-bottomsheet__searchable-icon" aria-hidden="true">
213
+ <sds-icon size="sm"><${searchIcon}/></sds-icon>
214
+ </span>
215
+ <input
216
+ class="sds-bottomsheet__searchable-input"
217
+ type="search"
218
+ value=state.query
219
+ placeholder=searchPlaceholder
220
+ aria-label=searchPlaceholder
221
+ on-input("handleSearch")
222
+ />
223
+ </div>
224
+ </div>
225
+ </if>
226
+
227
+ <if(multiple && showSelectAll)>
228
+ // The listener is on the wrapper. `sds-checkbox` is markup: it does not hydrate,
229
+ // it takes no `onChange`, and says so in its own header — so the handler here was
230
+ // waiting for a callback that is never called. The box ticked natively and nothing
231
+ // happened, exactly as in the Select. The native `change` bubbles; the wrapper
232
+ // hears it.
233
+ //
234
+ // It also wore `__header`, the sheet's own title bar class, which is 16px padding
235
+ // all round and a row layout meant for a title and a close button. Its own class
236
+ // now, so changing one does not move the other.
237
+ <div class="sds-bottomsheet__select-all" on-change("toggleAll")>
238
+ <sds-checkbox
239
+ label=selectAllLabel
240
+ checked=allState.checked
241
+ indeterminate=allState.indeterminate
242
+ />
243
+ </div>
244
+ </if>
245
+
246
+ <div class="sds-bottomsheet__body sds-scroll sds-scroll--thin">
247
+ <if(showList)>
248
+ <ul class="sds-bottomsheet__list" id=listId role="listbox" aria-label=title
249
+ aria-multiselectable=(multiple ? "true" : null)>
250
+ <if(!visible.length)>
251
+ <li class="sds-bottomsheet__empty">${emptyMessage}</li>
252
+ </if>
253
+ <for|option| of=visible>
254
+ <if(option.section)>
255
+ // A section heading, not a header. It wore `__header`, whose rule is a
256
+ // 16px-padded flex COLUMN with a bottom border — a container for the search
257
+ // box and the select-all row, not a label. So "Capitais" rendered as a
258
+ // block the height of a panel section with a rule under it.
259
+ <li class="sds-bottomsheet__section" role="presentation">${option.label}</li>
260
+ </if>
261
+ <else>
262
+ <li
263
+ class=[
264
+ "sds-bottomsheet__item",
265
+ isSelected(value, option.value) ? "sds-bottomsheet__item--active" : "",
266
+ option.disabled ? "sds-bottomsheet__item--disabled" : "",
267
+ ].filter(Boolean).join(" ")
268
+ role="none"
269
+ >
270
+ <button
271
+ type="button"
272
+ class="sds-bottomsheet__item-button"
273
+ role="option"
274
+ aria-selected=(isSelected(value, option.value) ? "true" : "false")
275
+ disabled=(option.disabled || null)
276
+ on-click("choose", option)
277
+ >
278
+ <if(multiple)>
279
+ <span class="sds-bottomsheet__item-checkbox" aria-hidden="true">
280
+ <sds-checkbox checked=isSelected(value, option.value) disabled=option.disabled/>
281
+ </span>
282
+ </if>
283
+ <if(option.icon)>
284
+ <span class="sds-bottomsheet__item-icon-left" aria-hidden="true">
285
+ <sds-icon><${(option.icon && option.icon.renderBody) || option.icon}/></sds-icon>
286
+ </span>
287
+ </if>
288
+ <span class="sds-bottomsheet__item-label">${option.label}</span>
289
+ <if(!multiple && isSelected(value, option.value))>
290
+ // The mark on the chosen row. `.sds-bottomsheet__item-check` has been
291
+ // in the stylesheet since the port with nothing rendering it, so a
292
+ // single-select sheet showed which row was chosen only by its
293
+ // background — and `aria-selected` said so to a screen reader while
294
+ // the eye had to infer it.
295
+ <span class="sds-bottomsheet__item-check" aria-hidden="true">
296
+ <sds-icon size="sm"><${checkIcon}/></sds-icon>
297
+ </span>
298
+ </if>
299
+ </button>
300
+ </li>
301
+ </else>
302
+ </for>
303
+ </ul>
304
+ </if>
305
+ <else>
306
+ <${input.renderBody}/>
307
+ </else>
308
+ </div>
309
+
310
+ <if(input.footer)>
311
+ <div class="sds-bottomsheet__footer">
312
+ <${input.footer}/>
313
+ </div>
314
+ </if>
315
+ <else-if(showFooter)>
316
+ // `clearable` OR `multiple`, which is KDLS's gate. It read `multiple` alone here,
317
+ // so a single-select sheet asked to be clearable got no footer and the "Limpar" it
318
+ // was promised never appeared. (The `showFooter` above said the same thing in a
319
+ // way that could not be false — `multiple && (clearable || true)` — and nothing
320
+ // read it; the template decided again, differently, down here.)
321
+ <div class="sds-bottomsheet__footer">
322
+ <div class="sds-bottomsheet__footer-actions">
323
+ <if(clearable)>
324
+ <sds-button variant="ghost" size="md" label=clearLabel
325
+ disabled=(!hasValue) onClick=() => component.clear()/>
326
+ </if>
327
+ // Only a multiple-select sheet needs one: a single choice closes the sheet by
328
+ // being made, so an "Aplicar" there is a button with nothing left to apply.
329
+ // Full width when it stands alone, which is what KDLS does.
330
+ <if(multiple)>
331
+ <sds-button variant="primary" size="md" label=applyLabel
332
+ width=(clearable ? "hug" : "fill")
333
+ onClick=() => input.onClose?.()/>
334
+ </if>
335
+ </div>
336
+ </div>
337
+ </else-if>
338
+ </div>
339
+ </div>
340
+ </if>
@@ -0,0 +1,88 @@
1
+ /* SDS BulkActionBar.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/BulkActionBar/BulkActionBar.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ */
5
+
6
+ /* Two shadows were written as literals and are snapped to the system here, the same two
7
+ the Drawer had: 0.15 is not a step on the opacity scale (05/10/20/30/40/50) so it reads
8
+ --color-opacity-20, and the dark one was black because --color-opacity-* inverts with the
9
+ theme — that value is --color-scrim. *//* ── BulkActionBar ──────────────────────────────────────────────────────
10
+ Always-fixed bar shown when one or more rows are selected — rendered via
11
+ portal into document.body, always pinned to the bottom of the viewport.
12
+ BEM root: .sds-bulk-action-bar
13
+ z-index: 100 — see component-rules.md z-index scale
14
+ ─────────────────────────────────────────────────────────────────── */
15
+
16
+ .sds-bulk-action-bar {
17
+ position: fixed;
18
+ bottom: 0;
19
+ left: 0;
20
+ right: 0;
21
+ height: 80px;
22
+ display: flex;
23
+ align-items: center;
24
+ gap: var(--spacing-6);
25
+ padding: 0 var(--spacing-6);
26
+ background: var(--component-bg-color);
27
+ border-top: 1px solid var(--border-static-color);
28
+ box-shadow: 0 -4px 24px var(--color-opacity-20);
29
+ z-index: var(--z-sticky);
30
+ }
31
+ /* ── Identity — countLabel + "Limpar" (default), or ✕ + countLabel (dismiss mode) ──
32
+ Gap is asymmetric: to the right of countLabel it's spacing-6 (default mode, before "Limpar");
33
+ to the left of countLabel (dismiss mode, after the ✕) it stays the tighter spacing-2. ── */
34
+ /* Divergence: the identity is pinned left in both modes.
35
+
36
+ KDLS pushes it to the far right when the bar is in clear mode, so the count — the one
37
+ thing on this bar that changes, and the thing it announces — moved depending on which
38
+ of two props the caller passed. The bar is fixed over everything while it is up; what
39
+ it says about itself belongs at the start of the line, and the actions at the end. */
40
+ .sds-bulk-action-bar__identity {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: var(--spacing-4);
44
+ flex-shrink: 0;
45
+ }
46
+
47
+ .sds-bulk-action-bar--dismiss .sds-bulk-action-bar__identity {
48
+ gap: var(--spacing-2);
49
+ }
50
+ .sds-bulk-action-bar__label {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: var(--spacing-2);
54
+ font-family: var(--font-family-base);
55
+ }
56
+ /* ── Divider — same height as the primary Button (48px). Absent entirely in dismiss mode (not rendered) ── */
57
+ /* ── Groups + secondary + primary ── */
58
+ /* The actions end the line, whichever way the identity was built. */
59
+ .sds-bulk-action-bar__groups {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: var(--spacing-6);
63
+ margin-left: auto;
64
+ }
65
+ .sds-bulk-action-bar--dismiss .sds-bulk-action-bar__groups {
66
+ margin-left: auto; /* dismiss mode: this cluster is what gets pushed right, identity stays left */
67
+ }
68
+ .sds-bulk-action-bar__group {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: 0;
72
+ }
73
+ .sds-bulk-action-bar__group-label {
74
+ font-family: var(--font-family-base);
75
+ font-size: var(--font-size-label-sm);
76
+ font-weight: var(--font-weight-medium);
77
+ color: var(--label-color);
78
+ }
79
+ .sds-bulk-action-bar__group-actions {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: var(--spacing-4);
83
+ }
84
+ /* ── Dark mode ── */
85
+ [data-theme="dark"] .sds-bulk-action-bar {
86
+ border-top-color: var(--border-interactive-color);
87
+ box-shadow: 0 -4px 24px var(--color-scrim);
88
+ }
@@ -0,0 +1,186 @@
1
+ <!-- sds-bulk-action-bar — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/BulkActionBar/BulkActionBar.tsx.
3
+
4
+ The bar that appears when rows are selected: how many, a way to let go of them, and
5
+ the things that can be done to them. Every action follows a promise the caller
6
+ returns, which is what lets a button spin without being told to.
7
+
8
+ `onClear` and `onDismiss` are two shapes of the same idea and KDLS makes them mutually
9
+ exclusive in the type system, which does not exist here — so the check is at render
10
+ and it throws, naming both. Getting it wrong silently would draw two ways out of the
11
+ same bar, or none.
12
+
13
+ Divergences:
14
+
15
+ No portal, for the reason measured in components/Floating. The bar is position:fixed,
16
+ so it floats over the page from wherever it is written; what a portal would add is
17
+ escaping an ancestor with a transform, and the component warns about that case through
18
+ sds-floating when it is used inside one.
19
+
20
+ It is a <section> with an accessible name and the count lives in an aria-live region.
21
+ KDLS renders a bare div, so a keyboard user who selects a row is given a bar full of
22
+ new actions and told nothing about it — the count changing is the one thing that
23
+ needs announcing here. -->
24
+
25
+ import dismissIcon from "../../icons/generated/fluent-dismiss-regular.marko";
26
+
27
+ class {
28
+ onCreate() {
29
+ this.state = { primary: false, secondary: false, group: {} };
30
+ }
31
+
32
+ async run(mark, handler) {
33
+ const result = handler?.();
34
+ if (!(result instanceof Promise)) return;
35
+ mark(true);
36
+ try {
37
+ await result;
38
+ } finally {
39
+ mark(false);
40
+ }
41
+ }
42
+
43
+ handlePrimary() {
44
+ return this.run((v) => { this.state.primary = v; }, this.input.onPrimary);
45
+ }
46
+
47
+ handleSecondary() {
48
+ return this.run((v) => { this.state.secondary = v; }, this.input.onSecondary);
49
+ }
50
+
51
+ handleGroupAction(key, action) {
52
+ return this.run((v) => {
53
+ this.state.group = { ...this.state.group, [key]: v };
54
+ }, action.onClick);
55
+ }
56
+ }
57
+
58
+ $ {
59
+ const {
60
+ countLabel,
61
+ groups = [],
62
+ primaryLabel, onPrimary, primaryLoading = false, primaryDisabled = false, primaryDisabledTooltip,
63
+ secondaryLabel, onSecondary, secondaryLoading = false, secondaryDisabled = false, secondaryDisabledTooltip,
64
+ actions = [],
65
+ onClear,
66
+ onDismiss,
67
+ clearLabel = "Limpar",
68
+ dismissLabel = "Fechar",
69
+ label = "Ações em massa",
70
+ class: extraClass = "",
71
+ } = input;
72
+
73
+ if (countLabel === undefined) throw new Error("sds-bulk-action-bar needs a `countLabel`.");
74
+ if (Boolean(onClear) === Boolean(onDismiss)) {
75
+ throw new Error("sds-bulk-action-bar needs exactly one of `onClear` or `onDismiss` — one draws a Limpar link before the count, the other a ✕.");
76
+ }
77
+
78
+ // Two shapes, and a bar is one of them.
79
+ //
80
+ // grouped — `groups`, each a labelled set of links. A list of things you can do to
81
+ // the selection, sorted by what they are about.
82
+ // buttons — `primaryLabel`, `secondaryLabel` and as many loose `actions` links as
83
+ // needed. One act stands out, one stands beside it, the rest are links.
84
+ //
85
+ // Mixing them puts a labelled group next to an unlabelled primary and asks the reader
86
+ // to work out whether the group heading covers the button after it. KDLS allows the
87
+ // mix; this refuses it, for the same reason it refuses onClear with onDismiss.
88
+ const grouped = groups.length > 0;
89
+ const buttons = Boolean(primaryLabel) || Boolean(secondaryLabel) || actions.length > 0;
90
+ if (grouped && buttons) {
91
+ throw new Error("sds-bulk-action-bar takes either `groups` or the button set (`primaryLabel`, `secondaryLabel`, `actions`) — not both. A labelled group beside an unlabelled button leaves the reader guessing how far the label reaches.");
92
+ }
93
+
94
+ const isDismiss = Boolean(onDismiss);
95
+ const classes = [
96
+ "sds-bulk-action-bar",
97
+ isDismiss ? "sds-bulk-action-bar--dismiss" : "",
98
+ extraClass,
99
+ ].filter(Boolean).join(" ");
100
+ }
101
+
102
+ <section class=classes aria-label=label>
103
+ <!-- The way out comes first, then what it applies to. KDLS puts the clear link after
104
+ the count and the dismiss button before it, so the same bar read in two different
105
+ orders depending on which prop the caller passed. One order: leave, then the count
106
+ it would leave. -->
107
+ <div class="sds-bulk-action-bar__identity">
108
+ <if(isDismiss)>
109
+ <sds-icon-button variant="neutral" aria-label=dismissLabel onClick=onDismiss>
110
+ <@icon><${dismissIcon}/></@icon>
111
+ </sds-icon-button>
112
+ </if>
113
+ <else>
114
+ <sds-button variant="link" label=clearLabel onClick=onClear class="sds-bulk-action-bar__clear"/>
115
+ </else>
116
+
117
+ <!-- Body weight, not title: the count is a fact about the selection, not a heading
118
+ over the bar. KDLS sets it in titleSm, which makes it shout beside the links. -->
119
+ <sds-text as="span" variant="bodyMd" class="sds-bulk-action-bar__label" aria-live="polite">
120
+ ${countLabel}
121
+ </sds-text>
122
+ </div>
123
+
124
+ <div class="sds-bulk-action-bar__groups">
125
+ <for|group, i| of=groups>
126
+ <div class="sds-bulk-action-bar__group">
127
+ <if(group.groupLabel)>
128
+ <span class="sds-bulk-action-bar__group-label">${group.groupLabel}</span>
129
+ </if>
130
+ <div class="sds-bulk-action-bar__group-actions">
131
+ <for|action, j| of=(group.actions || [])>
132
+ <sds-button
133
+ variant="link"
134
+ label=action.label
135
+ onClick=() => component.handleGroupAction(`${i}-${j}`, action)
136
+ isLoading=(state.group[`${i}-${j}`] || Boolean(action.loading))
137
+ disabled=action.disabled
138
+ disabledTooltip=action.disabledTooltip
139
+ disabledTooltipPosition="top"
140
+ class="sds-bulk-action-bar__group-button"
141
+ />
142
+ </for>
143
+ </div>
144
+ </div>
145
+ </for>
146
+
147
+ <for|action, i| of=actions>
148
+ <sds-button
149
+ variant="link"
150
+ label=action.label
151
+ onClick=() => component.handleGroupAction(`action-${i}`, action)
152
+ isLoading=(state.group[`action-${i}`] || Boolean(action.loading))
153
+ disabled=action.disabled
154
+ disabledTooltip=action.disabledTooltip
155
+ disabledTooltipPosition="top"
156
+ class="sds-bulk-action-bar__group-button"
157
+ />
158
+ </for>
159
+
160
+ <if(secondaryLabel)>
161
+ <sds-button
162
+ variant="ghost"
163
+ label=secondaryLabel
164
+ onClick=() => component.handleSecondary()
165
+ isLoading=(state.secondary || secondaryLoading)
166
+ disabled=secondaryDisabled
167
+ disabledTooltip=secondaryDisabledTooltip
168
+ disabledTooltipPosition="top"
169
+ class="sds-bulk-action-bar__secondary"
170
+ />
171
+ </if>
172
+
173
+ <if(primaryLabel)>
174
+ <sds-button
175
+ variant="primary"
176
+ label=primaryLabel
177
+ onClick=() => component.handlePrimary()
178
+ isLoading=(state.primary || primaryLoading)
179
+ disabled=primaryDisabled
180
+ disabledTooltip=primaryDisabledTooltip
181
+ disabledTooltipPosition="top"
182
+ class="sds-bulk-action-bar__primary"
183
+ />
184
+ </if>
185
+ </div>
186
+ </section>