@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,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
|
+
}
|