@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,180 @@
|
|
|
1
|
+
/* SDS Uploader.
|
|
2
|
+
Ported from kdls-design-system v2.5.1 (0f3edf8) components/Uploader/Uploader.css
|
|
3
|
+
by scripts/port-css.mjs. Divergences beyond the mechanical rename are noted inline.
|
|
4
|
+
|
|
5
|
+
Rules removed with the brand families they targeted:
|
|
6
|
+
[data-color-scheme="lya"] .sds-uploader--dragging
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* Three literals snapped to the system. The overlay that dims a preview is --color-scrim,
|
|
10
|
+
black in both themes for the same reason the modal's veil is. The two hover states on it
|
|
11
|
+
sit on that dark overlay rather than on the page, so they read --color-opacity-inverse-*,
|
|
12
|
+
the family that runs against the theme — the ordinary one would draw a dark veil on an
|
|
13
|
+
already dark surface. The alphas move from .15/.25 to the .10/.20 the scale offers. *//* ============================================================
|
|
14
|
+
SDS Uploader
|
|
15
|
+
============================================================ */
|
|
16
|
+
|
|
17
|
+
.sds-uploader {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
gap: var(--spacing-1);
|
|
23
|
+
width: 100%;
|
|
24
|
+
min-height: 140px;
|
|
25
|
+
padding: var(--spacing-5) var(--spacing-4);
|
|
26
|
+
border: 2px dashed var(--border-interactive-color);
|
|
27
|
+
border-radius: var(--radius-md);
|
|
28
|
+
background: var(--color-neutral-10);
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
font-family: var(--font-family-base);
|
|
31
|
+
font-size: var(--font-size-body-sm);
|
|
32
|
+
color: var(--body-color);
|
|
33
|
+
text-align: center;
|
|
34
|
+
transition: border-color 120ms ease, background-color 120ms ease;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|
|
37
|
+
.sds-uploader:hover,
|
|
38
|
+
.sds-uploader--dragging {
|
|
39
|
+
border-color: var(--color-primary-50-solid);
|
|
40
|
+
background: var(--color-primary-10);
|
|
41
|
+
}
|
|
42
|
+
.sds-uploader:focus-visible {
|
|
43
|
+
outline: none;
|
|
44
|
+
box-shadow: var(--focus-ring);
|
|
45
|
+
}
|
|
46
|
+
/* ── Hidden input ── */
|
|
47
|
+
.sds-uploader__input {
|
|
48
|
+
display: none;
|
|
49
|
+
}
|
|
50
|
+
/* ── Icon ── */
|
|
51
|
+
.sds-uploader__icon {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
font-size: 24px;
|
|
54
|
+
margin-bottom: var(--spacing-1);
|
|
55
|
+
}
|
|
56
|
+
/* ── Hint text ── */
|
|
57
|
+
.sds-uploader__hint {
|
|
58
|
+
color: var(--color-neutral-50);
|
|
59
|
+
}
|
|
60
|
+
/* ── Formats ── */
|
|
61
|
+
.sds-uploader__formats {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: var(--spacing-1);
|
|
65
|
+
margin-top: var(--spacing-1);
|
|
66
|
+
}
|
|
67
|
+
/* ── Filled state ── */
|
|
68
|
+
.sds-uploader--filled {
|
|
69
|
+
padding: 0;
|
|
70
|
+
border: 1px solid var(--border-interactive-color);
|
|
71
|
+
border-style: solid;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
min-height: unset;
|
|
74
|
+
cursor: default;
|
|
75
|
+
position: relative;
|
|
76
|
+
display: block;
|
|
77
|
+
}
|
|
78
|
+
.sds-uploader__preview {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 200px;
|
|
81
|
+
object-fit: cover;
|
|
82
|
+
display: block;
|
|
83
|
+
}
|
|
84
|
+
/* ── Overlay actions ── */
|
|
85
|
+
.sds-uploader__overlay {
|
|
86
|
+
position: absolute;
|
|
87
|
+
bottom: 0;
|
|
88
|
+
left: 0;
|
|
89
|
+
right: 0;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: flex-end;
|
|
93
|
+
gap: var(--spacing-1);
|
|
94
|
+
padding: var(--spacing-2) var(--spacing-3);
|
|
95
|
+
background: var(--color-scrim);
|
|
96
|
+
}
|
|
97
|
+
.sds-uploader__overlay .sds-icon-button--neutral .sds-icon-button__icon {
|
|
98
|
+
color: var(--color-white);
|
|
99
|
+
}
|
|
100
|
+
.sds-uploader__overlay .sds-icon-button--24:hover:not(:disabled)::before {
|
|
101
|
+
background: var(--color-opacity-inverse-10);
|
|
102
|
+
}
|
|
103
|
+
.sds-uploader__overlay .sds-icon-button--24:active:not(:disabled)::before {
|
|
104
|
+
background: var(--color-opacity-inverse-20);
|
|
105
|
+
}
|
|
106
|
+
/* ── Edit modal body ── */
|
|
107
|
+
.sds-uploader__edit-body {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: var(--spacing-3);
|
|
111
|
+
}
|
|
112
|
+
.sds-uploader__edit-canvas {
|
|
113
|
+
position: relative;
|
|
114
|
+
background: repeating-conic-gradient(var(--color-neutral-20) 0% 25%, var(--component-bg-color) 0% 50%)
|
|
115
|
+
0 0 / 16px 16px;
|
|
116
|
+
border: 1px solid var(--color-neutral-50);
|
|
117
|
+
border-radius: var(--radius-md);
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
min-height: 240px;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
}
|
|
124
|
+
.sds-uploader__edit-img {
|
|
125
|
+
max-width: 100%;
|
|
126
|
+
max-height: 240px;
|
|
127
|
+
object-fit: contain;
|
|
128
|
+
display: block;
|
|
129
|
+
transform-origin: center center;
|
|
130
|
+
transform: scale(var(--img-zoom, 1)) rotate(var(--img-rotate, 0deg));
|
|
131
|
+
transition: transform 200ms ease;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
}
|
|
134
|
+
/* ── Crop overlay ── */
|
|
135
|
+
.sds-uploader__crop-box {
|
|
136
|
+
position: absolute;
|
|
137
|
+
left: var(--crop-x, 10%);
|
|
138
|
+
top: var(--crop-y, 10%);
|
|
139
|
+
width: var(--crop-w, 80%);
|
|
140
|
+
height: var(--crop-h, 80%);
|
|
141
|
+
border: 2px solid var(--color-white);
|
|
142
|
+
box-shadow: 0 0 0 9999px var(--color-scrim);
|
|
143
|
+
cursor: move;
|
|
144
|
+
box-sizing: border-box;
|
|
145
|
+
}
|
|
146
|
+
.sds-uploader__crop-handle {
|
|
147
|
+
position: absolute;
|
|
148
|
+
width: 10px;
|
|
149
|
+
height: 10px;
|
|
150
|
+
background: var(--color-white);
|
|
151
|
+
border-radius: 2px;
|
|
152
|
+
box-shadow: 0 0 0 1px var(--color-scrim);
|
|
153
|
+
}
|
|
154
|
+
.sds-uploader__crop-handle--nw { top: -5px; left: -5px; cursor: nw-resize; }
|
|
155
|
+
.sds-uploader__crop-handle--ne { top: -5px; right: -5px; cursor: ne-resize; }
|
|
156
|
+
.sds-uploader__crop-handle--sw { bottom: -5px; left: -5px; cursor: sw-resize; }
|
|
157
|
+
.sds-uploader__crop-handle--se { bottom: -5px; right: -5px; cursor: se-resize; }
|
|
158
|
+
/* ── Edit controls ── */
|
|
159
|
+
.sds-uploader__edit-controls {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
gap: var(--spacing-2);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* The drop zone is a <button>, which brings the browser's own control styling — these take
|
|
167
|
+
it off. KDLS uses a div with role="button" and does not need them. */
|
|
168
|
+
button.sds-uploader {
|
|
169
|
+
appearance: none;
|
|
170
|
+
font: inherit;
|
|
171
|
+
color: inherit;
|
|
172
|
+
text-align: center;
|
|
173
|
+
width: 100%;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* A file that could not be read. KDLS has no error path at all, so the visitor is left
|
|
177
|
+
looking at an unchanged drop zone with nothing to explain it. */
|
|
178
|
+
.sds-uploader__error {
|
|
179
|
+
color: var(--color-error-50);
|
|
180
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the crop box sits and how it moves.
|
|
3
|
+
*
|
|
4
|
+
* Ported from kdls-design-system v2.5.1 (0f3edf8) components/Uploader/Uploader.tsx, where
|
|
5
|
+
* it lives inside two closures and a useEffect. Pulled out because it is pure arithmetic
|
|
6
|
+
* with a dozen edge cases — the corner that is being dragged decides which side moves, a
|
|
7
|
+
* ratio constraint makes the height follow the width, and everything has to stay inside
|
|
8
|
+
* the canvas — and every one of those is a place an off-by-one hides behind a picture that
|
|
9
|
+
* looks roughly right.
|
|
10
|
+
*
|
|
11
|
+
* Everything is in percentages of the canvas, which is how the CSS positions it. The only
|
|
12
|
+
* place pixels appear is in converting a ratio, because an aspect ratio is a statement
|
|
13
|
+
* about pixels and the canvas is rarely square.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The smallest the box may get, in percent, so it cannot be dragged out of existence. */
|
|
17
|
+
export const MIN_SIZE = 10;
|
|
18
|
+
|
|
19
|
+
export const HANDLES = ['nw', 'ne', 'sw', 'se', 'center'];
|
|
20
|
+
|
|
21
|
+
/** The default box when there is no constraint: centred, filling most of the canvas. */
|
|
22
|
+
export const FREE_BOX = { x: 10, y: 10, w: 80, h: 80 };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An aspect ratio expressed in the percentage space the box lives in.
|
|
26
|
+
*
|
|
27
|
+
* "16:9" is a statement about pixels. The box is measured in percentages of a canvas that
|
|
28
|
+
* is usually not square, so the ratio has to be scaled by the canvas's own proportions or
|
|
29
|
+
* a 16:9 crop comes out the wrong shape on every canvas but one.
|
|
30
|
+
*/
|
|
31
|
+
export function ratioInPercentSpace(crop, rect) {
|
|
32
|
+
if (!crop || !rect?.width || !rect?.height) return null;
|
|
33
|
+
|
|
34
|
+
if (typeof crop === 'string') {
|
|
35
|
+
const parts = crop.split(':').map(Number);
|
|
36
|
+
if (parts.length !== 2 || parts.some(Number.isNaN) || !parts[1]) return null;
|
|
37
|
+
return (parts[0] / parts[1]) * (rect.height / rect.width);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof crop === 'object' && crop.width && crop.height) {
|
|
41
|
+
return (crop.width / crop.height) * (rect.height / rect.width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The box to start from: centred, and as large as it can be at 80% of the canvas.
|
|
49
|
+
*
|
|
50
|
+
* The 80% is KDLS's, and it is the right shape of answer — a crop frame that touches the
|
|
51
|
+
* edges gives nowhere to grab it from.
|
|
52
|
+
*/
|
|
53
|
+
export function initialCropBox(crop, rect) {
|
|
54
|
+
if (!crop) return { ...FREE_BOX };
|
|
55
|
+
if (!rect?.width || !rect?.height) return null;
|
|
56
|
+
|
|
57
|
+
const centred = (wPct, hPct) => ({ x: (100 - wPct) / 2, y: (100 - hPct) / 2, w: wPct, h: hPct });
|
|
58
|
+
|
|
59
|
+
if (typeof crop === 'string') {
|
|
60
|
+
const parts = crop.split(':').map(Number);
|
|
61
|
+
if (parts.length !== 2 || parts.some(Number.isNaN) || !parts[1]) return null;
|
|
62
|
+
const ratio = parts[0] / parts[1];
|
|
63
|
+
const widthPx = rect.width * 0.8;
|
|
64
|
+
const heightPx = widthPx / ratio;
|
|
65
|
+
// Fit by width if that leaves room; otherwise the height is what is short.
|
|
66
|
+
if (heightPx <= rect.height * 0.8) {
|
|
67
|
+
return centred((widthPx / rect.width) * 100, (heightPx / rect.height) * 100);
|
|
68
|
+
}
|
|
69
|
+
const cappedHeight = rect.height * 0.8;
|
|
70
|
+
const cappedWidth = cappedHeight * ratio;
|
|
71
|
+
return centred((cappedWidth / rect.width) * 100, (cappedHeight / rect.height) * 100);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof crop === 'object' && crop.width && crop.height) {
|
|
75
|
+
const scale = Math.min(rect.width * 0.8 / crop.width, rect.height * 0.8 / crop.height);
|
|
76
|
+
return centred(
|
|
77
|
+
(crop.width * scale / rect.width) * 100,
|
|
78
|
+
(crop.height * scale / rect.height) * 100,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The box after a drag, given where it started and how far the pointer has moved.
|
|
87
|
+
*
|
|
88
|
+
* `dx` and `dy` are already in percent. `ratio` is the percentage-space ratio from
|
|
89
|
+
* ratioInPercentSpace, or null for free resizing.
|
|
90
|
+
*/
|
|
91
|
+
export function dragCropBox(start, handle, dx, dy, ratio = null) {
|
|
92
|
+
let { x, y, w, h } = start;
|
|
93
|
+
|
|
94
|
+
if (handle === 'center') {
|
|
95
|
+
// Moving, not resizing: the size is fixed and only the position is clamped.
|
|
96
|
+
return {
|
|
97
|
+
x: Math.max(0, Math.min(100 - w, start.x + dx)),
|
|
98
|
+
y: Math.max(0, Math.min(100 - h, start.y + dy)),
|
|
99
|
+
w,
|
|
100
|
+
h,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Width: the east handles push the right edge, the west handles pull the left one.
|
|
105
|
+
if (handle === 'se' || handle === 'ne') {
|
|
106
|
+
w = Math.max(MIN_SIZE, Math.min(100 - start.x, start.w + dx));
|
|
107
|
+
} else {
|
|
108
|
+
const newX = Math.max(0, Math.min(start.x + start.w - MIN_SIZE, start.x + dx));
|
|
109
|
+
w = start.x + start.w - newX;
|
|
110
|
+
x = newX;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ratio !== null) {
|
|
114
|
+
// Constrained: the height is whatever the ratio says, and a north handle grows upward.
|
|
115
|
+
h = w / ratio;
|
|
116
|
+
if (handle === 'nw' || handle === 'ne') y = Math.max(0, start.y + start.h - h);
|
|
117
|
+
} else if (handle === 'se' || handle === 'sw') {
|
|
118
|
+
h = Math.max(MIN_SIZE, Math.min(100 - start.y, start.h + dy));
|
|
119
|
+
} else {
|
|
120
|
+
const newY = Math.max(0, Math.min(start.y + start.h - MIN_SIZE, start.y + dy));
|
|
121
|
+
h = start.y + start.h - newY;
|
|
122
|
+
y = newY;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Whatever the above produced, it has to fit on the canvas.
|
|
126
|
+
if (x < 0) { w += x; x = 0; }
|
|
127
|
+
if (y < 0) { h += y; y = 0; }
|
|
128
|
+
if (x + w > 100) w = 100 - x;
|
|
129
|
+
if (y + h > 100) h = 100 - y;
|
|
130
|
+
|
|
131
|
+
return { x, y, w, h };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const ZOOM_MIN = 0.5;
|
|
135
|
+
export const ZOOM_MAX = 4;
|
|
136
|
+
export const ZOOM_STEP = 0.25;
|
|
137
|
+
|
|
138
|
+
export const zoomIn = (zoom) => Math.min(ZOOM_MAX, +(zoom + ZOOM_STEP).toFixed(2));
|
|
139
|
+
export const zoomOut = (zoom) => Math.max(ZOOM_MIN, +(zoom - ZOOM_STEP).toFixed(2));
|
|
140
|
+
export const rotateLeft = (deg) => (deg - 90 + 360) % 360;
|
|
141
|
+
export const rotateRight = (deg) => (deg + 90) % 360;
|
|
142
|
+
|
|
143
|
+
/** What each mode will take, and how to say so. */
|
|
144
|
+
export const UPLOADER_MODES = ['image', 'document'];
|
|
145
|
+
|
|
146
|
+
export const ACCEPT = {
|
|
147
|
+
image: '.jpg,.jpeg,.png,image/jpeg,image/png',
|
|
148
|
+
document: '.pdf,.docx,.doc,.jpeg,.jpg,.png',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const FORMATS = {
|
|
152
|
+
image: 'JPG, JPEG ou PNG',
|
|
153
|
+
document: 'PDF, DOCX, JPEG ou PNG',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function assertUploaderMode(mode) {
|
|
157
|
+
if (!UPLOADER_MODES.includes(mode)) {
|
|
158
|
+
throw new Error(`Unknown uploader mode: "${mode}". Allowed: ${UPLOADER_MODES.join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
return mode;
|
|
161
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<!-- sds-uploader — ported from kdls-design-system v2.5.1 (0f3edf8)
|
|
2
|
+
components/Uploader/Uploader.tsx.
|
|
3
|
+
|
|
4
|
+
Two states in one component, as in KDLS: a drop zone when there is nothing, and a
|
|
5
|
+
preview with an editor when there is. The editor is an sds-modal with zoom, rotation
|
|
6
|
+
and a draggable crop frame.
|
|
7
|
+
|
|
8
|
+
The geometry is in crop-geometry.js and tested there — the corner being dragged decides
|
|
9
|
+
which side moves, a ratio makes the height follow the width, and everything has to stay
|
|
10
|
+
on the canvas. Each of those is a place an off-by-one hides behind a picture that looks
|
|
11
|
+
roughly right, and a crop that is subtly wrong is a photograph published with someone's
|
|
12
|
+
head cut off.
|
|
13
|
+
|
|
14
|
+
Divergences:
|
|
15
|
+
|
|
16
|
+
The empty state is a <button>, not a div with role="button" and tabIndex. KDLS's needs
|
|
17
|
+
three hand-written behaviours to imitate one — Enter, Space and focus — and it still
|
|
18
|
+
announces nothing about what it does. This one is a real control with a label.
|
|
19
|
+
|
|
20
|
+
KDLS's download button has no handler at all: it renders, it is focusable, and clicking
|
|
21
|
+
it does nothing. Here it is rendered only when the caller passes onDownload, because a
|
|
22
|
+
control that cannot be told what to do should not be drawn.
|
|
23
|
+
|
|
24
|
+
The file reader reports failure. KDLS attaches onload and nothing else, so a file that
|
|
25
|
+
cannot be read leaves the visitor looking at an unchanged drop zone with no idea why. -->
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
initialCropBox, dragCropBox, ratioInPercentSpace, zoomIn, zoomOut, rotateLeft, rotateRight,
|
|
29
|
+
assertUploaderMode, ACCEPT, FORMATS, FREE_BOX,
|
|
30
|
+
} from "./crop-geometry.js";
|
|
31
|
+
import deleteIcon from "../../icons/generated/fluent-delete-regular.marko";
|
|
32
|
+
import downloadIcon from "../../icons/generated/fluent-arrow-download-regular.marko";
|
|
33
|
+
import cropIcon from "../../icons/generated/fluent-crop-regular.marko";
|
|
34
|
+
import zoomOutIcon from "../../icons/generated/fluent-zoom-out-regular.marko";
|
|
35
|
+
import zoomInIcon from "../../icons/generated/fluent-zoom-in-regular.marko";
|
|
36
|
+
import rotateLeftIcon from "../../icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko";
|
|
37
|
+
import rotateRightIcon from "../../icons/generated/fluent-arrow-rotate-clockwise-regular.marko";
|
|
38
|
+
import imageIcon from "../../icons/generated/fluent-image-regular.marko";
|
|
39
|
+
import documentIcon from "../../icons/generated/fluent-document-regular.marko";
|
|
40
|
+
import infoIcon from "../../icons/generated/fluent-info-regular.marko";
|
|
41
|
+
|
|
42
|
+
class {
|
|
43
|
+
onCreate() {
|
|
44
|
+
this.state = {
|
|
45
|
+
dragging: false,
|
|
46
|
+
editing: false,
|
|
47
|
+
zoom: 1,
|
|
48
|
+
rotate: 0,
|
|
49
|
+
cropBox: { ...FREE_BOX },
|
|
50
|
+
error: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
openEditor() {
|
|
55
|
+
this.state.zoom = 1;
|
|
56
|
+
this.state.rotate = 0;
|
|
57
|
+
this.state.editing = true;
|
|
58
|
+
if (!this.input.crop) {
|
|
59
|
+
this.state.cropBox = { ...FREE_BOX };
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// One frame, so the canvas is painted and can be measured.
|
|
63
|
+
requestAnimationFrame(() => {
|
|
64
|
+
const canvas = this.getEl("canvas");
|
|
65
|
+
if (!canvas) return;
|
|
66
|
+
const box = initialCropBox(this.input.crop, canvas.getBoundingClientRect());
|
|
67
|
+
if (box) this.state.cropBox = box;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
closeEditor() { this.state.editing = false; }
|
|
72
|
+
|
|
73
|
+
startCropDrag(handle, event) {
|
|
74
|
+
event.stopPropagation();
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
const canvas = this.getEl("canvas");
|
|
77
|
+
if (!canvas) return;
|
|
78
|
+
|
|
79
|
+
const rect = canvas.getBoundingClientRect();
|
|
80
|
+
const startX = event.clientX;
|
|
81
|
+
const startY = event.clientY;
|
|
82
|
+
const start = { ...this.state.cropBox };
|
|
83
|
+
const ratio = ratioInPercentSpace(this.input.crop, rect);
|
|
84
|
+
|
|
85
|
+
const move = (moveEvent) => {
|
|
86
|
+
const dx = ((moveEvent.clientX - startX) / rect.width) * 100;
|
|
87
|
+
const dy = ((moveEvent.clientY - startY) / rect.height) * 100;
|
|
88
|
+
this.state.cropBox = dragCropBox(start, handle, dx, dy, ratio);
|
|
89
|
+
};
|
|
90
|
+
const up = () => {
|
|
91
|
+
window.removeEventListener("mousemove", move);
|
|
92
|
+
window.removeEventListener("mouseup", up);
|
|
93
|
+
};
|
|
94
|
+
window.addEventListener("mousemove", move);
|
|
95
|
+
window.addEventListener("mouseup", up);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
read(file) {
|
|
99
|
+
if (!file) return;
|
|
100
|
+
const reader = new FileReader();
|
|
101
|
+
reader.onload = (event) => {
|
|
102
|
+
this.state.error = null;
|
|
103
|
+
this.input.onChange?.(event.target?.result ?? null);
|
|
104
|
+
};
|
|
105
|
+
// KDLS attaches no error handler, so an unreadable file leaves the visitor looking at
|
|
106
|
+
// an unchanged drop zone with nothing to explain it.
|
|
107
|
+
reader.onerror = () => {
|
|
108
|
+
this.state.error = "Não foi possível ler o arquivo.";
|
|
109
|
+
this.input.onError?.(reader.error);
|
|
110
|
+
};
|
|
111
|
+
reader.readAsDataURL(file);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
handleInput(event) {
|
|
115
|
+
const file = event.target.files?.[0];
|
|
116
|
+
if (file) this.read(file);
|
|
117
|
+
// Cleared so choosing the same file twice in a row still fires a change.
|
|
118
|
+
event.target.value = "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
handleDragOver(event) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
this.state.dragging = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleDragLeave() { this.state.dragging = false; }
|
|
127
|
+
|
|
128
|
+
handleDrop(event) {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
this.state.dragging = false;
|
|
131
|
+
const file = event.dataTransfer?.files?.[0];
|
|
132
|
+
if (file) this.read(file);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pick() { this.getEl("file")?.click(); }
|
|
136
|
+
|
|
137
|
+
remove() { this.input.onChange?.(null); }
|
|
138
|
+
|
|
139
|
+
zoomIn() { this.state.zoom = zoomIn(this.state.zoom); }
|
|
140
|
+
zoomOut() { this.state.zoom = zoomOut(this.state.zoom); }
|
|
141
|
+
turnLeft() { this.state.rotate = rotateLeft(this.state.rotate); }
|
|
142
|
+
turnRight() { this.state.rotate = rotateRight(this.state.rotate); }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
$ {
|
|
146
|
+
const {
|
|
147
|
+
mode = "image",
|
|
148
|
+
value = null,
|
|
149
|
+
crop,
|
|
150
|
+
onDownload,
|
|
151
|
+
removeLabel = "Remover",
|
|
152
|
+
downloadLabel = "Baixar",
|
|
153
|
+
editLabel = "Tratar",
|
|
154
|
+
previewAlt = "Arquivo enviado",
|
|
155
|
+
class: extraClass = "",
|
|
156
|
+
} = input;
|
|
157
|
+
|
|
158
|
+
assertUploaderMode(mode);
|
|
159
|
+
|
|
160
|
+
const linkLabel = mode === "image" ? "Adicionar imagem" : "Adicionar arquivo";
|
|
161
|
+
const box = state.cropBox;
|
|
162
|
+
const cropStyle = `--crop-x:${box.x}%;--crop-y:${box.y}%;--crop-w:${box.w}%;--crop-h:${box.h}%`;
|
|
163
|
+
const imageStyle = `--img-zoom:${state.zoom};--img-rotate:${state.rotate}deg`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
<if(value)>
|
|
167
|
+
<div class=`sds-uploader sds-uploader--filled ${extraClass}`.trim()>
|
|
168
|
+
<img src=value alt=previewAlt class="sds-uploader__preview"/>
|
|
169
|
+
<div class="sds-uploader__overlay">
|
|
170
|
+
<sds-icon-button aria-label=removeLabel onClick=() => component.remove()>
|
|
171
|
+
<@icon><${deleteIcon}/></@icon>
|
|
172
|
+
</sds-icon-button>
|
|
173
|
+
<if(onDownload)>
|
|
174
|
+
<sds-icon-button aria-label=downloadLabel onClick=onDownload>
|
|
175
|
+
<@icon><${downloadIcon}/></@icon>
|
|
176
|
+
</sds-icon-button>
|
|
177
|
+
</if>
|
|
178
|
+
<sds-icon-button aria-label=editLabel onClick=() => component.openEditor()>
|
|
179
|
+
<@icon><${cropIcon}/></@icon>
|
|
180
|
+
</sds-icon-button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<sds-modal
|
|
185
|
+
isOpen=state.editing
|
|
186
|
+
title="Editar imagem"
|
|
187
|
+
primaryLabel="Salvar"
|
|
188
|
+
secondaryLabel="Cancelar"
|
|
189
|
+
onClose=() => component.closeEditor()
|
|
190
|
+
onPrimary=() => component.closeEditor()
|
|
191
|
+
onSecondary=() => component.closeEditor()
|
|
192
|
+
>
|
|
193
|
+
<div class="sds-uploader__edit-body">
|
|
194
|
+
<div key="canvas" class="sds-uploader__edit-canvas">
|
|
195
|
+
<img src=value alt=previewAlt class="sds-uploader__edit-img" style=imageStyle/>
|
|
196
|
+
|
|
197
|
+
<if(crop)>
|
|
198
|
+
<div
|
|
199
|
+
class="sds-uploader__crop-box"
|
|
200
|
+
style=cropStyle
|
|
201
|
+
on-mousedown("startCropDrag", "center")
|
|
202
|
+
>
|
|
203
|
+
<for|handle| of=["nw", "ne", "sw", "se"]>
|
|
204
|
+
<div
|
|
205
|
+
class=`sds-uploader__crop-handle sds-uploader__crop-handle--${handle}`
|
|
206
|
+
on-mousedown("startCropDrag", handle)
|
|
207
|
+
/>
|
|
208
|
+
</for>
|
|
209
|
+
</div>
|
|
210
|
+
</if>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div class="sds-uploader__edit-controls">
|
|
214
|
+
<sds-icon-button aria-label="Diminuir zoom" onClick=() => component.zoomOut()>
|
|
215
|
+
<@icon><${zoomOutIcon}/></@icon>
|
|
216
|
+
</sds-icon-button>
|
|
217
|
+
<sds-icon-button aria-label="Aumentar zoom" onClick=() => component.zoomIn()>
|
|
218
|
+
<@icon><${zoomInIcon}/></@icon>
|
|
219
|
+
</sds-icon-button>
|
|
220
|
+
<sds-icon-button aria-label="Girar esquerda" onClick=() => component.turnLeft()>
|
|
221
|
+
<@icon><${rotateLeftIcon}/></@icon>
|
|
222
|
+
</sds-icon-button>
|
|
223
|
+
<sds-icon-button aria-label="Girar direita" onClick=() => component.turnRight()>
|
|
224
|
+
<@icon><${rotateRightIcon}/></@icon>
|
|
225
|
+
</sds-icon-button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</sds-modal>
|
|
229
|
+
</if>
|
|
230
|
+
<else>
|
|
231
|
+
<!-- A real <button>, not a div wearing role="button": KDLS hand-writes Enter, Space and
|
|
232
|
+
a tabindex to imitate one, and still announces nothing about what it does. -->
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class=[
|
|
236
|
+
"sds-uploader",
|
|
237
|
+
state.dragging ? "sds-uploader--dragging" : "",
|
|
238
|
+
extraClass,
|
|
239
|
+
].filter(Boolean).join(" ")
|
|
240
|
+
on-dragover("handleDragOver")
|
|
241
|
+
on-dragleave("handleDragLeave")
|
|
242
|
+
on-drop("handleDrop")
|
|
243
|
+
on-click("pick")
|
|
244
|
+
>
|
|
245
|
+
<input
|
|
246
|
+
key="file"
|
|
247
|
+
type="file"
|
|
248
|
+
accept=ACCEPT[mode]
|
|
249
|
+
class="sds-uploader__input"
|
|
250
|
+
aria-hidden="true"
|
|
251
|
+
tabindex="-1"
|
|
252
|
+
on-change("handleInput")
|
|
253
|
+
/>
|
|
254
|
+
<span class="sds-uploader__icon" aria-hidden="true">
|
|
255
|
+
<sds-icon><${mode === "image" ? imageIcon : documentIcon}/></sds-icon>
|
|
256
|
+
</span>
|
|
257
|
+
<sds-text as="span" variant="titleSm">${linkLabel}</sds-text>
|
|
258
|
+
<span class="sds-uploader__hint">ou arraste e solte aqui.</span>
|
|
259
|
+
<span class="sds-uploader__formats">
|
|
260
|
+
<sds-icon><${infoIcon}/></sds-icon>
|
|
261
|
+
<sds-text as="span" variant="labelRegular">Formatos suportados: ${FORMATS[mode]}</sds-text>
|
|
262
|
+
</span>
|
|
263
|
+
<if(state.error)>
|
|
264
|
+
<sds-text as="span" variant="labelRegular" class="sds-uploader__error" role="alert">
|
|
265
|
+
${state.error}
|
|
266
|
+
</sds-text>
|
|
267
|
+
</if>
|
|
268
|
+
</button>
|
|
269
|
+
</else>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
KDLS Scrollbar — styled scrollbar
|
|
3
|
+
|
|
4
|
+
Two widths:
|
|
5
|
+
.sds-scroll normal width (auto)
|
|
6
|
+
.sds-scroll--thin thin bar
|
|
7
|
+
|
|
8
|
+
Thumb color: neutral-30 (auto-inverts in dark — no dark block).
|
|
9
|
+
Uses the system's native bar, so it keeps the arrow.
|
|
10
|
+
|
|
11
|
+
Where to apply it (the container needs its own overflow):
|
|
12
|
+
<div className="sds-scroll sds-scroll--thin">…</div>
|
|
13
|
+
<html class="sds-scroll"> (page's root bar)
|
|
14
|
+
|
|
15
|
+
Support: Chrome, Edge and Firefox (scrollbar-width + scrollbar-color).
|
|
16
|
+
Safari doesn't support these properties → shows the default native bar.
|
|
17
|
+
============================================================ */
|
|
18
|
+
|
|
19
|
+
.sds-scroll {
|
|
20
|
+
scrollbar-width: auto;
|
|
21
|
+
scrollbar-color: var(--color-neutral-30) transparent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sds-scroll--thin {
|
|
25
|
+
scrollbar-width: thin;
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Putting a hyphenated attribute back together.
|
|
3
|
+
*
|
|
4
|
+
* Marko camelCases a hyphenated attribute written on a *component* tag: `aria-live` arrives
|
|
5
|
+
* in `input` as `ariaLive`. A component that forwards its leftover input onto a native
|
|
6
|
+
* element with a spread therefore writes `arialive="polite"` — an attribute with no meaning
|
|
7
|
+
* to anything, silently, with no error and nothing visibly wrong.
|
|
8
|
+
*
|
|
9
|
+
* It was found on the bulk action bar's count, where `aria-live` was the whole point: the
|
|
10
|
+
* number changing is the one thing a screen reader needs told when rows are selected.
|
|
11
|
+
*
|
|
12
|
+
* Only `aria-*` and `data-*` are converted. Those are the two families where a hyphen is
|
|
13
|
+
* part of the contract and a camelCase key is always a mangled one; everything else is left
|
|
14
|
+
* exactly as the caller wrote it.
|
|
15
|
+
*/
|
|
16
|
+
export function rehyphenate(attributes) {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const key of Object.keys(attributes)) {
|
|
19
|
+
if (/^(aria|data)[A-Z]/.test(key)) {
|
|
20
|
+
const hyphenated = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
21
|
+
out[hyphenated] = attributes[key];
|
|
22
|
+
} else {
|
|
23
|
+
out[key] = attributes[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the layout changes, and which width is being asked about.
|
|
3
|
+
*
|
|
4
|
+
* Two different questions live here, and confusing them is the bug this module exists to
|
|
5
|
+
* make hard.
|
|
6
|
+
*
|
|
7
|
+
* **The viewport's width** is what `@media` asks about, and it is the right question for
|
|
8
|
+
* anything that fills the screen: the grid's columns, a modal that becomes a bottom sheet,
|
|
9
|
+
* a drawer that becomes full-bleed. Those numbers are `BREAKPOINT_MIN_WIDTH`, in
|
|
10
|
+
* components/Grid/grid-variants.js, because the stylesheet is where they are enforced.
|
|
11
|
+
*
|
|
12
|
+
* **A component's own width** is what a ResizeObserver asks about, and it is the right
|
|
13
|
+
* question for anything that can be put in a column. A table in a 400px sidebar is a
|
|
14
|
+
* narrow table on a 2560px screen, and a header inside a narrow shell is a narrow header.
|
|
15
|
+
* `@media` cannot see that — it only knows the window. That is what `OWN_WIDTH_MOBILE` is.
|
|
16
|
+
*
|
|
17
|
+
* It lived in components/Table/table-logic.js, which made the Header import its mobile
|
|
18
|
+
* threshold from the Table. The number is not the table's; it is the width below which a
|
|
19
|
+
* row of controls stops fitting side by side, which is a fact about fingers and about
|
|
20
|
+
* this design language, not about tables.
|
|
21
|
+
*/
|
|
22
|
+
export const OWN_WIDTH_MOBILE = 680;
|
|
23
|
+
|
|
24
|
+
/** True when an element is narrow enough to need the stacked layout. */
|
|
25
|
+
export function isNarrow(width) {
|
|
26
|
+
return width > 0 && width <= OWN_WIDTH_MOBILE;
|
|
27
|
+
}
|