@shadow-js/ui 0.0.0-stage → 1.0.0

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 (190) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2293 -0
  5. package/dist/dev/cli/schema.d.ts +73 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +141 -0
  9. package/dist/dev/locales/ar.js +128 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +141 -0
  12. package/dist/dev/locales/en.js +128 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +29 -0
  15. package/dist/dev/registry/utils/utils.js +27 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +187 -0
  18. package/dist/dev/theme/theme-logic.js +184 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +90 -0
  21. package/dist/dev/theme/theme-provider.js +150 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
@@ -0,0 +1,494 @@
1
+ import { type Accessor, onMount, splitProps, useMemo, useSignal } from "@shadow-js/core";
2
+ import {
3
+ createPartContext,
4
+ mergeElementProps,
5
+ PasswordToggleField,
6
+ useBaseId,
7
+ } from "@shadow-js/primitives";
8
+ import { type ComponentProps, For, type JSX, Show } from "@shadow-js/web";
9
+ import { useUiStrings } from "@/lib/ui-strings";
10
+ import { type ClassValue, cn } from "@/lib/utils";
11
+
12
+ /**
13
+ * Props of {@link PasswordInput}: the attributes of an `input`, which go to the input, plus the
14
+ * visibility options below. `disabled` disables both the input and the toggle. `class` styles the
15
+ * wrapper, so its width is the width of the field.
16
+ * @since 1.0.0
17
+ * @see {@link PasswordInput}
18
+ */
19
+ export type PasswordInputProps = Omit<ComponentProps<"input">, "children"> & {
20
+ /**
21
+ * Controlled visibility: `true` shows the password as text. Clicks on the toggle only report the
22
+ * change through `onVisibleChange`.
23
+ * @default none (uncontrolled)
24
+ */
25
+ readonly visible?: boolean | undefined;
26
+ /**
27
+ * Whether the password is shown at first, when uncontrolled.
28
+ * @default false
29
+ */
30
+ readonly defaultVisible?: boolean | undefined;
31
+ /**
32
+ * Called with the new visibility when the user shows or hides the password.
33
+ * @default none
34
+ */
35
+ readonly onVisibleChange?: ((visible: boolean) => void) | undefined;
36
+ /**
37
+ * The accessible name of the toggle, in the page's language. The name stays the same while the
38
+ * password is shown.
39
+ * @default "Show password"
40
+ */
41
+ readonly toggleLabel?: string | undefined;
42
+ /**
43
+ * Shows "Caps Lock is on" under the field while the password is hidden, the field has focus and Caps
44
+ * Lock is on, so a mistyped password is not a mystery. The hint is a polite status and the input's
45
+ * `aria-describedby` names it while it shows. Turn it off with `false`.
46
+ * @default true
47
+ */
48
+ readonly capsLockHint?: boolean | undefined;
49
+ /**
50
+ * The slot under the field: a {@link PasswordStrength}, or your own part that reads the field with
51
+ * {@link usePasswordInput}.
52
+ * @default none
53
+ */
54
+ readonly children?: JSX.Element | undefined;
55
+ };
56
+
57
+ /** The props as the implementation sees them. */
58
+ type Loose = {
59
+ readonly class?: ClassValue;
60
+ readonly id?: string | undefined;
61
+ readonly visible?: boolean | undefined;
62
+ readonly defaultVisible?: boolean | undefined;
63
+ readonly onVisibleChange?: ((visible: boolean) => void) | undefined;
64
+ readonly toggleLabel?: string | undefined;
65
+ readonly capsLockHint?: boolean | undefined;
66
+ readonly disabled?: boolean | undefined;
67
+ readonly children?: JSX.Element;
68
+ readonly [prop: string]: unknown;
69
+ };
70
+
71
+ /** An eye: the password is hidden, the toggle offers to show it. Inline SVG, as the icons are. */
72
+ function EyeIcon(props: ComponentProps<"svg">): JSX.Element {
73
+ const [local, rest] = splitProps(props, ["class"]);
74
+ return (
75
+ <svg
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ width="24"
78
+ height="24"
79
+ viewBox="0 0 24 24"
80
+ fill="none"
81
+ stroke="currentColor"
82
+ stroke-width="2"
83
+ stroke-linecap="round"
84
+ stroke-linejoin="round"
85
+ aria-hidden="true"
86
+ class={cn("size-4", local.class)}
87
+ {...rest}
88
+ >
89
+ <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
90
+ <circle cx="12" cy="12" r="3" />
91
+ </svg>
92
+ );
93
+ }
94
+
95
+ /** An eye with a slash: the password is shown, the toggle offers to hide it. Inline SVG. */
96
+ function EyeOffIcon(props: ComponentProps<"svg">): JSX.Element {
97
+ const [local, rest] = splitProps(props, ["class"]);
98
+ return (
99
+ <svg
100
+ xmlns="http://www.w3.org/2000/svg"
101
+ width="24"
102
+ height="24"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ stroke-width="2"
107
+ stroke-linecap="round"
108
+ stroke-linejoin="round"
109
+ aria-hidden="true"
110
+ class={cn("size-4", local.class)}
111
+ {...rest}
112
+ >
113
+ <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
114
+ <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
115
+ <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
116
+ <path d="m2 2 20 20" />
117
+ </svg>
118
+ );
119
+ }
120
+
121
+ /** `autocorrect` is an iOS Safari attribute that the JSX types do not list. @internal */
122
+ const SAFARI_ATTRIBUTES: Readonly<Record<string, string>> = { autocorrect: "off" };
123
+
124
+ /** What a password field shares with the parts inside it. @internal */
125
+ interface PasswordInputContextValue {
126
+ readonly value: Accessor<string>;
127
+ readonly visible: Accessor<boolean>;
128
+ /** Adds an element id to the input's `aria-describedby` while the part is mounted. */
129
+ readonly describe: (id: string) => () => void;
130
+ }
131
+
132
+ const passwordInputContext = createPartContext<PasswordInputContextValue>("PasswordInput");
133
+
134
+ /**
135
+ * What {@link usePasswordInput} returns: the password typed so far and whether it is shown.
136
+ * @see {@link usePasswordInput}
137
+ * @since 1.0.0
138
+ */
139
+ export interface PasswordInputState {
140
+ /** The password in the field, as typed (or the controlled `value`). Empty on the server. */
141
+ readonly value: Accessor<string>;
142
+ /** Whether the password is shown as text. */
143
+ readonly visible: Accessor<boolean>;
144
+ }
145
+
146
+ /**
147
+ * Reads the field that the calling part sits in, to build your own parts for the strength meter slot:
148
+ * a list of rules that turn green, a count of characters. Call it inside a {@link PasswordInput}.
149
+ *
150
+ * @returns Accessors of the typed value and the visibility.
151
+ * @throws Error when called outside a {@link PasswordInput}.
152
+ * @example
153
+ * import { PasswordInput, usePasswordInput } from "@/components/ui/password-input";
154
+ *
155
+ * function Length() {
156
+ * const field = usePasswordInput();
157
+ * return <p>{field.value().length} characters</p>;
158
+ * }
159
+ *
160
+ * export function Signup() {
161
+ * return (
162
+ * <PasswordInput aria-label="Password" autocomplete="new-password">
163
+ * <Length />
164
+ * </PasswordInput>
165
+ * );
166
+ * }
167
+ * @see {@link PasswordStrength}
168
+ * @since 1.0.0
169
+ */
170
+ function usePasswordInput(): PasswordInputState {
171
+ const context = passwordInputContext.use("usePasswordInput");
172
+ return { value: context.value, visible: context.visible };
173
+ }
174
+
175
+ /** Passwords that rate as the weakest whatever their length. @internal */
176
+ const COMMON = new Set([
177
+ "12345678",
178
+ "123456789",
179
+ "1234567890",
180
+ "11111111",
181
+ "00000000",
182
+ "password",
183
+ "password1",
184
+ "qwertyui",
185
+ "qwertyuiop",
186
+ "iloveyou",
187
+ "letmein1",
188
+ ]);
189
+
190
+ /**
191
+ * Rates a password from `0` (very weak) to `4` (strong) with a rule of thumb: length counts first (8, 12
192
+ * and 16 characters), and a mix of at least three kinds of characters (lowercase or other letters,
193
+ * uppercase letters, digits, symbols) adds one more level. A password shorter than 8 characters, and a
194
+ * very common one, is `0`. Characters are counted as people see them, so an emoji is one. It is a hint,
195
+ * not a guarantee: for real checks use a library such as zxcvbn and pass it as `evaluate`.
196
+ *
197
+ * @param password The password.
198
+ * @returns The level, from `0` to `4`.
199
+ * @example
200
+ * import { scorePassword } from "@/components/ui/password-input";
201
+ *
202
+ * export const level = scorePassword("Abcdefghijkl123!"); // 4
203
+ * @see {@link PasswordStrength}
204
+ * @since 1.0.0
205
+ */
206
+ function scorePassword(password: string): 0 | 1 | 2 | 3 | 4 {
207
+ const length = [...password].length;
208
+ if (length < 8 || COMMON.has(password.toLowerCase())) return 0;
209
+ const kinds = [/[\p{Ll}\p{Lo}\p{Lm}]/u, /[\p{Lu}\p{Lt}]/u, /\p{Nd}/u, /[^\p{L}\p{Nd}]/u].filter(
210
+ (kind) => kind.test(password),
211
+ ).length;
212
+ const base = length < 12 ? 1 : length < 16 ? 2 : 3;
213
+ return Math.min(4, base + (kinds >= 3 ? 1 : 0)) as 1 | 2 | 3 | 4;
214
+ }
215
+
216
+ /**
217
+ * Props of {@link PasswordStrength}: the attributes of the wrapper `div`, plus how to rate and name the
218
+ * levels.
219
+ * @see {@link PasswordStrength}
220
+ * @since 1.0.0
221
+ */
222
+ export interface PasswordStrengthProps extends ComponentProps<"div"> {
223
+ /**
224
+ * Rates the password from `0` to `4`. Use it to plug in a stronger checker.
225
+ * @default scorePassword
226
+ */
227
+ readonly evaluate?: ((password: string) => 0 | 1 | 2 | 3 | 4) | undefined;
228
+ /**
229
+ * The words for the five levels, from the weakest to the strongest.
230
+ * @default the UI strings password.level.0 to password.level.4 ("Very weak" to "Strong")
231
+ */
232
+ readonly labels?: readonly string[] | undefined;
233
+ /**
234
+ * The accessible name of the meter.
235
+ * @default the UI string password.strength ("Password strength")
236
+ */
237
+ readonly label?: string | undefined;
238
+ }
239
+
240
+ /**
241
+ * The strength meter for the slot of a {@link PasswordInput}: four bars that fill with the level of the
242
+ * password, and the level as text. Put it inside the field.
243
+ *
244
+ * Keyboard and ARIA: not interactive. The bars are a `role="meter"` named "Password strength", with
245
+ * `aria-valuemin` 0, `aria-valuemax` 4, `aria-valuenow` and, once something is typed, `aria-valuetext`
246
+ * (the level word). The word is also shown in a polite status that the input's `aria-describedby` names,
247
+ * so the level is read when the field is focused and announced when it changes. An empty field shows
248
+ * level 0 and no word. The bars have forced-colors styles, and the color change is not the only cue: the
249
+ * number of filled bars and the word say the same.
250
+ *
251
+ * @param props See {@link PasswordStrengthProps}.
252
+ * @returns The meter.
253
+ * @throws Error when rendered outside a {@link PasswordInput}.
254
+ * @example
255
+ * import { PasswordInput, PasswordStrength } from "@/components/ui/password-input";
256
+ *
257
+ * export function NewPassword() {
258
+ * return (
259
+ * <PasswordInput aria-label="New password" autocomplete="new-password">
260
+ * <PasswordStrength />
261
+ * </PasswordInput>
262
+ * );
263
+ * }
264
+ * @see {@link scorePassword}
265
+ * @see {@link usePasswordInput}
266
+ * @since 1.0.0
267
+ */
268
+ function PasswordStrength(props: PasswordStrengthProps): JSX.Element {
269
+ const field = passwordInputContext.use("PasswordStrength");
270
+ const t = useUiStrings();
271
+ const [local, rest] = splitProps(props, ["class", "evaluate", "labels", "label"]);
272
+ const textId = `${useBaseId()()}-strength`;
273
+ onMount(() => field.describe(textId));
274
+ const score = useMemo((): 0 | 1 | 2 | 3 | 4 => {
275
+ const evaluate = local.evaluate as ((password: string) => 0 | 1 | 2 | 3 | 4) | undefined;
276
+ return field.value() === "" ? 0 : (evaluate ?? scorePassword)(field.value());
277
+ });
278
+ const word = (): string => {
279
+ if (field.value() === "") return "";
280
+ const given = local.labels as readonly string[] | undefined;
281
+ if (given?.[score()] !== undefined) return given[score()] as string;
282
+ switch (score()) {
283
+ case 0:
284
+ return t("password.level.0", "Very weak");
285
+ case 1:
286
+ return t("password.level.1", "Weak");
287
+ case 2:
288
+ return t("password.level.2", "Fair");
289
+ case 3:
290
+ return t("password.level.3", "Good");
291
+ default:
292
+ return t("password.level.4", "Strong");
293
+ }
294
+ };
295
+ return (
296
+ <div
297
+ data-slot="password-strength"
298
+ data-score={score()}
299
+ class={cn("group/strength mt-2 grid gap-1.5", local.class)}
300
+ {...rest}
301
+ >
302
+ <div
303
+ role="meter"
304
+ aria-label={
305
+ (local.label as string | undefined) ?? t("password.strength", "Password strength")
306
+ }
307
+ aria-valuemin={0}
308
+ aria-valuemax={4}
309
+ aria-valuenow={score()}
310
+ aria-valuetext={word() === "" ? undefined : word()}
311
+ class="flex gap-1"
312
+ >
313
+ <For each={[1, 2, 3, 4]}>
314
+ {(level) => (
315
+ <span
316
+ data-slot="password-strength-segment"
317
+ data-active={field.value() !== "" && score() >= level ? "true" : "false"}
318
+ class="h-1.5 flex-1 rounded-full bg-muted transition-colors motion-reduce:transition-none data-[active=true]:bg-destructive group-data-[score=2]/strength:data-[active=true]:bg-chart-4 group-data-[score=3]/strength:data-[active=true]:bg-primary group-data-[score=4]/strength:data-[active=true]:bg-primary forced-colors:data-[active=true]:bg-[Highlight]"
319
+ />
320
+ )}
321
+ </For>
322
+ </div>
323
+ <p
324
+ data-slot="password-strength-label"
325
+ id={textId}
326
+ role="status"
327
+ aria-live="polite"
328
+ class="min-h-4 text-xs text-muted-foreground"
329
+ >
330
+ {word()}
331
+ </p>
332
+ </div>
333
+ );
334
+ }
335
+
336
+ /**
337
+ * A password field with a button that shows and hides the password. The input is a `password`
338
+ * field until the toggle shows it as text, and the icon changes with the state. It is built on the
339
+ * `PasswordToggleField` primitive, which owns the visibility and the toggle's ARIA. Name the input
340
+ * with a {@link Label} whose `htmlFor` points at the input's `id`.
341
+ *
342
+ * Children are the strength meter slot: put a {@link PasswordStrength}, or your own part that calls
343
+ * {@link usePasswordInput}, inside, and it is placed under the field. A Caps Lock hint appears under the
344
+ * field when it helps (see `capsLockHint`).
345
+ *
346
+ * Keyboard and ARIA: Tab reaches the toggle, which is a `button` with `type="button"`, so it never
347
+ * submits a form. Enter or Space flips the visibility. The toggle has `aria-pressed` (true while the
348
+ * password is shown) and `aria-controls`, which names the input. Its name is "Show password" unless
349
+ * you pass `toggleLabel`, and it keeps that name while the password is shown. The input has no name
350
+ * of its own: give it a label through `id` and `htmlFor`, or pass `aria-label`. `autocomplete`
351
+ * defaults to `current-password`; pass `new-password` on a sign-up form. The input also turns off
352
+ * `autocapitalize`, `spellcheck` and `autocorrect`, which would capitalize the first letter or send the
353
+ * word to a spelling service; pass your own values to change them. The words of the toggle, the hint
354
+ * and the meter come from the UI strings provider unless a prop sets them.
355
+ *
356
+ * The component does not store the password or send it anywhere: it only switches the input between
357
+ * `password` and `text`, and keeps the typed text in memory for the meter. A form around the field
358
+ * submits the value when the input has a `name`, as it does for any input.
359
+ *
360
+ * @param props The input's attributes, plus `visible`, `defaultVisible`, `onVisibleChange`,
361
+ * `toggleLabel` and `capsLockHint`. `class` styles the wrapper. `children` is the slot.
362
+ * @returns The wrapper `div` with the input and the toggle inside.
363
+ * @example
364
+ * import { Label } from "@/components/ui/label";
365
+ * import { PasswordInput, PasswordStrength } from "@/components/ui/password-input";
366
+ *
367
+ * <div>
368
+ * <Label htmlFor="password">Password</Label>
369
+ * <PasswordInput id="password" name="password" autocomplete="new-password">
370
+ * <PasswordStrength />
371
+ * </PasswordInput>
372
+ * </div>
373
+ * @since 1.0.0
374
+ * @see {@link PasswordInputProps}
375
+ */
376
+ function PasswordInput(props: PasswordInputProps): JSX.Element {
377
+ const [local, rest] = splitProps(props as unknown as Loose, [
378
+ "class",
379
+ "id",
380
+ "visible",
381
+ "defaultVisible",
382
+ "onVisibleChange",
383
+ "toggleLabel",
384
+ "capsLockHint",
385
+ "disabled",
386
+ "children",
387
+ ]);
388
+ const t = useUiStrings();
389
+ const [typed, setTyped] = useSignal("");
390
+ const [shown, setShown] = useSignal(local.defaultVisible === true);
391
+ const [caps, setCaps] = useSignal(false);
392
+ const [described, setDescribed] = useSignal<readonly string[]>([]);
393
+ const baseId = useBaseId()();
394
+ const hintId = `${baseId}-caps-lock`;
395
+ let element: HTMLInputElement | undefined;
396
+ const visible = (): boolean => local.visible ?? shown();
397
+ const value = (): string => (typeof rest["value"] === "string" ? rest["value"] : typed());
398
+ const showHint = (): boolean => local.capsLockHint !== false && caps() && !visible();
399
+ const update = (event: KeyboardEvent): void => {
400
+ setCaps(event.getModifierState?.("CapsLock") === true);
401
+ };
402
+ onMount(() => {
403
+ if (element === undefined) return undefined;
404
+ const input = element;
405
+ setTyped(input.value);
406
+ // A form reset puts the default value back after the event, with no `input` event of its own.
407
+ let timer: ReturnType<typeof setTimeout> | undefined;
408
+ const reset = (): void => {
409
+ clearTimeout(timer);
410
+ timer = setTimeout(() => setTyped(input.value), 0);
411
+ };
412
+ input.form?.addEventListener("reset", reset);
413
+ return () => {
414
+ clearTimeout(timer);
415
+ input.form?.removeEventListener("reset", reset);
416
+ };
417
+ });
418
+ const describedBy = (): string | undefined => {
419
+ const ids = [
420
+ rest["aria-describedby"] as string | undefined,
421
+ ...described(),
422
+ showHint() ? hintId : undefined,
423
+ ].filter((id) => id !== undefined && id !== "");
424
+ return ids.length === 0 ? undefined : ids.join(" ");
425
+ };
426
+ const handlers = mergeElementProps(
427
+ {
428
+ onInput: (event: Event) => setTyped((event.target as HTMLInputElement).value),
429
+ onKeyDown: update,
430
+ onKeyUp: update,
431
+ onBlur: () => setCaps(false),
432
+ ref: (node: HTMLInputElement) => {
433
+ element = node;
434
+ },
435
+ },
436
+ rest,
437
+ );
438
+ return (
439
+ <passwordInputContext.Provider
440
+ value={{
441
+ value,
442
+ visible,
443
+ describe: (id) => {
444
+ setDescribed((ids) => [...ids, id]);
445
+ return () => setDescribed((ids) => ids.filter((known) => known !== id));
446
+ },
447
+ }}
448
+ >
449
+ <PasswordToggleField
450
+ data-slot="password-input"
451
+ class={cn("relative w-full", local.class)}
452
+ visible={local.visible}
453
+ defaultVisible={local.defaultVisible}
454
+ onVisibleChange={(next: boolean) => {
455
+ setShown(next);
456
+ local.onVisibleChange?.(next);
457
+ }}
458
+ disabled={local.disabled}
459
+ >
460
+ <PasswordToggleField.Input
461
+ autocapitalize="none"
462
+ spellcheck={false}
463
+ {...SAFARI_ATTRIBUTES}
464
+ {...handlers}
465
+ aria-describedby={describedBy()}
466
+ data-slot="password-input-field"
467
+ id={local.id}
468
+ class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent ps-3 pe-10 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40 motion-reduce:transition-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent"
469
+ />
470
+ <PasswordToggleField.Toggle
471
+ data-slot="password-input-toggle"
472
+ aria-label={local.toggleLabel ?? t("password.show", "Show password")}
473
+ class="group absolute end-0 top-0 flex h-9 w-10 items-center justify-center rounded-e-md text-muted-foreground outline-none transition-[color,box-shadow] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none forced-colors:text-[ButtonText] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent"
474
+ >
475
+ <EyeIcon class="group-data-[state=visible]:hidden" />
476
+ <EyeOffIcon class="hidden group-data-[state=visible]:block" />
477
+ </PasswordToggleField.Toggle>
478
+ <Show when={showHint()}>
479
+ <p
480
+ data-slot="password-input-caps-lock"
481
+ id={hintId}
482
+ role="status"
483
+ class="mt-2 text-xs text-muted-foreground"
484
+ >
485
+ {t("password.capsLock", "Caps Lock is on")}
486
+ </p>
487
+ </Show>
488
+ {local.children}
489
+ </PasswordToggleField>
490
+ </passwordInputContext.Provider>
491
+ );
492
+ }
493
+
494
+ export { PasswordInput, PasswordStrength, scorePassword, usePasswordInput };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Popover",
3
+ "description": "Displays rich content in a portal, triggered by a button."
4
+ }