use-password-policy 2.0.0 → 3.1.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/index.mjs CHANGED
@@ -1,240 +1,278 @@
1
+ 'use client';
2
+
1
3
  // src/use-password-policy.ts
2
- import { useMemo } from "react";
3
- var DEFAULT_POLICIES = [
4
- {
5
- name: "minLength",
6
- optionsKey: "minLength",
7
- test: (password, options) => password.length >= options.minLength
8
- },
9
- {
10
- name: "uppercase",
11
- optionsKey: "uppercaseCheck",
12
- test: (password, options) => options.uppercaseRegex.test(password)
13
- },
14
- {
15
- name: "lowercase",
16
- optionsKey: "lowercaseCheck",
17
- test: (password, options) => options.lowercaseRegex.test(password)
18
- },
19
- {
20
- name: "number",
21
- optionsKey: "numberCheck",
22
- test: (password, options) => options.numberRegex.test(password)
23
- },
24
- {
25
- name: "specialChar",
26
- optionsKey: "specialCharCheck",
27
- test: (password, options) => options.specialCharRegex.test(password)
28
- }
29
- ];
30
- var DEFAULT_OPTIONS = {
31
- minLength: 8,
32
- lowercaseCheck: true,
33
- uppercaseCheck: true,
34
- numberCheck: true,
35
- specialCharCheck: true,
36
- customRules: [],
37
- lowercaseRegex: /[a-z]/,
38
- uppercaseRegex: /[A-Z]/,
39
- numberRegex: /\d/,
40
- specialCharRegex: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/
41
- };
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { applyBreachResult, validatePassword } from "use-password-policy/core";
6
+ import { checkPwnedPassword } from "use-password-policy/core";
42
7
  var usePasswordPolicy = (options = {}) => {
43
8
  const { password = "" } = options;
44
- const mergedOptions = useMemo(
45
- () => ({ ...DEFAULT_OPTIONS, ...options }),
46
- [options]
47
- );
48
- const activePolicies = useMemo(() => {
49
- const activeDefaultPolicies = DEFAULT_POLICIES.filter(
50
- (policy) => policy.optionsKey && mergedOptions[policy.optionsKey]
51
- );
52
- return [...activeDefaultPolicies, ...mergedOptions.customRules];
53
- }, [mergedOptions]);
54
- const policyState = useMemo(() => {
55
- const state = {};
56
- for (const policy of activePolicies) {
57
- state[policy.name] = policy.test(password, mergedOptions);
58
- }
59
- return state;
60
- }, [password, activePolicies, mergedOptions]);
61
- const { score, label, isValid } = useMemo(() => {
62
- const passedPolicies = Object.values(policyState).filter(Boolean);
63
- const score2 = passedPolicies.length;
64
- const totalPolicies = activePolicies.length;
65
- const strengthPercentage = totalPolicies > 0 ? score2 / totalPolicies : 0;
66
- let label2 = "Very Weak";
67
- if (strengthPercentage >= 1) {
68
- label2 = "Very Strong";
69
- } else if (strengthPercentage >= 0.75) {
70
- label2 = "Strong";
71
- } else if (strengthPercentage >= 0.5) {
72
- label2 = "Medium";
73
- } else if (strengthPercentage > 0) {
74
- label2 = "Weak";
9
+ const result = useMemo(() => validatePassword(password, options), [password, options]);
10
+ const breach = options.breachCheck ? options.breachCheck === true ? {} : options.breachCheck : null;
11
+ const pwned = usePwnedPassword(password, {
12
+ enabled: Boolean(breach) && result.isValid,
13
+ debounceMs: breach == null ? void 0 : breach.debounceMs,
14
+ fetch: breach == null ? void 0 : breach.fetch,
15
+ endpoint: breach == null ? void 0 : breach.endpoint,
16
+ padding: breach == null ? void 0 : breach.padding
17
+ });
18
+ if (!breach) return { password, ...result };
19
+ return { password, ...applyBreachResult(result, { status: pwned.status, count: pwned.count }, options) };
20
+ };
21
+ function usePwnedPassword(password, options = {}) {
22
+ const { enabled = true, debounceMs = 500, fetch: customFetch, endpoint, padding } = options;
23
+ const [state, setState] = useState({ status: "idle", count: 0, isPwned: false });
24
+ useEffect(() => {
25
+ if (!enabled || !password) {
26
+ setState({ status: "idle", count: 0, isPwned: false });
27
+ return;
75
28
  }
76
- return {
77
- score: score2,
78
- label: label2,
79
- isValid: score2 === totalPolicies && totalPolicies > 0
29
+ const controller = new AbortController();
30
+ setState((s) => s.status === "checking" ? s : { status: "checking", count: 0, isPwned: false });
31
+ const timer = setTimeout(() => {
32
+ checkPwnedPassword(password, { signal: controller.signal, fetch: customFetch, endpoint, padding }).then((count) => {
33
+ if (!controller.signal.aborted) {
34
+ setState({ status: count > 0 ? "pwned" : "safe", count, isPwned: count > 0 });
35
+ }
36
+ }).catch((error) => {
37
+ if (!controller.signal.aborted) setState({ status: "error", count: 0, isPwned: false, error });
38
+ });
39
+ }, debounceMs);
40
+ return () => {
41
+ clearTimeout(timer);
42
+ controller.abort();
80
43
  };
81
- }, [policyState, activePolicies]);
82
- return {
83
- password,
84
- policyState,
85
- isValid,
86
- strengthScore: score,
87
- strengthLabel: label
88
- };
89
- };
44
+ }, [password, enabled, debounceMs, customFetch, endpoint, padding]);
45
+ return state;
46
+ }
90
47
 
91
48
  // src/PasswordPolicyInput.tsx
92
- import { useState, useEffect } from "react";
93
- import styled, { css } from "styled-components";
94
- import { jsx, jsxs } from "react/jsx-runtime";
95
- var STRENGTH_COLOR_MAP = {
96
- "Very Weak": "var(--rpp-weak)",
97
- "Weak": "var(--rpp-weak)",
98
- "Medium": "var(--rpp-medium)",
99
- "Strong": "var(--rpp-success)",
100
- "Very Strong": "var(--rpp-success)"
101
- };
102
- var Container = styled.div`
103
- /* CSS variables define the component's internal theme */
49
+ import * as React from "react";
50
+ import { forwardRef, useEffect as useEffect2, useRef, useState as useState2 } from "react";
51
+
52
+ // src/styles.ts
53
+ var passwordPolicyInputCss = `
54
+ :where(.rpp-root) {
104
55
  --rpp-accent: #646cff;
105
- --rpp-success: #27ae60;
56
+ --rpp-success: #1e8449;
106
57
  --rpp-danger: #c0392b;
107
- --rpp-weak: #f39c12;
108
- --rpp-medium: #d35400;
109
- --rpp-bg: #f9f9f9;
110
- --rpp-border: #e0e0e0;
111
- --rpp-text: #333;
112
-
113
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
58
+ --rpp-weak: #d35400;
59
+ --rpp-medium: #b8860b;
60
+ --rpp-bg: #ffffff;
61
+ --rpp-border: #d0d0d0;
62
+ --rpp-text: #222222;
63
+ --rpp-muted: #6b6b6b;
64
+ --rpp-radius: 6px;
65
+ font-family: inherit;
66
+ color: var(--rpp-text);
114
67
  display: flex;
115
68
  flex-direction: column;
116
69
  gap: 0.75rem;
117
- `;
118
- var InputWrapper = styled.div`
119
- position: relative;
120
- `;
121
- var Input = styled.input`
70
+ }
71
+ :where(.rpp-root) .rpp-field { position: relative; }
72
+ :where(.rpp-root) .rpp-input {
122
73
  box-sizing: border-box;
123
74
  width: 100%;
124
- padding: 0.75rem;
75
+ padding: 0.75rem 2.75rem 0.75rem 0.75rem;
125
76
  border: 1px solid var(--rpp-border);
126
- border-radius: 6px;
77
+ border-radius: var(--rpp-radius);
78
+ background: var(--rpp-bg);
79
+ color: var(--rpp-text);
80
+ font: inherit;
127
81
  font-size: 1rem;
128
-
129
- &:focus {
130
- border-color: var(--rpp-accent);
131
- outline: none;
132
- }
133
- `;
134
- var ToggleButton = styled.button`
82
+ }
83
+ :where(.rpp-root) .rpp-input:focus-visible {
84
+ outline: 2px solid var(--rpp-accent);
85
+ outline-offset: 1px;
86
+ border-color: var(--rpp-accent);
87
+ }
88
+ :where(.rpp-root) .rpp-toggle {
135
89
  position: absolute;
136
90
  top: 50%;
137
- right: 0.5rem;
91
+ right: 0.375rem;
138
92
  transform: translateY(-50%);
139
- background: none;
140
- border: none;
141
- cursor: pointer;
142
- padding: 0.5rem;
143
- color: #888;
144
93
  display: flex;
145
94
  align-items: center;
146
- `;
147
- var StrengthMeter = styled.div`
148
- display: flex;
149
- gap: 0.25rem;
150
- height: 6px;
151
- `;
152
- var StrengthMeterSegment = styled.div`
95
+ padding: 0.375rem;
96
+ border: 0;
97
+ border-radius: var(--rpp-radius);
98
+ background: none;
99
+ color: var(--rpp-muted);
100
+ cursor: pointer;
101
+ }
102
+ :where(.rpp-root) .rpp-toggle:focus-visible { outline: 2px solid var(--rpp-accent); }
103
+ :where(.rpp-root) .rpp-toggle:focus:not(:focus-visible) { outline: none; }
104
+ :where(.rpp-root) .rpp-meter { display: flex; gap: 0.25rem; height: 6px; }
105
+ :where(.rpp-root) .rpp-segment {
153
106
  flex: 1;
154
- background-color: var(--rpp-border);
155
107
  border-radius: 3px;
156
- transition: background-color 0.3s;
157
-
158
- ${({ isFilled, strengthLabel }) => (
159
- // FIX: Explicitly type props
160
- isFilled && css`
161
- background-color: ${STRENGTH_COLOR_MAP[strengthLabel]};
162
- `
163
- )}
164
- `;
165
- var RequirementsList = styled.ul`
108
+ background: var(--rpp-border);
109
+ transition: background-color 0.25s;
110
+ }
111
+ :where(.rpp-root[data-strength="very-weak"]) .rpp-segment[data-filled] { background: var(--rpp-danger); }
112
+ :where(.rpp-root[data-strength="weak"]) .rpp-segment[data-filled] { background: var(--rpp-weak); }
113
+ :where(.rpp-root[data-strength="medium"]) .rpp-segment[data-filled] { background: var(--rpp-medium); }
114
+ :where(.rpp-root[data-strength="strong"]) .rpp-segment[data-filled],
115
+ :where(.rpp-root[data-strength="very-strong"]) .rpp-segment[data-filled] { background: var(--rpp-success); }
116
+ :where(.rpp-root) .rpp-strength-label,
117
+ :where(.rpp-root) .rpp-feedback { margin: 0; font-size: 0.8125rem; color: var(--rpp-muted); }
118
+ :where(.rpp-root) .rpp-requirements {
166
119
  list-style: none;
167
- padding: 0;
168
120
  margin: 0;
121
+ padding: 0;
169
122
  display: flex;
170
123
  flex-direction: column;
171
- gap: 0.5rem;
124
+ gap: 0.375rem;
172
125
  font-size: 0.875rem;
173
- `;
174
- var RequirementItem = styled.li`
175
- display: flex;
176
- align-items: center;
177
- gap: 0.5rem;
178
- transition: color 0.2s;
179
- /* FIX: Explicitly type props */
180
- color: ${({ passed }) => passed ? "var(--rpp-success)" : "var(--rpp-danger)"};
181
- `;
182
- var EyeIcon = () => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
126
+ }
127
+ :where(.rpp-root) .rpp-requirement { display: flex; align-items: center; gap: 0.5rem; color: var(--rpp-danger); }
128
+ :where(.rpp-root) .rpp-requirement[data-passed] { color: var(--rpp-success); }
129
+ :where(.rpp-root) .rpp-requirement[data-pending] { color: var(--rpp-muted); }
130
+ :where(.rpp-root) .rpp-icon { width: 1em; text-align: center; font-weight: 700; }
131
+ :where(.rpp-root) .rpp-sr-only {
132
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
133
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
134
+ }
135
+ `.trim();
136
+
137
+ // src/PasswordPolicyInput.tsx
138
+ import { jsx, jsxs } from "react/jsx-runtime";
139
+ var idCounter = 0;
140
+ var useFallbackId = () => {
141
+ const [id] = useState2(() => `rpp-${++idCounter}`);
142
+ return id;
143
+ };
144
+ var _a;
145
+ var useStableId = (_a = React.useId) != null ? _a : useFallbackId;
146
+ var EyeIcon = () => /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", focusable: "false", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
183
147
  /* @__PURE__ */ jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
184
148
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
185
149
  ] });
