@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,262 @@
1
+ <!-- sds-drawer — ported from kdls-design-system v2.5.1 (0f3edf8) components/Drawer/Drawer.tsx.
2
+
3
+ A panel that slides in from the side and covers the page behind a veil. Same behaviour
4
+ as sds-modal in every way that matters — Escape, the veil, the scroll lock, the focus
5
+ trap, the confirmation before closing — and the same foundation underneath, so the two
6
+ cannot drift apart. What differs is the geometry and a header that can carry a back
7
+ button and an action of its own.
8
+
9
+ Divergences are Modal's, for Modal's reasons: it manages focus, which KDLS does not;
10
+ it does not portal, because Marko 5 has none; and the confirmation is drawn inside
11
+ this overlay rather than by nesting a second one.
12
+
13
+ One of its own: a drawer that slides from the left or the right is a complementary
14
+ region, not a dialog, when it carries no decision — but KDLS renders role="dialog" for
15
+ both cases and so does this, because a drawer here always takes the whole keyboard and
16
+ a visitor cannot reach the page behind it. Calling it anything else would be describing
17
+ a screen it does not offer.
18
+
19
+ The four confirm defaults — `confirmTitle`, `confirmMessage`, `confirmActionLabel`
20
+ and `confirmCancelLabel` — are reworded, and this is the one place in the port where the
21
+ wording is itself the divergence — so it is Rafael's to keep or revert.
22
+
23
+ KDLS asks "Fechar sem salvar?" over buttons labelled "Fechar" and "Cancelar". In a dialog
24
+ whose whole subject is closing, "Cancelar" does not say what it cancels: the closing, or
25
+ the editing it would discard. The rewrite names the outcome on each button — "Descartar"
26
+ against "Continuar editando" — which is what a destructive confirmation is for, and the
27
+ message says what is lost rather than narrating where it happened.
28
+ -->
29
+
30
+ import { assertPlacement } from "../Floating/floating-position.js";
31
+ import { lockScroll, unlockScroll, trapFocus } from "../../foundations/overlay.js";
32
+ import dismissIcon from "../../icons/generated/fluent-dismiss-regular.marko";
33
+ import backIcon from "../../icons/generated/fluent-chevron-left-regular.marko";
34
+
35
+ class {
36
+ onCreate() {
37
+ this.state = { confirming: false };
38
+ this.release = null;
39
+ }
40
+
41
+ onMount() {
42
+ this.onKeyDown = (event) => {
43
+ if (event.key !== "Escape" || !this.input.isOpen) return;
44
+ event.stopPropagation();
45
+ // With the confirmation up, Escape dismisses the QUESTION, not the drawer. It used
46
+ // to reach `requestClose`, which sees `confirming` already true and closes — so the
47
+ // key you press to back out of a dialog threw away the edits the dialog exists to
48
+ // protect. Escape always undoes the most recent thing that opened.
49
+ if (this.state.confirming) {
50
+ this.cancelConfirm();
51
+ return;
52
+ }
53
+ this.requestClose();
54
+ };
55
+ document.addEventListener("keydown", this.onKeyDown);
56
+ if (this.input.isOpen) this.open();
57
+ }
58
+
59
+ onUpdate() {
60
+ if (this.wantsConfirmFocus && this.state.confirming) {
61
+ this.wantsConfirmFocus = false;
62
+ this.retrap(this.getEl("confirm"));
63
+ }
64
+ if (this.input.isOpen && !this.locked) this.open();
65
+ else if (!this.input.isOpen && this.locked) this.shut();
66
+ }
67
+
68
+ onDestroy() {
69
+ document.removeEventListener("keydown", this.onKeyDown);
70
+ this.shut();
71
+ }
72
+
73
+ open() {
74
+ this.locked = true;
75
+ lockScroll();
76
+ requestAnimationFrame(() => {
77
+ if (!this.input.isOpen) return;
78
+ this.release = trapFocus(this.getEl("panel"));
79
+ });
80
+ }
81
+
82
+ shut() {
83
+ if (!this.locked) return;
84
+ this.locked = false;
85
+ unlockScroll();
86
+ this.release?.();
87
+ this.release = null;
88
+ this.state.confirming = false;
89
+ }
90
+
91
+ requestClose() {
92
+ if (this.input.confirmClose && !this.state.confirming) {
93
+ this.state.confirming = true;
94
+ // On update, once the dialog exists in the DOM.
95
+ this.wantsConfirmFocus = true;
96
+ return;
97
+ }
98
+ this.input.onClose?.();
99
+ }
100
+
101
+ handleOverlayClick(event) {
102
+ if (this.input.disableOverlayClose) return;
103
+ if (event.target !== this.getEl("overlay")) return;
104
+ this.requestClose();
105
+ }
106
+
107
+ /**
108
+ * Move the focus trap onto whichever of the two is on top.
109
+ *
110
+ * It was bound to the drawer panel and stayed there, so the confirmation — a sibling of
111
+ * that panel, not a descendant — was outside the trap: Tab could not reach "Descartar"
112
+ * or "Continuar editando", and a keyboard user had no way to answer the question at all.
113
+ */
114
+ retrap(element) {
115
+ this.release?.();
116
+ this.release = element ? trapFocus(element) : null;
117
+ }
118
+
119
+ cancelConfirm() {
120
+ this.state.confirming = false;
121
+ this.retrap(this.getEl("panel"));
122
+ }
123
+
124
+ confirmAndClose() {
125
+ this.state.confirming = false;
126
+ this.wantsConfirmFocus = false;
127
+ this.input.onClose?.();
128
+ }
129
+ }
130
+
131
+ $ {
132
+ const {
133
+ isOpen,
134
+ title,
135
+ titleTooltip,
136
+ titleTooltipPosition = "bottom",
137
+ placement = "right",
138
+ onBack,
139
+ backLabel = "Voltar",
140
+ headerActionLabel, onHeaderAction, headerActionLoading = false, headerActionDisabled, headerActionDisabledTooltip, headerActionDisabledTooltipPosition = "top",
141
+ tertiaryLabel, onTertiary, tertiaryLoading = false, tertiaryDisabled, tertiaryDisabledTooltip, tertiaryDisabledTooltipPosition = "top",
142
+ secondaryLabel, onSecondary, secondaryLoading = false, secondaryDisabled, secondaryDisabledTooltip, secondaryDisabledTooltipPosition = "top",
143
+ primaryLabel, onPrimary, primaryLoading = false, primaryDisabled, primaryDisabledTooltip, primaryDisabledTooltipPosition = "top",
144
+ closeLabel = "Fechar",
145
+ zIndex,
146
+ confirmTitle = "Descartar alterações?",
147
+ confirmMessage = "As alterações feitas não serão salvas.",
148
+ confirmActionLabel = "Descartar",
149
+ confirmCancelLabel = "Continuar editando",
150
+ class: extraClass = "",
151
+ } = input;
152
+
153
+ assertPlacement(titleTooltipPosition);
154
+ assertPlacement(headerActionDisabledTooltipPosition);
155
+ assertPlacement(tertiaryDisabledTooltipPosition);
156
+ assertPlacement(secondaryDisabledTooltipPosition);
157
+ assertPlacement(primaryDisabledTooltipPosition);
158
+
159
+ if (!title) throw new Error("sds-drawer needs a `title` — it is the panel's accessible name.");
160
+ if (placement !== "right" && placement !== "left") {
161
+ throw new Error(`Unknown drawer placement: "${placement}". Allowed: right, left`);
162
+ }
163
+
164
+ const hasFooter = Boolean(input.footer || tertiaryLabel || secondaryLabel || primaryLabel);
165
+ const overlayStyle = zIndex !== undefined ? `z-index:${zIndex}` : null;
166
+
167
+ const panelClass = [
168
+ "sds-drawer",
169
+ placement !== "right" ? `sds-drawer--${placement}` : "",
170
+ extraClass,
171
+ ].filter(Boolean).join(" ");
172
+ }
173
+
174
+ <if(isOpen)>
175
+ <div
176
+ key="overlay"
177
+ class="sds-drawer-overlay"
178
+ style=overlayStyle
179
+ role="dialog"
180
+ aria-modal="true"
181
+ aria-label=title
182
+ on-click("handleOverlayClick")
183
+ >
184
+ <div key="panel" class=panelClass>
185
+ <div class="sds-drawer__header">
186
+ <if(onBack)>
187
+ <sds-icon-button variant="neutral" aria-label=backLabel onClick=onBack>
188
+ <@icon><${backIcon}/></@icon>
189
+ </sds-icon-button>
190
+ </if>
191
+
192
+ <div class="sds-drawer__title-group">
193
+ <span class="sds-drawer__title">${title}</span>
194
+ <if(titleTooltip)>
195
+ <sds-tooltip content=titleTooltip placement=titleTooltipPosition/>
196
+ </if>
197
+ </div>
198
+
199
+ <div class="sds-drawer__header-actions">
200
+ <if(headerActionLabel)>
201
+ <sds-button variant="link" size="md" label=headerActionLabel
202
+ onClick=onHeaderAction isLoading=headerActionLoading disabled=headerActionDisabled
203
+ disabledTooltip=headerActionDisabledTooltip disabledTooltipPosition=headerActionDisabledTooltipPosition/>
204
+ </if>
205
+ <sds-icon-button variant="neutral" aria-label=closeLabel onClick=() => component.requestClose()>
206
+ <@icon><${dismissIcon}/></@icon>
207
+ </sds-icon-button>
208
+ </div>
209
+ </div>
210
+
211
+ <div class="sds-drawer__body sds-scroll sds-scroll--thin">
212
+ <${input.renderBody}/>
213
+ </div>
214
+
215
+ <if(hasFooter)>
216
+ <div class="sds-drawer__footer">
217
+ <if(input.footer)>
218
+ <${input.footer}/>
219
+ </if>
220
+ <else>
221
+ <if(tertiaryLabel)>
222
+ <sds-button variant="link" size="md" label=tertiaryLabel
223
+ onClick=onTertiary isLoading=tertiaryLoading disabled=tertiaryDisabled
224
+ disabledTooltip=tertiaryDisabledTooltip disabledTooltipPosition=tertiaryDisabledTooltipPosition/>
225
+ </if>
226
+ <if(secondaryLabel)>
227
+ <sds-button variant="ghost" size="md" label=secondaryLabel
228
+ onClick=onSecondary isLoading=secondaryLoading disabled=secondaryDisabled
229
+ disabledTooltip=secondaryDisabledTooltip disabledTooltipPosition=secondaryDisabledTooltipPosition/>
230
+ </if>
231
+ <if(primaryLabel)>
232
+ <sds-button variant="primary" size="md" label=primaryLabel
233
+ onClick=onPrimary isLoading=primaryLoading disabled=primaryDisabled
234
+ disabledTooltip=primaryDisabledTooltip disabledTooltipPosition=primaryDisabledTooltipPosition/>
235
+ </if>
236
+ </else>
237
+ </div>
238
+ </if>
239
+ </div>
240
+
241
+ <if(state.confirming)>
242
+ <div class="sds-drawer-overlay sds-drawer-overlay--confirm" role="dialog" aria-modal="true" aria-label=confirmTitle>
243
+ <div key="confirm" class="sds-modal sds-modal--confirm">
244
+ <div class="sds-modal__header">
245
+ <div class="sds-modal__title-group">
246
+ <sds-text as="span" variant="titleMd" class="sds-modal__title">${confirmTitle}</sds-text>
247
+ </div>
248
+ </div>
249
+ <div class="sds-modal__body">
250
+ <sds-text as="p" variant="bodyMd">${confirmMessage}</sds-text>
251
+ </div>
252
+ <div class="sds-modal__footer">
253
+ <sds-button variant="ghost" size="md" label=confirmCancelLabel
254
+ onClick=() => component.cancelConfirm()/>
255
+ <sds-button variant="primary" size="md" label=confirmActionLabel
256
+ onClick=() => component.confirmAndClose()/>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </if>
261
+ </div>
262
+ </if>
@@ -0,0 +1,28 @@
1
+ /* SDS Floating.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Floating/Floating.css, which
3
+ is three lines — React kept everything else in inline styles from the positioning hook.
4
+ SDS keeps the computed coordinates inline too (they change per instance, per scroll) and
5
+ puts everything that does not change here. */
6
+
7
+ .sds-floating {
8
+ display: inline-flex;
9
+ flex-direction: column;
10
+ }
11
+
12
+ /* The panel is measured before it is shown: a panel nobody has measured is 0×0, and a flip
13
+ decided on 0×0 puts it on the wrong side in front of the visitor. So it renders, gets
14
+ measured, and is revealed in one step.
15
+
16
+ Hidden with visibility rather than display:none, because display:none has no size to
17
+ measure — which is the whole problem this is solving. pointer-events keeps the invisible
18
+ panel from swallowing a click meant for what is underneath it. */
19
+ .sds-floating__panel {
20
+ visibility: hidden;
21
+ pointer-events: none;
22
+ z-index: var(--z-floating);
23
+ }
24
+
25
+ .sds-floating__panel--positioned {
26
+ visibility: visible;
27
+ pointer-events: auto;
28
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Where a floating panel goes.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/useFloatingPosition.ts and
5
+ * components/getVisibleBounds.ts. React put this in a hook; here it is plain functions,
6
+ * which is what it always was — the hook part was only the subscription to scroll and
7
+ * resize, and that lives in the component.
8
+ *
9
+ * It is pure and it reads no DOM, so the interesting cases — the flip at the edge, the
10
+ * corner alignments, a panel inside a scroll container — are testable without a browser.
11
+ * That matters: this is the maths that decides whether a menu opens over the button the
12
+ * visitor was about to click.
13
+ */
14
+
15
+ export const FLOATING_SIDES = ['top', 'bottom', 'left', 'right'];
16
+ export const FLOATING_ALIGNS = ['start', 'center', 'end'];
17
+
18
+ /** The twelve placements, in KDLS's camelCase vocabulary (Ant Design's convention). */
19
+ export const FLOATING_PLACEMENTS = [
20
+ 'top', 'topLeft', 'topRight',
21
+ 'right', 'rightTop', 'rightBottom',
22
+ 'bottom', 'bottomLeft', 'bottomRight',
23
+ 'left', 'leftTop', 'leftBottom',
24
+ ];
25
+
26
+ export const FLOATING_TRIGGERS = ['click', 'hover', 'focus', 'contextMenu'];
27
+
28
+ export const DEFAULT_GAP = 4;
29
+
30
+ export function assertPlacement(placement) {
31
+ if (!FLOATING_PLACEMENTS.includes(placement)) {
32
+ throw new Error(`Unknown floating placement: "${placement}". Allowed: ${FLOATING_PLACEMENTS.join(', ')}`);
33
+ }
34
+ return placement;
35
+ }
36
+
37
+ export function assertTrigger(trigger) {
38
+ for (const mode of [].concat(trigger)) {
39
+ if (!FLOATING_TRIGGERS.includes(mode)) {
40
+ throw new Error(`Unknown floating trigger: "${mode}". Allowed: ${FLOATING_TRIGGERS.join(', ')}`);
41
+ }
42
+ }
43
+ return trigger;
44
+ }
45
+
46
+ /**
47
+ * "bottomLeft" -> { side: 'bottom', align: 'start' }.
48
+ *
49
+ * The corner word means different things by side, which is the trap: on top and bottom it
50
+ * is horizontal (Left/Right), on left and right it is vertical (Top/Bottom). So
51
+ * `leftTop` is the left side aligned to the *start*, not the top side.
52
+ */
53
+ export function parseFloatingPlacement(placement) {
54
+ const match = /^([a-z]+)([A-Z][a-z]+)?$/.exec(placement);
55
+ if (!match) throw new Error(`Unparseable floating placement: "${placement}"`);
56
+ const [, side, corner] = match;
57
+ if (!corner) return { side, align: 'center' };
58
+ const startWord = side === 'top' || side === 'bottom' ? 'Left' : 'Top';
59
+ return { side, align: corner === startWord ? 'start' : 'end' };
60
+ }
61
+
62
+ export function joinFloatingPlacement(side, align) {
63
+ if (align === 'center') return side;
64
+ const vertical = side === 'top' || side === 'bottom';
65
+ const startWord = vertical ? 'Left' : 'Top';
66
+ const endWord = vertical ? 'Right' : 'Bottom';
67
+ return side + (align === 'start' ? startWord : endWord);
68
+ }
69
+
70
+ /** camelCase placement -> kebab-case, for building a BEM modifier. */
71
+ export const camelToKebab = (s) => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
72
+
73
+ /**
74
+ * The style for a side and align that have already been decided.
75
+ *
76
+ * Alignment is done with `transform` percentages, which are relative to the panel itself —
77
+ * so this never needs to know how big the panel is. Only the flip does.
78
+ */
79
+ export function floatingStyleFor(anchor, side, align, gap) {
80
+ switch (side) {
81
+ case 'bottom':
82
+ if (align === 'start') return { top: anchor.bottom + gap, left: anchor.left };
83
+ if (align === 'end') return { top: anchor.bottom + gap, left: anchor.right, transform: 'translateX(-100%)' };
84
+ return { top: anchor.bottom + gap, left: anchor.left + anchor.width / 2, transform: 'translateX(-50%)' };
85
+ case 'top':
86
+ if (align === 'start') return { top: anchor.top - gap, left: anchor.left, transform: 'translateY(-100%)' };
87
+ if (align === 'end') return { top: anchor.top - gap, left: anchor.right, transform: 'translate(-100%, -100%)' };
88
+ return { top: anchor.top - gap, left: anchor.left + anchor.width / 2, transform: 'translate(-50%, -100%)' };
89
+ case 'right':
90
+ if (align === 'start') return { top: anchor.top, left: anchor.right + gap };
91
+ if (align === 'end') return { top: anchor.bottom, left: anchor.right + gap, transform: 'translateY(-100%)' };
92
+ return { top: anchor.top + anchor.height / 2, left: anchor.right + gap, transform: 'translateY(-50%)' };
93
+ case 'left':
94
+ if (align === 'start') return { top: anchor.top, left: anchor.left - gap, transform: 'translateX(-100%)' };
95
+ if (align === 'end') return { top: anchor.bottom, left: anchor.left - gap, transform: 'translate(-100%, -100%)' };
96
+ return { top: anchor.top + anchor.height / 2, left: anchor.left - gap, transform: 'translate(-100%, -50%)' };
97
+ default:
98
+ throw new Error(`Unknown floating side: "${side}". Allowed: ${FLOATING_SIDES.join(', ')}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Only the side flips; the alignment never changes.
104
+ *
105
+ * A panel that has not been measured yet is 0x0, and a 0x0 panel fits everywhere — so it
106
+ * would never flip, and would then be revealed on the wrong side. The caller measures
107
+ * first and positions second for exactly this reason.
108
+ */
109
+ export function flipSide(anchor, panelSize, side, gap, bounds) {
110
+ if (panelSize.width === 0 && panelSize.height === 0) return side;
111
+ if (side === 'bottom' && anchor.bottom + gap + panelSize.height > bounds.bottom) return 'top';
112
+ if (side === 'top' && anchor.top - gap - panelSize.height < bounds.top) return 'bottom';
113
+ if (side === 'right' && anchor.right + gap + panelSize.width > bounds.right) return 'left';
114
+ if (side === 'left' && anchor.left - gap - panelSize.width < bounds.left) return 'right';
115
+ return side;
116
+ }
117
+
118
+
119
+ /**
120
+ * How far the panel pulls itself back along each axis, as a fraction of its own size.
121
+ *
122
+ * This is the same information as the `transform` in floatingStyleFor, said as numbers.
123
+ * It exists because a percentage transform cannot be clamped: to know whether the panel
124
+ * runs off the screen you have to resolve it against the panel's real width, and to move
125
+ * it back you have to replace the percentage with a number. A test asserts the two agree
126
+ * for all twelve placements, so neither can drift.
127
+ */
128
+ export function shiftFor(side, align) {
129
+ const fraction = (a) => (a === 'start' ? 0 : a === 'end' ? -1 : -0.5);
130
+ if (side === 'top' || side === 'bottom') {
131
+ return { x: fraction(align), y: side === 'top' ? -1 : 0 };
132
+ }
133
+ return { x: side === 'left' ? -1 : 0, y: fraction(align) };
134
+ }
135
+
136
+ export function transformFor({ x, y }) {
137
+ const pct = (n) => `${n * 100}%`;
138
+ if (!x && !y) return undefined;
139
+ if (!y) return `translateX(${pct(x)})`;
140
+ if (!x) return `translateY(${pct(y)})`;
141
+ return `translate(${pct(x)}, ${pct(y)})`;
142
+ }
143
+
144
+ /**
145
+ * Keeps the panel on screen across the axis the flip does not handle.
146
+ *
147
+ * The flip only moves the panel to the other side of the trigger — it never shifts it
148
+ * sideways. So a centred panel on a trigger near the left edge hangs off the screen, and
149
+ * KDLS does this too: measured in the portal, a 322px panel centred on a trigger at x=24
150
+ * lands at left: -102. Twelve placements and none of them notices.
151
+ *
152
+ * The margin keeps the panel from sitting flush against the edge, which reads as clipped
153
+ * even when it is not.
154
+ */
155
+ export function clampToBounds(style, panelSize, side, align, bounds, margin = 8) {
156
+ const shift = shiftFor(side, align);
157
+ const across = side === 'top' || side === 'bottom' ? 'x' : 'y';
158
+
159
+ const size = across === 'x' ? panelSize.width : panelSize.height;
160
+ // Nothing measured yet: clamping to a size of zero would pin the panel to the edge.
161
+ if (!size) return style;
162
+
163
+ const axis = across === 'x' ? 'left' : 'top';
164
+ const low = across === 'x' ? bounds.left : bounds.top;
165
+ const high = across === 'x' ? bounds.right : bounds.bottom;
166
+
167
+ const edge = style[axis] + shift[across] * size;
168
+ // A panel taller or wider than the space it has cannot be made to fit; pin it to the
169
+ // near edge rather than pushing it off the far one.
170
+ const clamped = Math.max(low + margin, Math.min(edge, high - size - margin));
171
+ if (clamped === edge) return style;
172
+
173
+ const rest = { ...shift, [across]: 0 };
174
+ return { ...style, [axis]: clamped, transform: transformFor(rest) };
175
+ }
176
+
177
+ export function computeFloatingPosition(anchor, panelSize, side, align, gap, bounds) {
178
+ const effectiveSide = flipSide(anchor, panelSize, side, gap, bounds);
179
+ const style = floatingStyleFor(anchor, effectiveSide, align, gap);
180
+ return {
181
+ style: clampToBounds(style, panelSize, effectiveSide, align, bounds),
182
+ effectiveSide,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * How much of the viewport this element can actually be seen in, once its scrolling
188
+ * ancestors have had their say.
189
+ *
190
+ * `scrollOnly` skips `overflow: hidden`: a panel positioned with `position: fixed` escapes
191
+ * a hidden ancestor anyway, so treating one as a boundary would flip the panel for a wall
192
+ * that is not there. Scroll containers are different — the panel does not escape those in
193
+ * any useful sense, because the content it is anchored to moves inside them.
194
+ */
195
+ export function getVisibleBounds(el, scrollOnly = false, view = globalThis) {
196
+ let top = 0;
197
+ let bottom = view.innerHeight;
198
+ let left = 0;
199
+ let right = view.innerWidth;
200
+
201
+ const clips = scrollOnly ? /(scroll|auto)/ : /(hidden|scroll|auto)/;
202
+ let current = el?.parentElement ?? null;
203
+
204
+ while (current && current !== view.document?.documentElement) {
205
+ if (current === view.document?.body) break;
206
+ const { overflowX, overflowY } = view.getComputedStyle(current);
207
+ // Clamp an axis only when that axis really clips. A strip that scrolls sideways must
208
+ // not clamp the vertical one, or a panel flips for a boundary it never touched.
209
+ const clipsY = clips.test(overflowY) && current.scrollHeight > current.clientHeight;
210
+ const clipsX = clips.test(overflowX) && current.scrollWidth > current.clientWidth;
211
+ if (clipsY || clipsX) {
212
+ const box = current.getBoundingClientRect();
213
+ if (clipsY) { top = Math.max(top, box.top); bottom = Math.min(bottom, box.bottom); }
214
+ if (clipsX) { left = Math.max(left, box.left); right = Math.min(right, box.right); }
215
+ }
216
+ current = current.parentElement;
217
+ }
218
+
219
+ return { top, bottom, left, right };
220
+ }
221
+
222
+ /**
223
+ * The ancestor that quietly breaks `position: fixed`, if there is one.
224
+ *
225
+ * `transform`, `filter`, `perspective`, `backdrop-filter`, `contain: paint` and
226
+ * `will-change` on any of those make an element the containing block for its fixed
227
+ * descendants — so the panel stops being positioned against the viewport and starts being
228
+ * positioned, and clipped, against that ancestor. React solves this with a portal to
229
+ * <body>. Marko 5 has no portal: a node moved to <body> is put back where the template
230
+ * says it belongs on the component's next re-render, and a floating panel re-renders every
231
+ * time it opens. Measured in the portal, 2026-09-10.
232
+ *
233
+ * So SDS positions with `fixed` and reports the case it cannot handle instead of drawing
234
+ * the panel in the wrong place with no explanation.
235
+ */
236
+ export function findFixedContainingBlock(el, view = globalThis) {
237
+ let current = el?.parentElement ?? null;
238
+ while (current && current !== view.document?.documentElement) {
239
+ const style = view.getComputedStyle(current);
240
+ const breaks =
241
+ (style.transform && style.transform !== 'none') ||
242
+ (style.filter && style.filter !== 'none') ||
243
+ (style.perspective && style.perspective !== 'none') ||
244
+ (style.backdropFilter && style.backdropFilter !== 'none') ||
245
+ /paint|strict|content/.test(style.contain || '') ||
246
+ /transform|filter|perspective/.test(style.willChange || '');
247
+ if (breaks) return current;
248
+ current = current.parentElement;
249
+ }
250
+ return null;
251
+ }