@enigmax/primitives 0.6.0 → 0.8.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/{chunk-WCB7V7XO.js → chunk-42Y7OOJM.js} +183 -3
- package/dist/index.d.ts +86 -1
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +154 -4
- package/dist/react/index.js +276 -3
- package/package.json +9 -6
- package/recipes/input/styles.css +54 -0
- package/recipes/input/tailwind.tsx +84 -0
- package/recipes/{marquee.css.tsx → marquee/css.tsx} +1 -1
- package/registry.json +82 -37
- package/src/core/input.ts +26 -3
- package/src/core/password.ts +252 -0
- package/src/index.ts +10 -0
- package/src/react/button.tsx +80 -0
- package/src/react/index.ts +21 -0
- package/src/react/input.tsx +429 -0
- package/recipes/input.css +0 -32
- package/recipes/input.css.tsx +0 -28
- package/recipes/input.tailwind.tsx +0 -44
- /package/recipes/{marquee.css → marquee/styles.css} +0 -0
- /package/recipes/{marquee.tailwind.tsx → marquee/tailwind.tsx} +0 -0
- /package/recipes/{search.tailwind.tsx → search/tailwind.tsx} +0 -0
|
@@ -293,8 +293,21 @@ function createMarquee(lane, track, options = {}) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
// src/core/input.ts
|
|
296
|
-
var
|
|
297
|
-
|
|
296
|
+
var INPUT_ICON_PATHS = {
|
|
297
|
+
eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],
|
|
298
|
+
eyeOff: [
|
|
299
|
+
"M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9",
|
|
300
|
+
"m2 2 20 20",
|
|
301
|
+
"M9.9 9.9a3 3 0 0 0 4.2 4.2"
|
|
302
|
+
],
|
|
303
|
+
generate: ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"]
|
|
304
|
+
};
|
|
305
|
+
function iconMarkup(paths) {
|
|
306
|
+
const body = paths.map((path) => `<path d="${path}"/>`).join("");
|
|
307
|
+
return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
308
|
+
}
|
|
309
|
+
var EYE = iconMarkup(INPUT_ICON_PATHS.eye);
|
|
310
|
+
var EYE_OFF = iconMarkup(INPUT_ICON_PATHS.eyeOff);
|
|
298
311
|
function resolve(value, state) {
|
|
299
312
|
return typeof value === "function" ? value(state) : value;
|
|
300
313
|
}
|
|
@@ -637,4 +650,171 @@ function createButton(options = {}) {
|
|
|
637
650
|
};
|
|
638
651
|
}
|
|
639
652
|
|
|
640
|
-
|
|
653
|
+
// src/core/password.ts
|
|
654
|
+
var LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
|
655
|
+
var UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
656
|
+
var DIGITS = "0123456789";
|
|
657
|
+
var SYMBOLS = "!@#$%^&*()-_=+[]{};:,.?";
|
|
658
|
+
var AMBIGUOUS = "Il1O0";
|
|
659
|
+
function randomIndex(bound) {
|
|
660
|
+
const random = globalThis.crypto?.getRandomValues?.bind(globalThis.crypto);
|
|
661
|
+
if (!random) {
|
|
662
|
+
throw new Error("Generating a password needs crypto.getRandomValues, which browsers only expose over HTTPS (or on localhost).");
|
|
663
|
+
}
|
|
664
|
+
const limit = Math.floor(2 ** 32 / bound) * bound;
|
|
665
|
+
const buffer = new Uint32Array(1);
|
|
666
|
+
let value;
|
|
667
|
+
do {
|
|
668
|
+
random(buffer);
|
|
669
|
+
value = buffer[0];
|
|
670
|
+
} while (value >= limit);
|
|
671
|
+
return value % bound;
|
|
672
|
+
}
|
|
673
|
+
function pick(alphabet) {
|
|
674
|
+
return alphabet[randomIndex(alphabet.length)];
|
|
675
|
+
}
|
|
676
|
+
function classes(options) {
|
|
677
|
+
const { lowercase = true, uppercase = true, digits = true, symbols = true, excludeAmbiguous = false, exclude = "" } = options;
|
|
678
|
+
const banned = /* @__PURE__ */ new Set([...excludeAmbiguous ? AMBIGUOUS : "", ...exclude]);
|
|
679
|
+
const clean = (source) => [...source].filter((character) => !banned.has(character)).join("");
|
|
680
|
+
return [
|
|
681
|
+
lowercase ? clean(LOWERCASE) : "",
|
|
682
|
+
uppercase ? clean(UPPERCASE) : "",
|
|
683
|
+
digits ? clean(DIGITS) : "",
|
|
684
|
+
symbols ? clean(SYMBOLS) : ""
|
|
685
|
+
].filter(Boolean);
|
|
686
|
+
}
|
|
687
|
+
function shuffle(characters) {
|
|
688
|
+
for (let index = characters.length - 1; index > 0; index--) {
|
|
689
|
+
const swap = randomIndex(index + 1);
|
|
690
|
+
[characters[index], characters[swap]] = [characters[swap], characters[index]];
|
|
691
|
+
}
|
|
692
|
+
return characters;
|
|
693
|
+
}
|
|
694
|
+
function generatePassword(options = {}) {
|
|
695
|
+
const { length = 20, requireEachClass = true } = options;
|
|
696
|
+
const pools = classes(options);
|
|
697
|
+
if (!pools.length) throw new Error("generatePassword: every character class was excluded.");
|
|
698
|
+
if (length < 1) throw new Error("generatePassword: length must be at least 1.");
|
|
699
|
+
if (requireEachClass && length < pools.length) {
|
|
700
|
+
throw new Error(`generatePassword: length ${length} cannot hold one character from each of the ${pools.length} classes requested.`);
|
|
701
|
+
}
|
|
702
|
+
const everything = pools.join("");
|
|
703
|
+
const required = requireEachClass ? pools.map(pick) : [];
|
|
704
|
+
const rest = Array.from({ length: length - required.length }, () => pick(everything));
|
|
705
|
+
return shuffle([...required, ...rest]).join("");
|
|
706
|
+
}
|
|
707
|
+
var COMMON = /* @__PURE__ */ new Set([
|
|
708
|
+
"password",
|
|
709
|
+
"passwd",
|
|
710
|
+
"123456",
|
|
711
|
+
"12345678",
|
|
712
|
+
"123456789",
|
|
713
|
+
"1234567890",
|
|
714
|
+
"qwerty",
|
|
715
|
+
"qwertyuiop",
|
|
716
|
+
"abc123",
|
|
717
|
+
"111111",
|
|
718
|
+
"123123",
|
|
719
|
+
"admin",
|
|
720
|
+
"letmein",
|
|
721
|
+
"welcome",
|
|
722
|
+
"monkey",
|
|
723
|
+
"dragon",
|
|
724
|
+
"sunshine",
|
|
725
|
+
"iloveyou",
|
|
726
|
+
"princess",
|
|
727
|
+
"football",
|
|
728
|
+
"baseball",
|
|
729
|
+
"master",
|
|
730
|
+
"shadow",
|
|
731
|
+
"superman",
|
|
732
|
+
"batman",
|
|
733
|
+
"trustno1",
|
|
734
|
+
"hello",
|
|
735
|
+
"freedom",
|
|
736
|
+
"whatever",
|
|
737
|
+
"starwars",
|
|
738
|
+
"changeme",
|
|
739
|
+
"secret",
|
|
740
|
+
"login",
|
|
741
|
+
"root",
|
|
742
|
+
"toor",
|
|
743
|
+
"test",
|
|
744
|
+
"guest",
|
|
745
|
+
"azerty",
|
|
746
|
+
"1q2w3e4r",
|
|
747
|
+
"zaq12wsx"
|
|
748
|
+
]);
|
|
749
|
+
var SEQUENCES = ["abcdefghijklmnopqrstuvwxyz", "0123456789", "qwertyuiop", "asdfghjkl", "zxcvbnm"];
|
|
750
|
+
function poolSize(password) {
|
|
751
|
+
let size = 0;
|
|
752
|
+
if (/[a-z]/.test(password)) size += 26;
|
|
753
|
+
if (/[A-Z]/.test(password)) size += 26;
|
|
754
|
+
if (/\d/.test(password)) size += 10;
|
|
755
|
+
if (/[^\w\s]|_/.test(password)) size += SYMBOLS.length;
|
|
756
|
+
if (/\s/.test(password)) size += 1;
|
|
757
|
+
return size || 1;
|
|
758
|
+
}
|
|
759
|
+
function longestRun(password) {
|
|
760
|
+
let longest = 1, run = 1;
|
|
761
|
+
for (let index = 1; index < password.length; index++) {
|
|
762
|
+
run = password[index] === password[index - 1] ? run + 1 : 1;
|
|
763
|
+
longest = Math.max(longest, run);
|
|
764
|
+
}
|
|
765
|
+
return longest;
|
|
766
|
+
}
|
|
767
|
+
function longestSequence(password) {
|
|
768
|
+
const lower = password.toLowerCase();
|
|
769
|
+
let longest = 0;
|
|
770
|
+
for (const line of SEQUENCES) {
|
|
771
|
+
const reversed = [...line].reverse().join("");
|
|
772
|
+
for (const source of [line, reversed]) {
|
|
773
|
+
for (let start = 0; start < source.length; start++) {
|
|
774
|
+
for (let end = source.length; end > start + longest; end--) {
|
|
775
|
+
if (lower.includes(source.slice(start, end))) {
|
|
776
|
+
longest = Math.max(longest, end - start);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return longest;
|
|
784
|
+
}
|
|
785
|
+
function core(password) {
|
|
786
|
+
return password.toLowerCase().replace(/^[^a-z]+/, "").replace(/[^a-z]+$/, "").replace(/[0!@$]/g, (character) => ({ "0": "o", "!": "i", "@": "a", "$": "s" })[character] ?? character);
|
|
787
|
+
}
|
|
788
|
+
function estimatePasswordStrength(password, options = {}) {
|
|
789
|
+
if (!password) return { score: 0, bits: 0, warnings: [], empty: true };
|
|
790
|
+
const warnings = [];
|
|
791
|
+
let bits = password.length * Math.log2(poolSize(password));
|
|
792
|
+
const stripped = core(password);
|
|
793
|
+
if (COMMON.has(password.toLowerCase()) || COMMON.has(stripped)) {
|
|
794
|
+
bits = Math.min(bits, 8);
|
|
795
|
+
warnings.push("This is one of the most common passwords there is.");
|
|
796
|
+
}
|
|
797
|
+
for (const input of options.userInputs ?? []) {
|
|
798
|
+
const needle = input.trim().toLowerCase();
|
|
799
|
+
if (needle.length < 4 || !password.toLowerCase().includes(needle)) continue;
|
|
800
|
+
bits = Math.min(bits, 16);
|
|
801
|
+
warnings.push("It contains something you already typed on this form.");
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
const run = longestRun(password);
|
|
805
|
+
if (run >= 3) {
|
|
806
|
+
bits -= (run - 2) * Math.log2(poolSize(password));
|
|
807
|
+
warnings.push("A character repeats several times in a row.");
|
|
808
|
+
}
|
|
809
|
+
const sequence = longestSequence(password);
|
|
810
|
+
if (sequence >= 4) {
|
|
811
|
+
bits -= sequence * Math.log2(poolSize(password)) * 0.75;
|
|
812
|
+
warnings.push("Part of it runs straight along the keyboard or the alphabet.");
|
|
813
|
+
}
|
|
814
|
+
if (password.length < 8) warnings.push("Short passwords fall to a brute force whatever they contain.");
|
|
815
|
+
bits = Math.max(0, Math.round(bits));
|
|
816
|
+
const score = bits < 28 ? 0 : bits < 40 ? 1 : bits < 60 ? 2 : bits < 80 ? 3 : 4;
|
|
817
|
+
return { score, bits, warnings, empty: false };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export { INPUT_ICON_PATHS, createButton, createInput, createMarquee, estimatePasswordStrength, generatePassword, iconMarkup };
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,21 @@ interface InputInstance {
|
|
|
155
155
|
update(options: Partial<InputOptions>): void;
|
|
156
156
|
destroy(): void;
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The built-in glyphs, as path data.
|
|
160
|
+
*
|
|
161
|
+
* Path data rather than markup because there are two renderers: this file writes an SVG
|
|
162
|
+
* string into a button it created, and the React component builds elements. Keeping the
|
|
163
|
+
* shapes here means one definition, and a theme that replaces an icon replaces it in both.
|
|
164
|
+
* Everything is stroked with `currentColor` at 1em, so an icon inherits the field's text.
|
|
165
|
+
*/
|
|
166
|
+
declare const INPUT_ICON_PATHS: {
|
|
167
|
+
readonly eye: readonly ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"];
|
|
168
|
+
readonly eyeOff: readonly ["M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9", "m2 2 20 20", "M9.9 9.9a3 3 0 0 0 4.2 4.2"];
|
|
169
|
+
readonly generate: readonly ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"];
|
|
170
|
+
};
|
|
171
|
+
/** The same shapes as a standalone SVG string, for the DOM renderer below. */
|
|
172
|
+
declare function iconMarkup(paths: readonly string[]): string;
|
|
158
173
|
/**
|
|
159
174
|
* Wire a field's in-field actions.
|
|
160
175
|
*
|
|
@@ -224,4 +239,74 @@ interface ButtonInstance {
|
|
|
224
239
|
}
|
|
225
240
|
declare function createButton(options?: ButtonOptions): ButtonInstance;
|
|
226
241
|
|
|
227
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Making a password, and judging one.
|
|
244
|
+
*
|
|
245
|
+
* Both are opt-in. A sign-in form wants neither: offering to generate a password where one
|
|
246
|
+
* already exists is noise, and scoring one the visitor cannot change is worse. They belong
|
|
247
|
+
* on a registration form and a change-password form, which is where an agent should switch
|
|
248
|
+
* them on.
|
|
249
|
+
*/
|
|
250
|
+
/** Character classes a generated password can draw from. */
|
|
251
|
+
interface PasswordAlphabet {
|
|
252
|
+
lowercase?: boolean;
|
|
253
|
+
uppercase?: boolean;
|
|
254
|
+
digits?: boolean;
|
|
255
|
+
symbols?: boolean;
|
|
256
|
+
}
|
|
257
|
+
interface GeneratePasswordOptions extends PasswordAlphabet {
|
|
258
|
+
/** Default 20. Long beats clever: length is the only term that scales. */
|
|
259
|
+
length?: number;
|
|
260
|
+
/**
|
|
261
|
+
* Drop the characters that are read wrong off a screen or off paper - I l 1 O 0.
|
|
262
|
+
* Worth it when the password will be typed by hand, not worth the entropy otherwise.
|
|
263
|
+
*/
|
|
264
|
+
excludeAmbiguous?: boolean;
|
|
265
|
+
/** Characters to remove from every class, e.g. ones your backend rejects. */
|
|
266
|
+
exclude?: string;
|
|
267
|
+
/**
|
|
268
|
+
* Guarantee at least one character from every class asked for. Most password policies
|
|
269
|
+
* demand it; it costs a little entropy, because it removes every password that happens
|
|
270
|
+
* to lack one.
|
|
271
|
+
*/
|
|
272
|
+
requireEachClass?: boolean;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* A random password from the classes asked for.
|
|
276
|
+
*
|
|
277
|
+
* @throws when the runtime has no CSPRNG, or when the options ask for something impossible
|
|
278
|
+
* (every class excluded, or a length too short to hold one of each).
|
|
279
|
+
*/
|
|
280
|
+
declare function generatePassword(options?: GeneratePasswordOptions): string;
|
|
281
|
+
type PasswordScore = 0 | 1 | 2 | 3 | 4;
|
|
282
|
+
interface PasswordStrengthReport {
|
|
283
|
+
/** 0 worst, 4 best. What the bars under the field render. */
|
|
284
|
+
score: PasswordScore;
|
|
285
|
+
/** Estimated bits of entropy after the penalties below. */
|
|
286
|
+
bits: number;
|
|
287
|
+
/**
|
|
288
|
+
* Why it scored what it scored, worst first. Show the first one; showing all of them
|
|
289
|
+
* turns a hint into a lecture.
|
|
290
|
+
*/
|
|
291
|
+
warnings: string[];
|
|
292
|
+
/** Empty field. Render nothing rather than a zero score, which reads as a failure. */
|
|
293
|
+
empty: boolean;
|
|
294
|
+
}
|
|
295
|
+
interface EstimateOptions {
|
|
296
|
+
/**
|
|
297
|
+
* Values the visitor has already typed elsewhere - email, name, company. A password
|
|
298
|
+
* containing one of them is guessable by anyone who has the sign-up form in front of
|
|
299
|
+
* them, and no character-class rule catches it.
|
|
300
|
+
*/
|
|
301
|
+
userInputs?: string[];
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Score a password.
|
|
305
|
+
*
|
|
306
|
+
* The bits are an estimate and the bands are a convention, not a measurement - they exist
|
|
307
|
+
* to move a bar, not to certify anything. Swap this out for zxcvbn where the number has to
|
|
308
|
+
* mean something, and check the breach corpus for the cases no estimator can see.
|
|
309
|
+
*/
|
|
310
|
+
declare function estimatePasswordStrength(password: string, options?: EstimateOptions): PasswordStrengthReport;
|
|
311
|
+
|
|
312
|
+
export { type ButtonCooldown, type ButtonElement, type ButtonInstance, type ButtonOptions, type ButtonState, type EstimateOptions, type GeneratePasswordOptions, INPUT_ICON_PATHS, type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeHover, type MarqueeInstance, type MarqueeOptions, type PasswordAlphabet, type PasswordScore, type PasswordStrengthReport, createButton, createInput, createMarquee, estimatePasswordStrength, generatePassword, iconMarkup };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createButton, createInput, createMarquee } from './chunk-
|
|
1
|
+
export { INPUT_ICON_PATHS, createButton, createInput, createMarquee, estimatePasswordStrength, generatePassword, iconMarkup } from './chunk-42Y7OOJM.js';
|
|
2
2
|
export { createSearch } from './chunk-UZFEEFMF.js';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RefObject, ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
|
|
3
|
+
import { MarqueeOptions, InputOptions, ButtonState, ButtonOptions, GeneratePasswordOptions, EstimateOptions, PasswordStrengthReport } from '../index.js';
|
|
4
|
+
export { InputAction, InputIcon, MarqueeHover, MarqueeInstance, PasswordScore, estimatePasswordStrength, generatePassword } from '../index.js';
|
|
4
5
|
import { b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
5
6
|
export { F as FuseConstructor } from '../search-CsO3L1Lw.js';
|
|
6
7
|
|
|
@@ -128,4 +129,153 @@ interface UseButtonResult extends ButtonState {
|
|
|
128
129
|
*/
|
|
129
130
|
declare function useButton(options?: ButtonOptions): UseButtonResult;
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
/**
|
|
133
|
+
* `<Button>` - the component, for the ninety percent.
|
|
134
|
+
*
|
|
135
|
+
* ```tsx
|
|
136
|
+
* <Button onPress={save}>Save</Button>
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* That is the whole thing: disabled while the work runs, `aria-busy`, `data-loading`, and a
|
|
140
|
+
* cooldown afterwards if you asked for one. `useButton` is still there for a button whose
|
|
141
|
+
* markup is nothing like a button - a card, a table row - but reaching for it to render an
|
|
142
|
+
* ordinary one means writing the same six lines at every call site, which is how they drift.
|
|
143
|
+
*/
|
|
144
|
+
interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRef<"button">, "onClick" | "onChange" | "children" | "type"> {
|
|
145
|
+
/**
|
|
146
|
+
* What to render. Defaults to `a` when there is an href and `button` otherwise, which
|
|
147
|
+
* is what you want until a router is involved - then pass its Link and keep the rest.
|
|
148
|
+
*
|
|
149
|
+
* ```tsx
|
|
150
|
+
* <Button as={Link} href="/settings">Settings</Button>
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
as?: ElementType;
|
|
154
|
+
/**
|
|
155
|
+
* The label, or a function of the state for one that changes with it.
|
|
156
|
+
*
|
|
157
|
+
* ```tsx
|
|
158
|
+
* <Button onPress={send} cooldown={30_000}>
|
|
159
|
+
* {({ loading, cooldown }) => loading ? "Sending" : cooldown ? `Wait ${Math.ceil(cooldown / 1000)}s` : "Resend"}
|
|
160
|
+
* </Button>
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
children?: ReactNode | ((state: ButtonState) => ReactNode);
|
|
164
|
+
/** Replaces the label while async work runs - a spinner, or just different words. */
|
|
165
|
+
pending?: ReactNode;
|
|
166
|
+
/** `type` on a real button. Ignored on a link, which has none. */
|
|
167
|
+
type?: "button" | "submit" | "reset";
|
|
168
|
+
}
|
|
169
|
+
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLElement>>;
|
|
170
|
+
|
|
171
|
+
/** An extra button inside the field. The built-ins are the same shape. */
|
|
172
|
+
interface FieldAction {
|
|
173
|
+
/** Stable id. Lands on `data-enigma-input-action`, and replaces a built-in of the same name. */
|
|
174
|
+
name: string;
|
|
175
|
+
/** Accessible name. Becomes `aria-label` and `title`. */
|
|
176
|
+
label: string;
|
|
177
|
+
icon: ReactNode;
|
|
178
|
+
onSelect: () => void;
|
|
179
|
+
/** Renders `aria-pressed`. Omit for actions that are not toggles. */
|
|
180
|
+
pressed?: boolean;
|
|
181
|
+
/** Default true. A false action is not rendered at all. */
|
|
182
|
+
visible?: boolean;
|
|
183
|
+
}
|
|
184
|
+
type BreachStatus = "idle" | "checking" | "safe" | "breached" | "error";
|
|
185
|
+
interface BreachState {
|
|
186
|
+
status: BreachStatus;
|
|
187
|
+
/** How many breaches the password appears in. 0 unless `status` is "breached". */
|
|
188
|
+
count: number;
|
|
189
|
+
/** Whatever the checker threw. The form decides whether that is worth showing. */
|
|
190
|
+
error: unknown;
|
|
191
|
+
}
|
|
192
|
+
type BreachChecker = (password: string, options: {
|
|
193
|
+
signal: AbortSignal;
|
|
194
|
+
}) => Promise<{
|
|
195
|
+
breached: boolean;
|
|
196
|
+
count: number;
|
|
197
|
+
}>;
|
|
198
|
+
interface InputProps extends Omit<ComponentPropsWithoutRef<"input">, "children"> {
|
|
199
|
+
/** The reveal toggle. Defaults to on for `type="password"` and off for everything else. */
|
|
200
|
+
reveal?: boolean;
|
|
201
|
+
revealLabels?: {
|
|
202
|
+
show?: string;
|
|
203
|
+
hide?: string;
|
|
204
|
+
};
|
|
205
|
+
/** Offer to generate a password. `true` for the defaults, or the generator's options. */
|
|
206
|
+
generate?: boolean | GeneratePasswordOptions;
|
|
207
|
+
generateLabel?: string;
|
|
208
|
+
/** Show what was generated. On by default - a password nobody can read is not usable. */
|
|
209
|
+
revealOnGenerate?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Also copy it to the clipboard. OFF by default and worth leaving off: the clipboard is
|
|
212
|
+
* shared with every other app on the machine and is not cleared.
|
|
213
|
+
*/
|
|
214
|
+
copyOnGenerate?: boolean;
|
|
215
|
+
onGenerate?: (password: string) => void;
|
|
216
|
+
/** Extra buttons, or a replacement for `reveal` / `generate` by name. */
|
|
217
|
+
actions?: FieldAction[];
|
|
218
|
+
/** Which end the buttons sit at. Position them yourself; this only orders the markup. */
|
|
219
|
+
position?: "start" | "end";
|
|
220
|
+
/** Score the password as it is typed, and render the meter under the field. */
|
|
221
|
+
strength?: boolean | EstimateOptions;
|
|
222
|
+
onStrengthChange?: (report: PasswordStrengthReport) => void;
|
|
223
|
+
/**
|
|
224
|
+
* Check the password against a breach corpus - pass `checkPasswordBreach` from
|
|
225
|
+
* @enigmax/utils, or your own. It is a prop rather than a built-in because it makes a
|
|
226
|
+
* network request, and that is not a decision a field should take on its own.
|
|
227
|
+
*/
|
|
228
|
+
breach?: BreachChecker;
|
|
229
|
+
/** Quiet time before a check fires, in ms. Default 500. */
|
|
230
|
+
breachDelay?: number;
|
|
231
|
+
onBreachChange?: (state: BreachState) => void;
|
|
232
|
+
/** Props for the element wrapping the field, its buttons and the meter. */
|
|
233
|
+
wrapperProps?: ComponentPropsWithoutRef<"div">;
|
|
234
|
+
/** Props for the row holding the field and its buttons - this is what you position. */
|
|
235
|
+
fieldProps?: ComponentPropsWithoutRef<"div">;
|
|
236
|
+
/**
|
|
237
|
+
* Classes for the parts you cannot reach with a ref, which is what Tailwind needs.
|
|
238
|
+
* `className` still goes to the `<input>` itself, where you would expect it.
|
|
239
|
+
*/
|
|
240
|
+
classNames?: {
|
|
241
|
+
actions?: string;
|
|
242
|
+
action?: string;
|
|
243
|
+
strength?: PasswordStrengthProps["classNames"];
|
|
244
|
+
};
|
|
245
|
+
/** Rendered inside the wrapper, after the meter. Your error message goes here. */
|
|
246
|
+
children?: ReactNode;
|
|
247
|
+
}
|
|
248
|
+
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
249
|
+
interface PasswordStrengthProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
250
|
+
/** The password to score. Ignored when `report` is given. */
|
|
251
|
+
value?: string;
|
|
252
|
+
/** A report you already have, e.g. from `<Input onStrengthChange>`. */
|
|
253
|
+
report?: PasswordStrengthReport;
|
|
254
|
+
userInputs?: string[];
|
|
255
|
+
/** Bars to draw. Five, so each score has one of its own. */
|
|
256
|
+
segments?: number;
|
|
257
|
+
/** Your own wording, worst first. */
|
|
258
|
+
labels?: readonly string[];
|
|
259
|
+
/** Show the top warning under the bars. On by default; it is the useful half. */
|
|
260
|
+
showWarning?: boolean;
|
|
261
|
+
/**
|
|
262
|
+
* Classes for the inner parts. The score is on the ROOT, so a segment colours itself
|
|
263
|
+
* with a group variant - `group-data-[score=0]/strength:bg-red-600` and so on.
|
|
264
|
+
*/
|
|
265
|
+
classNames?: {
|
|
266
|
+
track?: string;
|
|
267
|
+
segment?: string;
|
|
268
|
+
label?: string;
|
|
269
|
+
warning?: string;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The bars under a password field.
|
|
274
|
+
*
|
|
275
|
+
* Structure and state only - `data-score` on the root and `data-filled` per segment are
|
|
276
|
+
* where the colours attach. The component picks no colours, because red-through-green is a
|
|
277
|
+
* palette decision and this package does not own one.
|
|
278
|
+
*/
|
|
279
|
+
declare function PasswordStrength({ value, report, userInputs, segments, labels, showWarning, classNames, ...props }: PasswordStrengthProps): ReactNode;
|
|
280
|
+
|
|
281
|
+
export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, GeneratePasswordOptions, Input, InputOptions, type InputProps, MarqueeOptions, PasswordStrength, type PasswordStrengthProps, PasswordStrengthReport, SearchMatch, SearchOptions, type UseButtonResult, type UseInputResult, type UseMarqueeResult, type UseSearchResult, useButton, useInput, useMarquee, useSearch };
|