@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,492 @@
1
+ <!-- sds-date-picker — ported from kdls-design-system v2.5.1 (0f3edf8)
2
+ components/DatePicker/DatePicker.tsx.
3
+
4
+ A trigger and a calendar: single date, range, date with time, or time alone. The month
5
+ grid, the bounds, the disabled days and times, and what the trigger says are all in
6
+ date-logic.js and tested there — 41 cases, because dates are where off-by-ones live and
7
+ none of them needs a browser to find.
8
+
9
+ Divergences:
10
+
11
+ The dark theme's range highlight was Kenlo blue at 15%, written as a literal in three
12
+ rules. It is the installed scheme's own colour at 15% now — see DatePicker.css.
13
+
14
+ The grid is a <table> with real headers. KDLS renders nested divs, so a screen reader
15
+ is read forty loose numbers with no way to know which column is which day: "15" without
16
+ "quarta-feira" is not a date. The weekday header is a full name, visually abbreviated.
17
+
18
+ Every day is a <button> rather than a div with a click handler, so the calendar can be
19
+ used from the keyboard at all — and arrow keys move through it, which KDLS does not
20
+ offer in any mode.
21
+
22
+ The two invalid combinations KDLS forbids in the type system are checked at render:
23
+ a range with a time, and time-only with disablePast. Neither means anything, and
24
+ silently ignoring one is how a caller ends up debugging a control that quietly does
25
+ half of what it was told. -->
26
+
27
+ import {
28
+ MONTHS, WEEKDAYS, WEEKDAY_NAMES, pad2, formatTime, isSameDay, triggerLabel,
29
+ effectiveBounds, isDisabledDate, isDisabledTime, monthGrid, yearGrid, timeSlots,
30
+ isBetween, orderRange, DEFAULT_TIME_INTERVAL,
31
+ } from "./date-logic.js";
32
+ import { assertFeedback as assertInputFeedback, assertWidth as assertInputWidth }
33
+ from "../Input/input-types.js";
34
+ import calendarIcon from "../../icons/generated/fluent-calendar-regular.marko";
35
+ import chevronDown from "../../icons/generated/fluent-chevron-down-regular.marko";
36
+ import dismissIcon from "../../icons/generated/fluent-dismiss-regular.marko";
37
+ import clockIcon from "../../icons/generated/fluent-clock-regular.marko";
38
+ import chevronLeft from "../../icons/generated/fluent-chevron-left-regular.marko";
39
+ import chevronRight from "../../icons/generated/fluent-chevron-right-regular.marko";
40
+
41
+ class {
42
+ onCreate(input) {
43
+ const single = input.isRange ? null : (input.value ?? null);
44
+ const start = input.isRange ? (input.value?.start ?? null) : null;
45
+ this.state = {
46
+ open: false,
47
+ // `input.today`, not `new Date()`. The prop exists so a caller can say what "today"
48
+ // is — a test pinning a month, a page rendered on a server in another timezone — and
49
+ // every other read of today here goes through it. This one did not, so the calendar
50
+ // opened on the real month while everything else in it agreed on the given one.
51
+ view: input.defaultViewDate ?? single ?? start ?? input.today ?? new Date(),
52
+ mode: "calendar",
53
+ rangeStep: "start",
54
+ pendingDate: null,
55
+ };
56
+ }
57
+
58
+ get today() {
59
+ return this.input.today ?? new Date();
60
+ }
61
+
62
+ get bounds() {
63
+ const { minDate, maxDate, disablePast, disableFuture } = this.input;
64
+ return effectiveBounds({ minDate, maxDate, disablePast, disableFuture, today: this.today });
65
+ }
66
+
67
+ dayDisabled(date) {
68
+ const { min, max } = this.bounds;
69
+ const { filterDate, disableToday, excludeDates, includeDates } = this.input;
70
+ return isDisabledDate(date, {
71
+ min, max, filterDate, disableToday, excludeDates, includeDates, today: this.today,
72
+ });
73
+ }
74
+
75
+ slotDisabled(slot) {
76
+ const { min, max } = this.bounds;
77
+ const { onlyTime, hasTime, filterTime, includeTimes, excludeTimes, isRange } = this.input;
78
+ const single = isRange ? null : (this.input.value ?? null);
79
+ return isDisabledTime(slot, {
80
+ min,
81
+ max,
82
+ onlyTime,
83
+ hasTime,
84
+ reference: this.state.pendingDate ?? single,
85
+ filterTime,
86
+ includeTimes,
87
+ excludeTimes,
88
+ });
89
+ }
90
+
91
+ setOpen(next) {
92
+ if (this.input.disabled || this.input.isLoading) return;
93
+ this.state.open = next;
94
+ if (!next) this.state.mode = "calendar";
95
+ }
96
+
97
+ navigate(direction) {
98
+ const view = this.state.view;
99
+ const year = view.getFullYear();
100
+ const month = view.getMonth();
101
+ if (this.state.mode === "year") {
102
+ this.state.view = new Date(yearGrid(year)[0] + direction * 16, month, 1);
103
+ } else if (this.state.mode === "month-grid") {
104
+ this.state.view = new Date(year + direction, month, 1);
105
+ } else {
106
+ this.state.view = new Date(year, month + direction, 1);
107
+ }
108
+ }
109
+
110
+ setMode(mode) { this.state.mode = mode; }
111
+
112
+ chooseMonth(month) {
113
+ this.state.view = new Date(this.state.view.getFullYear(), month, 1);
114
+ this.state.mode = "calendar";
115
+ }
116
+
117
+ chooseYear(year) {
118
+ this.state.view = new Date(year, this.state.view.getMonth(), 1);
119
+ this.state.mode = "month-grid";
120
+ }
121
+
122
+ chooseDay(date) {
123
+ if (this.dayDisabled(date)) return;
124
+ const { isRange, hasTime, onlyTime, onChange } = this.input;
125
+
126
+ if (!isRange) {
127
+ if (hasTime) {
128
+ // The date is held until a time is picked: a half-answer is not an answer.
129
+ this.state.pendingDate = date;
130
+ return;
131
+ }
132
+ onChange?.(date);
133
+ if (!onlyTime) this.setOpen(false);
134
+ return;
135
+ }
136
+
137
+ const start = this.input.value?.start ?? null;
138
+ if (this.state.rangeStep === "start" || !start) {
139
+ onChange?.({ start: date, end: null });
140
+ this.state.rangeStep = "end";
141
+ return;
142
+ }
143
+ onChange?.(orderRange(start, date));
144
+ this.state.rangeStep = "start";
145
+ this.setOpen(false);
146
+ }
147
+
148
+ chooseTime(slot) {
149
+ if (this.slotDisabled(slot)) return;
150
+ const base = this.state.pendingDate ?? this.input.value ?? this.today;
151
+ this.state.pendingDate = null;
152
+ this.input.onChange?.(new Date(
153
+ base.getFullYear(), base.getMonth(), base.getDate(),
154
+ slot.getHours(), slot.getMinutes(),
155
+ ));
156
+ this.setOpen(false);
157
+ }
158
+
159
+ /**
160
+ * Back to the current month. Navigation, not selection — nothing is chosen, the calendar
161
+ * just stops showing 2031 because you went looking there. Ported from KDLS `handleToday`,
162
+ * which SDS had dropped: after a few taps on "próximo mês" there was no way back but the
163
+ * same number of taps in the other direction.
164
+ */
165
+ goToToday() {
166
+ this.state.view = this.today;
167
+ this.state.mode = "calendar";
168
+ }
169
+
170
+ clear() {
171
+ this.input.onChange?.(this.input.isRange ? { start: null, end: null } : null);
172
+ this.state.pendingDate = null;
173
+ this.state.rangeStep = "start";
174
+ this.setOpen(false);
175
+ }
176
+
177
+ /**
178
+ * Arrow keys through the grid.
179
+ *
180
+ * KDLS binds click and nothing else, in every mode, so a calendar cannot be used without
181
+ * a mouse — on a control whose alternative is typing a date into a field it does not have.
182
+ */
183
+ handleGridKey(date, event) {
184
+ const steps = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
185
+ const step = steps[event.key];
186
+ if (step === undefined) return;
187
+ event.preventDefault();
188
+ const next = new Date(date.getFullYear(), date.getMonth(), date.getDate() + step);
189
+ this.state.view = next;
190
+ // The grid is re-rendered around the new date; focus follows it by key.
191
+ requestAnimationFrame(() => {
192
+ this.getEl(`day-${next.getFullYear()}-${next.getMonth()}-${next.getDate()}`)?.focus();
193
+ });
194
+ }
195
+ }
196
+
197
+ $ {
198
+ const {
199
+ label,
200
+ trigger = "input",
201
+ width = "hug",
202
+ value,
203
+ isRange = false,
204
+ hasTime: hasTimeInput = false,
205
+ onlyTime: onlyTimeInput = false,
206
+ disabled = false,
207
+ isLoading = false,
208
+ feedback = "none",
209
+ hint,
210
+ name,
211
+ required = false,
212
+ timeIntervals = DEFAULT_TIME_INTERVAL,
213
+ clearLabel = "Limpar",
214
+ todayLabel = "Hoje",
215
+ // Distinct from `clearLabel` on purpose. Both clear, and while the panel is open both
216
+ // are on screen at once — two buttons announced as "Limpar" is a screen reader reading
217
+ // the same word twice for two different controls.
218
+ clearValueLabel,
219
+ class: extraClass = "",
220
+ } = input;
221
+
222
+ // The trigger renders an Input, so its width and feedback are the Input's vocabulary.
223
+ assertInputWidth(width);
224
+ assertInputFeedback(feedback);
225
+
226
+ // KDLS forbids these in the type system, which does not exist here. Silently ignoring
227
+ // half of what a caller asked for is how they end up debugging the wrong thing.
228
+ if (isRange && (hasTimeInput || onlyTimeInput)) {
229
+ throw new Error("sds-date-picker: `isRange` cannot be combined with `hasTime` or `onlyTime` — a range has two ends and a time has one.");
230
+ }
231
+ if (onlyTimeInput && (input.disablePast || input.disableFuture || input.disableToday)) {
232
+ throw new Error("sds-date-picker: `onlyTime` has no calendar, so `disablePast`, `disableFuture` and `disableToday` mean nothing with it.");
233
+ }
234
+ if (trigger !== "input" && trigger !== "pill") {
235
+ throw new Error(`Unknown date picker trigger: "${trigger}". Allowed: input, pill`);
236
+ }
237
+
238
+ const hasTime = isRange ? false : hasTimeInput;
239
+ const onlyTime = isRange ? false : onlyTimeInput;
240
+ const single = isRange ? null : (value ?? null);
241
+ const start = isRange ? (value?.start ?? null) : null;
242
+ const end = isRange ? (value?.end ?? null) : null;
243
+
244
+ const text = triggerLabel({ isRange, value, hasTime, onlyTime });
245
+ const hasValue = isRange ? Boolean(value?.start || value?.end) : Boolean(value);
246
+ const clearedName = clearValueLabel
247
+ ?? (onlyTime ? "Limpar a hora escolhida" : "Limpar a data escolhida");
248
+ const fallbackLabel = isRange ? "Período" : "Data";
249
+ const fieldLabel = label ?? fallbackLabel;
250
+
251
+ const view = state.view;
252
+ const year = view.getFullYear();
253
+ const month = view.getMonth();
254
+ const weeks = monthGrid(year, month);
255
+ const years = yearGrid(year);
256
+ const slots = (hasTime || onlyTime) ? timeSlots(timeIntervals, component.today) : [];
257
+
258
+ const showingTime = !state.pendingDate && (hasTime || onlyTime) && single;
259
+ const selectedHour = showingTime ? single.getHours() : null;
260
+ const selectedMinute = showingTime ? single.getMinutes() : null;
261
+
262
+ const headerLabel = state.mode === "year"
263
+ ? `${years[0]} – ${years[15]}`
264
+ : state.mode === "month-grid" ? String(year) : `${MONTHS[month]} ${year}`;
265
+
266
+ const prevLabel = state.mode === "year" ? "Anos anteriores"
267
+ : state.mode === "month-grid" ? "Ano anterior" : "Mês anterior";
268
+ const nextLabel = state.mode === "year" ? "Próximos anos"
269
+ : state.mode === "month-grid" ? "Próximo ano" : "Próximo mês";
270
+
271
+ const hiddenValue = isRange
272
+ ? (!start ? "" : !end ? start.toISOString() : `${start.toISOString()}/${end.toISOString()}`)
273
+ : (single ? single.toISOString() : "");
274
+
275
+ const panelClass = [
276
+ "sds-datepicker",
277
+ onlyTime ? "sds-datepicker--only-time" : "",
278
+ hasTime ? "sds-datepicker--has-time" : "",
279
+ disabled ? "sds-datepicker--disabled" : "",
280
+ ].filter(Boolean).join(" ");
281
+ }
282
+
283
+ <div class=[
284
+ "sds-datepicker-root",
285
+ width === "fill" ? "sds-datepicker-root--fill" : "",
286
+ extraClass,
287
+ ].filter(Boolean).join(" ")>
288
+ <sds-floating
289
+ open=state.open
290
+ onOpenChange=(next) => component.setOpen(next)
291
+ placement="bottomLeft"
292
+ trigger="click"
293
+ >
294
+ <@content>
295
+ <div class=panelClass>
296
+ <if(!onlyTime)>
297
+ <div class="sds-datepicker__calendar-col">
298
+ <div class="sds-datepicker__header">
299
+ <sds-icon-button variant="neutral" size="sm" aria-label=prevLabel
300
+ onClick=() => component.navigate(-1)>
301
+ <@icon><${chevronLeft}/></@icon>
302
+ </sds-icon-button>
303
+
304
+ <sds-button
305
+ variant="link"
306
+ size="md"
307
+ label=headerLabel
308
+ onClick=() => component.setMode(state.mode === "calendar" ? "month-grid" : state.mode === "month-grid" ? "year" : "calendar")
309
+ />
310
+
311
+ <sds-icon-button variant="neutral" size="sm" aria-label=nextLabel
312
+ onClick=() => component.navigate(1)>
313
+ <@icon><${chevronRight}/></@icon>
314
+ </sds-icon-button>
315
+ </div>
316
+
317
+ <if(state.mode === "calendar")>
318
+ <!-- A real table. KDLS nests divs, so a screen reader meets forty loose
319
+ numbers and cannot tell which column is which day — "15" without
320
+ "quarta-feira" is not a date. -->
321
+ <table class="sds-datepicker__grid" role="grid" aria-label=`${MONTHS[month]} ${year}`>
322
+ <thead>
323
+ <tr class="sds-datepicker__weekdays">
324
+ <for|initial, index| of=WEEKDAYS>
325
+ <th scope="col" abbr=WEEKDAY_NAMES[index]>
326
+ <span aria-hidden="true">${initial}</span>
327
+ <span class="sds-visually-hidden">${WEEKDAY_NAMES[index]}</span>
328
+ </th>
329
+ </for>
330
+ </tr>
331
+ </thead>
332
+ <tbody>
333
+ <for|week| of=weeks>
334
+ <tr class="sds-datepicker__week-row">
335
+ <for|cell| of=week>
336
+ <td class=[
337
+ "sds-datepicker__day-wrap",
338
+ isRange && isBetween(cell.date, start, end) ? "sds-datepicker__day-wrap--in-range" : "",
339
+ isRange && start && end && isSameDay(cell.date, start) ? "sds-datepicker__day-wrap--range-start" : "",
340
+ isRange && start && end && isSameDay(cell.date, end) ? "sds-datepicker__day-wrap--range-end" : "",
341
+ ].filter(Boolean).join(" ")>
342
+ <button
343
+ key=`day-${cell.date.getFullYear()}-${cell.date.getMonth()}-${cell.date.getDate()}`
344
+ type="button"
345
+ class=[
346
+ "sds-datepicker__day",
347
+ !cell.current ? "sds-datepicker__day--other" : "",
348
+ isSameDay(cell.date, component.today) ? "sds-datepicker__day--today" : "",
349
+ (isSameDay(cell.date, single) || isSameDay(cell.date, start) || isSameDay(cell.date, end))
350
+ ? "sds-datepicker__day--selected" : "",
351
+ component.dayDisabled(cell.date) ? "sds-datepicker__day--disabled" : "",
352
+ ].filter(Boolean).join(" ")
353
+ disabled=(component.dayDisabled(cell.date) || null)
354
+ aria-current=(isSameDay(cell.date, component.today) ? "date" : null)
355
+ aria-selected=(isSameDay(cell.date, single) || isSameDay(cell.date, start) || isSameDay(cell.date, end) ? "true" : "false")
356
+ on-click("chooseDay", cell.date)
357
+ on-keydown("handleGridKey", cell.date)
358
+ >
359
+ ${cell.date.getDate()}
360
+ </button>
361
+ </td>
362
+ </for>
363
+ </tr>
364
+ </for>
365
+ </tbody>
366
+ </table>
367
+ </if>
368
+
369
+ <else-if(state.mode === "month-grid")>
370
+ <div class="sds-datepicker__month-grid">
371
+ <for|name, index| of=MONTHS>
372
+ <button
373
+ type="button"
374
+ class=[
375
+ "sds-datepicker__month-item",
376
+ index === month ? "sds-datepicker__month-item--selected" : "",
377
+ index === component.today.getMonth() && year === component.today.getFullYear()
378
+ ? "sds-datepicker__month-item--today" : "",
379
+ ].filter(Boolean).join(" ")
380
+ on-click("chooseMonth", index)
381
+ >
382
+ ${name.slice(0, 3)}
383
+ </button>
384
+ </for>
385
+ </div>
386
+ </else-if>
387
+
388
+ <else>
389
+ <div class="sds-datepicker__year-grid">
390
+ <for|candidate| of=years>
391
+ <button
392
+ type="button"
393
+ class=[
394
+ "sds-datepicker__year-item",
395
+ candidate === year ? "sds-datepicker__year-item--selected" : "",
396
+ candidate === component.today.getFullYear() ? "sds-datepicker__year-item--today" : "",
397
+ ].filter(Boolean).join(" ")
398
+ on-click("chooseYear", candidate)
399
+ >
400
+ ${candidate}
401
+ </button>
402
+ </for>
403
+ </div>
404
+ </else>
405
+ </div>
406
+ </if>
407
+
408
+ <if(hasTime || onlyTime)>
409
+ <div class="sds-datepicker__time-panel">
410
+ <div class="sds-datepicker__time-list sds-scroll sds-scroll--thin"
411
+ role="listbox" aria-label="Horário">
412
+ <for|slot| of=slots>
413
+ <button
414
+ type="button"
415
+ role="option"
416
+ aria-selected=(selectedHour === slot.getHours() && selectedMinute === slot.getMinutes() ? "true" : "false")
417
+ class=[
418
+ "sds-datepicker__time-slot",
419
+ selectedHour === slot.getHours() && selectedMinute === slot.getMinutes()
420
+ ? "sds-datepicker__time-slot--selected" : "",
421
+ component.slotDisabled(slot) ? "sds-datepicker__time-slot--disabled" : "",
422
+ ].filter(Boolean).join(" ")
423
+ disabled=(component.slotDisabled(slot) || null)
424
+ on-click("chooseTime", slot)
425
+ >
426
+ ${formatTime(slot)}
427
+ </button>
428
+ </for>
429
+ </div>
430
+ </div>
431
+ </if>
432
+
433
+ <div class="sds-datepicker__footer">
434
+ <sds-button variant="link" size="md" label=clearLabel onClick=() => component.clear()/>
435
+ // No calendar, nothing to navigate back to: the time-only panel is a list of
436
+ // slots. KDLS makes the same exception.
437
+ <if(!onlyTime)>
438
+ <sds-button variant="link" size="md" label=todayLabel onClick=() => component.goToToday()/>
439
+ </if>
440
+ </div>
441
+ </div>
442
+ </@content>
443
+
444
+ <if(trigger === "pill")>
445
+ <!-- No `active`. A pill has two jobs and they colour differently: as a filter you
446
+ toggle, being on is a state of the data and `--on` fills it with the theme's
447
+ colour; as the trigger of a list, being open or holding a value is a state of
448
+ the control, and filling it would say the filter is applied when it is only the
449
+ thing you press. KDLS passes `active` to a Pill in Pagination — where the pill
450
+ IS the current page — and nowhere else. -->
451
+ <sds-pill
452
+ label=(text || fieldLabel)
453
+ width=width
454
+ iconLeft=(onlyTime ? clockIcon : calendarIcon)
455
+ iconRight=(hasValue ? dismissIcon : chevronDown)
456
+ onClickIconRight=(hasValue && !disabled ? () => component.clear() : undefined)
457
+ iconRightLabel=clearedName
458
+ isLoading=isLoading
459
+ disabled=disabled
460
+ aria-haspopup="dialog"
461
+ aria-expanded=(state.open ? "true" : "false")
462
+ />
463
+ </if>
464
+ <else>
465
+ // The calendar sits on the left and the right side is the way out, which is how
466
+ // KDLS has it and how every other field here now reads: SearchBar swaps its
467
+ // magnifier for a clear button the moment there is something to clear. The chevron
468
+ // stands there while there is not, so the field still says it opens something.
469
+ <sds-input
470
+ label=fieldLabel
471
+ value=text
472
+ readOnly
473
+ disabled=disabled
474
+ required=required
475
+ feedback=feedback
476
+ hint=hint
477
+ isLoading=isLoading
478
+ width=width
479
+ iconLeft=(onlyTime ? clockIcon : calendarIcon)
480
+ iconRight=(hasValue ? dismissIcon : chevronDown)
481
+ onClickIconRight=(hasValue && !disabled ? () => component.clear() : undefined)
482
+ iconRightLabel=clearedName
483
+ />
484
+ </else>
485
+ </sds-floating>
486
+
487
+ <if(name)>
488
+ <!-- What a form submits. The trigger is a read-only field or a pill, neither of which
489
+ posts anything on its own. -->
490
+ <input type="hidden" name=name value=hiddenValue/>
491
+ </if>
492
+ </div>
@@ -0,0 +1,174 @@
1
+ /* SDS Drawer.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Drawer/Drawer.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ */
5
+
6
+ /* Two values were written outside the system and are snapped back to it here.
7
+
8
+ The panel shadow was rgba(26,26,26,0.15). The opacity scale runs 05/10/20/30/40/50, so
9
+ 0.15 is not a step on it — it reads --color-opacity-20 now, a touch stronger, which is
10
+ the direction a shadow separating a panel from the page wants to go.
11
+
12
+ The dark-mode veil and shadow were black literals. --color-opacity-* inverts with the
13
+ theme and would light the screen up instead of dimming it, which is why they were
14
+ literals at all; --color-scrim is that value as a token. *//* ============================================================
15
+ SDS Drawer
16
+ Width: 600px (default) | full height | slides from right/left
17
+ Header: Bold 24px, border-bottom neutral-30
18
+ Footer: py-16, border-top neutral-30
19
+ ============================================================ */
20
+
21
+ /* ── Overlay ── */
22
+ .sds-drawer-overlay {
23
+ position: fixed;
24
+ inset: 0;
25
+ background: var(--color-opacity-50);
26
+ z-index: var(--z-overlay);
27
+ animation: sds-overlay-in 120ms ease;
28
+ }
29
+ @keyframes sds-overlay-in {
30
+ from { opacity: 0; }
31
+ to { opacity: 1; }
32
+ }
33
+ /* ── Container: slides from right, no border-radius ── */
34
+ .sds-drawer {
35
+ position: fixed;
36
+ top: 0;
37
+ right: 0;
38
+ bottom: 0;
39
+ width: var(--drawer-width, 600px);
40
+ max-width: 100vw;
41
+ background: var(--component-bg-color);
42
+ display: flex;
43
+ flex-direction: column;
44
+ z-index: calc(var(--z-overlay) + 1);
45
+ animation: sds-drawer-in 250ms var(--ease-standard);
46
+ box-shadow: -4px 0px 24px var(--color-opacity-20);
47
+ }
48
+ .sds-drawer--left {
49
+ right: auto;
50
+ left: 0;
51
+ box-shadow: 4px 0px 24px var(--color-opacity-20);
52
+ animation: sds-drawer-in-left 250ms var(--ease-standard);
53
+ }
54
+ @keyframes sds-drawer-in {
55
+ from { transform: translateX(100%); }
56
+ to { transform: translateX(0); }
57
+ }
58
+ @keyframes sds-drawer-in-left {
59
+ from { transform: translateX(-100%); }
60
+ to { transform: translateX(0); }
61
+ }
62
+ /* ── Header: pt-[24px] pb-[24px] px-[24px] + border-bottom ── */
63
+ .sds-drawer__header {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--spacing-4);
67
+ padding: var(--spacing-6);
68
+ border-bottom: 1px solid var(--border-interactive-color);
69
+ background: var(--component-bg-color);
70
+ flex-shrink: 0;
71
+ }
72
+ .sds-drawer__title-group {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: var(--spacing-4);
76
+ flex: 1;
77
+ min-width: 0;
78
+ }
79
+ .sds-drawer__title-group .sds-tooltip-wrapper {
80
+ flex-shrink: 0;
81
+ }
82
+ /* Title: Manrope Bold 24px, tracking -0.8px, neutral-90 */
83
+ .sds-drawer__title {
84
+ font-family: var(--font-family-base);
85
+ font-size: var(--font-size-title-lg);
86
+ font-weight: var(--font-weight-bold);
87
+ line-height: var(--lh-title-lg);
88
+ letter-spacing: -0.8px;
89
+ color: var(--color-neutral-90);
90
+ overflow: hidden;
91
+ white-space: nowrap;
92
+ text-overflow: ellipsis;
93
+ }
94
+ .sds-drawer__header-actions {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: var(--spacing-4);
98
+ flex-shrink: 0;
99
+ }
100
+ /* ── Body: scrollable, 24px top/bottom padding ── */
101
+ .sds-drawer__body {
102
+ flex: 1;
103
+ overflow-y: auto;
104
+ padding: var(--spacing-6);
105
+ }
106
+ /* ── Footer: px-[24px] py-[16px] + border-top ── */
107
+ .sds-drawer__footer {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: flex-end;
111
+ /* 16px between the buttons, decided by Rafael looking at a three-button footer. KDLS
112
+ uses 24 and the Card here already used 16, so the system disagreed with itself; 16 is
113
+ the side that keeps a row of actions reading as one group. */
114
+ gap: var(--spacing-4);
115
+ padding: var(--spacing-4) var(--spacing-6);
116
+ border-top: 1px solid var(--border-interactive-color);
117
+ background: var(--component-bg-color);
118
+ flex-shrink: 0;
119
+ }
120
+ /* ── Confirmation modal — rendered above the drawer ── */
121
+ .sds-modal-overlay:has(.sds-drawer__confirm-modal) {
122
+ z-index: calc(var(--z-overlay) + 100);
123
+ }
124
+ /* ── Dark mode ── */
125
+ [data-theme="dark"] .sds-drawer-overlay {
126
+ background: var(--color-scrim);
127
+ }
128
+ [data-theme="dark"] .sds-drawer {
129
+ box-shadow: -4px 0px 24px var(--color-scrim);
130
+ }
131
+ [data-theme="dark"] .sds-drawer--left {
132
+ box-shadow: 4px 0px 24px var(--color-scrim);
133
+ }
134
+ [data-theme="dark"] .sds-drawer__header {
135
+ border-bottom-color: var(--border-interactive-color);
136
+ }
137
+ [data-theme="dark"] .sds-drawer__footer {
138
+ border-top-color: var(--border-interactive-color);
139
+ }
140
+ /* ── Mobile (≤799px): same lateral drawer as desktop, just full width —
141
+ the base rule's max-width: 100vw only kicks in below 600px, so widths
142
+ between 600 and 799px need an explicit override here. The footer also
143
+ switches to primary/secondary/tertiary stacked full-width instead of
144
+ sitting in a row ── */
145
+ @media (max-width: 799px) {
146
+ .sds-drawer {
147
+ width: 100%;
148
+ }
149
+ .sds-drawer__footer {
150
+ flex-direction: column-reverse;
151
+ align-items: stretch;
152
+ gap: var(--spacing-4);
153
+ }
154
+ .sds-drawer__footer .sds-button {
155
+ width: 100%;
156
+ }
157
+ }
158
+
159
+ /* The confirmation, drawn inside the drawer's own overlay and wearing the modal's panel —
160
+ a question about leaving is a dialog, not a second drawer. A second veil over the first
161
+ would double the dimming, so this one only holds the panel and centres it. */
162
+ .sds-drawer-overlay--confirm {
163
+ background: transparent;
164
+ /* Above the panel, not below it. It was `z-index: 1` while `.sds-drawer` sits at
165
+ `--z-overlay + 1` — both fixed, both children of the same container — so the question
166
+ was painted underneath the thing it was asking about. Measured in Chrome: the element
167
+ at the centre of the confirmation was `.sds-drawer__body`. You pressed close, an
168
+ invisible dialog opened, and the drawer would not close: stuck, with nothing on
169
+ screen to say why. */
170
+ z-index: calc(var(--z-overlay) + 2);
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ }