@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,530 @@
|
|
|
1
|
+
<!-- sds-input — ported from kdls-design-system v2.5.1 (0f3edf8) components/Input/Input.tsx.
|
|
2
|
+
|
|
3
|
+
A text field with a floating label, per-type masking and validation, an optional
|
|
4
|
+
incrementer for numbers, and a textarea mode.
|
|
5
|
+
|
|
6
|
+
It hydrates. A field that masks as you type, reveals a password and validates on blur is
|
|
7
|
+
interactive by definition; there is no stateless half of it to ship. Its input is a label,
|
|
8
|
+
a value and flags.
|
|
9
|
+
|
|
10
|
+
Divergences:
|
|
11
|
+
|
|
12
|
+
`tel-inter` — the phone with a country selector — IS ported, and the flags are emoji rather
|
|
13
|
+
than images fetched from flagcdn.com: that request hands a third party the IP address of
|
|
14
|
+
every visitor who opens a form. See input-countries.js. The old note, kept for the shape
|
|
15
|
+
of the argument it made:
|
|
16
|
+
|
|
17
|
+
`tel-inter` — the country selector — needed a dropdown, which
|
|
18
|
+
arrives with Menu. It also needs a decision first: KDLS fetches each flag from
|
|
19
|
+
flagcdn.com, which hands the visitor's IP to a third party on every phone field. That is
|
|
20
|
+
the same shape as the Google Fonts problem already recorded, and it should be settled
|
|
21
|
+
rather than carried across by reflex.
|
|
22
|
+
|
|
23
|
+
`defaultCountry`, `onlyCountries`, `excludeCountries` and `onCountryChange` belong to the
|
|
24
|
+
international phone. KDLS fetches its flags from flagcdn.com,
|
|
25
|
+
which hands the visitor's IP to a third party on every phone field on every page — the same
|
|
26
|
+
shape of exposure as the Google Fonts question, and Rafael's call rather than mine. The type
|
|
27
|
+
itself is absent, so the four attributes that configure it are too.
|
|
28
|
+
|
|
29
|
+
Form integration is here. KDLS reads validateOn from a React context, which Marko 5 has no
|
|
30
|
+
equivalent of — so sds-form writes it on the <form> element and the field asks the nearest
|
|
31
|
+
one when it mounts, which is what context does expressed in the tree both already share.
|
|
32
|
+
See components/Form/form-context.js. An explicit `validateOn` on the field still wins, as
|
|
33
|
+
it does in KDLS. Without a form around it the field validates itself on blur, which is
|
|
34
|
+
what it does with no
|
|
35
|
+
Form anyway; how a Form coordinates its fields is a design decision of its own.
|
|
36
|
+
|
|
37
|
+
The password eye and the incrementer chevrons were inline SVG in KDLS. They come from the
|
|
38
|
+
icon set now, so they follow the icon sizing and colour like everything else.
|
|
39
|
+
|
|
40
|
+
Validation runs on blur rather than on change. Telling someone their CPF is invalid while
|
|
41
|
+
they are still typing the fourth digit is noise; KDLS's default was 'change' and this is a
|
|
42
|
+
deliberate departure, reversible through validateOn. -->
|
|
43
|
+
|
|
44
|
+
import { countryOf, flagOf, assertCountry, maskNational, searchCountries } from "./input-countries.js";
|
|
45
|
+
import { assertInputType, assertFeedback, assertWidth, formatValue, nativeTypeFor,
|
|
46
|
+
inputModeFor, validate, isInternationalPhone, DOCUMENT_TYPES, PHONE_TYPES } from "./input-types.js";
|
|
47
|
+
import { validateOnFor, SUBMIT_EVENT } from "../Form/form-context.js";
|
|
48
|
+
import { rehyphenate } from "../../foundations/attrs.js";
|
|
49
|
+
|
|
50
|
+
class {
|
|
51
|
+
onCreate(input) {
|
|
52
|
+
this.state = {
|
|
53
|
+
value: input.value ?? input.defaultValue ?? "",
|
|
54
|
+
country: input.defaultCountry ?? "br",
|
|
55
|
+
countryQuery: "",
|
|
56
|
+
showPassword: false,
|
|
57
|
+
error: null,
|
|
58
|
+
touched: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Write a value the component decided on, and tell the caller about it.
|
|
64
|
+
*
|
|
65
|
+
* The two places that do this — choosing a country, and the incrementer's +/− — passed
|
|
66
|
+
* `null` as the event. KDLS delivers a real one, so a consumer written against KDLS and
|
|
67
|
+
* reading `event.target.value` crashed on the first press of +. There is nothing exotic
|
|
68
|
+
* about what they need: the element the value now lives on.
|
|
69
|
+
*
|
|
70
|
+
* The value is written onto the element as well as into state. A field the reader has
|
|
71
|
+
* typed in holds its own value, and a re-render does not take it back — the same reason
|
|
72
|
+
* `handleInput` writes the mask back rather than trusting the next render to. The
|
|
73
|
+
* element is found through the root rather than by key, because the field is rendered
|
|
74
|
+
* inside a branch (input or textarea) and `getEl` does not resolve it from here.
|
|
75
|
+
*/
|
|
76
|
+
report(value) {
|
|
77
|
+
const native = (this.el ?? this.getEl("field-root"))?.querySelector(".sds-input__native");
|
|
78
|
+
if (native) native.value = value;
|
|
79
|
+
// Before mount there is no element to point at, and a caller still gets the shape it
|
|
80
|
+
// reads rather than a null it has to guard.
|
|
81
|
+
this.input.onChange?.({ target: native ?? { value } }, value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The country is the caller's when they hold it, ours otherwise. */
|
|
85
|
+
get country() {
|
|
86
|
+
return countryOf(this.input.country ?? this.state.country);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Choosing a country re-masks what is already typed: the digits are the same number,
|
|
91
|
+
* and only the way it is written changes.
|
|
92
|
+
*/
|
|
93
|
+
pickCountry(code) {
|
|
94
|
+
if (this.input.country === undefined) this.state.country = code;
|
|
95
|
+
this.input.onCountryChange?.(code);
|
|
96
|
+
|
|
97
|
+
const digits = String(this.currentValue).replace(/\D/g, "");
|
|
98
|
+
const masked = maskNational(digits, code);
|
|
99
|
+
|
|
100
|
+
if (this.input.value === undefined) this.state.value = masked;
|
|
101
|
+
this.report(masked);
|
|
102
|
+
this.state.countryQuery = "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The countries the open list shows.
|
|
107
|
+
*
|
|
108
|
+
* A getter rather than something computed in `onInput`, because `onInput` does not run
|
|
109
|
+
* when only state moves — and the query is state. See `.claude/component-rules.md` §1.
|
|
110
|
+
*/
|
|
111
|
+
get countryOptions() {
|
|
112
|
+
return searchCountries(this.state.countryQuery);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setCountryQuery(query) {
|
|
116
|
+
this.state.countryQuery = query;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The next opening starts from the whole list, not from the last search. */
|
|
120
|
+
onCountryMenuOpen(open) {
|
|
121
|
+
if (!open) this.state.countryQuery = "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onMount() {
|
|
125
|
+
const root = this.getEl("field-root");
|
|
126
|
+
// What the form around this field asked for, read once when it mounts. There is no
|
|
127
|
+
// context in Marko 5; see components/Form/form-context.js for why this is the shape.
|
|
128
|
+
this.formValidateOn = validateOnFor(root);
|
|
129
|
+
|
|
130
|
+
// A form that has been submitted wants every field to say what is wrong with it, even
|
|
131
|
+
// the ones nobody touched — which is the whole point of validateOn: "submit".
|
|
132
|
+
this.onFormSubmit = () => {
|
|
133
|
+
this.state.touched = true;
|
|
134
|
+
this.state.error = this.validateNow(this.currentValue);
|
|
135
|
+
};
|
|
136
|
+
root?.addEventListener(SUBMIT_EVENT, this.onFormSubmit);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onDestroy() {
|
|
140
|
+
this.getEl("field-root")?.removeEventListener(SUBMIT_EVENT, this.onFormSubmit);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get currentValue() {
|
|
144
|
+
return this.input.value !== undefined ? this.input.value : this.state.value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** How this field writes what is typed into it — by country when it has one. */
|
|
148
|
+
maskFor(raw) {
|
|
149
|
+
const { type = "text" } = this.input;
|
|
150
|
+
// The country selector is what `tel-inter` IS, rather than a flag set beside another
|
|
151
|
+
// type — see input-types.js. Decided by Rafael.
|
|
152
|
+
return isInternationalPhone(type)
|
|
153
|
+
? maskNational(raw, this.country.code)
|
|
154
|
+
: formatValue(raw, type);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
handleInput(event) {
|
|
158
|
+
const { onChange } = this.input;
|
|
159
|
+
const masked = this.maskFor(event.target.value);
|
|
160
|
+
|
|
161
|
+
// Writing the masked value back keeps the field and the model in step: without it the
|
|
162
|
+
// separators the mask inserts would be typed over on the next keystroke.
|
|
163
|
+
if (masked !== event.target.value) event.target.value = masked;
|
|
164
|
+
if (this.input.value === undefined) this.state.value = masked;
|
|
165
|
+
|
|
166
|
+
// Once a field has been marked invalid, revalidate as they fix it — leaving the error
|
|
167
|
+
// up while someone corrects it is the thing that makes forms feel broken. A form asking
|
|
168
|
+
// for `change` validates from the first keystroke instead of waiting to be left.
|
|
169
|
+
if (this.state.touched || this.validateOn === "change") {
|
|
170
|
+
this.state.touched = true;
|
|
171
|
+
this.state.error = this.validateNow(masked);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onChange?.(event, masked);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
handleBlur(event) {
|
|
178
|
+
this.input.onBlur?.(event);
|
|
179
|
+
if (this.validateOn === "submit") return;
|
|
180
|
+
this.state.touched = true;
|
|
181
|
+
this.state.error = this.validateNow(this.currentValue);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The field's own answer, or the form's, or the default. */
|
|
185
|
+
get validateOn() {
|
|
186
|
+
return this.input.validateOn ?? this.formValidateOn ?? "blur";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
validateNow(value) {
|
|
190
|
+
const { type = "text", label, rules, required, validateDocument = true } = this.input;
|
|
191
|
+
return validate(value, { type, label, rules, required, validateDocument });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
togglePassword() {
|
|
195
|
+
this.state.showPassword = !this.state.showPassword;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
step(direction) {
|
|
199
|
+
const { min, max, step = 1 } = this.input;
|
|
200
|
+
const current = parseFloat(this.currentValue || "0") || 0;
|
|
201
|
+
let next = current + direction * step;
|
|
202
|
+
if (min !== undefined) next = Math.max(next, min);
|
|
203
|
+
if (max !== undefined) next = Math.min(next, max);
|
|
204
|
+
|
|
205
|
+
const value = String(next);
|
|
206
|
+
if (this.input.value === undefined) this.state.value = value;
|
|
207
|
+
this.report(value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
stepUp() { this.step(1); }
|
|
211
|
+
stepDown() { this.step(-1); }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
$ {
|
|
215
|
+
const {
|
|
216
|
+
label,
|
|
217
|
+
type = "text",
|
|
218
|
+
hint,
|
|
219
|
+
feedback = "none",
|
|
220
|
+
width = "fill",
|
|
221
|
+
placeholder,
|
|
222
|
+
name,
|
|
223
|
+
// `component.id`, not a random string. This one is read by the label's `for`, by
|
|
224
|
+
// `aria-describedby` and by the hint's own id — and being generated inside the render
|
|
225
|
+
// block it changed on EVERY render, so on a field the reader is typing into it changed
|
|
226
|
+
// on every keystroke, and it differed between the server render and hydration. Marko
|
|
227
|
+
// gives a stateful component an id that is stable across both.
|
|
228
|
+
id = `sds-input-${component.id}`,
|
|
229
|
+
rows = 3,
|
|
230
|
+
min,
|
|
231
|
+
max,
|
|
232
|
+
step,
|
|
233
|
+
disabled = false,
|
|
234
|
+
readOnly = false,
|
|
235
|
+
required = false,
|
|
236
|
+
isLoading = false,
|
|
237
|
+
showIncrementer = false,
|
|
238
|
+
notificationCount,
|
|
239
|
+
iconLeft,
|
|
240
|
+
defaultCountry,
|
|
241
|
+
country: countryInput,
|
|
242
|
+
onCountryChange,
|
|
243
|
+
iconRight,
|
|
244
|
+
onClickIconRight,
|
|
245
|
+
autoComplete,
|
|
246
|
+
autoFocus,
|
|
247
|
+
tabIndex,
|
|
248
|
+
onKeyDown,
|
|
249
|
+
onFocus,
|
|
250
|
+
maxLength,
|
|
251
|
+
minLength,
|
|
252
|
+
pattern,
|
|
253
|
+
class: extraClass = "",
|
|
254
|
+
} = input;
|
|
255
|
+
|
|
256
|
+
// A field in a Kenlo site is 56px. Decided by Rafael, 2026-09-10: SearchBar is the only
|
|
257
|
+
// one that varies, at 40 and 48, and it gets those from sds-search-bar. Accepting a `size`
|
|
258
|
+
// here would offer every field in the product a variation that only one of them wants.
|
|
259
|
+
if (input.size) {
|
|
260
|
+
throw new Error("sds-input takes no `size` — a field is 56px. The 40px and 48px shells belong to sds-search-bar, which sds-search-bar is built on.");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!label) throw new Error("sds-input needs a `label` — the floating label is the field's name.");
|
|
264
|
+
assertInputType(type);
|
|
265
|
+
assertFeedback(feedback);
|
|
266
|
+
assertWidth(width);
|
|
267
|
+
|
|
268
|
+
const renderable = (value) => (value && value.renderBody) || value;
|
|
269
|
+
|
|
270
|
+
const value = component.currentValue;
|
|
271
|
+
// The render masks the value too, and it has to mask it the same way the typing did:
|
|
272
|
+
// `formatValue` knows types and nothing about countries, so on an international phone it
|
|
273
|
+
// wrote the Brazilian mask back over whatever country had just been chosen.
|
|
274
|
+
const displayValue = component.maskFor(value);
|
|
275
|
+
|
|
276
|
+
// An explicit feedback wins: a caller that knows the server rejected the value should
|
|
277
|
+
// not have it overwritten by a local check that passes.
|
|
278
|
+
const effectiveFeedback = feedback !== "none" ? feedback : (state.error ? "error" : "none");
|
|
279
|
+
const effectiveHint = hint ?? state.error ?? null;
|
|
280
|
+
const hintId = `${id}-hint`;
|
|
281
|
+
|
|
282
|
+
const loading = isLoading && !disabled;
|
|
283
|
+
const isTextarea = type === "textbox";
|
|
284
|
+
const showsIncrementer = type === "number" && showIncrementer;
|
|
285
|
+
assertCountry(countryInput ?? defaultCountry);
|
|
286
|
+
|
|
287
|
+
// A country selector only makes sense on a phone, and it replaces the left icon: the
|
|
288
|
+
// flag IS the icon there.
|
|
289
|
+
// The country selector is the type, not a flag beside it: `tel-inter` has one and
|
|
290
|
+
// nothing else does. It used to be `phoneInternational && (tel || cel)` — a boolean that
|
|
291
|
+
// meant nothing on eleven of the twelve types, and had to be guarded against the twelfth.
|
|
292
|
+
// Decided by Rafael; KDLS keeps the boolean.
|
|
293
|
+
const showsCountry = isInternationalPhone(type);
|
|
294
|
+
const country = component.country;
|
|
295
|
+
const showsLeftIcon = iconLeft && type !== "email" && !showsCountry;
|
|
296
|
+
|
|
297
|
+
// Three layers, which KDLS has and an earlier version of this port flattened into two:
|
|
298
|
+
// an outer box that owns the width, a column holding the field and its hint, and the
|
|
299
|
+
// field itself — which is the 56px flex row the icons sit inside. Collapsing them left
|
|
300
|
+
// .sds-input as a block, so the icon, the field and the toggle stacked vertically
|
|
301
|
+
// instead of sitting side by side.
|
|
302
|
+
const outerClass = [`sds-input-box sds-input-box--${width}`, extraClass].filter(Boolean).join(" ");
|
|
303
|
+
|
|
304
|
+
const fieldBoxClass = [
|
|
305
|
+
"sds-input",
|
|
306
|
+
// KDLS marks only the textbox. A modifier per type produced ten classes no rule
|
|
307
|
+
// matched — dead weight in every field's class attribute.
|
|
308
|
+
type === "textbox" ? "sds-input--textbox" : "",
|
|
309
|
+
effectiveFeedback !== "none" ? `sds-input--feedback-${effectiveFeedback}` : "",
|
|
310
|
+
disabled ? "sds-input--disabled" : "",
|
|
311
|
+
loading ? "sds-input--loading" : "",
|
|
312
|
+
value ? "sds-input--filled" : "",
|
|
313
|
+
].filter(Boolean).join(" ");
|
|
314
|
+
|
|
315
|
+
const nativeAttrs = {
|
|
316
|
+
id,
|
|
317
|
+
name,
|
|
318
|
+
autocomplete: autoComplete,
|
|
319
|
+
placeholder,
|
|
320
|
+
disabled: disabled || null,
|
|
321
|
+
readonly: (readOnly || isLoading) || null,
|
|
322
|
+
required: (!disabled && required) || null,
|
|
323
|
+
maxlength: maxLength,
|
|
324
|
+
// Two constraints the port accepted and then dropped: neither reached the control, so
|
|
325
|
+
// a field asked to refuse a shape or a minimum length refused nothing at all, silently.
|
|
326
|
+
pattern: pattern ?? null,
|
|
327
|
+
minlength: minLength ?? null,
|
|
328
|
+
"aria-required": (!disabled && required) ? "true" : null,
|
|
329
|
+
"aria-invalid": effectiveFeedback === "error" ? "true" : null,
|
|
330
|
+
"aria-describedby": effectiveHint ? hintId : null,
|
|
331
|
+
// A wrapper that turns this field into something else has to be able to say so on
|
|
332
|
+
// the element the reader lands on. Select's input trigger is a listbox's combobox and
|
|
333
|
+
// was announcing itself as a plain readonly field — no role, no expanded state.
|
|
334
|
+
// Marko camelCases hyphenated attributes on a component tag, so they come back
|
|
335
|
+
// through rehyphenate; see foundations/attrs.js.
|
|
336
|
+
...rehyphenate(Object.fromEntries(
|
|
337
|
+
Object.entries(input).filter(([key]) => /^(aria|data)[A-Z-]/.test(key)),
|
|
338
|
+
)),
|
|
339
|
+
role: input.role ?? null,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
<div key="field-root" class=outerClass data-sds-field>
|
|
344
|
+
<div class=`sds-input-wrapper ${disabled ? "sds-input-wrapper--disabled" : ""}`.trim()>
|
|
345
|
+
// `rows` reached the <textarea> attribute and stopped there, and the stylesheet pins
|
|
346
|
+
// `min-height` to a 114px fallback that wins — so every textbox was the same height
|
|
347
|
+
// whatever was asked for. KDLS sets this variable from `rows`; the port kept the rule
|
|
348
|
+
// that reads it and lost the line that writes it. 24px a line plus the 42px the label
|
|
349
|
+
// and padding take, which is KDLS's arithmetic.
|
|
350
|
+
<div
|
|
351
|
+
class=fieldBoxClass
|
|
352
|
+
aria-busy=(loading ? "true" : null)
|
|
353
|
+
style=(isTextarea ? `--sds-textarea-min-height: ${rows * 24 + 42}px` : null)
|
|
354
|
+
>
|
|
355
|
+
<if(showsCountry)>
|
|
356
|
+
<!-- The flag opens the list and the dial code sits beside it, which is the whole
|
|
357
|
+
control: KDLS draws the same pair and fetches the flag as an image from a
|
|
358
|
+
third party. The flag is decorative — the country's name is in the list and
|
|
359
|
+
the button is named by its aria-label — so a Windows reader seeing "BR"
|
|
360
|
+
instead of 🇧🇷 loses nothing. -->
|
|
361
|
+
<sds-menu
|
|
362
|
+
class="sds-input__country"
|
|
363
|
+
trigger="custom"
|
|
364
|
+
placement="bottomLeft"
|
|
365
|
+
aria-label=`País: ${country.name}`
|
|
366
|
+
onOpenChange=(open) => component.onCountryMenuOpen(open)
|
|
367
|
+
emptyMessage="Nenhum país encontrado"
|
|
368
|
+
items=component.countryOptions.map((option) => ({
|
|
369
|
+
label: `${flagOf(option.code)} ${option.name} ${option.ddi}`,
|
|
370
|
+
active: option.code === country.code,
|
|
371
|
+
onClick: () => component.pickCountry(option.code),
|
|
372
|
+
}))
|
|
373
|
+
>
|
|
374
|
+
<!-- Fifteen rows is past the point where reading beats typing, and the panel is
|
|
375
|
+
capped at seven of them, so the rest are below the fold. The box goes in the
|
|
376
|
+
menu's header slot, which does not scroll with the list and which the menu
|
|
377
|
+
already focuses on open — so the list opens ready for the first keystroke. -->
|
|
378
|
+
<@header>
|
|
379
|
+
<div class="sds-input__country-search">
|
|
380
|
+
<sds-search-bar
|
|
381
|
+
size="sm"
|
|
382
|
+
labelMode="placeholder"
|
|
383
|
+
label="Buscar país"
|
|
384
|
+
filled=(state.countryQuery.length > 0)
|
|
385
|
+
onClear=() => component.setCountryQuery("")
|
|
386
|
+
>
|
|
387
|
+
<input
|
|
388
|
+
type="text"
|
|
389
|
+
class="sds-search-bar__input"
|
|
390
|
+
value=state.countryQuery
|
|
391
|
+
on-input((event) => component.setCountryQuery(event.target.value))
|
|
392
|
+
/>
|
|
393
|
+
</sds-search-bar>
|
|
394
|
+
</div>
|
|
395
|
+
</@header>
|
|
396
|
+
|
|
397
|
+
<@triggerBody>
|
|
398
|
+
<span class="sds-input__country-flag" aria-hidden="true">${flagOf(country.code)}</span>
|
|
399
|
+
<span class="sds-input__country-ddi">${country.ddi}</span>
|
|
400
|
+
<span class="sds-input__country-chevron" aria-hidden="true">
|
|
401
|
+
<sds-icon size="sm"><fluent-chevron-down-regular/></sds-icon>
|
|
402
|
+
</span>
|
|
403
|
+
</@triggerBody>
|
|
404
|
+
</sds-menu>
|
|
405
|
+
</if>
|
|
406
|
+
<else-if(showsLeftIcon)>
|
|
407
|
+
<span class="sds-input__icon sds-input__icon--left" aria-hidden="true">
|
|
408
|
+
<sds-icon><${renderable(iconLeft)}/></sds-icon>
|
|
409
|
+
</span>
|
|
410
|
+
</else-if>
|
|
411
|
+
|
|
412
|
+
<div class="sds-input__field" data-label=label>
|
|
413
|
+
<sds-text as="label" variant="bodyMd" for=id
|
|
414
|
+
class=`sds-input__label ${required && !disabled ? "sds-input__label--required" : ""}`.trim()>
|
|
415
|
+
${label}
|
|
416
|
+
</sds-text>
|
|
417
|
+
|
|
418
|
+
<if(isTextarea)>
|
|
419
|
+
<textarea
|
|
420
|
+
class="sds-input__native sds-input__native--textarea sds-scroll sds-scroll--thin"
|
|
421
|
+
rows=rows
|
|
422
|
+
value=displayValue
|
|
423
|
+
autofocus=(autoFocus || null)
|
|
424
|
+
tabindex=tabIndex
|
|
425
|
+
on-input("handleInput")
|
|
426
|
+
on-blur("handleBlur")
|
|
427
|
+
on-keydown(onKeyDown)
|
|
428
|
+
on-focus(onFocus)
|
|
429
|
+
...nativeAttrs
|
|
430
|
+
/>
|
|
431
|
+
</if>
|
|
432
|
+
<else>
|
|
433
|
+
<input
|
|
434
|
+
key="native"
|
|
435
|
+
class="sds-input__native"
|
|
436
|
+
type=nativeTypeFor(type, state.showPassword)
|
|
437
|
+
inputmode=inputModeFor(type)
|
|
438
|
+
value=displayValue
|
|
439
|
+
min=(type === "number" ? min : null)
|
|
440
|
+
max=(type === "number" ? max : null)
|
|
441
|
+
step=(type === "number" ? step : null)
|
|
442
|
+
autofocus=(autoFocus || null)
|
|
443
|
+
tabindex=tabIndex
|
|
444
|
+
on-input("handleInput")
|
|
445
|
+
on-blur("handleBlur")
|
|
446
|
+
on-keydown(onKeyDown)
|
|
447
|
+
on-focus(onFocus)
|
|
448
|
+
...nativeAttrs
|
|
449
|
+
/>
|
|
450
|
+
</else>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<if(type === "password")>
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
class="sds-input__password-toggle"
|
|
457
|
+
aria-label=(state.showPassword ? "Ocultar senha" : "Exibir senha")
|
|
458
|
+
aria-pressed=(state.showPassword ? "true" : "false")
|
|
459
|
+
on-click("togglePassword")
|
|
460
|
+
>
|
|
461
|
+
<sds-icon size="md">
|
|
462
|
+
<if(state.showPassword)><fluent-eye-off-regular/></if>
|
|
463
|
+
<else><fluent-eye-regular/></else>
|
|
464
|
+
</sds-icon>
|
|
465
|
+
</button>
|
|
466
|
+
</if>
|
|
467
|
+
|
|
468
|
+
<if(showsIncrementer)>
|
|
469
|
+
<div class="sds-input__spinner">
|
|
470
|
+
<button type="button" class="sds-input__spinner-btn" tabindex="-1"
|
|
471
|
+
aria-label="Aumentar"
|
|
472
|
+
disabled=((disabled || (max !== undefined && parseFloat(value || "0") >= max)) || null)
|
|
473
|
+
on-click("stepUp")>
|
|
474
|
+
<sds-icon size="xs"><fluent-chevron-up-regular/></sds-icon>
|
|
475
|
+
</button>
|
|
476
|
+
<button type="button" class="sds-input__spinner-btn" tabindex="-1"
|
|
477
|
+
aria-label="Diminuir"
|
|
478
|
+
disabled=((disabled || (min !== undefined && parseFloat(value || "0") <= min)) || null)
|
|
479
|
+
on-click("stepDown")>
|
|
480
|
+
<sds-icon size="xs"><fluent-chevron-down-regular/></sds-icon>
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
</if>
|
|
484
|
+
|
|
485
|
+
<if(notificationCount !== undefined && notificationCount > 0)>
|
|
486
|
+
<!-- No wrapper: KDLS renders it as a direct flex child of the field box, and a
|
|
487
|
+
wrapper with no rule behind it is an unstyled block in the middle of a row. -->
|
|
488
|
+
<sds-notification count=notificationCount size=24 showPlus/>
|
|
489
|
+
</if>
|
|
490
|
+
|
|
491
|
+
<if(loading)>
|
|
492
|
+
<span class="sds-input__icon sds-input__icon--right">
|
|
493
|
+
<sds-spinner size="sm" label="Carregando"/>
|
|
494
|
+
</span>
|
|
495
|
+
</if>
|
|
496
|
+
<else-if(iconRight)>
|
|
497
|
+
<span class="sds-input__icon sds-input__icon--right" aria-hidden=(onClickIconRight ? null : "true")>
|
|
498
|
+
<if(onClickIconRight)>
|
|
499
|
+
<!-- KDLS hung the handler on the icon itself, which rendered a clickable
|
|
500
|
+
<span>: no focus, no Enter, no name. An action inside a field is a
|
|
501
|
+
button. -->
|
|
502
|
+
<sds-icon-button
|
|
503
|
+
variant="neutral"
|
|
504
|
+
size="sm"
|
|
505
|
+
aria-label=(input.iconRightLabel ?? "Ação do campo")
|
|
506
|
+
disabled=(disabled || null)
|
|
507
|
+
onClick=onClickIconRight
|
|
508
|
+
>
|
|
509
|
+
<@icon><${renderable(iconRight)}/></@icon>
|
|
510
|
+
</sds-icon-button>
|
|
511
|
+
</if>
|
|
512
|
+
<else>
|
|
513
|
+
<sds-icon><${renderable(iconRight)}/></sds-icon>
|
|
514
|
+
</else>
|
|
515
|
+
</span>
|
|
516
|
+
</else-if>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<if(effectiveHint)>
|
|
520
|
+
<div class="sds-input__hint-row">
|
|
521
|
+
<!-- The feedback modifier is what colours it, and it was never written: the hint
|
|
522
|
+
carried `color="inherit"` and inherited the body's ink, so every hint read as
|
|
523
|
+
ordinary text and an error hint read as an ordinary hint. -->
|
|
524
|
+
<sds-text as="span" id=hintId variant="labelRegular" class=`sds-input__hint sds-input__hint--${effectiveFeedback}`>
|
|
525
|
+
${effectiveHint}
|
|
526
|
+
</sds-text>
|
|
527
|
+
</div>
|
|
528
|
+
</if>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|