@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.
- package/README.md +129 -0
- package/components/Accordion/Accordion.css +186 -0
- package/components/Accordion/accordion-variants.js +17 -0
- package/components/Accordion/accordion.marko +264 -0
- package/components/AccordionGroup/AccordionGroup.css +35 -0
- package/components/AccordionGroup/accordion-group.marko +17 -0
- package/components/Alert/Alert.css +104 -0
- package/components/Alert/alert-variants.js +35 -0
- package/components/Alert/alert.marko +94 -0
- package/components/AttachmentList/AttachmentList.css +92 -0
- package/components/AttachmentList/attachment-list.marko +51 -0
- package/components/AttachmentList/attachment-row.marko +167 -0
- package/components/Avatar/Avatar.css +110 -0
- package/components/Avatar/avatar-variants.js +12 -0
- package/components/Avatar/avatar.marko +71 -0
- package/components/Badge/Badge.css +381 -0
- package/components/Badge/badge-dismissible.marko +143 -0
- package/components/Badge/badge-variants.js +35 -0
- package/components/Badge/badge.marko +134 -0
- package/components/BottomSheet/BottomSheet.css +300 -0
- package/components/BottomSheet/bottom-sheet.marko +340 -0
- package/components/BulkActionBar/BulkActionBar.css +88 -0
- package/components/BulkActionBar/bulk-action-bar.marko +186 -0
- package/components/Button/Button.css +340 -0
- package/components/Button/button-variants.js +25 -0
- package/components/Button/button.marko +139 -0
- package/components/Card/Card.css +391 -0
- package/components/Card/card-body.marko +5 -0
- package/components/Card/card-collapsible.marko +94 -0
- package/components/Card/card-footer.marko +12 -0
- package/components/Card/card-header.marko +30 -0
- package/components/Card/card-info-pair.marko +17 -0
- package/components/Card/card-info-row.marko +12 -0
- package/components/Card/card-section.marko +14 -0
- package/components/Card/card-variants.js +24 -0
- package/components/Card/card.marko +124 -0
- package/components/Checkbox/Checkbox.css +249 -0
- package/components/Checkbox/checkbox-variants.js +21 -0
- package/components/Checkbox/checkbox.marko +128 -0
- package/components/ColorPicker/ColorPicker.css +253 -0
- package/components/ColorPicker/color-maths.js +104 -0
- package/components/ColorPicker/color-picker.marko +474 -0
- package/components/DatePicker/DatePicker.css +429 -0
- package/components/DatePicker/date-logic.js +225 -0
- package/components/DatePicker/date-picker.marko +492 -0
- package/components/Drawer/Drawer.css +174 -0
- package/components/Drawer/drawer.marko +262 -0
- package/components/Floating/Floating.css +28 -0
- package/components/Floating/floating-position.js +251 -0
- package/components/Floating/floating.marko +291 -0
- package/components/Form/Form.css +15 -0
- package/components/Form/form-context.js +69 -0
- package/components/Form/form.marko +73 -0
- package/components/GradientPicker/GradientPicker.css +179 -0
- package/components/GradientPicker/gradient-picker.marko +398 -0
- package/components/Grid/Grid.css +161 -0
- package/components/Grid/col.marko +36 -0
- package/components/Grid/grid-variants.js +43 -0
- package/components/Grid/grid.marko +25 -0
- package/components/Header/Header.css +457 -0
- package/components/Header/header.marko +523 -0
- package/components/Icon/Icon.css +44 -0
- package/components/Icon/icon-colors.js +31 -0
- package/components/Icon/icon.marko +56 -0
- package/components/IconButton/IconButton.css +85 -0
- package/components/IconButton/icon-button-variants.js +18 -0
- package/components/IconButton/icon-button.marko +103 -0
- package/components/Incrementer/Incrementer.css +94 -0
- package/components/Incrementer/incrementer.marko +70 -0
- package/components/Input/Input.css +547 -0
- package/components/Input/input-countries.js +109 -0
- package/components/Input/input-masks.js +218 -0
- package/components/Input/input-types.js +194 -0
- package/components/Input/input.marko +530 -0
- package/components/Menu/Menu.css +651 -0
- package/components/Menu/menu-variants.js +89 -0
- package/components/Menu/menu.marko +560 -0
- package/components/Modal/Modal.css +160 -0
- package/components/Modal/modal.marko +296 -0
- package/components/Nav/Nav.css +298 -0
- package/components/Nav/nav-variants.js +23 -0
- package/components/Nav/nav.marko +164 -0
- package/components/Notification/Notification.css +78 -0
- package/components/Notification/notification-variants.js +30 -0
- package/components/Notification/notification.marko +55 -0
- package/components/Pagination/Pagination.css +78 -0
- package/components/Pagination/pagination-pages.js +66 -0
- package/components/Pagination/pagination.marko +141 -0
- package/components/Pill/Pill.css +193 -0
- package/components/Pill/pill-variants.js +16 -0
- package/components/Pill/pill.marko +165 -0
- package/components/Popover/Popover.css +121 -0
- package/components/Popover/popover.marko +96 -0
- package/components/Radio/Radio.css +230 -0
- package/components/Radio/radio-variants.js +16 -0
- package/components/Radio/radio.marko +93 -0
- package/components/RadioGroup/RadioGroup.css +32 -0
- package/components/RadioGroup/radio-group.marko +93 -0
- package/components/SearchBar/SearchBar.css +181 -0
- package/components/SearchBar/search-bar-variants.js +24 -0
- package/components/SearchBar/search-bar.marko +157 -0
- package/components/Select/Select.css +427 -0
- package/components/Select/select-options.js +163 -0
- package/components/Select/select.marko +449 -0
- package/components/Separator/Separator.css +22 -0
- package/components/Separator/separator.marko +14 -0
- package/components/Skeleton/Skeleton.css +100 -0
- package/components/Skeleton/skeleton-avatar.marko +28 -0
- package/components/Skeleton/skeleton-button.marko +22 -0
- package/components/Skeleton/skeleton-image.marko +20 -0
- package/components/Skeleton/skeleton-input.marko +22 -0
- package/components/Skeleton/skeleton-variants.js +16 -0
- package/components/Skeleton/skeleton.marko +53 -0
- package/components/Spinner/Spinner.css +173 -0
- package/components/Spinner/spinner-geometry.js +39 -0
- package/components/Spinner/spinner-overlay.marko +97 -0
- package/components/Spinner/spinner.marko +88 -0
- package/components/StatCard/StatCard.css +90 -0
- package/components/StatCard/stat-card-group.marko +5 -0
- package/components/StatCard/stat-card.marko +57 -0
- package/components/Switch/Switch.css +209 -0
- package/components/Switch/switch.marko +99 -0
- package/components/Table/Table.css +270 -0
- package/components/Table/table-cell.marko +28 -0
- package/components/Table/table-header.marko +27 -0
- package/components/Table/table-logic.js +129 -0
- package/components/Table/table.marko +372 -0
- package/components/Text/Text.css +79 -0
- package/components/Text/text-variants.js +25 -0
- package/components/Text/text.marko +48 -0
- package/components/TextEditor/TextEditor.css +78 -0
- package/components/TextEditor/safe-url.js +50 -0
- package/components/TextEditor/text-editor-commands.js +22 -0
- package/components/TextEditor/text-editor.marko +253 -0
- package/components/Toast/Toast.css +88 -0
- package/components/Toast/toast-api.js +19 -0
- package/components/Toast/toast-store.js +118 -0
- package/components/Toast/toast-variants.js +16 -0
- package/components/Toast/toast.marko +93 -0
- package/components/Toast/toaster.marko +70 -0
- package/components/Toggle/Toggle.css +169 -0
- package/components/Toggle/toggle.marko +93 -0
- package/components/Tooltip/Tooltip.css +115 -0
- package/components/Tooltip/tooltip.marko +77 -0
- package/components/Uploader/Uploader.css +180 -0
- package/components/Uploader/crop-geometry.js +161 -0
- package/components/Uploader/uploader.marko +269 -0
- package/foundations/Scrollbar/Scrollbar.css +26 -0
- package/foundations/attrs.js +27 -0
- package/foundations/breakpoints.js +27 -0
- package/foundations/contrast.js +74 -0
- package/foundations/custom-scheme.js +222 -0
- package/foundations/overlay.js +108 -0
- package/foundations/styles.css +284 -0
- package/foundations/text.js +22 -0
- package/foundations/theme-server.js +81 -0
- package/foundations/theme.js +42 -0
- package/foundations/tokens.css +1656 -0
- package/icons/custom/custom-video-horizontal-filled.marko +6 -0
- package/icons/custom/custom-video-horizontal-regular.marko +6 -0
- package/icons/custom/custom-video-tour-3d-filled.marko +7 -0
- package/icons/custom/custom-video-tour-3d-regular.marko +9 -0
- package/icons/custom/custom-video-vertical-filled.marko +6 -0
- package/icons/custom/custom-video-vertical-regular.marko +6 -0
- package/icons/generated/fluent-add-regular.marko +3 -0
- package/icons/generated/fluent-alert-regular.marko +3 -0
- package/icons/generated/fluent-apps-regular.marko +3 -0
- package/icons/generated/fluent-arrow-download-regular.marko +3 -0
- package/icons/generated/fluent-arrow-left-regular.marko +3 -0
- package/icons/generated/fluent-arrow-right-regular.marko +3 -0
- package/icons/generated/fluent-arrow-rotate-clockwise-regular.marko +3 -0
- package/icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko +3 -0
- package/icons/generated/fluent-border-all-regular.marko +3 -0
- package/icons/generated/fluent-calendar-regular.marko +3 -0
- package/icons/generated/fluent-checkmark-circle-filled.marko +3 -0
- package/icons/generated/fluent-checkmark-regular.marko +3 -0
- package/icons/generated/fluent-chevron-down-regular.marko +3 -0
- package/icons/generated/fluent-chevron-left-regular.marko +3 -0
- package/icons/generated/fluent-chevron-right-regular.marko +3 -0
- package/icons/generated/fluent-chevron-up-regular.marko +3 -0
- package/icons/generated/fluent-circle-half-fill-regular.marko +3 -0
- package/icons/generated/fluent-clock-regular.marko +3 -0
- package/icons/generated/fluent-color-regular.marko +3 -0
- package/icons/generated/fluent-crop-regular.marko +3 -0
- package/icons/generated/fluent-dark-theme-regular.marko +3 -0
- package/icons/generated/fluent-delete-regular.marko +3 -0
- package/icons/generated/fluent-desktop-regular.marko +3 -0
- package/icons/generated/fluent-dismiss-circle-filled.marko +3 -0
- package/icons/generated/fluent-dismiss-regular.marko +3 -0
- package/icons/generated/fluent-document-pdf-regular.marko +3 -0
- package/icons/generated/fluent-document-regular.marko +3 -0
- package/icons/generated/fluent-eye-off-regular.marko +3 -0
- package/icons/generated/fluent-eye-regular.marko +3 -0
- package/icons/generated/fluent-eyedropper-regular.marko +3 -0
- package/icons/generated/fluent-grid-regular.marko +3 -0
- package/icons/generated/fluent-history-regular.marko +3 -0
- package/icons/generated/fluent-home-regular.marko +3 -0
- package/icons/generated/fluent-image-regular.marko +3 -0
- package/icons/generated/fluent-info-regular.marko +3 -0
- package/icons/generated/fluent-link-regular.marko +3 -0
- package/icons/generated/fluent-more-horizontal-regular.marko +3 -0
- package/icons/generated/fluent-navigation-regular.marko +3 -0
- package/icons/generated/fluent-paint-brush-regular.marko +3 -0
- package/icons/generated/fluent-question-circle-regular.marko +3 -0
- package/icons/generated/fluent-search-regular.marko +3 -0
- package/icons/generated/fluent-settings-regular.marko +3 -0
- package/icons/generated/fluent-square-hint-regular.marko +3 -0
- package/icons/generated/fluent-subtract-regular.marko +3 -0
- package/icons/generated/fluent-text-align-center-regular.marko +3 -0
- package/icons/generated/fluent-text-align-left-regular.marko +3 -0
- package/icons/generated/fluent-text-align-right-regular.marko +3 -0
- package/icons/generated/fluent-text-bold-regular.marko +3 -0
- package/icons/generated/fluent-text-bullet-list-regular.marko +3 -0
- package/icons/generated/fluent-text-font-regular.marko +3 -0
- package/icons/generated/fluent-text-font-size-regular.marko +3 -0
- package/icons/generated/fluent-text-italic-regular.marko +3 -0
- package/icons/generated/fluent-text-number-list-ltr-regular.marko +3 -0
- package/icons/generated/fluent-video-regular.marko +3 -0
- package/icons/generated/fluent-warning-filled.marko +3 -0
- package/icons/generated/fluent-weather-moon-regular.marko +3 -0
- package/icons/generated/fluent-weather-sunny-regular.marko +3 -0
- package/icons/generated/fluent-zoom-in-regular.marko +3 -0
- package/icons/generated/fluent-zoom-out-regular.marko +3 -0
- package/icons/manifest.json +61 -0
- package/icons/social/social-facebook-color.marko +7 -0
- package/icons/social/social-facebook.marko +6 -0
- package/icons/social/social-google-color.marko +20 -0
- package/icons/social/social-google.marko +6 -0
- package/icons/social/social-instagram-color.marko +16 -0
- package/icons/social/social-instagram.marko +6 -0
- package/icons/social/social-linkedin-color.marko +8 -0
- package/icons/social/social-linkedin.marko +8 -0
- package/icons/social/social-messenger-color.marko +15 -0
- package/icons/social/social-messenger.marko +6 -0
- package/icons/social/social-pinterest-color.marko +6 -0
- package/icons/social/social-pinterest.marko +6 -0
- package/icons/social/social-telegram-color.marko +7 -0
- package/icons/social/social-telegram.marko +6 -0
- package/icons/social/social-whatsapp-color.marko +8 -0
- package/icons/social/social-whatsapp.marko +7 -0
- package/icons/social/social-x-color.marko +7 -0
- package/icons/social/social-x.marko +6 -0
- package/icons/social/social-youtube-color.marko +7 -0
- package/icons/social/social-youtube.marko +6 -0
- package/marko.json +203 -0
- 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>
|