@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,547 @@
1
+ /* SDS Input.
2
+ Ported from kdls-design-system v2.5.1 (0f3edf8) components/Input/Input.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-input__native
7
+ [data-color-scheme="lya"] .sds-input:focus-within:not(.sds-input--feedback-success):not(.sds-input--feedback-alert):not(.sds-input--feedback-error)
8
+ *//* ============================================================
9
+ SDS Input
10
+ Height: 56px | Radius: corner-radius-medium (12px default)
11
+ Floating label pattern
12
+ ============================================================ */
13
+
14
+ .sds-input-phone-wrap--hug { width: fit-content; min-width: 160px; }
15
+ .sds-input-phone-wrap--fill { width: 100%; }
16
+ .sds-input-wrapper {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 0;
20
+ width: 100%;
21
+ }
22
+ /* ── Container ── */
23
+ .sds-input {
24
+ position: relative;
25
+ display: flex;
26
+ align-items: center;
27
+ gap: var(--spacing-3);
28
+ height: 56px;
29
+ min-height: 56px;
30
+ padding: 0 var(--spacing-4);
31
+ border: 1px solid var(--border-interactive-color);
32
+ border-radius: var(--radius-md);
33
+ background-color: var(--component-bg-color);
34
+ transition: border-color 120ms ease, box-shadow 120ms ease;
35
+ cursor: text;
36
+ box-sizing: border-box;
37
+ }
38
+ .sds-input:hover:not(:focus-within):not(.sds-input--disabled):not(.sds-input--loading) {
39
+ border-color: var(--color-primary-50-solid);
40
+ }
41
+ .sds-input:focus-within {
42
+ border-color: var(--color-primary-50-solid);
43
+ box-shadow: var(--focus-ring);
44
+ }
45
+ /* ── Feedback states ── */
46
+ .sds-input--feedback-success {
47
+ border-color: var(--color-success-50);
48
+ }
49
+ .sds-input--feedback-success:focus-within {
50
+ border-color: var(--color-success-50);
51
+ box-shadow: var(--focus-ring);
52
+ }
53
+ .sds-input--feedback-alert {
54
+ border-color: var(--color-alert-50);
55
+ }
56
+ .sds-input--feedback-alert:focus-within {
57
+ border-color: var(--color-alert-50);
58
+ box-shadow: var(--focus-ring);
59
+ }
60
+ .sds-input--feedback-error {
61
+ border-color: var(--color-error-50);
62
+ }
63
+ .sds-input--feedback-error:focus-within {
64
+ border-color: var(--color-error-50);
65
+ box-shadow: var(--focus-ring);
66
+ }
67
+ /* ── Loading ── */
68
+ .sds-input--loading {
69
+ cursor: wait;
70
+ }
71
+ .sds-input--loading > * { pointer-events: none; }
72
+ /* ── Disabled ── */
73
+ .sds-input-wrapper--disabled {
74
+ cursor: not-allowed; /* wrapper is the mouse target → shows the cursor */
75
+ }
76
+ .sds-input--disabled > * {
77
+ pointer-events: none; /* blocks the children; the box remains the mouse target and shows the cursor */
78
+ }
79
+ .sds-input--disabled {
80
+ background-color: var(--color-neutral-20);
81
+ border-color: var(--border-static-color);
82
+ cursor: not-allowed;
83
+ }
84
+ .sds-input--disabled .sds-input__native {
85
+ color: var(--color-neutral-40);
86
+ -webkit-text-fill-color: var(--color-neutral-40);
87
+ }
88
+ .sds-input--disabled .sds-input__icon,
89
+ .sds-input--disabled .sds-input__password-toggle,
90
+ .sds-input--disabled .sds-input__spinner-btn {
91
+ color: var(--color-neutral-40);
92
+ }
93
+ /* ── Field: wrapper for the floating label + native input ── */
94
+ .sds-input__field {
95
+ flex: 1;
96
+ position: relative;
97
+ display: flex;
98
+ flex-direction: column;
99
+ justify-content: center;
100
+ min-height: 52px;
101
+ min-width: 0;
102
+ }
103
+ /* Invisible spacer that mirrors the floating label's text so hug mode
104
+ grows the input when the label is wider than the native field */
105
+ .sds-input__field::before {
106
+ content: attr(data-label);
107
+ font-family: var(--font-family-base);
108
+ font-size: var(--font-size-body-md);
109
+ font-weight: var(--font-weight-regular);
110
+ white-space: nowrap;
111
+ display: block;
112
+ height: 0;
113
+ overflow: hidden;
114
+ visibility: hidden;
115
+ pointer-events: none;
116
+ }
117
+ /* ── Floating label ── */
118
+ .sds-input__label {
119
+ position: absolute;
120
+ left: 0;
121
+ top: 50%;
122
+ transform: translateY(-50%);
123
+ line-height: 1;
124
+ pointer-events: none;
125
+ transform-origin: top left;
126
+ transition:
127
+ top 120ms ease,
128
+ font-size 120ms ease,
129
+ color 120ms ease,
130
+ transform 120ms ease;
131
+ white-space: nowrap;
132
+ }
133
+ .sds-input__field .sds-input__label {
134
+ color: var(--color-neutral-50);
135
+ }
136
+ /* Floating: filled or focused */
137
+ .sds-input--filled .sds-input__label,
138
+ .sds-input:focus-within .sds-input__label {
139
+ top: var(--spacing-2);
140
+ transform: none;
141
+ font-size: var(--font-size-label-sm);
142
+ line-height: var(--lh-label-sm);
143
+ }
144
+ /* Label color in feedback states */
145
+ .sds-input--feedback-success .sds-input__label { color: var(--color-success-50); }
146
+ .sds-input--feedback-alert .sds-input__label { color: var(--color-alert-50); }
147
+ .sds-input--feedback-error .sds-input__label { color: var(--color-error-50); }
148
+ .sds-input--disabled .sds-input__label {
149
+ color: var(--color-neutral-40);
150
+ }
151
+ /* Required asterisk */
152
+ .sds-input__label--required::after {
153
+ content: ' *';
154
+ color: var(--color-error-50);
155
+ }
156
+ /* ── Native input ── */
157
+ .sds-input__native {
158
+ border: none;
159
+ outline: none;
160
+ background: transparent;
161
+ font-family: var(--font-family-base);
162
+ font-size: var(--font-size-body-md); /* 16px */
163
+ font-weight: var(--font-weight-regular);
164
+ color: var(--body-color);
165
+ width: 100%;
166
+ padding-top: var(--spacing-5); /* space for the floating label */
167
+ padding-bottom: var(--spacing-1);
168
+ line-height: 1;
169
+ caret-color: var(--color-primary-50-solid);
170
+ }
171
+ .sds-input__native::placeholder {
172
+ color: transparent; /* hidden at rest — the label acts as the placeholder */
173
+ }
174
+ .sds-input__native:focus::placeholder {
175
+ color: var(--placeholder-color);
176
+ }
177
+ /* Hides the browser's native spinner on type="number" — always */
178
+ .sds-input__native[type="number"]::-webkit-inner-spin-button,
179
+ .sds-input__native[type="number"]::-webkit-outer-spin-button {
180
+ -webkit-appearance: none;
181
+ margin: 0;
182
+ }
183
+ .sds-input__native[type="number"] {
184
+ -moz-appearance: textfield;
185
+ }
186
+ /* ── Spinner custom (type="number") ── */
187
+ .sds-input__spinner {
188
+ display: flex;
189
+ flex-direction: column;
190
+ align-items: center;
191
+ justify-content: center;
192
+ flex-shrink: 0;
193
+ gap: 0;
194
+ width: 32px;
195
+ height: 100%;
196
+ border-left: 1px solid var(--border-static-color);
197
+ margin-right: calc(-1 * var(--spacing-4));
198
+ opacity: 0;
199
+ pointer-events: none;
200
+ transition: opacity 120ms ease;
201
+ }
202
+ .sds-input:focus-within .sds-input__spinner {
203
+ opacity: 1;
204
+ pointer-events: auto;
205
+ }
206
+ .sds-input__spinner-btn {
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ width: 24px;
211
+ height: 22px;
212
+ padding: 0;
213
+ border: none;
214
+ background: transparent;
215
+ color: var(--body-color);
216
+ cursor: pointer;
217
+ border-radius: var(--radius-xs);
218
+ transition: background-color 120ms ease;
219
+ outline: none;
220
+ }
221
+ .sds-input__spinner-btn:hover:not(:disabled) {
222
+ background-color: var(--color-opacity-05);
223
+ }
224
+ .sds-input__spinner-btn:active:not(:disabled) {
225
+ background-color: var(--color-opacity-10);
226
+ }
227
+ .sds-input__spinner-btn:focus-visible {
228
+ box-shadow: var(--focus-ring);
229
+ }
230
+ .sds-input__spinner-btn:disabled {
231
+ cursor: not-allowed;
232
+ }
233
+ /* ── Icons ── */
234
+ .sds-input__icon {
235
+ display: inline-flex;
236
+ align-items: center;
237
+ font-size: 24px;
238
+ flex-shrink: 0;
239
+ width: 24px;
240
+ height: 24px;
241
+ }
242
+ /* ── Clickable right icon ── */
243
+ .sds-input__icon--right--clickable {
244
+ cursor: pointer;
245
+ border-radius: var(--radius-sm);
246
+ transition: background-color 120ms ease;
247
+ width: auto;
248
+ height: auto;
249
+ padding: var(--spacing-0-5);
250
+ }
251
+ .sds-input__icon--right--clickable:hover { background-color: var(--color-opacity-05); }
252
+ .sds-input__icon--right--clickable:active { background-color: var(--color-opacity-10); }
253
+ /* ── Password toggle ── */
254
+ .sds-input__password-toggle {
255
+ display: inline-flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ width: 24px;
259
+ height: 24px;
260
+ padding: 0;
261
+ border: none;
262
+ background: transparent;
263
+ color: var(--body-color);
264
+ cursor: pointer;
265
+ flex-shrink: 0;
266
+ border-radius: var(--radius-xs);
267
+ outline: none;
268
+ }
269
+ .sds-input__password-toggle:focus-visible {
270
+ box-shadow: var(--focus-ring);
271
+ }
272
+ /* ── International phone variant ── */
273
+ .sds-input--phone {
274
+ padding-left: 0;
275
+ overflow: hidden;
276
+ }
277
+ /* Flush against the left edge.
278
+
279
+ The field pads itself 16px on each side, and the country selector is the one thing that
280
+ should not sit inside that padding: it is a control with a ground of its own, and it
281
+ already runs the full height of the field. Left 16px in, it read as a chip floating in
282
+ the field rather than as the field's left end. Rafael, looking at it: it should be stuck
283
+ to the left corner the same way it is stuck to the top and the bottom.
284
+
285
+ A negative margin cancels the field's padding rather than removing it, so nothing else
286
+ moves — the text after the selector keeps the gap it had. And the left corners take the
287
+ field's own radius, or a square block would push out through the rounded corner. */
288
+ /* Flush against the left edge.
289
+
290
+ The field pads itself 16px on each side, and the country selector is the one thing that
291
+ should not sit inside that padding: it is a control with a ground of its own, and it
292
+ already runs the full height of the field. Left 16px in, it read as a chip floating in
293
+ the field rather than as the field's left end. Rafael, looking at it: it should be stuck
294
+ to the left corner the same way it is stuck to the top and the bottom.
295
+
296
+ A negative margin cancels the field's padding rather than removing it, so nothing else
297
+ moves — the text after the selector keeps the gap it had. And the left corners take the
298
+ field's own radius, or a square block would push out through the rounded corner. */
299
+ .sds-input__country {
300
+ display: flex;
301
+ align-items: center;
302
+ /* The selector IS a menu, so `.sds-menu`'s own `flex-direction: column` lands on this
303
+ same element — and Menu.css loads after this file, so it wins on order. Centring the
304
+ main axis is what actually keeps the flag and the DDI in the middle of the field;
305
+ before this they sat flush against the top. */
306
+ justify-content: center;
307
+ padding: 0 var(--spacing-3);
308
+ margin-left: calc(var(--spacing-4) * -1);
309
+ border-top-left-radius: inherit;
310
+ border-bottom-left-radius: inherit;
311
+ /* 100% of the field's CONTENT box: 56px less the 1px border on each side. A fixed 56px
312
+ here overflowed by exactly those two pixels and painted over the field's own stroke. */
313
+ height: 100%;
314
+ background-color: var(--color-neutral-10);
315
+ border: none;
316
+ cursor: pointer;
317
+ flex-shrink: 0;
318
+ transition: background-color 120ms ease;
319
+ }
320
+ .sds-input__country:hover:not(:disabled) {
321
+ background-color: var(--color-neutral-20);
322
+ }
323
+ .sds-input__country:focus-visible {
324
+ outline: none;
325
+ box-shadow: var(--focus-ring-inset);
326
+ }
327
+ .sds-input__country-flag {
328
+ display: block;
329
+ border-radius: 2px;
330
+ flex-shrink: 0;
331
+ }
332
+ /* The affordance that says the flag and the dial code are a choice and not a decoration.
333
+ `sm` is the 16px icon token — the same chevron, at the same size, that Menu's own
334
+ trigger button and Select draw. It turns over when the list is open; the custom trigger
335
+ carries no open class, but `aria-expanded` already says so and costs nothing. */
336
+ .sds-input__country-chevron {
337
+ display: inline-flex;
338
+ align-items: center;
339
+ flex-shrink: 0;
340
+ color: var(--color-neutral-60);
341
+ transition: transform 120ms ease;
342
+ }
343
+ [aria-expanded="true"] .sds-input__country-chevron {
344
+ transform: rotate(180deg);
345
+ }
346
+ .sds-input--phone .sds-input__field {
347
+ /* margin-left instead of padding-left: moves the absolute label + input together,
348
+ keeping the two aligned */
349
+ margin-left: var(--spacing-3);
350
+ }
351
+ /* ── Hint / helper message ── */
352
+ .sds-input__hint-row {
353
+ display: grid;
354
+ grid-template-columns: 1fr auto;
355
+ column-gap: var(--spacing-2);
356
+ align-items: start;
357
+ margin-top: var(--spacing-1);
358
+ contain: inline-size;
359
+ }
360
+ .sds-input__hint {
361
+ font-family: var(--font-family-base);
362
+ font-size: var(--font-size-label-sm); /* 12px */
363
+ line-height: 12px;
364
+ font-weight: var(--font-weight-regular);
365
+ margin-top: var(--spacing-1);
366
+ padding-left: var(--spacing-4); /* aligns with the input's text */
367
+ display: block;
368
+ }
369
+ .sds-input__hint-row .sds-input__hint {
370
+ margin-top: 0;
371
+ overflow-wrap: break-word;
372
+ word-break: break-word;
373
+ min-width: 0;
374
+ }
375
+ .sds-input__count {
376
+ font-family: var(--font-family-base);
377
+ font-size: var(--font-size-label-sm);
378
+ line-height: 12px;
379
+ font-weight: var(--font-weight-regular);
380
+ color: var(--label-color);
381
+ padding-right: var(--spacing-4);
382
+ grid-column: 2;
383
+ grid-row: 1;
384
+ }
385
+ /* Neutral 50 in light, neutral 40 in dark — the token carries both, so the hint does not
386
+ need a rule per theme. */
387
+ .sds-input__hint--none { color: var(--color-neutral-50-40); }
388
+ .sds-input__hint--success { color: var(--color-success-50); }
389
+ .sds-input__hint--alert { color: var(--color-alert-50); }
390
+ .sds-input__hint--error { color: var(--color-error-50); }
391
+ .sds-input__hint--disabled { color: var(--color-neutral-40); }
392
+ /* ── Textbox (textarea) variant ── */
393
+ .sds-input--textbox {
394
+ height: auto;
395
+ min-height: var(--sds-textarea-min-height, 114px);
396
+ align-items: flex-start;
397
+ resize: vertical;
398
+ overflow: hidden;
399
+ }
400
+ /* Field: fixed padding-top — guarantees constant height regardless of state */
401
+ .sds-input--textbox .sds-input__field {
402
+ min-height: 0;
403
+ padding-top: var(--spacing-7);
404
+ padding-bottom: var(--spacing-3);
405
+ align-self: stretch;
406
+ }
407
+ .sds-input--textbox .sds-input__label {
408
+ top: 26px;
409
+ transform: translateY(-50%);
410
+ line-height: 1;
411
+ }
412
+ /* Label: floated = small at the top */
413
+ .sds-input--filled.sds-input--textbox .sds-input__label,
414
+ .sds-input--textbox:focus-within .sds-input__label {
415
+ top: var(--spacing-2);
416
+ font-size: var(--font-size-label-sm);
417
+ line-height: var(--lh-label-sm);
418
+ }
419
+ .sds-input__native--textarea {
420
+ padding-top: 0;
421
+ padding-bottom: 0;
422
+ resize: none;
423
+ flex: 1;
424
+ line-height: var(--lh-body-md);
425
+ overflow-y: auto;
426
+ width: 100%;
427
+ }
428
+ /* ── Dark mode ── */
429
+ [data-theme="dark"] .sds-input__country {
430
+ background-color: var(--color-neutral-20);
431
+ }
432
+ /* ── Phone country dropdown ── */
433
+ .sds-input-phone-wrap {
434
+ position: relative;
435
+ }
436
+ .sds-phone-dropdown {
437
+ position: absolute;
438
+ top: calc(100% + var(--spacing-1));
439
+ left: 0;
440
+ z-index: var(--z-floating);
441
+ background: var(--component-bg-color);
442
+ border: 1px solid var(--border-static-color);
443
+ border-radius: var(--radius-md);
444
+ box-shadow: var(--shadow-drop-md);
445
+ min-width: 220px;
446
+ max-height: 320px;
447
+ overflow-y: auto;
448
+ padding: var(--spacing-1);
449
+ }
450
+ .sds-input-phone-wrap--hug .sds-phone-dropdown { min-width: 100%; }
451
+ .sds-input-phone-wrap--fill .sds-phone-dropdown { width: 100%; }
452
+ .sds-phone-dropdown__item {
453
+ display: flex;
454
+ align-items: center;
455
+ gap: var(--spacing-2);
456
+ width: 100%;
457
+ padding: var(--spacing-2) var(--spacing-4);
458
+ border: none;
459
+ background: transparent;
460
+ border-radius: var(--radius-sm);
461
+ cursor: pointer;
462
+ text-align: left;
463
+ font-family: var(--font-family-base);
464
+ font-size: var(--font-size-body-md);
465
+ font-weight: var(--font-weight-regular);
466
+ color: var(--color-neutral-80);
467
+ transition: background 120ms ease;
468
+ }
469
+ .sds-phone-dropdown__item:hover {
470
+ background: var(--color-neutral-10);
471
+ color: var(--color-neutral-10-on);
472
+ }
473
+ .sds-phone-dropdown__item:focus-visible {
474
+ outline: none;
475
+ box-shadow: var(--focus-ring);
476
+ }
477
+ .sds-phone-dropdown__item--active {
478
+ background: var(--color-primary-10);
479
+ color: var(--color-primary-60-solid);
480
+ font-weight: var(--font-weight-medium);
481
+ }
482
+ .sds-phone-dropdown__flag { display: block; width: 20px; height: 15px; border-radius: 2px; flex-shrink: 0; }
483
+ .sds-phone-dropdown__name { flex: 1; }
484
+ .sds-phone-dropdown__ddi {
485
+ flex-shrink: 0;
486
+ }
487
+ [data-theme="dark"] .sds-phone-dropdown {
488
+ background: var(--color-neutral-10);
489
+ border-color: var(--border-interactive-color);
490
+ }
491
+
492
+ /* ── Width box ─────────────────────────────────────────────────────────────────
493
+ KDLS named this wrapper .kdls-input-phone-wrap, which is what it was built for and not
494
+ what it does: it owns the field's width, phone or not. Renamed, with the original rules
495
+ kept above so nothing else has to move. */
496
+ .sds-input-box { position: relative; }
497
+ .sds-input-box--hug { width: fit-content; min-width: 160px; }
498
+ .sds-input-box--fill { width: 100%; }
499
+
500
+ /* ── The country selector on a tel-inter field ────────────────────────────────
501
+ It sits where the left icon would, because it is the left icon there: a flag and a
502
+ dial code, and the whole pair opens the list. */
503
+ .sds-input__country .sds-menu__trigger-custom {
504
+ display: inline-flex;
505
+ align-items: center;
506
+ gap: var(--spacing-1);
507
+ /* No padding of its own: the selector around it already pads 12px on each side, and an
508
+ extra 8px on the right left the chevron sitting short of the edge. */
509
+ border-radius: var(--radius-xs);
510
+ cursor: pointer;
511
+ }
512
+
513
+ .sds-input__country .sds-menu__trigger-custom:focus-visible {
514
+ /* Menu paints this trigger's ring as an `outline`; here it is a box-shadow, and without
515
+ clearing the outline the country selector wore both rings at once. */
516
+ outline: none;
517
+ box-shadow: var(--focus-ring);
518
+ }
519
+
520
+ /* Seven rows and a half.
521
+ Fifteen countries is a panel taller than the field it hangs off, and a list that runs
522
+ past the fold reads as the whole list — the eight below it are never looked for. Seven
523
+ whole rows plus half of the eighth is the cap: enough to choose from without scrolling
524
+ in the common case, and the cut-off row is what says there is more. The cap is on the
525
+ body rather than the panel so the search box above it stays put while the rows move. */
526
+ .sds-input__country .sds-menu__panel-body {
527
+ max-height: calc(7.5 * var(--sds-menu-item-height));
528
+ }
529
+ /* Only a minimum width — the padding is the menu header's, and so is the full width. */
530
+ .sds-input__country-search {
531
+ min-width: 240px;
532
+ }
533
+ .sds-input__country-flag {
534
+ font-size: 18px;
535
+ line-height: 1;
536
+ }
537
+
538
+ .sds-input__country-ddi {
539
+ font-family: var(--font-family-base);
540
+ font-size: var(--font-size-body-md);
541
+ color: var(--body-color);
542
+ white-space: nowrap;
543
+ }
544
+
545
+ .sds-input--disabled .sds-input__country-ddi {
546
+ color: var(--color-neutral-40);
547
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The countries a `tel-inter` field can be set to, and their flags.
3
+ *
4
+ * Ported from kdls-design-system v2.5.1 (0f3edf8) components/Input/Input.tsx
5
+ * (PHONE_COUNTRIES). Same fifteen, same order — Brazil first, because that is who this
6
+ * is for.
7
+ *
8
+ * Divergence, and the reason this file exists at all: KDLS draws each flag as
9
+ * `<img src="https://flagcdn.com/20x15/br.png">`. That is a request to a third party for
10
+ * every visitor who opens a form, which hands that third party the visitor's IP address
11
+ * and the page they were on. A phone field is not worth that, and a design system should
12
+ * not make the decision on a consumer's behalf without them noticing.
13
+ *
14
+ * The flag here is an emoji, built from the country code itself: the two letters shifted
15
+ * into the Unicode regional-indicator block make the flag, so there is nothing to fetch,
16
+ * nothing to bundle, and nothing to go stale.
17
+ *
18
+ * Known limit: Windows ships no flag glyphs, so a Windows reader sees the two letters
19
+ * instead — "BR" rather than 🇧🇷. That is legible and correct, which the fetched image is
20
+ * not when the network is slow or the CDN is blocked. Shipping fifteen SVGs is the other
21
+ * way and is open if the letters turn out to bother anyone.
22
+ */
23
+
24
+ import { fold } from '../../foundations/text.js';
25
+
26
+ /** `br` → 🇧🇷. Two ASCII letters, shifted into the regional-indicator block. */
27
+ export function flagOf(code) {
28
+ return [...code.toUpperCase()]
29
+ .map((letter) => String.fromCodePoint(0x1f1e6 + letter.charCodeAt(0) - 65))
30
+ .join('');
31
+ }
32
+
33
+ export const PHONE_COUNTRIES = [
34
+ { code: 'br', ddi: '+55', name: 'Brasil' },
35
+ { code: 'us', ddi: '+1', name: 'Estados Unidos' },
36
+ { code: 'ar', ddi: '+54', name: 'Argentina' },
37
+ { code: 'mx', ddi: '+52', name: 'México' },
38
+ { code: 'cl', ddi: '+56', name: 'Chile' },
39
+ { code: 'co', ddi: '+57', name: 'Colômbia' },
40
+ { code: 'pt', ddi: '+351', name: 'Portugal' },
41
+ { code: 'gb', ddi: '+44', name: 'Reino Unido' },
42
+ { code: 'de', ddi: '+49', name: 'Alemanha' },
43
+ { code: 'fr', ddi: '+33', name: 'França' },
44
+ { code: 'es', ddi: '+34', name: 'Espanha' },
45
+ { code: 'it', ddi: '+39', name: 'Itália' },
46
+ { code: 'jp', ddi: '+81', name: 'Japão' },
47
+ { code: 'cn', ddi: '+86', name: 'China' },
48
+ { code: 'in', ddi: '+91', name: 'Índia' },
49
+ ];
50
+
51
+ export const PHONE_COUNTRY_CODES = PHONE_COUNTRIES.map((country) => country.code);
52
+
53
+ /**
54
+ * The countries a search box's text should turn up.
55
+ *
56
+ * Three ways in, because there are three ways someone knows a country here: the name they
57
+ * would say out loud, the dial code they are copying off a business card, and the two
58
+ * letters they know from a domain or a flag. "55", "+55", "br" and "brasil" all find
59
+ * Brazil; an empty box finds everything.
60
+ */
61
+ export function searchCountries(query) {
62
+ const needle = fold(query).replace(/^\+/, '');
63
+ if (!needle) return PHONE_COUNTRIES;
64
+ return PHONE_COUNTRIES.filter((country) =>
65
+ fold(country.name).includes(needle)
66
+ || country.ddi.replace('+', '').startsWith(needle)
67
+ || country.code.startsWith(needle));
68
+ }
69
+
70
+ export function countryOf(code) {
71
+ return PHONE_COUNTRIES.find((country) => country.code === code) ?? PHONE_COUNTRIES[0];
72
+ }
73
+
74
+ export function assertCountry(code) {
75
+ if (code !== undefined && !PHONE_COUNTRY_CODES.includes(code)) {
76
+ throw new Error(
77
+ `Unknown phone country: "${code}". Allowed: ${PHONE_COUNTRY_CODES.join(', ')}`,
78
+ );
79
+ }
80
+ return code;
81
+ }
82
+
83
+ /**
84
+ * The national number, masked the way that country writes it.
85
+ *
86
+ * Brazil is the only one with a mask of its own here, which is what KDLS does too: the
87
+ * rest are grouped in a way that reads as a phone number without claiming to know the
88
+ * country's real format. A wrong mask is worse than none — it refuses digits the reader
89
+ * is holding in their hand.
90
+ */
91
+ export function maskNational(digits, code) {
92
+ const only = digits.replace(/\D/g, '');
93
+
94
+ if (code === 'br') {
95
+ // (11) 91234-5678 and (11) 1234-5678: the mobile ninth digit moves the split.
96
+ const trimmed = only.slice(0, 11);
97
+ if (trimmed.length <= 2) return trimmed.length ? `(${trimmed}` : '';
98
+ const area = trimmed.slice(0, 2);
99
+ const rest = trimmed.slice(2);
100
+ if (rest.length <= 4) return `(${area}) ${rest}`;
101
+ const split = rest.length > 8 ? 5 : 4;
102
+ return `(${area}) ${rest.slice(0, split)}-${rest.slice(split)}`;
103
+ }
104
+
105
+ // Everywhere else: groups of three, then four, which reads as a number without
106
+ // pretending to be that country's own convention.
107
+ const trimmed = only.slice(0, 15);
108
+ return trimmed.replace(/(\d{3})(?=\d)/g, '$1 ').trim();
109
+ }