@enigmax/primitives 0.17.0 → 0.18.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.
- package/dist/button-CaXaqG_K.d.ts +63 -0
- package/dist/chunk-2QFTRNAZ.js +88 -0
- package/dist/chunk-6BGBYUSZ.js +114 -0
- package/dist/chunk-AU3H5WIY.js +107 -0
- package/dist/chunk-F25CGNQC.js +19 -0
- package/dist/chunk-HC2ME5PU.js +168 -0
- package/dist/chunk-HS3X3XCW.js +43 -0
- package/dist/chunk-IIT6U7LM.js +249 -0
- package/dist/chunk-IXVMRVD4.js +96 -0
- package/dist/chunk-KJINGUQN.js +188 -0
- package/dist/chunk-LKA2UG4P.js +542 -0
- package/dist/chunk-MMQPZGSU.js +161 -0
- package/dist/chunk-OCMI7R6H.js +79 -0
- package/dist/chunk-QYMUIW5I.js +28 -0
- package/dist/chunk-S653GLSF.js +17 -0
- package/dist/chunk-SNYUBXWQ.js +149 -0
- package/dist/chunk-U3V4EHOB.js +41 -0
- package/dist/chunk-UOSSNUSC.js +309 -0
- package/dist/chunk-WSB655JB.js +47 -0
- package/dist/chunk-XNNQRA35.js +31 -0
- package/dist/chunk-XQHCZAPJ.js +102 -0
- package/dist/chunk-ZCUFYBPB.js +154 -0
- package/dist/chunk-ZWR2EXHQ.js +55 -0
- package/dist/flags-BBJc9unY.d.ts +133 -0
- package/dist/index-D-ARvWpQ.d.ts +306 -0
- package/dist/index-DNXPtY9X.d.ts +144 -0
- package/dist/index.d.ts +16 -602
- package/dist/index.js +9 -1
- package/dist/input-BwXjFenq.d.ts +77 -0
- package/dist/marquee-CJ3Uwy3E.d.ts +81 -0
- package/dist/network-D2LsBG_k.d.ts +39 -0
- package/dist/next/index.d.ts +20 -3
- package/dist/next/index.js +23 -3
- package/dist/notifications-BpVV6sel.d.ts +70 -0
- package/dist/password-3DRQYAYQ.js +2 -0
- package/dist/password-C8lG4Zm9.d.ts +71 -0
- package/dist/password-FB2CUEKJ.js +1 -0
- package/dist/react/button.d.ts +75 -0
- package/dist/react/button.js +4 -0
- package/dist/react/flag.d.ts +37 -0
- package/dist/react/flag.js +3 -0
- package/dist/react/index.d.ts +28 -312
- package/dist/react/index.js +22 -2
- package/dist/react/input.d.ts +4 -0
- package/dist/react/input.js +3 -0
- package/dist/react/marquee.d.ts +44 -0
- package/dist/react/marquee.js +3 -0
- package/dist/react/network.d.ts +20 -0
- package/dist/react/network.js +3 -0
- package/dist/react/notifications.d.ts +17 -0
- package/dist/react/notifications.js +3 -0
- package/dist/react/palette.d.ts +3 -0
- package/dist/react/palette.js +4 -0
- package/dist/react/relative-time.d.ts +21 -0
- package/dist/react/relative-time.js +3 -0
- package/dist/react/search.d.ts +30 -0
- package/dist/react/search.js +3 -0
- package/dist/react/slot.d.ts +47 -0
- package/dist/react/slot.js +2 -0
- package/dist/react/toast.d.ts +40 -0
- package/dist/react/toast.js +4 -0
- package/dist/react-router/index.d.ts +20 -3
- package/dist/react-router/index.js +23 -3
- package/dist/relative-time-YpRTG7YH.d.ts +106 -0
- package/dist/search-P4OHCRXD.js +50 -0
- package/package.json +52 -2
- package/registry.json +175 -18
- package/src/core/flags.ts +88 -42
- package/src/core/input-icons.ts +34 -0
- package/src/core/input.ts +5 -21
- package/src/core/palette.ts +0 -0
- package/src/index.ts +2 -2
- package/src/react/button.tsx +70 -4
- package/src/react/flag.tsx +18 -6
- package/src/react/index.ts +40 -5
- package/src/react/input/icon.tsx +17 -0
- package/src/react/input/index.tsx +307 -0
- package/src/react/input/password.tsx +174 -0
- package/src/react/input/search.tsx +82 -0
- package/src/react/input/types.ts +146 -0
- package/src/react/input/write-value.ts +18 -0
- package/src/react/palette/context.ts +60 -0
- package/src/react/palette/index.tsx +66 -0
- package/src/react/palette/root.tsx +621 -0
- package/src/react/slot.tsx +91 -0
- package/src/react/use-button.ts +3 -1
- package/dist/chunk-4GLYRF4B.js +0 -1259
- package/dist/chunk-JFNND6P4.js +0 -749
- package/src/react/input.tsx +0 -429
package/src/react/input.tsx
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { INPUT_ICON_PATHS } from "@/core/input";
|
|
4
|
-
import {
|
|
5
|
-
forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState,
|
|
6
|
-
type ComponentPropsWithoutRef, type ChangeEvent, type ReactNode
|
|
7
|
-
} from "react";
|
|
8
|
-
import {
|
|
9
|
-
generatePassword, estimatePasswordStrength,
|
|
10
|
-
type GeneratePasswordOptions, type EstimateOptions, type PasswordStrengthReport, type PasswordScore
|
|
11
|
-
} from "@/core/password";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* `<Input>` - a real field you pass props to, with the affordances a password field needs
|
|
15
|
-
* and none of the styling.
|
|
16
|
-
*
|
|
17
|
-
* Everything here is rendered by React. The imperative `createInput` is the adapter for
|
|
18
|
-
* pages that are not React; using it from a component would mean two owners of the same
|
|
19
|
-
* DOM, which is the thing that makes a wrapper feel wrong to work with.
|
|
20
|
-
*
|
|
21
|
-
* Nothing switches on by itself except the reveal, which a password field always wants.
|
|
22
|
-
* The generator, the strength meter and the breach check are props, because they belong on
|
|
23
|
-
* a registration or change-password form and are noise anywhere else.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const ICON_PROPS = {
|
|
27
|
-
viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "none",
|
|
28
|
-
stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
|
|
29
|
-
"aria-hidden": true
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
32
|
-
function Icon({ paths }: { paths: readonly string[]; }): ReactNode {
|
|
33
|
-
return <svg {...ICON_PROPS}>{paths.map((path) => <path key={path} d={path} />)}</svg>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** An extra button inside the field. The built-ins are the same shape. */
|
|
37
|
-
export interface FieldAction {
|
|
38
|
-
/** Stable id. Lands on `data-enigma-input-action`, and replaces a built-in of the same name. */
|
|
39
|
-
name: string;
|
|
40
|
-
/** Accessible name. Becomes `aria-label` and `title`. */
|
|
41
|
-
label: string;
|
|
42
|
-
icon: ReactNode;
|
|
43
|
-
onSelect: () => void;
|
|
44
|
-
/** Renders `aria-pressed`. Omit for actions that are not toggles. */
|
|
45
|
-
pressed?: boolean;
|
|
46
|
-
/** Default true. A false action is not rendered at all. */
|
|
47
|
-
visible?: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type BreachStatus = "idle" | "checking" | "safe" | "breached" | "error";
|
|
51
|
-
|
|
52
|
-
export interface BreachState {
|
|
53
|
-
status: BreachStatus;
|
|
54
|
-
/** How many breaches the password appears in. 0 unless `status` is "breached". */
|
|
55
|
-
count: number;
|
|
56
|
-
/** Whatever the checker threw. The form decides whether that is worth showing. */
|
|
57
|
-
error: unknown;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type BreachChecker = (password: string, options: { signal: AbortSignal; }) => Promise<{ breached: boolean; count: number; }>;
|
|
61
|
-
|
|
62
|
-
export interface InputProps extends Omit<ComponentPropsWithoutRef<"input">, "children"> {
|
|
63
|
-
/** The reveal toggle. Defaults to on for `type="password"` and off for everything else. */
|
|
64
|
-
reveal?: boolean;
|
|
65
|
-
revealLabels?: { show?: string; hide?: string; };
|
|
66
|
-
/** Offer to generate a password. `true` for the defaults, or the generator's options. */
|
|
67
|
-
generate?: boolean | GeneratePasswordOptions;
|
|
68
|
-
generateLabel?: string;
|
|
69
|
-
/** Show what was generated. On by default - a password nobody can read is not usable. */
|
|
70
|
-
revealOnGenerate?: boolean;
|
|
71
|
-
/**
|
|
72
|
-
* Also copy it to the clipboard. OFF by default and worth leaving off: the clipboard is
|
|
73
|
-
* shared with every other app on the machine and is not cleared.
|
|
74
|
-
*/
|
|
75
|
-
copyOnGenerate?: boolean;
|
|
76
|
-
onGenerate?: (password: string) => void;
|
|
77
|
-
/** Extra buttons, or a replacement for `reveal` / `generate` by name. */
|
|
78
|
-
actions?: FieldAction[];
|
|
79
|
-
/** Which end the buttons sit at. Position them yourself; this only orders the markup. */
|
|
80
|
-
position?: "start" | "end";
|
|
81
|
-
/** Score the password as it is typed, and render the meter under the field. */
|
|
82
|
-
strength?: boolean | EstimateOptions;
|
|
83
|
-
onStrengthChange?: (report: PasswordStrengthReport) => void;
|
|
84
|
-
/**
|
|
85
|
-
* Check the password against a breach corpus - pass `checkPasswordBreach` from
|
|
86
|
-
* @enigmax/utils, or your own. It is a prop rather than a built-in because it makes a
|
|
87
|
-
* network request, and that is not a decision a field should take on its own.
|
|
88
|
-
*/
|
|
89
|
-
breach?: BreachChecker;
|
|
90
|
-
/** Quiet time before a check fires, in ms. Default 500. */
|
|
91
|
-
breachDelay?: number;
|
|
92
|
-
onBreachChange?: (state: BreachState) => void;
|
|
93
|
-
/** Props for the element wrapping the field, its buttons and the meter. */
|
|
94
|
-
wrapperProps?: ComponentPropsWithoutRef<"div">;
|
|
95
|
-
/** Props for the row holding the field and its buttons - this is what you position. */
|
|
96
|
-
fieldProps?: ComponentPropsWithoutRef<"div">;
|
|
97
|
-
/**
|
|
98
|
-
* Classes for the parts you cannot reach with a ref, which is what Tailwind needs.
|
|
99
|
-
* `className` still goes to the `<input>` itself, where you would expect it.
|
|
100
|
-
*/
|
|
101
|
-
classNames?: {
|
|
102
|
-
actions?: string;
|
|
103
|
-
action?: string;
|
|
104
|
-
strength?: PasswordStrengthProps["classNames"];
|
|
105
|
-
};
|
|
106
|
-
/** Rendered inside the wrapper, after the meter. Your error message goes here. */
|
|
107
|
-
children?: ReactNode;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Write a value the way a keystroke would.
|
|
112
|
-
*
|
|
113
|
-
* Assigning `input.value` directly is invisible to React: it tracks the last value it
|
|
114
|
-
* rendered and skips the change event when the DOM disagrees with it. Going through the
|
|
115
|
-
* prototype's setter and dispatching a bubbling `input` event makes the generator behave
|
|
116
|
-
* exactly like typing - which is what makes it work with a controlled field, an
|
|
117
|
-
* uncontrolled one, and a form library, without knowing which it is in.
|
|
118
|
-
*/
|
|
119
|
-
function writeValue(input: HTMLInputElement, next: string): void {
|
|
120
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
121
|
-
if (setter) setter.call(input, next);
|
|
122
|
-
else input.value = next;
|
|
123
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
127
|
-
reveal,
|
|
128
|
-
revealLabels,
|
|
129
|
-
generate = false,
|
|
130
|
-
generateLabel = "Generate a password",
|
|
131
|
-
revealOnGenerate = true,
|
|
132
|
-
copyOnGenerate = false,
|
|
133
|
-
onGenerate,
|
|
134
|
-
actions = [],
|
|
135
|
-
position = "end",
|
|
136
|
-
strength = false,
|
|
137
|
-
onStrengthChange,
|
|
138
|
-
breach,
|
|
139
|
-
breachDelay = 500,
|
|
140
|
-
onBreachChange,
|
|
141
|
-
wrapperProps,
|
|
142
|
-
fieldProps,
|
|
143
|
-
classNames,
|
|
144
|
-
children,
|
|
145
|
-
type = "text",
|
|
146
|
-
onChange,
|
|
147
|
-
...props
|
|
148
|
-
}, forwardedRef) {
|
|
149
|
-
const innerRef = useRef<HTMLInputElement | null>(null);
|
|
150
|
-
const [revealed, setRevealed] = useState(false);
|
|
151
|
-
const describedBy = useId();
|
|
152
|
-
|
|
153
|
-
const isPassword = type === "password";
|
|
154
|
-
const showReveal = reveal ?? isPassword;
|
|
155
|
-
const showGenerate = generate !== false && isPassword;
|
|
156
|
-
|
|
157
|
-
// Tracked whether the field is controlled or not, because the meter and the breach
|
|
158
|
-
// check need the current value and an uncontrolled field never reports it as a prop.
|
|
159
|
-
const controlled = props.value !== undefined;
|
|
160
|
-
const [ownValue, setOwnValue] = useState(String(props.defaultValue ?? ""));
|
|
161
|
-
const value = controlled ? String(props.value ?? "") : ownValue;
|
|
162
|
-
|
|
163
|
-
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
|
164
|
-
if (!controlled) setOwnValue(event.target.value);
|
|
165
|
-
onChange?.(event);
|
|
166
|
-
}, [controlled, onChange]);
|
|
167
|
-
|
|
168
|
-
/* -------- the reveal, and the caret it would otherwise eat -------- */
|
|
169
|
-
|
|
170
|
-
// Captured BEFORE the type switch. Assigning `type` while an input is focused resets
|
|
171
|
-
// the caret to 0 in Chromium, and it does it one MACROTASK later - so anything read
|
|
172
|
-
// after the switch is already the clobbered value.
|
|
173
|
-
const caret = useRef<[number, number] | null>(null);
|
|
174
|
-
|
|
175
|
-
const toggleReveal = useCallback(() => {
|
|
176
|
-
const input = innerRef.current;
|
|
177
|
-
if (input && typeof document !== "undefined" && document.activeElement === input) {
|
|
178
|
-
try {
|
|
179
|
-
const { selectionStart, selectionEnd } = input;
|
|
180
|
-
caret.current = selectionStart === null || selectionEnd === null ? null : [selectionStart, selectionEnd];
|
|
181
|
-
} catch {
|
|
182
|
-
caret.current = null; // selection is not supported on every input type
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
setRevealed((current) => !current);
|
|
186
|
-
}, []);
|
|
187
|
-
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
const selection = caret.current;
|
|
190
|
-
if (!selection) return;
|
|
191
|
-
caret.current = null;
|
|
192
|
-
|
|
193
|
-
const restore = (): void => {
|
|
194
|
-
const input = innerRef.current;
|
|
195
|
-
if (!input || document.activeElement !== input) return;
|
|
196
|
-
try { input.setSelectionRange(selection[0], selection[1]); } catch { /* unsupported */ }
|
|
197
|
-
};
|
|
198
|
-
restore();
|
|
199
|
-
// And again on the next macrotask, which is where Chromium actually clobbers it.
|
|
200
|
-
// A restore that only runs inline silently loses. Focus is re-checked first, so a
|
|
201
|
-
// visitor who clicked elsewhere meanwhile is not dragged back.
|
|
202
|
-
const timer = setTimeout(restore, 0);
|
|
203
|
-
return () => clearTimeout(timer);
|
|
204
|
-
}, [revealed]);
|
|
205
|
-
|
|
206
|
-
/* -------- the generator -------- */
|
|
207
|
-
|
|
208
|
-
const handleGenerate = useCallback(() => {
|
|
209
|
-
const input = innerRef.current;
|
|
210
|
-
if (!input) return;
|
|
211
|
-
const password = generatePassword(typeof generate === "object" ? generate : {});
|
|
212
|
-
writeValue(input, password);
|
|
213
|
-
if (revealOnGenerate) setRevealed(true);
|
|
214
|
-
if (copyOnGenerate) void navigator.clipboard?.writeText(password).catch(() => {
|
|
215
|
-
// Denied permission or an insecure context. The password is in the field, which
|
|
216
|
-
// is the part that matters, so there is nothing to report.
|
|
217
|
-
});
|
|
218
|
-
input.focus();
|
|
219
|
-
onGenerate?.(password);
|
|
220
|
-
}, [generate, revealOnGenerate, copyOnGenerate, onGenerate]);
|
|
221
|
-
|
|
222
|
-
/* -------- strength -------- */
|
|
223
|
-
|
|
224
|
-
const estimateOptions = typeof strength === "object" ? strength : undefined;
|
|
225
|
-
const userInputs = estimateOptions?.userInputs;
|
|
226
|
-
const report = useMemo(
|
|
227
|
-
() => (strength === false ? null : estimatePasswordStrength(value, { userInputs })),
|
|
228
|
-
// The array is compared by identity, so a literal would re-estimate every render.
|
|
229
|
-
[strength, value, userInputs]
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
const strengthListener = useRef(onStrengthChange);
|
|
233
|
-
strengthListener.current = onStrengthChange;
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
if (report) strengthListener.current?.(report);
|
|
236
|
-
}, [report]);
|
|
237
|
-
|
|
238
|
-
/* -------- the breach check -------- */
|
|
239
|
-
|
|
240
|
-
const [breachState, setBreachState] = useState<BreachState>({ status: "idle", count: 0, error: null });
|
|
241
|
-
const breachListener = useRef(onBreachChange);
|
|
242
|
-
breachListener.current = onBreachChange;
|
|
243
|
-
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
if (!breach || !value) {
|
|
246
|
-
setBreachState({ status: "idle", count: 0, error: null });
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
const controller = new AbortController();
|
|
250
|
-
const timer = setTimeout(() => {
|
|
251
|
-
setBreachState({ status: "checking", count: 0, error: null });
|
|
252
|
-
breach(value, { signal: controller.signal })
|
|
253
|
-
.then((result) => {
|
|
254
|
-
if (controller.signal.aborted) return;
|
|
255
|
-
setBreachState({ status: result.breached ? "breached" : "safe", count: result.count, error: null });
|
|
256
|
-
})
|
|
257
|
-
.catch((error: unknown) => {
|
|
258
|
-
// An abort is this effect cleaning up after itself, not a failure.
|
|
259
|
-
if (controller.signal.aborted) return;
|
|
260
|
-
setBreachState({ status: "error", count: 0, error });
|
|
261
|
-
});
|
|
262
|
-
}, breachDelay);
|
|
263
|
-
|
|
264
|
-
return () => {
|
|
265
|
-
clearTimeout(timer);
|
|
266
|
-
// Cancels the request in flight, so the answer to a password that is three
|
|
267
|
-
// keystrokes old can never overwrite the answer to the current one.
|
|
268
|
-
controller.abort();
|
|
269
|
-
};
|
|
270
|
-
}, [breach, value, breachDelay]);
|
|
271
|
-
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
breachListener.current?.(breachState);
|
|
274
|
-
}, [breachState]);
|
|
275
|
-
|
|
276
|
-
/* -------- rendering -------- */
|
|
277
|
-
|
|
278
|
-
const builtIn: FieldAction[] = [];
|
|
279
|
-
if (showGenerate) {
|
|
280
|
-
builtIn.push({
|
|
281
|
-
name: "generate",
|
|
282
|
-
label: generateLabel,
|
|
283
|
-
icon: <Icon paths={INPUT_ICON_PATHS.generate} />,
|
|
284
|
-
onSelect: handleGenerate
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
if (showReveal) {
|
|
288
|
-
builtIn.push({
|
|
289
|
-
name: "reveal",
|
|
290
|
-
label: revealed ? revealLabels?.hide ?? "Hide password" : revealLabels?.show ?? "Show password",
|
|
291
|
-
icon: <Icon paths={revealed ? INPUT_ICON_PATHS.eyeOff : INPUT_ICON_PATHS.eye} />,
|
|
292
|
-
pressed: revealed,
|
|
293
|
-
onSelect: toggleReveal
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const overridden = new Set(actions.map((action) => action.name));
|
|
298
|
-
const rendered = [...builtIn.filter((action) => !overridden.has(action.name)), ...actions]
|
|
299
|
-
.filter((action) => action.visible !== false);
|
|
300
|
-
|
|
301
|
-
const locked = props.disabled === true || props.readOnly === true;
|
|
302
|
-
const buttons = rendered.length === 0 ? null : (
|
|
303
|
-
<span data-enigma-input-actions="" data-position={position} className={classNames?.actions}>
|
|
304
|
-
{rendered.map((action) => (
|
|
305
|
-
<button
|
|
306
|
-
key={action.name}
|
|
307
|
-
type="button"
|
|
308
|
-
data-enigma-input-action={action.name}
|
|
309
|
-
className={classNames?.action}
|
|
310
|
-
aria-label={action.label}
|
|
311
|
-
title={action.label}
|
|
312
|
-
aria-pressed={action.pressed}
|
|
313
|
-
disabled={locked}
|
|
314
|
-
// Keeps focus in the field: the visitor is mid-word and expects to keep
|
|
315
|
-
// typing, and a caret that never left needs no restoring.
|
|
316
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
317
|
-
onClick={action.onSelect}
|
|
318
|
-
>
|
|
319
|
-
{action.icon}
|
|
320
|
-
</button>
|
|
321
|
-
))}
|
|
322
|
-
</span>
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
return (
|
|
326
|
-
<div
|
|
327
|
-
{...wrapperProps}
|
|
328
|
-
data-enigma-input-root=""
|
|
329
|
-
data-revealed={revealed ? "" : undefined}
|
|
330
|
-
data-breached={breachState.status === "breached" ? "" : undefined}
|
|
331
|
-
data-score={report && !report.empty ? report.score : undefined}
|
|
332
|
-
>
|
|
333
|
-
<div {...fieldProps} data-enigma-input-field="">
|
|
334
|
-
{position === "start" && buttons}
|
|
335
|
-
<input
|
|
336
|
-
{...props}
|
|
337
|
-
ref={(node) => {
|
|
338
|
-
innerRef.current = node;
|
|
339
|
-
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
340
|
-
else if (forwardedRef) forwardedRef.current = node;
|
|
341
|
-
}}
|
|
342
|
-
// Revealing a password is a type switch, which is what the caret dance
|
|
343
|
-
// above exists for.
|
|
344
|
-
type={revealed && isPassword ? "text" : type}
|
|
345
|
-
onChange={handleChange}
|
|
346
|
-
data-enigma-input=""
|
|
347
|
-
aria-describedby={report ? describedBy : props["aria-describedby"]}
|
|
348
|
-
/>
|
|
349
|
-
{position === "end" && buttons}
|
|
350
|
-
</div>
|
|
351
|
-
{report && <PasswordStrength id={describedBy} report={report} classNames={classNames?.strength} />}
|
|
352
|
-
{children}
|
|
353
|
-
</div>
|
|
354
|
-
);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
const SCORE_LABELS = ["Very weak", "Weak", "Fair", "Strong", "Very strong"] as const;
|
|
358
|
-
|
|
359
|
-
export interface PasswordStrengthProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
360
|
-
/** The password to score. Ignored when `report` is given. */
|
|
361
|
-
value?: string;
|
|
362
|
-
/** A report you already have, e.g. from `<Input onStrengthChange>`. */
|
|
363
|
-
report?: PasswordStrengthReport;
|
|
364
|
-
userInputs?: string[];
|
|
365
|
-
/** Bars to draw. Five, so each score has one of its own. */
|
|
366
|
-
segments?: number;
|
|
367
|
-
/** Your own wording, worst first. */
|
|
368
|
-
labels?: readonly string[];
|
|
369
|
-
/** Show the top warning under the bars. On by default; it is the useful half. */
|
|
370
|
-
showWarning?: boolean;
|
|
371
|
-
/**
|
|
372
|
-
* Classes for the inner parts. The score is on the ROOT, so a segment colours itself
|
|
373
|
-
* with a group variant - `group-data-[score=0]/strength:bg-red-600` and so on.
|
|
374
|
-
*/
|
|
375
|
-
classNames?: { track?: string; segment?: string; label?: string; warning?: string; };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* The bars under a password field.
|
|
380
|
-
*
|
|
381
|
-
* Structure and state only - `data-score` on the root and `data-filled` per segment are
|
|
382
|
-
* where the colours attach. The component picks no colours, because red-through-green is a
|
|
383
|
-
* palette decision and this package does not own one.
|
|
384
|
-
*/
|
|
385
|
-
export function PasswordStrength({
|
|
386
|
-
value = "",
|
|
387
|
-
report,
|
|
388
|
-
userInputs,
|
|
389
|
-
segments = 5,
|
|
390
|
-
labels = SCORE_LABELS,
|
|
391
|
-
showWarning = true,
|
|
392
|
-
classNames,
|
|
393
|
-
...props
|
|
394
|
-
}: PasswordStrengthProps): ReactNode {
|
|
395
|
-
const computed = useMemo(
|
|
396
|
-
() => report ?? estimatePasswordStrength(value, { userInputs }),
|
|
397
|
-
[report, value, userInputs]
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
const score: PasswordScore = computed.score;
|
|
401
|
-
return (
|
|
402
|
-
<div
|
|
403
|
-
{...props}
|
|
404
|
-
data-enigma-password-strength=""
|
|
405
|
-
data-score={computed.empty ? undefined : score}
|
|
406
|
-
data-empty={computed.empty ? "" : undefined}
|
|
407
|
-
>
|
|
408
|
-
<div data-enigma-password-strength-track="" aria-hidden="true" className={classNames?.track}>
|
|
409
|
-
{Array.from({ length: segments }, (unused, index) => (
|
|
410
|
-
<span
|
|
411
|
-
key={index}
|
|
412
|
-
data-enigma-password-strength-segment=""
|
|
413
|
-
className={classNames?.segment}
|
|
414
|
-
// Score 0 still fills one bar: an empty track next to a filled
|
|
415
|
-
// field reads as "not measured", not as "this is a bad password".
|
|
416
|
-
data-filled={!computed.empty && index <= score ? "" : undefined}
|
|
417
|
-
/>
|
|
418
|
-
))}
|
|
419
|
-
</div>
|
|
420
|
-
{/* Announced when the band changes, which is rarely enough not to chatter. */}
|
|
421
|
-
<p data-enigma-password-strength-label="" role="status" aria-live="polite" className={classNames?.label}>
|
|
422
|
-
{computed.empty ? "" : labels[score] ?? ""}
|
|
423
|
-
</p>
|
|
424
|
-
{showWarning && !computed.empty && computed.warnings.length > 0 && (
|
|
425
|
-
<p data-enigma-password-strength-warning="" className={classNames?.warning}>{computed.warnings[0]}</p>
|
|
426
|
-
)}
|
|
427
|
-
</div>
|
|
428
|
-
);
|
|
429
|
-
}
|