186
- var EyeOffIcon = () => /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
150
+ var EyeOffIcon = () => /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", focusable: "false", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
187
151
  /* @__PURE__ */ jsx("path", { d: "M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" }),
188
152
  /* @__PURE__ */ jsx("line", { x1: "1", y1: "1", x2: "23", y2: "23" })
189
153
  ] });
190
- var PasswordPolicyInput = ({
191
- policyOptions,
192
- onPasswordChange,
193
- showStrengthMeter = true,
194
- showRequirementsList = true,
195
- showToggleButton = true,
196
- className,
197
- ...restInputProps
198
- }) => {
199
- const [password, setPassword] = useState("");
200
- const [showPassword, setShowPassword] = useState(false);
201
- const validation = usePasswordPolicy({ ...policyOptions, password });
202
- const { policyState, strengthScore, strengthLabel } = validation;
203
- useEffect(() => {
204
- onPasswordChange?.(password, validation);
205
- }, [password, validation, onPasswordChange]);
206
- const formatPolicyName = (name) => name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
207
- const handlePasswordChange = (e) => {
208
- setPassword(e.target.value);
209
- };
210
- return /* @__PURE__ */ jsxs(Container, { className, children: [
211
- /* @__PURE__ */ jsxs(InputWrapper, { children: [
212
- /* @__PURE__ */ jsx(
213
- Input,
154
+ var strengthKey = (label) => label.toLowerCase().replace(/\s+/g, "-");
155
+ var PasswordPolicyInput = forwardRef(
156
+ function PasswordPolicyInput2({
157
+ value,
158
+ defaultValue,
159
+ policyOptions,
160
+ onPasswordChange,
161
+ onChange,
162
+ showStrengthMeter = true,
163
+ showStrengthLabel = false,
164
+ showRequirementsList = true,
165
+ showToggleButton = true,
166
+ toggleLabels = { show: "Show password", hide: "Hide password" },
167
+ unstyled = false,
168
+ className,
169
+ inputClassName,
170
+ id,
171
+ "aria-describedby": ariaDescribedBy,
172
+ ...inputProps
173
+ }, ref) {
174
+ var _a2, _b;
175
+ const generatedId = useStableId();
176
+ const inputId = id != null ? id : `${generatedId}-input`;
177
+ const listId = `${generatedId}-requirements`;
178
+ const feedbackId = `${generatedId}-feedback`;
179
+ const isControlled = value !== void 0;
180
+ const [internalValue, setInternalValue] = useState2(defaultValue != null ? defaultValue : "");
181
+ const password = isControlled ? value : internalValue;
182
+ const [visible, setVisible] = useState2(false);
183
+ const validation = usePasswordPolicy({ ...policyOptions, password });
184
+ const { requirements, strengthPercent, strengthLabel, isValid, estimate } = validation;
185
+ const onPasswordChangeRef = useRef(onPasswordChange);
186
+ onPasswordChangeRef.current = onPasswordChange;
187
+ const reportKey = `${password}\0${isValid}\0${(_b = (_a2 = validation.breach) == null ? void 0 : _a2.status) != null ? _b : ""}`;
188
+ const lastReported = useRef(reportKey);
189
+ useEffect2(() => {
190
+ var _a3;
191
+ if (lastReported.current === reportKey) return;
192
+ lastReported.current = reportKey;
193
+ (_a3 = onPasswordChangeRef.current) == null ? void 0 : _a3.call(onPasswordChangeRef, password, validation);
194
+ });
195
+ const handleChange = (e) => {
196
+ if (!isControlled) setInternalValue(e.target.value);
197
+ onChange == null ? void 0 : onChange(e);
198
+ };
199
+ const segments = 5;
200
+ const filled = strengthPercent > 0 ? Math.max(1, Math.round(strengthPercent * segments)) : 0;
201
+ const feedback = password && (estimate == null ? void 0 : estimate.feedback);
202
+ const describedBy = [ariaDescribedBy, showRequirementsList ? listId : null, feedback ? feedbackId : null].filter(Boolean).join(" ") || void 0;
203
+ const rootClass = className ? `rpp-root ${className}` : "rpp-root";
204
+ return /* @__PURE__ */ jsxs("div", { className: rootClass, "data-strength": strengthKey(strengthLabel), "data-valid": isValid || void 0, children: [
205
+ !unstyled && /* @__PURE__ */ jsx("style", { children: passwordPolicyInputCss }),
206
+ /* @__PURE__ */ jsxs("div", { className: "rpp-field", children: [
207
+ /* @__PURE__ */ jsx(
208
+ "input",
209
+ {
210
+ ...inputProps,
211
+ ref,
212
+ id: inputId,
213
+ className: inputClassName ? `rpp-input ${inputClassName}` : "rpp-input",
214
+ type: visible ? "text" : "password",
215
+ value: password,
216
+ onChange: handleChange,
217
+ "aria-describedby": describedBy,
218
+ "aria-invalid": password.length > 0 && !isValid ? true : void 0
219
+ }
220
+ ),
221
+ showToggleButton && /* @__PURE__ */ jsx(
222
+ "button",
223
+ {
224
+ type: "button",
225
+ className: "rpp-toggle",
226
+ onClick: () => setVisible((v) => !v),
227
+ "aria-label": visible ? toggleLabels.hide : toggleLabels.show,
228
+ "aria-pressed": visible,
229
+ "aria-controls": inputId,
230
+ children: visible ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {})
231
+ }
232
+ )
233
+ ] }),
234
+ showStrengthMeter && /* @__PURE__ */ jsx(
235
+ "div",
214
236
  {
215
- type: showPassword ? "text" : "password",
216
- value: password,
217
- onChange: handlePasswordChange,
218
- ...restInputProps
237
+ className: "rpp-meter",
238
+ role: "meter",
239
+ "aria-label": "Password strength",
240
+ "aria-valuemin": 0,
241
+ "aria-valuemax": 100,
242
+ "aria-valuenow": Math.round(strengthPercent * 100),
243
+ "aria-valuetext": strengthLabel,
244
+ children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx("div", { className: "rpp-segment", "data-filled": i < filled || void 0 }, i))
219
245
  }
220
246
  ),
221
- showToggleButton && /* @__PURE__ */ jsx(ToggleButton, { type: "button", onClick: () => setShowPassword(!showPassword), children: showPassword ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {}) })
222
- ] }),
223
- showStrengthMeter && /* @__PURE__ */ jsx(StrengthMeter, { children: Array.from({ length: 5 }).map((_, index) => /* @__PURE__ */ jsx(
224
- StrengthMeterSegment,
225
- {
226
- isFilled: strengthScore > index,
227
- strengthLabel
228
- },
229
- index
230
- )) }),
231
- showRequirementsList && /* @__PURE__ */ jsx(RequirementsList, { children: Object.entries(policyState).map(([name, passed]) => /* @__PURE__ */ jsxs(RequirementItem, { passed, children: [
232
- passed ? "\u2713" : "\u2717",
233
- /* @__PURE__ */ jsx("span", { children: formatPolicyName(name) })
234
- ] }, name)) })
235
- ] });
236
- };
247
+ showStrengthMeter && showStrengthLabel && /* @__PURE__ */ jsx("p", { className: "rpp-strength-label", children: strengthLabel }),
248
+ showStrengthMeter && feedback && /* @__PURE__ */ jsx("p", { className: "rpp-feedback", id: feedbackId, children: feedback }),
249
+ showRequirementsList && /* @__PURE__ */ jsx("ul", { className: "rpp-requirements", id: listId, "aria-label": "Password requirements", children: requirements.map(({ name, passed, message, pending }) => {
250
+ var _a3, _b2;
251
+ return /* @__PURE__ */ jsxs(
252
+ "li",
253
+ {
254
+ className: "rpp-requirement",
255
+ "data-passed": passed || void 0,
256
+ "data-pending": pending || void 0,
257
+ "data-rule": name,
258
+ children: [
259
+ /* @__PURE__ */ jsx("span", { className: "rpp-icon", "aria-hidden": "true", children: pending ? ((_a3 = validation.breach) == null ? void 0 : _a3.status) === "checking" ? "\u2026" : "\u25CB" : passed ? "\u2713" : "\u2717" }),
260
+ /* @__PURE__ */ jsx("span", { children: message }),
261
+ /* @__PURE__ */ jsx("span", { className: "rpp-sr-only", children: pending ? ((_b2 = validation.breach) == null ? void 0 : _b2.status) === "checking" ? " (checking)" : " (checked once the others are met)" : passed ? " (met)" : " (not met)" })
262
+ ]
263
+ },
264
+ name
265
+ );
266
+ }) })
267
+ ] });
268
+ }
269
+ );
270
+
271
+ // src/index.ts
272
+ export * from "use-password-policy/core";
237
273
  export {
238
274
  PasswordPolicyInput,
239
- usePasswordPolicy
275
+ passwordPolicyInputCss,
276
+ usePasswordPolicy,
277
+ usePwnedPassword
240
278
  };
