@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,160 @@
1
+ /* SDS Modal.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Modal/Modal.css
3
+ by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
4
+ *//* ============================================================
5
+ SDS Modal
6
+ Size: 600px default | radius: --radius-md (12px)
7
+ Shadow: 0 0 2px / 0 0 16px rgba(26,26,26,0.30)
8
+ ============================================================ */
9
+
10
+ /* ── Overlay ── */
11
+ .sds-modal-overlay {
12
+ position: fixed;
13
+ inset: 0;
14
+ background: var(--color-opacity-50);
15
+ z-index: var(--z-modal);
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ animation: sds-overlay-in 120ms ease;
20
+ /* Allow click-through to close */
21
+ padding: var(--spacing-6);
22
+ box-sizing: border-box;
23
+ }
24
+ @keyframes sds-overlay-in {
25
+ from { opacity: 0; }
26
+ to { opacity: 1; }
27
+ }
28
+ /* ── Container ── */
29
+ .sds-modal {
30
+ position: relative;
31
+ background: var(--component-bg-color);
32
+ /* Figma: corner-default = 12px = --radius-md */
33
+ border-radius: var(--radius-md);
34
+ box-shadow:
35
+ 0px 0px 2px var(--color-opacity-30),
36
+ 0px 0px 16px var(--color-opacity-30);
37
+ width: 600px;
38
+ max-width: 100%;
39
+ /* Flexible height: min for content, max so it doesn't overflow */
40
+ max-height: calc(100vh - 96px);
41
+ display: flex;
42
+ flex-direction: column;
43
+ overflow: hidden;
44
+ animation: sds-modal-in 200ms var(--ease-standard);
45
+ }
46
+ @keyframes sds-modal-in {
47
+ from { opacity: 0; transform: scale(0.96) translateY(8px); }
48
+ to { opacity: 1; transform: scale(1) translateY(0); }
49
+ }
50
+ /* ── Header: pt-[24px] pb-[24px] px-[24px] ── */
51
+ .sds-modal__header {
52
+ display: flex;
53
+ align-items: flex-start;
54
+ gap: var(--spacing-4);
55
+ padding: var(--spacing-6);
56
+ flex-shrink: 0;
57
+ }
58
+ .sds-modal__title-group {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: var(--spacing-4);
62
+ flex: 1;
63
+ min-width: 0;
64
+ }
65
+ .sds-modal__title-group .sds-tooltip-wrapper {
66
+ flex-shrink: 0;
67
+ }
68
+ .sds-modal__title {
69
+ overflow: hidden;
70
+ white-space: nowrap;
71
+ text-overflow: ellipsis;
72
+ }
73
+ .sds-modal__header-actions {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: var(--spacing-4);
77
+ flex-shrink: 0;
78
+ }
79
+ /* ── Body ── */
80
+ .sds-modal__body {
81
+ flex: 1;
82
+ overflow-y: auto;
83
+ padding: 0 var(--spacing-6);
84
+ }
85
+ /* ── Footer: pt-[24px] pb-[24px] px-[24px] ── */
86
+ .sds-modal__footer {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: flex-end;
90
+ /* 16px between the buttons, decided by Rafael looking at a three-button footer. KDLS
91
+ uses 24 and the Card here already used 16, so the system disagreed with itself; 16 is
92
+ the side that keeps a row of actions reading as one group. */
93
+ gap: var(--spacing-4);
94
+ padding: var(--spacing-6);
95
+ flex-shrink: 0;
96
+ }
97
+ /* ── Dark mode ── */
98
+ /* The veil is black in both themes. --color-opacity-* inverts with the theme, so in dark
99
+ mode it would light the screen up instead of dimming it — which is why KDLS wrote a
100
+ literal here. --color-scrim is that literal, as a token. */
101
+ [data-theme="dark"] .sds-modal-overlay {
102
+ background: var(--color-scrim);
103
+ }
104
+ /* Same two-layer shadow as the light theme, darker. --color-opacity-* inverts, so reading
105
+ it here would draw a white glow around the panel. */
106
+ [data-theme="dark"] .sds-modal {
107
+ background: var(--component-bg-color);
108
+ box-shadow:
109
+ 0px 0px 2px var(--color-scrim),
110
+ 0px 0px 16px var(--color-scrim);
111
+ }
112
+ /* ── Mobile: bottom sheet (≤799px) ── */
113
+ @media (max-width: 799px) {
114
+ .sds-modal-overlay {
115
+ align-items: flex-end;
116
+ padding: 0;
117
+ }
118
+ .sds-modal {
119
+ width: 100% !important;
120
+ max-height: 90vh;
121
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
122
+ animation: sds-modal-in-mobile 240ms var(--ease-spring);
123
+ }
124
+ /* Handle bar */
125
+ .sds-modal::before {
126
+ content: '';
127
+ display: block;
128
+ width: var(--spacing-10);
129
+ height: var(--spacing-1);
130
+ background: var(--color-neutral-30);
131
+ border-radius: var(--radius-full);
132
+ margin: var(--spacing-3) auto 0;
133
+ flex-shrink: 0;
134
+ }
135
+ .sds-modal__header {
136
+ padding-top: var(--spacing-4);
137
+ }
138
+ .sds-modal__footer {
139
+ flex-direction: column-reverse;
140
+ align-items: stretch;
141
+ }
142
+ .sds-modal__footer .sds-button {
143
+ width: 100%;
144
+ }
145
+ }
146
+ @keyframes sds-modal-in-mobile {
147
+ from { transform: translateY(100%); }
148
+ to { transform: translateY(0); }
149
+ }
150
+
151
+ /* The confirmation, drawn inside the modal's own overlay.
152
+ A second veil over the first would double the dimming; this one only holds the panel. */
153
+ .sds-modal-overlay--confirm {
154
+ background: transparent;
155
+ z-index: 1;
156
+ }
157
+
158
+ .sds-modal--confirm {
159
+ width: 420px;
160
+ }
@@ -0,0 +1,296 @@
1
+ <!-- sds-modal — ported from kdls-design-system v2.5.1 (0f3edf8) components/Modal/Modal.tsx.
2
+
3
+ A dialog over a veil: title, body, up to three actions, and an optional confirmation
4
+ before it closes.
5
+
6
+ Divergences:
7
+
8
+ It manages focus, which KDLS's does not manage at all. An open modal with no focus
9
+ trap is one a keyboard user Tabs straight out of, into the page behind it, with no
10
+ visible indication of where they are — and no way back but Tab through the whole
11
+ document. The shared machinery is in foundations/overlay.js, because Drawer and
12
+ BottomSheet need exactly the same thing and KDLS writes a third of it three times.
13
+
14
+ No portal. KDLS renders through createPortal into <body>; Marko 5 has no portal — see
15
+ components/Floating/floating.marko for the measurement. It does not matter as much
16
+ here: the overlay is position:fixed and covers the viewport, so the only thing a
17
+ portal would buy is escaping an ancestor that has a transform, a filter or
18
+ contain:paint. sds-floating warns about that case and this inherits the same limit.
19
+
20
+ Modal.confirm() is not ported. It is an imperative call that mounts a second React
21
+ root; the Marko equivalent is a different design, not a translation, and nothing in
22
+ this system needs it yet. The declarative confirmation — confirmClose — is here,
23
+ which is what the flow actually uses.
24
+
25
+ The body scroll lock is counted rather than set and unset, so a modal that opens a
26
+ drawer does not get the page's scrollbar back the moment the drawer closes.
27
+
28
+ The four confirm defaults — `confirmTitle`, `confirmMessage`, `confirmActionLabel`
29
+ and `confirmCancelLabel` — are reworded, and this is the one place in the port where the
30
+ wording is itself the divergence — so it is Rafael's to keep or revert.
31
+
32
+ KDLS asks "Fechar sem salvar?" over buttons labelled "Fechar" and "Cancelar". In a dialog
33
+ whose whole subject is closing, "Cancelar" does not say what it cancels: the closing, or
34
+ the editing it would discard. The rewrite names the outcome on each button — "Descartar"
35
+ against "Continuar editando" — which is what a destructive confirmation is for, and the
36
+ message says what is lost rather than narrating where it happened.
37
+ -->
38
+
39
+ import { assertPlacement } from "../Floating/floating-position.js";
40
+ import { lockScroll, unlockScroll, trapFocus } from "../../foundations/overlay.js";
41
+ import dismissIcon from "../../icons/generated/fluent-dismiss-regular.marko";
42
+
43
+ class {
44
+ onCreate() {
45
+ this.state = { confirming: false, running: {} };
46
+ this.release = null;
47
+ }
48
+
49
+ onMount() {
50
+ this.onKeyDown = (event) => {
51
+ if (event.key !== "Escape" || !this.input.isOpen) return;
52
+ event.stopPropagation();
53
+ // With the confirmation up, Escape dismisses the QUESTION. It used to reach
54
+ // `requestClose`, which sees `confirming` already true and closes — so the key you
55
+ // press to back out of a dialog discarded the work that dialog exists to protect.
56
+ // Measured on the portal: one Escape and both the question and the modal were gone.
57
+ // Escape always undoes the most recent thing that opened. Same fault as the Drawer.
58
+ if (this.state.confirming) {
59
+ this.cancelConfirm();
60
+ return;
61
+ }
62
+ this.requestClose();
63
+ };
64
+ document.addEventListener("keydown", this.onKeyDown);
65
+ if (this.input.isOpen) this.open();
66
+ }
67
+
68
+ onUpdate() {
69
+ if (this.wantsConfirmFocus && this.state.confirming) {
70
+ this.wantsConfirmFocus = false;
71
+ this.retrap(this.getEl("confirm"));
72
+ }
73
+ if (this.input.isOpen && !this.locked) this.open();
74
+ else if (!this.input.isOpen && this.locked) this.shut();
75
+ }
76
+
77
+ onDestroy() {
78
+ document.removeEventListener("keydown", this.onKeyDown);
79
+ this.shut();
80
+ }
81
+
82
+ open() {
83
+ this.locked = true;
84
+ lockScroll();
85
+ // One frame, so the panel is in the document and has something focusable in it.
86
+ requestAnimationFrame(() => {
87
+ if (!this.input.isOpen) return;
88
+ this.release = trapFocus(this.getEl("panel"));
89
+ });
90
+ }
91
+
92
+ shut() {
93
+ if (!this.locked) return;
94
+ this.locked = false;
95
+ unlockScroll();
96
+ this.release?.();
97
+ this.release = null;
98
+ this.state.confirming = false;
99
+ }
100
+
101
+ /** The close a visitor asked for, which may need confirming first. */
102
+ requestClose() {
103
+ if (this.input.confirmClose && !this.state.confirming) {
104
+ this.state.confirming = true;
105
+ // On update, once the dialog is in the document.
106
+ this.wantsConfirmFocus = true;
107
+ return;
108
+ }
109
+ this.input.onClose?.();
110
+ }
111
+
112
+ /**
113
+ * A click inside the panel is the panel's, and stops there.
114
+ *
115
+ * The overlay's handler already ignores clicks that did not land on it, so dismissal was
116
+ * correct — but the event still bubbled out of the overlay and into whatever the
117
+ * consumer had mounted the modal inside. A modal opened from inside a clickable Card
118
+ * re-triggered that card on every click in the dialog. KDLS stops it at the panel.
119
+ */
120
+ stopInside(event) {
121
+ event.stopPropagation();
122
+ }
123
+
124
+ handleOverlayClick(event) {
125
+ if (this.input.disableOverlayClose) return;
126
+ if (event.target !== this.getEl("overlay")) return;
127
+ this.requestClose();
128
+ }
129
+
130
+ /**
131
+ * Move the focus trap onto whichever of the two panels is on top.
132
+ *
133
+ * It was installed on the main panel and stayed there. The confirmation is a SIBLING of
134
+ * that panel inside the overlay, not a descendant, so it sat outside the trap: Tab could
135
+ * not reach "Descartar" or "Continuar editando" and a keyboard user could not answer the
136
+ * question at all.
137
+ */
138
+ retrap(element) {
139
+ this.release?.();
140
+ this.release = element ? trapFocus(element) : null;
141
+ }
142
+
143
+ cancelConfirm() {
144
+ this.state.confirming = false;
145
+ this.retrap(this.getEl("panel"));
146
+ }
147
+
148
+ /**
149
+ * Run a footer action, and show a spinner on its button while it is still running.
150
+ *
151
+ * KDLS turns the loading state on by itself whenever `onPrimary` returns a Promise, and
152
+ * documents it on the prop. The port passed the handler straight to the button, so
153
+ * `isLoading` only ever reflected the explicit `primaryLoading` — a caller who wrote an
154
+ * async save got no feedback at all, and nothing stopped them pressing it again while
155
+ * the first save was in flight.
156
+ *
157
+ * `sds-badge-dismissible` already does this; the shape is the same.
158
+ */
159
+ async run(which, handler) {
160
+ const result = handler?.();
161
+ if (!(result instanceof Promise)) return;
162
+ this.state.running = { ...this.state.running, [which]: true };
163
+ try {
164
+ await result;
165
+ } finally {
166
+ this.state.running = { ...this.state.running, [which]: false };
167
+ }
168
+ }
169
+
170
+ /** Loading is the caller's explicit flag OR a promise this component is still waiting on. */
171
+ busy(which, explicit) {
172
+ return Boolean(explicit || this.state.running?.[which]);
173
+ }
174
+
175
+ confirmAndClose() {
176
+ this.state.confirming = false;
177
+ this.wantsConfirmFocus = false;
178
+ this.input.onClose?.();
179
+ }
180
+ }
181
+
182
+ $ {
183
+ const {
184
+ isOpen,
185
+ title,
186
+ titleTooltip,
187
+ titleTooltipPosition = "top",
188
+ tertiaryLabel, onTertiary, tertiaryLoading = false, tertiaryDisabled, tertiaryDisabledTooltip, tertiaryDisabledTooltipPosition = "top",
189
+ secondaryLabel, onSecondary, secondaryLoading = false, secondaryDisabled, secondaryDisabledTooltip, secondaryDisabledTooltipPosition = "top",
190
+ primaryLabel, onPrimary, primaryLoading = false, primaryDisabled, primaryDisabledTooltip, primaryDisabledTooltipPosition = "top",
191
+ closeLabel = "Fechar",
192
+ zIndex,
193
+ confirmTitle = "Descartar alterações?",
194
+ confirmMessage = "As alterações feitas não serão salvas.",
195
+ confirmActionLabel = "Descartar",
196
+ confirmCancelLabel = "Continuar editando",
197
+ class: extraClass = "",
198
+ } = input;
199
+
200
+ assertPlacement(titleTooltipPosition);
201
+ assertPlacement(tertiaryDisabledTooltipPosition);
202
+ assertPlacement(secondaryDisabledTooltipPosition);
203
+ assertPlacement(primaryDisabledTooltipPosition);
204
+
205
+ if (!title) throw new Error("sds-modal needs a `title` — it is the dialog's accessible name.");
206
+
207
+ const hasFooter = Boolean(input.footer || tertiaryLabel || secondaryLabel || primaryLabel);
208
+ const overlayStyle = zIndex !== undefined ? `z-index:${zIndex}` : null;
209
+ }
210
+
211
+ <if(isOpen)>
212
+ <div
213
+ key="overlay"
214
+ class="sds-modal-overlay"
215
+ style=overlayStyle
216
+ role="dialog"
217
+ aria-modal="true"
218
+ aria-label=title
219
+ on-click("handleOverlayClick")
220
+ >
221
+ <div key="panel" class=`sds-modal ${extraClass}`.trim() on-click("stopInside")>
222
+ <div class="sds-modal__header">
223
+ <div class="sds-modal__title-group">
224
+ <sds-text as="span" variant="titleMd" class="sds-modal__title">${title}</sds-text>
225
+ <if(titleTooltip)>
226
+ <sds-tooltip content=titleTooltip placement=titleTooltipPosition/>
227
+ </if>
228
+ </div>
229
+ <div class="sds-modal__header-actions">
230
+ <sds-icon-button variant="neutral" aria-label=closeLabel onClick=() => component.requestClose()>
231
+ <@icon><${dismissIcon}/></@icon>
232
+ </sds-icon-button>
233
+ </div>
234
+ </div>
235
+
236
+ <if(input.renderBody)>
237
+ <div class="sds-modal__body sds-scroll sds-scroll--thin">
238
+ <${input.renderBody}/>
239
+ </div>
240
+ </if>
241
+
242
+ <if(hasFooter)>
243
+ <div class="sds-modal__footer">
244
+ <if(input.footer)>
245
+ <${input.footer}/>
246
+ </if>
247
+ <else>
248
+ <if(tertiaryLabel)>
249
+ <sds-button variant="link" size="md" label=tertiaryLabel
250
+ onClick=() => component.run("tertiary", onTertiary)
251
+ isLoading=component.busy("tertiary", tertiaryLoading) disabled=tertiaryDisabled
252
+ disabledTooltip=tertiaryDisabledTooltip disabledTooltipPosition=tertiaryDisabledTooltipPosition/>
253
+ </if>
254
+ <if(secondaryLabel)>
255
+ <sds-button variant="ghost" size="md" label=secondaryLabel
256
+ onClick=() => component.run("secondary", onSecondary)
257
+ isLoading=component.busy("secondary", secondaryLoading) disabled=secondaryDisabled
258
+ disabledTooltip=secondaryDisabledTooltip disabledTooltipPosition=secondaryDisabledTooltipPosition/>
259
+ </if>
260
+ <if(primaryLabel)>
261
+ <sds-button variant="primary" size="md" label=primaryLabel
262
+ onClick=() => component.run("primary", onPrimary)
263
+ isLoading=component.busy("primary", primaryLoading) disabled=primaryDisabled
264
+ disabledTooltip=primaryDisabledTooltip disabledTooltipPosition=primaryDisabledTooltipPosition/>
265
+ </if>
266
+ </else>
267
+ </div>
268
+ </if>
269
+ </div>
270
+
271
+ <!-- The confirmation is drawn inside this overlay rather than as a second modal:
272
+ nesting the component in itself would take a second scroll lock and a second focus
273
+ trap over the first, and the first would then restore focus to a panel that no
274
+ longer exists. -->
275
+ <if(state.confirming)>
276
+ <div class="sds-modal-overlay sds-modal-overlay--confirm" role="dialog" aria-modal="true" aria-label=confirmTitle>
277
+ <div key="confirm" class="sds-modal sds-modal--confirm">
278
+ <div class="sds-modal__header">
279
+ <div class="sds-modal__title-group">
280
+ <sds-text as="span" variant="titleMd" class="sds-modal__title">${confirmTitle}</sds-text>
281
+ </div>
282
+ </div>
283
+ <div class="sds-modal__body">
284
+ <sds-text as="p" variant="bodyMd">${confirmMessage}</sds-text>
285
+ </div>
286
+ <div class="sds-modal__footer">
287
+ <sds-button variant="ghost" size="md" label=confirmCancelLabel
288
+ onClick=() => component.cancelConfirm()/>
289
+ <sds-button variant="primary" size="md" label=confirmActionLabel
290
+ onClick=() => component.confirmAndClose()/>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </if>
295
+ </div>
296
+ </if>