@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.
Files changed (246) hide show
  1. package/README.md +129 -0
  2. package/components/Accordion/Accordion.css +186 -0
  3. package/components/Accordion/accordion-variants.js +17 -0
  4. package/components/Accordion/accordion.marko +264 -0
  5. package/components/AccordionGroup/AccordionGroup.css +35 -0
  6. package/components/AccordionGroup/accordion-group.marko +17 -0
  7. package/components/Alert/Alert.css +104 -0
  8. package/components/Alert/alert-variants.js +35 -0
  9. package/components/Alert/alert.marko +94 -0
  10. package/components/AttachmentList/AttachmentList.css +92 -0
  11. package/components/AttachmentList/attachment-list.marko +51 -0
  12. package/components/AttachmentList/attachment-row.marko +167 -0
  13. package/components/Avatar/Avatar.css +110 -0
  14. package/components/Avatar/avatar-variants.js +12 -0
  15. package/components/Avatar/avatar.marko +71 -0
  16. package/components/Badge/Badge.css +381 -0
  17. package/components/Badge/badge-dismissible.marko +143 -0
  18. package/components/Badge/badge-variants.js +35 -0
  19. package/components/Badge/badge.marko +134 -0
  20. package/components/BottomSheet/BottomSheet.css +300 -0
  21. package/components/BottomSheet/bottom-sheet.marko +340 -0
  22. package/components/BulkActionBar/BulkActionBar.css +88 -0
  23. package/components/BulkActionBar/bulk-action-bar.marko +186 -0
  24. package/components/Button/Button.css +340 -0
  25. package/components/Button/button-variants.js +25 -0
  26. package/components/Button/button.marko +139 -0
  27. package/components/Card/Card.css +391 -0
  28. package/components/Card/card-body.marko +5 -0
  29. package/components/Card/card-collapsible.marko +94 -0
  30. package/components/Card/card-footer.marko +12 -0
  31. package/components/Card/card-header.marko +30 -0
  32. package/components/Card/card-info-pair.marko +17 -0
  33. package/components/Card/card-info-row.marko +12 -0
  34. package/components/Card/card-section.marko +14 -0
  35. package/components/Card/card-variants.js +24 -0
  36. package/components/Card/card.marko +124 -0
  37. package/components/Checkbox/Checkbox.css +249 -0
  38. package/components/Checkbox/checkbox-variants.js +21 -0
  39. package/components/Checkbox/checkbox.marko +128 -0
  40. package/components/ColorPicker/ColorPicker.css +253 -0
  41. package/components/ColorPicker/color-maths.js +104 -0
  42. package/components/ColorPicker/color-picker.marko +474 -0
  43. package/components/DatePicker/DatePicker.css +429 -0
  44. package/components/DatePicker/date-logic.js +225 -0
  45. package/components/DatePicker/date-picker.marko +492 -0
  46. package/components/Drawer/Drawer.css +174 -0
  47. package/components/Drawer/drawer.marko +262 -0
  48. package/components/Floating/Floating.css +28 -0
  49. package/components/Floating/floating-position.js +251 -0
  50. package/components/Floating/floating.marko +291 -0
  51. package/components/Form/Form.css +15 -0
  52. package/components/Form/form-context.js +69 -0
  53. package/components/Form/form.marko +73 -0
  54. package/components/GradientPicker/GradientPicker.css +179 -0
  55. package/components/GradientPicker/gradient-picker.marko +398 -0
  56. package/components/Grid/Grid.css +161 -0
  57. package/components/Grid/col.marko +36 -0
  58. package/components/Grid/grid-variants.js +43 -0
  59. package/components/Grid/grid.marko +25 -0
  60. package/components/Header/Header.css +457 -0
  61. package/components/Header/header.marko +523 -0
  62. package/components/Icon/Icon.css +44 -0
  63. package/components/Icon/icon-colors.js +31 -0
  64. package/components/Icon/icon.marko +56 -0
  65. package/components/IconButton/IconButton.css +85 -0
  66. package/components/IconButton/icon-button-variants.js +18 -0
  67. package/components/IconButton/icon-button.marko +103 -0
  68. package/components/Incrementer/Incrementer.css +94 -0
  69. package/components/Incrementer/incrementer.marko +70 -0
  70. package/components/Input/Input.css +547 -0
  71. package/components/Input/input-countries.js +109 -0
  72. package/components/Input/input-masks.js +218 -0
  73. package/components/Input/input-types.js +194 -0
  74. package/components/Input/input.marko +530 -0
  75. package/components/Menu/Menu.css +651 -0
  76. package/components/Menu/menu-variants.js +89 -0
  77. package/components/Menu/menu.marko +560 -0
  78. package/components/Modal/Modal.css +160 -0
  79. package/components/Modal/modal.marko +296 -0
  80. package/components/Nav/Nav.css +298 -0
  81. package/components/Nav/nav-variants.js +23 -0
  82. package/components/Nav/nav.marko +164 -0
  83. package/components/Notification/Notification.css +78 -0
  84. package/components/Notification/notification-variants.js +30 -0
  85. package/components/Notification/notification.marko +55 -0
  86. package/components/Pagination/Pagination.css +78 -0
  87. package/components/Pagination/pagination-pages.js +66 -0
  88. package/components/Pagination/pagination.marko +141 -0
  89. package/components/Pill/Pill.css +193 -0
  90. package/components/Pill/pill-variants.js +16 -0
  91. package/components/Pill/pill.marko +165 -0
  92. package/components/Popover/Popover.css +121 -0
  93. package/components/Popover/popover.marko +96 -0
  94. package/components/Radio/Radio.css +230 -0
  95. package/components/Radio/radio-variants.js +16 -0
  96. package/components/Radio/radio.marko +93 -0
  97. package/components/RadioGroup/RadioGroup.css +32 -0
  98. package/components/RadioGroup/radio-group.marko +93 -0
  99. package/components/SearchBar/SearchBar.css +181 -0
  100. package/components/SearchBar/search-bar-variants.js +24 -0
  101. package/components/SearchBar/search-bar.marko +157 -0
  102. package/components/Select/Select.css +427 -0
  103. package/components/Select/select-options.js +163 -0
  104. package/components/Select/select.marko +449 -0
  105. package/components/Separator/Separator.css +22 -0
  106. package/components/Separator/separator.marko +14 -0
  107. package/components/Skeleton/Skeleton.css +100 -0
  108. package/components/Skeleton/skeleton-avatar.marko +28 -0
  109. package/components/Skeleton/skeleton-button.marko +22 -0
  110. package/components/Skeleton/skeleton-image.marko +20 -0
  111. package/components/Skeleton/skeleton-input.marko +22 -0
  112. package/components/Skeleton/skeleton-variants.js +16 -0
  113. package/components/Skeleton/skeleton.marko +53 -0
  114. package/components/Spinner/Spinner.css +173 -0
  115. package/components/Spinner/spinner-geometry.js +39 -0
  116. package/components/Spinner/spinner-overlay.marko +97 -0
  117. package/components/Spinner/spinner.marko +88 -0
  118. package/components/StatCard/StatCard.css +90 -0
  119. package/components/StatCard/stat-card-group.marko +5 -0
  120. package/components/StatCard/stat-card.marko +57 -0
  121. package/components/Switch/Switch.css +209 -0
  122. package/components/Switch/switch.marko +99 -0
  123. package/components/Table/Table.css +270 -0
  124. package/components/Table/table-cell.marko +28 -0
  125. package/components/Table/table-header.marko +27 -0
  126. package/components/Table/table-logic.js +129 -0
  127. package/components/Table/table.marko +372 -0
  128. package/components/Text/Text.css +79 -0
  129. package/components/Text/text-variants.js +25 -0
  130. package/components/Text/text.marko +48 -0
  131. package/components/TextEditor/TextEditor.css +78 -0
  132. package/components/TextEditor/safe-url.js +50 -0
  133. package/components/TextEditor/text-editor-commands.js +22 -0
  134. package/components/TextEditor/text-editor.marko +253 -0
  135. package/components/Toast/Toast.css +88 -0
  136. package/components/Toast/toast-api.js +19 -0
  137. package/components/Toast/toast-store.js +118 -0
  138. package/components/Toast/toast-variants.js +16 -0
  139. package/components/Toast/toast.marko +93 -0
  140. package/components/Toast/toaster.marko +70 -0
  141. package/components/Toggle/Toggle.css +169 -0
  142. package/components/Toggle/toggle.marko +93 -0
  143. package/components/Tooltip/Tooltip.css +115 -0
  144. package/components/Tooltip/tooltip.marko +77 -0
  145. package/components/Uploader/Uploader.css +180 -0
  146. package/components/Uploader/crop-geometry.js +161 -0
  147. package/components/Uploader/uploader.marko +269 -0
  148. package/foundations/Scrollbar/Scrollbar.css +26 -0
  149. package/foundations/attrs.js +27 -0
  150. package/foundations/breakpoints.js +27 -0
  151. package/foundations/contrast.js +74 -0
  152. package/foundations/custom-scheme.js +222 -0
  153. package/foundations/overlay.js +108 -0
  154. package/foundations/styles.css +284 -0
  155. package/foundations/text.js +22 -0
  156. package/foundations/theme-server.js +81 -0
  157. package/foundations/theme.js +42 -0
  158. package/foundations/tokens.css +1656 -0
  159. package/icons/custom/custom-video-horizontal-filled.marko +6 -0
  160. package/icons/custom/custom-video-horizontal-regular.marko +6 -0
  161. package/icons/custom/custom-video-tour-3d-filled.marko +7 -0
  162. package/icons/custom/custom-video-tour-3d-regular.marko +9 -0
  163. package/icons/custom/custom-video-vertical-filled.marko +6 -0
  164. package/icons/custom/custom-video-vertical-regular.marko +6 -0
  165. package/icons/generated/fluent-add-regular.marko +3 -0
  166. package/icons/generated/fluent-alert-regular.marko +3 -0
  167. package/icons/generated/fluent-apps-regular.marko +3 -0
  168. package/icons/generated/fluent-arrow-download-regular.marko +3 -0
  169. package/icons/generated/fluent-arrow-left-regular.marko +3 -0
  170. package/icons/generated/fluent-arrow-right-regular.marko +3 -0
  171. package/icons/generated/fluent-arrow-rotate-clockwise-regular.marko +3 -0
  172. package/icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko +3 -0
  173. package/icons/generated/fluent-border-all-regular.marko +3 -0
  174. package/icons/generated/fluent-calendar-regular.marko +3 -0
  175. package/icons/generated/fluent-checkmark-circle-filled.marko +3 -0
  176. package/icons/generated/fluent-checkmark-regular.marko +3 -0
  177. package/icons/generated/fluent-chevron-down-regular.marko +3 -0
  178. package/icons/generated/fluent-chevron-left-regular.marko +3 -0
  179. package/icons/generated/fluent-chevron-right-regular.marko +3 -0
  180. package/icons/generated/fluent-chevron-up-regular.marko +3 -0
  181. package/icons/generated/fluent-circle-half-fill-regular.marko +3 -0
  182. package/icons/generated/fluent-clock-regular.marko +3 -0
  183. package/icons/generated/fluent-color-regular.marko +3 -0
  184. package/icons/generated/fluent-crop-regular.marko +3 -0
  185. package/icons/generated/fluent-dark-theme-regular.marko +3 -0
  186. package/icons/generated/fluent-delete-regular.marko +3 -0
  187. package/icons/generated/fluent-desktop-regular.marko +3 -0
  188. package/icons/generated/fluent-dismiss-circle-filled.marko +3 -0
  189. package/icons/generated/fluent-dismiss-regular.marko +3 -0
  190. package/icons/generated/fluent-document-pdf-regular.marko +3 -0
  191. package/icons/generated/fluent-document-regular.marko +3 -0
  192. package/icons/generated/fluent-eye-off-regular.marko +3 -0
  193. package/icons/generated/fluent-eye-regular.marko +3 -0
  194. package/icons/generated/fluent-eyedropper-regular.marko +3 -0
  195. package/icons/generated/fluent-grid-regular.marko +3 -0
  196. package/icons/generated/fluent-history-regular.marko +3 -0
  197. package/icons/generated/fluent-home-regular.marko +3 -0
  198. package/icons/generated/fluent-image-regular.marko +3 -0
  199. package/icons/generated/fluent-info-regular.marko +3 -0
  200. package/icons/generated/fluent-link-regular.marko +3 -0
  201. package/icons/generated/fluent-more-horizontal-regular.marko +3 -0
  202. package/icons/generated/fluent-navigation-regular.marko +3 -0
  203. package/icons/generated/fluent-paint-brush-regular.marko +3 -0
  204. package/icons/generated/fluent-question-circle-regular.marko +3 -0
  205. package/icons/generated/fluent-search-regular.marko +3 -0
  206. package/icons/generated/fluent-settings-regular.marko +3 -0
  207. package/icons/generated/fluent-square-hint-regular.marko +3 -0
  208. package/icons/generated/fluent-subtract-regular.marko +3 -0
  209. package/icons/generated/fluent-text-align-center-regular.marko +3 -0
  210. package/icons/generated/fluent-text-align-left-regular.marko +3 -0
  211. package/icons/generated/fluent-text-align-right-regular.marko +3 -0
  212. package/icons/generated/fluent-text-bold-regular.marko +3 -0
  213. package/icons/generated/fluent-text-bullet-list-regular.marko +3 -0
  214. package/icons/generated/fluent-text-font-regular.marko +3 -0
  215. package/icons/generated/fluent-text-font-size-regular.marko +3 -0
  216. package/icons/generated/fluent-text-italic-regular.marko +3 -0
  217. package/icons/generated/fluent-text-number-list-ltr-regular.marko +3 -0
  218. package/icons/generated/fluent-video-regular.marko +3 -0
  219. package/icons/generated/fluent-warning-filled.marko +3 -0
  220. package/icons/generated/fluent-weather-moon-regular.marko +3 -0
  221. package/icons/generated/fluent-weather-sunny-regular.marko +3 -0
  222. package/icons/generated/fluent-zoom-in-regular.marko +3 -0
  223. package/icons/generated/fluent-zoom-out-regular.marko +3 -0
  224. package/icons/manifest.json +61 -0
  225. package/icons/social/social-facebook-color.marko +7 -0
  226. package/icons/social/social-facebook.marko +6 -0
  227. package/icons/social/social-google-color.marko +20 -0
  228. package/icons/social/social-google.marko +6 -0
  229. package/icons/social/social-instagram-color.marko +16 -0
  230. package/icons/social/social-instagram.marko +6 -0
  231. package/icons/social/social-linkedin-color.marko +8 -0
  232. package/icons/social/social-linkedin.marko +8 -0
  233. package/icons/social/social-messenger-color.marko +15 -0
  234. package/icons/social/social-messenger.marko +6 -0
  235. package/icons/social/social-pinterest-color.marko +6 -0
  236. package/icons/social/social-pinterest.marko +6 -0
  237. package/icons/social/social-telegram-color.marko +7 -0
  238. package/icons/social/social-telegram.marko +6 -0
  239. package/icons/social/social-whatsapp-color.marko +8 -0
  240. package/icons/social/social-whatsapp.marko +7 -0
  241. package/icons/social/social-x-color.marko +7 -0
  242. package/icons/social/social-x.marko +6 -0
  243. package/icons/social/social-youtube-color.marko +7 -0
  244. package/icons/social/social-youtube.marko +6 -0
  245. package/marko.json +203 -0
  246. 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>