@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,427 @@
|
|
|
1
|
+
/* SDS Select.
|
|
2
|
+
Ported from kdls-design-system v2.5.1 (0f3edf8) components/Select/Select.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-select__search-input
|
|
7
|
+
*//* ============================================================
|
|
8
|
+
SDS Dropdown
|
|
9
|
+
Same visual spec as Input — 56px height, floating label,
|
|
10
|
+
radius-md border, chevron-right icon
|
|
11
|
+
============================================================ */
|
|
12
|
+
|
|
13
|
+
.sds-select-wrapper {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
position: relative;
|
|
19
|
+
}
|
|
20
|
+
.sds-select-wrapper--hug { width: fit-content; }
|
|
21
|
+
.sds-select-wrapper--fill { width: 100%; }
|
|
22
|
+
.sds-select-wrapper--trigger-input.sds-select-wrapper--hug { min-width: 200px; }
|
|
23
|
+
.sds-select-wrapper--fill .sds-floating { width: 100%; }
|
|
24
|
+
/* ── Disabled wrapper — cursor on the wrapper; the trigger (Pill/Input) already blocks on its own ── */
|
|
25
|
+
.sds-select-wrapper--disabled {
|
|
26
|
+
cursor: not-allowed;
|
|
27
|
+
}
|
|
28
|
+
/* ── Loading wrapper (pill trigger) ── */
|
|
29
|
+
.sds-select-wrapper--loading,
|
|
30
|
+
.sds-select-wrapper--loading .sds-pill {
|
|
31
|
+
cursor: wait;
|
|
32
|
+
}
|
|
33
|
+
/* ── Trigger input wrapper ── */
|
|
34
|
+
.sds-select-input-wrap {
|
|
35
|
+
outline: none;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
}
|
|
38
|
+
.sds-select-input-wrap .sds-input,
|
|
39
|
+
.sds-select-input-wrap .sds-input__native {
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
}
|
|
42
|
+
.sds-select-input-wrap--loading,
|
|
43
|
+
.sds-select-input-wrap--loading .sds-input,
|
|
44
|
+
.sds-select-input-wrap--loading .sds-input__native {
|
|
45
|
+
cursor: wait;
|
|
46
|
+
}
|
|
47
|
+
/* disabled wins over the trigger input's cursor:pointer (higher specificity) */
|
|
48
|
+
.sds-select-wrapper--disabled .sds-select-input-wrap,
|
|
49
|
+
.sds-select-wrapper--disabled .sds-select-input-wrap .sds-input,
|
|
50
|
+
.sds-select-wrapper--disabled .sds-select-input-wrap .sds-input__native {
|
|
51
|
+
cursor: not-allowed;
|
|
52
|
+
}
|
|
53
|
+
.sds-select-input-wrap--open .sds-input:not(.sds-input--disabled) {
|
|
54
|
+
border-color: var(--color-primary-50-solid);
|
|
55
|
+
box-shadow: var(--focus-ring);
|
|
56
|
+
}
|
|
57
|
+
.sds-select-input-wrap:focus-visible .sds-input:not(.sds-input--disabled) {
|
|
58
|
+
border-color: var(--color-primary-50-solid);
|
|
59
|
+
box-shadow: var(--focus-ring);
|
|
60
|
+
}
|
|
61
|
+
/* ── Chevron rotation when open ── */
|
|
62
|
+
.sds-select-input-wrap .sds-input__icon--right:not(.sds-input__icon--right--clickable),
|
|
63
|
+
.sds-select-wrapper .sds-pill__icon-right:not(.sds-pill__icon-right--clickable) {
|
|
64
|
+
transition: transform 200ms ease;
|
|
65
|
+
}
|
|
66
|
+
.sds-select-input-wrap--open .sds-input__icon--right:not(.sds-input__icon--right--clickable),
|
|
67
|
+
.sds-select-wrapper--open .sds-pill__icon-right:not(.sds-pill__icon-right--clickable) {
|
|
68
|
+
transform: rotate(180deg);
|
|
69
|
+
}
|
|
70
|
+
/* ── Trigger nav — same visual recipe as .sds-menu__btn--navButton ── */
|
|
71
|
+
.sds-select__trigger--nav {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: var(--spacing-3);
|
|
75
|
+
height: 40px;
|
|
76
|
+
padding: var(--spacing-2) var(--spacing-4);
|
|
77
|
+
border: none;
|
|
78
|
+
background: transparent;
|
|
79
|
+
border-radius: 11px; /* soft, the install default */
|
|
80
|
+
font-family: var(--font-family-base);
|
|
81
|
+
font-size: var(--font-size-body-md);
|
|
82
|
+
font-weight: var(--font-weight-medium);
|
|
83
|
+
line-height: var(--lh-body-md);
|
|
84
|
+
color: var(--color-neutral-80);
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: background-color 120ms ease, color 120ms ease;
|
|
87
|
+
user-select: none;
|
|
88
|
+
box-sizing: border-box;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
max-width: 100%;
|
|
91
|
+
}
|
|
92
|
+
.sds-select__trigger-nav-label {
|
|
93
|
+
min-width: 0;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
text-overflow: ellipsis;
|
|
96
|
+
}
|
|
97
|
+
.sds-select__trigger--nav:hover:not(:disabled) { background-color: var(--color-opacity-05); }
|
|
98
|
+
.sds-select__trigger--nav:active:not(:disabled) { background-color: var(--color-opacity-10); }
|
|
99
|
+
.sds-select__trigger--nav:focus-visible {
|
|
100
|
+
outline: none;
|
|
101
|
+
box-shadow: var(--focus-ring);
|
|
102
|
+
}
|
|
103
|
+
/* Divergence: opening does not change the weight, only being the active item does — the
|
|
104
|
+
same split the Menu's nav trigger makes, for the same reason. Bold is wider, so bolding
|
|
105
|
+
on open widened the trigger while it was open and shifted everything after it on the
|
|
106
|
+
row. Which item is current does not change while you press it, so `--active` keeps its
|
|
107
|
+
weight; open is already said by the chevron and by the panel. */
|
|
108
|
+
.sds-select__trigger--nav.sds-select__trigger--open {
|
|
109
|
+
color: var(--color-neutral-90);
|
|
110
|
+
}
|
|
111
|
+
.sds-select__trigger--nav.sds-select__trigger--active {
|
|
112
|
+
color: var(--color-neutral-90);
|
|
113
|
+
font-weight: var(--font-weight-bold);
|
|
114
|
+
}
|
|
115
|
+
.sds-select__trigger--nav:disabled {
|
|
116
|
+
opacity: 0.4;
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
}
|
|
119
|
+
.sds-select__trigger-nav-chevron {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
transition: transform 120ms ease;
|
|
124
|
+
}
|
|
125
|
+
.sds-select__trigger--nav.sds-select__trigger--open .sds-select__trigger-nav-chevron {
|
|
126
|
+
transform: rotate(180deg);
|
|
127
|
+
}
|
|
128
|
+
/* ── Menu ──
|
|
129
|
+
Positioning (top/bottom/left, z-index, flip) is Floating's
|
|
130
|
+
responsibility. The menu only keeps the visual box and the widths:
|
|
131
|
+
min-width = max(200px, trigger width via Floating's matchTriggerWidth)
|
|
132
|
+
and it grows with the content (max-content). */
|
|
133
|
+
.sds-select__menu {
|
|
134
|
+
background: var(--component-bg-color);
|
|
135
|
+
border: 1px solid var(--border-static-color);
|
|
136
|
+
border-radius: var(--radius-md);
|
|
137
|
+
box-shadow: var(--shadow-drop-md);
|
|
138
|
+
/* Its own size, not the trigger's. `max(200px, 100%)` was carried from KDLS and is a
|
|
139
|
+
trap here: the panel is inside a position:fixed box, so a percentage resolves against
|
|
140
|
+
the viewport rather than the trigger. The input trigger gets its width matched by
|
|
141
|
+
sds-floating instead, in pixels it measured. */
|
|
142
|
+
min-width: 200px;
|
|
143
|
+
width: max-content;
|
|
144
|
+
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
}
|
|
148
|
+
/* ── Header (search + select-all) ── */
|
|
149
|
+
.sds-select__header {
|
|
150
|
+
flex-shrink: 0;
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: var(--spacing-4);
|
|
154
|
+
padding: var(--spacing-4) var(--spacing-5);
|
|
155
|
+
border-bottom: 1px solid var(--border-static-color);
|
|
156
|
+
}
|
|
157
|
+
/* ── Search ── */
|
|
158
|
+
|
|
159
|
+
.sds-select__search-field {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: var(--spacing-2);
|
|
163
|
+
height: 40px;
|
|
164
|
+
padding: 0 var(--spacing-3);
|
|
165
|
+
border: 1px solid var(--border-interactive-color);
|
|
166
|
+
border-radius: var(--radius-md);
|
|
167
|
+
background: var(--component-bg-color);
|
|
168
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
169
|
+
}
|
|
170
|
+
.sds-select__search-field:hover {
|
|
171
|
+
border-color: var(--color-primary-50-solid);
|
|
172
|
+
}
|
|
173
|
+
.sds-select__search-field:focus-within {
|
|
174
|
+
border-color: var(--color-primary-50-solid);
|
|
175
|
+
box-shadow: var(--focus-ring);
|
|
176
|
+
}
|
|
177
|
+
.sds-select__search-icon {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
font-size: 24px;
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
color: var(--color-neutral-90);
|
|
183
|
+
}
|
|
184
|
+
.sds-select__search-input {
|
|
185
|
+
flex: 1;
|
|
186
|
+
border: none;
|
|
187
|
+
outline: none;
|
|
188
|
+
background: transparent;
|
|
189
|
+
font-family: var(--font-family-base);
|
|
190
|
+
font-size: var(--font-size-body-md);
|
|
191
|
+
color: var(--color-neutral-90);
|
|
192
|
+
caret-color: var(--color-primary-50-solid);
|
|
193
|
+
min-width: 0;
|
|
194
|
+
}
|
|
195
|
+
.sds-select__search-input::placeholder { color: var(--placeholder-color); }
|
|
196
|
+
/* ── List ── */
|
|
197
|
+
/* The inset follows the radius scheme, or the corners clip.
|
|
198
|
+
|
|
199
|
+
A rounded rectangle of radius R has no material within about 0.29R of its corner along
|
|
200
|
+
the diagonal. A row inset by a flat 4px therefore has its top corners cut off by the
|
|
201
|
+
panel's own curve, and the more rounded the scheme the worse it gets: at `rounded` the
|
|
202
|
+
panel's radius is 32px and 4px of inset leaves the first and last rows visibly clipped.
|
|
203
|
+
|
|
204
|
+
0.3 of --radius-md is the smallest inset that clears it, and the max() keeps the flat and
|
|
205
|
+
soft schemes at the 4px they were designed with. */
|
|
206
|
+
.sds-select__list {
|
|
207
|
+
margin: 0;
|
|
208
|
+
padding: max(var(--spacing-1), calc(var(--radius-md) * 0.3));
|
|
209
|
+
list-style: none;
|
|
210
|
+
flex: 1;
|
|
211
|
+
min-height: 0;
|
|
212
|
+
overflow-y: auto;
|
|
213
|
+
}
|
|
214
|
+
/* The list takes focus so the keyboard has somewhere to land, and shows nothing for it.
|
|
215
|
+
Said no other way, the browser draws its own ring — an orange box around the whole
|
|
216
|
+
panel, which says "these" where the reader needs "this one". The option the reader is
|
|
217
|
+
on wears the ring instead, below. Menu carries the same pair for the same reason. */
|
|
218
|
+
.sds-select__list:focus,
|
|
219
|
+
.sds-select__list:focus-visible {
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
/* ── Select all — fixed header above the scrollable list ── */
|
|
223
|
+
|
|
224
|
+
/* ── Section header ── */
|
|
225
|
+
.sds-select__section-header {
|
|
226
|
+
padding: var(--spacing-2) var(--spacing-4) var(--spacing-1);
|
|
227
|
+
font-family: var(--font-family-base);
|
|
228
|
+
font-size: var(--font-size-label-sm);
|
|
229
|
+
font-weight: var(--font-weight-semibold);
|
|
230
|
+
color: var(--color-neutral-50);
|
|
231
|
+
list-style: none;
|
|
232
|
+
pointer-events: none;
|
|
233
|
+
box-sizing: border-box;
|
|
234
|
+
}
|
|
235
|
+
/* ── Option ── */
|
|
236
|
+
/* Native chrome reset for the option rows.
|
|
237
|
+
KDLS drew its options as <li> elements with click handlers; SDS renders a real <button>
|
|
238
|
+
inside each <li>, so keyboard activation, focus and the disabled state come from the
|
|
239
|
+
browser. Without this rule the browser's own styling shows through — measured in
|
|
240
|
+
Chromium: a 2px outset border, an #efefef background and centred text, which turns a
|
|
241
|
+
list of options into a column of OS push-buttons.
|
|
242
|
+
|
|
243
|
+
It sits *above* the rules that paint the option, not at the end of the file: `background:
|
|
244
|
+
none` has the same specificity as `.sds-select__option--selected`, so from below it would
|
|
245
|
+
erase the selected row's own background. */
|
|
246
|
+
.sds-select__option {
|
|
247
|
+
appearance: none;
|
|
248
|
+
border: 0;
|
|
249
|
+
background: none;
|
|
250
|
+
text-align: left;
|
|
251
|
+
/* A <button> sizes to fit-content even at display:flex, so without this the row
|
|
252
|
+
highlights only under its text — 140px of a 202px panel, measured. */
|
|
253
|
+
width: 100%;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.sds-select__option {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: var(--spacing-2);
|
|
260
|
+
padding: var(--spacing-2) var(--spacing-4);
|
|
261
|
+
/* 40px, like every other row in a menu or a dropdown — the padding plus the line box
|
|
262
|
+
came to 38, so this row was two pixels shorter than the same row in an sds-menu and
|
|
263
|
+
than the navButton it sits under. Rafael, measuring the theme bar. */
|
|
264
|
+
min-height: var(--sds-menu-item-height, 40px);
|
|
265
|
+
border-radius: 11px; /* soft, the install default — schemes at the end */
|
|
266
|
+
font-family: var(--font-family-base);
|
|
267
|
+
font-size: var(--font-size-body-md);
|
|
268
|
+
font-weight: var(--font-weight-regular);
|
|
269
|
+
color: var(--color-neutral-80);
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
transition: background 120ms ease;
|
|
272
|
+
list-style: none;
|
|
273
|
+
box-sizing: border-box;
|
|
274
|
+
}
|
|
275
|
+
.sds-select__option:hover {
|
|
276
|
+
background: var(--color-neutral-10);
|
|
277
|
+
color: var(--color-neutral-10-on);
|
|
278
|
+
}
|
|
279
|
+
/* The design system's ring, never the browser's. Inset rather than around: the row lives
|
|
280
|
+
in a scroll container, and a ring drawn outside its box is clipped at the panel edge. */
|
|
281
|
+
.sds-select__option:focus {
|
|
282
|
+
outline: none;
|
|
283
|
+
}
|
|
284
|
+
.sds-select__option:focus-visible {
|
|
285
|
+
outline: none;
|
|
286
|
+
background: var(--color-neutral-10);
|
|
287
|
+
color: var(--color-neutral-10-on);
|
|
288
|
+
box-shadow: var(--focus-ring-inset);
|
|
289
|
+
}
|
|
290
|
+
.sds-select__option--selected {
|
|
291
|
+
background: var(--color-neutral-10);
|
|
292
|
+
color: var(--color-neutral-10-on);
|
|
293
|
+
font-weight: var(--font-weight-bold);
|
|
294
|
+
}
|
|
295
|
+
.sds-select__option-icon {
|
|
296
|
+
display: inline-flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
font-size: 24px;
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
}
|
|
301
|
+
/* ── Empty state ── */
|
|
302
|
+
.sds-select__empty {
|
|
303
|
+
padding: var(--spacing-2) var(--spacing-4);
|
|
304
|
+
font-family: var(--font-family-base);
|
|
305
|
+
font-size: var(--font-size-body-md);
|
|
306
|
+
color: var(--color-neutral-50);
|
|
307
|
+
list-style: none;
|
|
308
|
+
box-sizing: border-box;
|
|
309
|
+
}
|
|
310
|
+
/* ── Loading state ── */
|
|
311
|
+
.sds-select__loading {
|
|
312
|
+
padding: var(--spacing-2) var(--spacing-4);
|
|
313
|
+
font-family: var(--font-family-base);
|
|
314
|
+
font-size: var(--font-size-body-md);
|
|
315
|
+
color: var(--color-neutral-50);
|
|
316
|
+
list-style: none;
|
|
317
|
+
box-sizing: border-box;
|
|
318
|
+
}
|
|
319
|
+
/* ── Footer ── */
|
|
320
|
+
.sds-select__footer {
|
|
321
|
+
flex-shrink: 0;
|
|
322
|
+
display: flex;
|
|
323
|
+
align-items: center;
|
|
324
|
+
justify-content: space-between;
|
|
325
|
+
padding: var(--spacing-4) var(--spacing-5);
|
|
326
|
+
border-top: 1px solid var(--border-static-color);
|
|
327
|
+
}
|
|
328
|
+
/* ── Dark mode ── */
|
|
329
|
+
[data-theme="dark"] .sds-select__menu {
|
|
330
|
+
background: var(--color-neutral-10);
|
|
331
|
+
border-color: var(--border-interactive-color);
|
|
332
|
+
}
|
|
333
|
+
[data-theme="dark"] .sds-select__header {
|
|
334
|
+
border-color: var(--border-interactive-color);
|
|
335
|
+
}
|
|
336
|
+
[data-theme="dark"] .sds-select__search-field {
|
|
337
|
+
border-color: var(--border-interactive-color);
|
|
338
|
+
}
|
|
339
|
+
[data-theme="dark"] .sds-select__footer {
|
|
340
|
+
border-color: var(--border-interactive-color);
|
|
341
|
+
}
|
|
342
|
+
[data-theme="dark"] .sds-select__option:hover,
|
|
343
|
+
[data-theme="dark"] .sds-select__option--selected {
|
|
344
|
+
background: var(--color-neutral-20);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* The clear control.
|
|
348
|
+
KDLS reaches clearing through the Input's own right icon. This Select puts the chevron
|
|
349
|
+
there, so the clear affordance is its own button — a divergence, and this is the rule
|
|
350
|
+
behind it rather than a class with nothing under it. */
|
|
351
|
+
.sds-select__clear {
|
|
352
|
+
position: absolute;
|
|
353
|
+
top: 50%;
|
|
354
|
+
right: var(--spacing-10, 40px);
|
|
355
|
+
transform: translateY(-50%);
|
|
356
|
+
display: inline-flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
justify-content: center;
|
|
359
|
+
appearance: none;
|
|
360
|
+
border: 0;
|
|
361
|
+
background: none;
|
|
362
|
+
padding: var(--spacing-1);
|
|
363
|
+
border-radius: var(--radius-xs);
|
|
364
|
+
color: var(--color-neutral-60);
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
z-index: 1;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.sds-select__clear:hover { background-color: var(--color-opacity-05); }
|
|
370
|
+
.sds-select__clear:focus-visible { outline: none; box-shadow: var(--focus-ring); }
|
|
371
|
+
|
|
372
|
+
/* The trigger keeps one width.
|
|
373
|
+
Its visible text is the current choice, so without this it grows and shrinks as the
|
|
374
|
+
visitor uses it and everything beside it in the toolbar moves. The invisible spacer
|
|
375
|
+
reserves the widest option the trigger could ever show — the same technique the field
|
|
376
|
+
shell uses for its label, pointed at a different string.
|
|
377
|
+
|
|
378
|
+
Zero height and overflow hidden, so it takes part in the width calculation and in
|
|
379
|
+
nothing else. */
|
|
380
|
+
.sds-select__trigger-nav-label[data-widest]::before {
|
|
381
|
+
content: attr(data-widest);
|
|
382
|
+
display: block;
|
|
383
|
+
height: 0;
|
|
384
|
+
overflow: hidden;
|
|
385
|
+
visibility: hidden;
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
font-weight: var(--font-weight-bold);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* The hint under a pill or nav trigger. The input trigger gets its own from sds-input;
|
|
391
|
+
these two had an aria-describedby pointing at a hint that was never rendered. */
|
|
392
|
+
.sds-select__hint {
|
|
393
|
+
display: block;
|
|
394
|
+
margin-top: var(--spacing-1);
|
|
395
|
+
color: var(--color-neutral-60);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* 40px tall, so the Pill's ladder: 0 / 11 / 15 / capsule. See Pill.css for the arithmetic
|
|
399
|
+
— the numbers are a fraction of the control's own height, which is what keeps every
|
|
400
|
+
40px control in the system reading as one family. `--radius-sm` stopped at 16px here,
|
|
401
|
+
so `rounded` was a rounded rectangle instead of a capsule. The bare value is `soft`'s,
|
|
402
|
+
because an install that never picks a scheme has no attribute to match on. */
|
|
403
|
+
[data-radius="flat"] .sds-select__trigger--nav { border-radius: 0; }
|
|
404
|
+
[data-radius="soft"] .sds-select__trigger--nav { border-radius: 11px; }
|
|
405
|
+
[data-radius="medium"] .sds-select__trigger--nav { border-radius: 15px; }
|
|
406
|
+
[data-radius="rounded"] .sds-select__trigger--nav { border-radius: var(--radius-full); }
|
|
407
|
+
|
|
408
|
+
/* ── Corner radius: the 40px family ───────────────────────────────────────────
|
|
409
|
+
A row in a menu, a select or a dropdown is 40px tall, which makes it the same control
|
|
410
|
+
as the Pill and the navButton — so it takes the same four values. Rafael, pointing at
|
|
411
|
+
the theme bar's own menu: these are reactive, but not in the right way; they should be
|
|
412
|
+
40px like the navButton and have the radius resolved here.
|
|
413
|
+
|
|
414
|
+
height 40px flat 0 soft 11px medium 15px rounded 20px (a capsule)
|
|
415
|
+
as a fraction 0 0.275 0.375 0.5
|
|
416
|
+
|
|
417
|
+
`--radius-sm` was what they had, and it tops out at 16px — so `rounded` drew a rounded
|
|
418
|
+
rectangle where every other 40px control in the system drew a capsule. See Pill.css for
|
|
419
|
+
the arithmetic; the numbers are a fraction of the control's own height, which is what
|
|
420
|
+
keeps the family reading as one thing.
|
|
421
|
+
|
|
422
|
+
The bare value is `soft`'s, because an install that never picks a scheme has no
|
|
423
|
+
attribute at all and falls through to it. */
|
|
424
|
+
[data-radius="flat"] .sds-select__option { border-radius: 0; }
|
|
425
|
+
[data-radius="soft"] .sds-select__option { border-radius: 11px; }
|
|
426
|
+
[data-radius="medium"] .sds-select__option { border-radius: 15px; }
|
|
427
|
+
[data-radius="rounded"] .sds-select__option { border-radius: var(--radius-full); }
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Option handling for the Select — normalising, searching and toggling.
|
|
3
|
+
*
|
|
4
|
+
* Ported from kdls-design-system v2.5.1 (0f3edf8) components/Select/Select.tsx, where it
|
|
5
|
+
* lived inside the component. Pulled out for the same reason as the Input's masks: it is
|
|
6
|
+
* pure, it is where a subtle mistake hides, and it can be tested on its own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fold } from '../../foundations/text.js';
|
|
10
|
+
|
|
11
|
+
export const SELECT_TRIGGERS = ['pill', 'input', 'navButton'];
|
|
12
|
+
export const SELECT_WIDTHS = ['hug', 'fill'];
|
|
13
|
+
|
|
14
|
+
function assertOneOf(label, value, allowed) {
|
|
15
|
+
if (!allowed.includes(value)) {
|
|
16
|
+
throw new Error(`Unknown select ${label}: "${value}". Allowed: ${allowed.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const assertTrigger = (v) => assertOneOf('trigger', v, SELECT_TRIGGERS);
|
|
22
|
+
export const assertWidth = (v) => assertOneOf('width', v, SELECT_WIDTHS);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options arrive three ways and leave as one shape.
|
|
26
|
+
*
|
|
27
|
+
* KDLS took a plain array of strings, an array of { label, value } objects, or the same
|
|
28
|
+
* under caller-chosen key names via `fieldNames`. All three survive, plus attribute tags,
|
|
29
|
+
* because a select built from an API response and one written out by hand are both real.
|
|
30
|
+
*/
|
|
31
|
+
export function normaliseOptions({ options = [], option, fieldNames = {} } = {}) {
|
|
32
|
+
const labelKey = fieldNames.label ?? 'label';
|
|
33
|
+
const valueKey = fieldNames.value ?? 'value';
|
|
34
|
+
const iconKey = fieldNames.icon ?? 'icon';
|
|
35
|
+
|
|
36
|
+
const fromArray = options.map((entry) => {
|
|
37
|
+
if (entry === null || typeof entry !== 'object') {
|
|
38
|
+
const text = String(entry);
|
|
39
|
+
return { label: text, value: text };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
label: String(entry[labelKey] ?? entry[valueKey] ?? ''),
|
|
43
|
+
value: entry[valueKey] ?? entry[labelKey],
|
|
44
|
+
// `iconLeft` as a fallback: that is the key KDLS's options carry, and an options
|
|
45
|
+
// array copied over from there rendered with no icons at all and no error. `icon` is
|
|
46
|
+
// still the name this system uses; the other is accepted, not documented.
|
|
47
|
+
icon: entry[iconKey] ?? entry.iconLeft,
|
|
48
|
+
// The trailing icon KDLS allows per row — carried, so a row can end in a glyph.
|
|
49
|
+
iconRight: entry.iconRight,
|
|
50
|
+
// Carried through because Marko has no pre-instantiated element: KDLS passes
|
|
51
|
+
// `icon={<RadiusCornerIcon r={r} />}`, props and all, and the nearest thing here is
|
|
52
|
+
// the template plus the props to render it with. Dropping these silently rendered
|
|
53
|
+
// the icon with no input at all.
|
|
54
|
+
iconProps: entry.iconProps,
|
|
55
|
+
disabled: Boolean(entry.disabled),
|
|
56
|
+
section: entry.section,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const tags = option === undefined ? [] : [].concat(option);
|
|
61
|
+
const fromTags = tags.map((tag) => ({
|
|
62
|
+
label: tag.label ?? String(tag.value ?? ''),
|
|
63
|
+
value: tag.value ?? tag.label,
|
|
64
|
+
icon: tag.icon,
|
|
65
|
+
disabled: Boolean(tag.disabled),
|
|
66
|
+
section: tag.section,
|
|
67
|
+
renderBody: tag.renderBody,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return [...fromArray, ...fromTags];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Case- and accent-insensitive, because "Sao Paulo" should find "São Paulo". */
|
|
74
|
+
export function matches(option, query) {
|
|
75
|
+
if (!query) return true;
|
|
76
|
+
return fold(option.label).includes(fold(query));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const filterOptions = (options, query) => options.filter((o) => matches(o, query));
|
|
80
|
+
|
|
81
|
+
/** The current selection as an array, whatever shape it arrived in. */
|
|
82
|
+
export function toArray(value) {
|
|
83
|
+
if (value === undefined || value === null || value === '') return [];
|
|
84
|
+
return Array.isArray(value) ? value : [value];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Adds or removes one value from a multiple selection. */
|
|
88
|
+
export function toggleValue(current, value) {
|
|
89
|
+
const list = toArray(current);
|
|
90
|
+
return list.includes(value) ? list.filter((v) => v !== value) : [...list, value];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const isSelected = (value, optionValue) => toArray(value).includes(optionValue);
|
|
94
|
+
|
|
95
|
+
/** What the trigger reads when it is closed. */
|
|
96
|
+
export function displayLabel(options, value, { placeholder = '', separator = ', ' } = {}) {
|
|
97
|
+
const selected = toArray(value);
|
|
98
|
+
if (!selected.length) return placeholder;
|
|
99
|
+
const labels = selected
|
|
100
|
+
.map((v) => options.find((o) => o.value === v)?.label ?? String(v));
|
|
101
|
+
return labels.join(separator);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Every value that can be chosen — a disabled option is not one of them. */
|
|
105
|
+
export const selectableValues = (options) => options
|
|
106
|
+
.filter((o) => !o.disabled && !o.section)
|
|
107
|
+
.map((o) => o.value);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The state of a "select all" control: checked when everything selectable is chosen,
|
|
111
|
+
* indeterminate when some of it is, unchecked when none.
|
|
112
|
+
*/
|
|
113
|
+
export function selectAllState(options, value) {
|
|
114
|
+
const all = selectableValues(options);
|
|
115
|
+
const chosen = toArray(value).filter((v) => all.includes(v));
|
|
116
|
+
if (!all.length || !chosen.length) return { checked: false, indeterminate: false };
|
|
117
|
+
if (chosen.length === all.length) return { checked: true, indeterminate: false };
|
|
118
|
+
return { checked: false, indeterminate: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What a pill trigger shows.
|
|
123
|
+
*
|
|
124
|
+
* Not displayLabel: a pill is a chip, and a chip that grows to "São Paulo, Rio de Janeiro,
|
|
125
|
+
* Salvador" stops being one. So it shows the first choice and the count of the rest goes
|
|
126
|
+
* to the notification badge — which is KDLS's behaviour, and the reason Pill has a
|
|
127
|
+
* notificationCount at all.
|
|
128
|
+
*
|
|
129
|
+
* With nothing chosen it falls back to the field's own name rather than the placeholder:
|
|
130
|
+
* an unselected chip reading "Cidade" is a filter waiting to be used, one reading
|
|
131
|
+
* "Selecione" is a form field that wandered into a toolbar.
|
|
132
|
+
*/
|
|
133
|
+
export function pillLabel(options, value, { label = '' } = {}) {
|
|
134
|
+
const selected = toArray(value);
|
|
135
|
+
if (!selected.length) return label;
|
|
136
|
+
const first = options.find((o) => o.value === selected[0]);
|
|
137
|
+
return first ? first.label : String(selected[0]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** How many choices the pill is not showing. */
|
|
141
|
+
export const hiddenChoiceCount = (value) => Math.max(0, toArray(value).length - 1);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The widest thing the trigger will ever have to show.
|
|
145
|
+
*
|
|
146
|
+
* A trigger whose label is the current choice changes width every time the choice changes,
|
|
147
|
+
* and everything beside it in the toolbar jumps. Reserving the widest option up front is
|
|
148
|
+
* what stops that — the same invisible-spacer technique the field shell uses for its label,
|
|
149
|
+
* pointed at a different string.
|
|
150
|
+
*
|
|
151
|
+
* Longest by character count, not by rendered width: the real answer needs a font and a
|
|
152
|
+
* canvas, and this is close enough that the reserved box is never visibly wrong. Ties go
|
|
153
|
+
* to the first, which keeps it stable across renders.
|
|
154
|
+
*/
|
|
155
|
+
export function widestLabel(options, fallback = '') {
|
|
156
|
+
let widest = fallback;
|
|
157
|
+
for (const option of options) {
|
|
158
|
+
if (option.section) continue;
|
|
159
|
+
const label = String(option.label ?? '');
|
|
160
|
+
if (label.length > widest.length) widest = label;
|
|
161
|
+
}
|
|
162
|
+
return widest;
|
|
163
|
+
}
|