@@ -0,0 +1,81 @@
1
+ :where(.rpp-root) {
2
+ --rpp-accent: #646cff;
3
+ --rpp-success: #1e8449;
4
+ --rpp-danger: #c0392b;
5
+ --rpp-weak: #d35400;
6
+ --rpp-medium: #b8860b;
7
+ --rpp-bg: #ffffff;
8
+ --rpp-border: #d0d0d0;
9
+ --rpp-text: #222222;
10
+ --rpp-muted: #6b6b6b;
11
+ --rpp-radius: 6px;
12
+ font-family: inherit;
13
+ color: var(--rpp-text);
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 0.75rem;
17
+ }
18
+ :where(.rpp-root) .rpp-field { position: relative; }
19
+ :where(.rpp-root) .rpp-input {
20
+ box-sizing: border-box;
21
+ width: 100%;
22
+ padding: 0.75rem 2.75rem 0.75rem 0.75rem;
23
+ border: 1px solid var(--rpp-border);
24
+ border-radius: var(--rpp-radius);
25
+ background: var(--rpp-bg);
26
+ color: var(--rpp-text);
27
+ font: inherit;
28
+ font-size: 1rem;
29
+ }
30
+ :where(.rpp-root) .rpp-input:focus-visible {
31
+ outline: 2px solid var(--rpp-accent);
32
+ outline-offset: 1px;
33
+ border-color: var(--rpp-accent);
34
+ }
35
+ :where(.rpp-root) .rpp-toggle {
36
+ position: absolute;
37
+ top: 50%;
38
+ right: 0.375rem;
39
+ transform: translateY(-50%);
40
+ display: flex;
41
+ align-items: center;
42
+ padding: 0.375rem;
43
+ border: 0;
44
+ border-radius: var(--rpp-radius);
45
+ background: none;
46
+ color: var(--rpp-muted);
47
+ cursor: pointer;
48
+ }
49
+ :where(.rpp-root) .rpp-toggle:focus-visible { outline: 2px solid var(--rpp-accent); }
50
+ :where(.rpp-root) .rpp-toggle:focus:not(:focus-visible) { outline: none; }
51
+ :where(.rpp-root) .rpp-meter { display: flex; gap: 0.25rem; height: 6px; }
52
+ :where(.rpp-root) .rpp-segment {
53
+ flex: 1;
54
+ border-radius: 3px;
55
+ background: var(--rpp-border);
56
+ transition: background-color 0.25s;
57
+ }
58
+ :where(.rpp-root[data-strength="very-weak"]) .rpp-segment[data-filled] { background: var(--rpp-danger); }
59
+ :where(.rpp-root[data-strength="weak"]) .rpp-segment[data-filled] { background: var(--rpp-weak); }
60
+ :where(.rpp-root[data-strength="medium"]) .rpp-segment[data-filled] { background: var(--rpp-medium); }
61
+ :where(.rpp-root[data-strength="strong"]) .rpp-segment[data-filled],
62
+ :where(.rpp-root[data-strength="very-strong"]) .rpp-segment[data-filled] { background: var(--rpp-success); }
63
+ :where(.rpp-root) .rpp-strength-label,
64
+ :where(.rpp-root) .rpp-feedback { margin: 0; font-size: 0.8125rem; color: var(--rpp-muted); }
65
+ :where(.rpp-root) .rpp-requirements {
66
+ list-style: none;
67
+ margin: 0;
68
+ padding: 0;
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: 0.375rem;
72
+ font-size: 0.875rem;
73
+ }
74
+ :where(.rpp-root) .rpp-requirement { display: flex; align-items: center; gap: 0.5rem; color: var(--rpp-danger); }
75
+ :where(.rpp-root) .rpp-requirement[data-passed] { color: var(--rpp-success); }
76
+ :where(.rpp-root) .rpp-requirement[data-pending] { color: var(--rpp-muted); }
77
+ :where(.rpp-root) .rpp-icon { width: 1em; text-align: center; font-weight: 700; }
78
+ :where(.rpp-root) .rpp-sr-only {
79
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
80
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
81
+ }