@enigmax/primitives 0.5.0 → 0.7.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-2DVVWPHM.js → chunk-42Y7OOJM.js} +343 -3
- package/dist/index.d.ts +148 -1
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +149 -4
- package/dist/react/index.js +303 -3
- package/package.json +9 -6
- package/recipes/input/styles.css +54 -0
- package/recipes/input/tailwind.tsx +84 -0
- package/registry.json +104 -19
- package/src/core/button.ts +238 -0
- package/src/core/input.ts +26 -3
- package/src/core/password.ts +252 -0
- package/src/index.ts +11 -0
- package/src/react/index.ts +22 -0
- package/src/react/input.tsx +429 -0
- package/src/react/use-button.ts +89 -0
- package/recipes/input.css +0 -32
- package/recipes/input.css.tsx +0 -28
- package/recipes/input.tailwind.tsx +0 -44
|
@@ -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
|
}
|
|
@@ -477,4 +490,331 @@ function createInput(input, options = {}) {
|
|
|
477
490
|
};
|
|
478
491
|
}
|
|
479
492
|
|
|
480
|
-
|
|
493
|
+
// src/core/button.ts
|
|
494
|
+
var TICK_MS = 100;
|
|
495
|
+
function store(cooldown) {
|
|
496
|
+
if (!cooldown.key || typeof window === "undefined") return null;
|
|
497
|
+
try {
|
|
498
|
+
const target = cooldown.storage === "local" ? window.localStorage : window.sessionStorage;
|
|
499
|
+
const probe = "__enigma_probe__";
|
|
500
|
+
target.setItem(probe, "1");
|
|
501
|
+
target.removeItem(probe);
|
|
502
|
+
return target;
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function normalize(cooldown) {
|
|
508
|
+
if (cooldown == null) return null;
|
|
509
|
+
return typeof cooldown === "number" ? { ms: cooldown } : cooldown;
|
|
510
|
+
}
|
|
511
|
+
function isTyping(target) {
|
|
512
|
+
const element = target;
|
|
513
|
+
if (!element) return false;
|
|
514
|
+
const tag = element.tagName;
|
|
515
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element.isContentEditable === true;
|
|
516
|
+
}
|
|
517
|
+
function createButton(options = {}) {
|
|
518
|
+
let opts = { ...options };
|
|
519
|
+
let loading = Boolean(opts.loading);
|
|
520
|
+
let readyAt = 0;
|
|
521
|
+
let timer = null;
|
|
522
|
+
let destroyed = false;
|
|
523
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
524
|
+
const storageKey = () => {
|
|
525
|
+
const cooldown = normalize(opts.cooldown);
|
|
526
|
+
return cooldown?.key ? `enigma:cooldown:${cooldown.key}` : null;
|
|
527
|
+
};
|
|
528
|
+
function restore() {
|
|
529
|
+
const cooldown = normalize(opts.cooldown);
|
|
530
|
+
if (!cooldown) return;
|
|
531
|
+
const target = store(cooldown);
|
|
532
|
+
const key = storageKey();
|
|
533
|
+
if (!target || !key) return;
|
|
534
|
+
const saved = Number(target.getItem(key));
|
|
535
|
+
if (Number.isFinite(saved) && saved > Date.now()) readyAt = saved;
|
|
536
|
+
}
|
|
537
|
+
function remaining() {
|
|
538
|
+
return Math.max(0, readyAt - Date.now());
|
|
539
|
+
}
|
|
540
|
+
function snapshot() {
|
|
541
|
+
const disabled = Boolean(opts.disabled);
|
|
542
|
+
const cooldown = remaining();
|
|
543
|
+
return {
|
|
544
|
+
element: opts.href ? "a" : "button",
|
|
545
|
+
available: !disabled && !loading && cooldown === 0,
|
|
546
|
+
loading,
|
|
547
|
+
disabled,
|
|
548
|
+
cooldown,
|
|
549
|
+
shortcut: opts.shortcut ?? null
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function emit() {
|
|
553
|
+
const state = snapshot();
|
|
554
|
+
opts.onChange?.(state);
|
|
555
|
+
for (const listener of listeners) listener(state);
|
|
556
|
+
}
|
|
557
|
+
function stopTicking() {
|
|
558
|
+
if (timer === null) return;
|
|
559
|
+
clearInterval(timer);
|
|
560
|
+
timer = null;
|
|
561
|
+
}
|
|
562
|
+
function startTicking() {
|
|
563
|
+
if (timer !== null || remaining() === 0) return;
|
|
564
|
+
timer = setInterval(() => {
|
|
565
|
+
if (remaining() > 0) {
|
|
566
|
+
emit();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
stopTicking();
|
|
570
|
+
emit();
|
|
571
|
+
}, TICK_MS);
|
|
572
|
+
}
|
|
573
|
+
function beginCooldown() {
|
|
574
|
+
const cooldown = normalize(opts.cooldown);
|
|
575
|
+
if (!cooldown || cooldown.ms <= 0) return;
|
|
576
|
+
readyAt = Date.now() + cooldown.ms;
|
|
577
|
+
const target = store(cooldown);
|
|
578
|
+
const key = storageKey();
|
|
579
|
+
if (target && key) {
|
|
580
|
+
try {
|
|
581
|
+
target.setItem(key, String(readyAt));
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
startTicking();
|
|
586
|
+
}
|
|
587
|
+
async function press(event) {
|
|
588
|
+
if (destroyed || !snapshot().available) return;
|
|
589
|
+
const result = opts.onPress?.(event);
|
|
590
|
+
if (result instanceof Promise) {
|
|
591
|
+
loading = true;
|
|
592
|
+
emit();
|
|
593
|
+
try {
|
|
594
|
+
await result;
|
|
595
|
+
} finally {
|
|
596
|
+
loading = false;
|
|
597
|
+
beginCooldown();
|
|
598
|
+
emit();
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
beginCooldown();
|
|
603
|
+
emit();
|
|
604
|
+
}
|
|
605
|
+
function onKeyDown(event) {
|
|
606
|
+
if (!opts.shortcut || isTyping(event.target)) return;
|
|
607
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
|
|
608
|
+
if (event.key.toLowerCase() !== opts.shortcut.toLowerCase()) return;
|
|
609
|
+
if (!snapshot().available) return;
|
|
610
|
+
event.preventDefault();
|
|
611
|
+
void press(event);
|
|
612
|
+
}
|
|
613
|
+
if (typeof window !== "undefined") {
|
|
614
|
+
restore();
|
|
615
|
+
startTicking();
|
|
616
|
+
window.addEventListener("keydown", onKeyDown);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
get state() {
|
|
620
|
+
return snapshot();
|
|
621
|
+
},
|
|
622
|
+
press,
|
|
623
|
+
update(next) {
|
|
624
|
+
const hadCooldown = JSON.stringify(normalize(opts.cooldown));
|
|
625
|
+
opts = { ...opts, ...next };
|
|
626
|
+
if (next.loading !== void 0) loading = Boolean(next.loading);
|
|
627
|
+
if (JSON.stringify(normalize(opts.cooldown)) !== hadCooldown) restore();
|
|
628
|
+
emit();
|
|
629
|
+
},
|
|
630
|
+
reset() {
|
|
631
|
+
readyAt = 0;
|
|
632
|
+
stopTicking();
|
|
633
|
+
const cooldown = normalize(opts.cooldown);
|
|
634
|
+
const key = storageKey();
|
|
635
|
+
if (cooldown && key) store(cooldown)?.removeItem(key);
|
|
636
|
+
emit();
|
|
637
|
+
},
|
|
638
|
+
subscribe(listener) {
|
|
639
|
+
listeners.add(listener);
|
|
640
|
+
return () => {
|
|
641
|
+
listeners.delete(listener);
|
|
642
|
+
};
|
|
643
|
+
},
|
|
644
|
+
destroy() {
|
|
645
|
+
destroyed = true;
|
|
646
|
+
stopTicking();
|
|
647
|
+
listeners.clear();
|
|
648
|
+
if (typeof window !== "undefined") window.removeEventListener("keydown", onKeyDown);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
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
|
*
|
|
@@ -162,4 +177,136 @@ interface InputInstance {
|
|
|
162
177
|
*/
|
|
163
178
|
declare function createInput(input: HTMLInputElement, options?: InputOptions): InputInstance;
|
|
164
179
|
|
|
165
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Button behaviour: what makes it unavailable, and everything that follows from that.
|
|
182
|
+
*
|
|
183
|
+
* Disabled, loading and a cooldown are three reasons for the same state, so they collapse
|
|
184
|
+
* into one `available` the renderer reads, instead of three flags every call site has to
|
|
185
|
+
* combine correctly. The element to render is reported rather than chosen, because a
|
|
186
|
+
* framework-agnostic package cannot import next/link.
|
|
187
|
+
*/
|
|
188
|
+
/** Which tag the consumer should render. An href makes it a link, and a link is not a button. */
|
|
189
|
+
type ButtonElement = "button" | "a";
|
|
190
|
+
interface ButtonCooldown {
|
|
191
|
+
/** How long the button stays unavailable after a press, in ms. */
|
|
192
|
+
ms: number;
|
|
193
|
+
/**
|
|
194
|
+
* Survive a reload under this key. Without it the cooldown is in memory only, and a
|
|
195
|
+
* refresh is a free retry - which is the whole thing a cooldown exists to prevent.
|
|
196
|
+
*/
|
|
197
|
+
key?: string;
|
|
198
|
+
storage?: "local" | "session";
|
|
199
|
+
}
|
|
200
|
+
interface ButtonOptions {
|
|
201
|
+
/** Turns into an `a`, and a link cannot be `disabled` - only `aria-disabled`. */
|
|
202
|
+
href?: string;
|
|
203
|
+
disabled?: boolean;
|
|
204
|
+
/** Unavailable and busy. Set it yourself, or let an async `onPress` manage it. */
|
|
205
|
+
loading?: boolean;
|
|
206
|
+
/** ms, or the full shape for a cooldown that outlives a reload. */
|
|
207
|
+
cooldown?: number | ButtonCooldown;
|
|
208
|
+
/**
|
|
209
|
+
* A single key that presses the button. Ignored while the visitor is typing, and
|
|
210
|
+
* while any modifier is held, so it never steals a real shortcut.
|
|
211
|
+
*/
|
|
212
|
+
shortcut?: string;
|
|
213
|
+
/** Async work flips `loading` for its duration and only then starts the cooldown. */
|
|
214
|
+
onPress?: (event?: Event) => void | Promise<void>;
|
|
215
|
+
/** Called whenever anything below changes. */
|
|
216
|
+
onChange?: (state: ButtonState) => void;
|
|
217
|
+
}
|
|
218
|
+
interface ButtonState {
|
|
219
|
+
/** The tag to render. */
|
|
220
|
+
element: ButtonElement;
|
|
221
|
+
/** Pressable: not disabled, not loading, not cooling down. */
|
|
222
|
+
available: boolean;
|
|
223
|
+
loading: boolean;
|
|
224
|
+
disabled: boolean;
|
|
225
|
+
/** ms left on the cooldown, 0 when there is none. */
|
|
226
|
+
cooldown: number;
|
|
227
|
+
/** The accessible name for the shortcut, when there is one. */
|
|
228
|
+
shortcut: string | null;
|
|
229
|
+
}
|
|
230
|
+
interface ButtonInstance {
|
|
231
|
+
readonly state: ButtonState;
|
|
232
|
+
/** Run the press as if it had been clicked. Ignored while unavailable. */
|
|
233
|
+
press(event?: Event): Promise<void>;
|
|
234
|
+
update(options: Partial<ButtonOptions>): void;
|
|
235
|
+
/** Clear a cooldown early, including its stored entry. */
|
|
236
|
+
reset(): void;
|
|
237
|
+
subscribe(listener: (state: ButtonState) => void): () => void;
|
|
238
|
+
destroy(): void;
|
|
239
|
+
}
|
|
240
|
+
declare function createButton(options?: ButtonOptions): ButtonInstance;
|
|
241
|
+
|
|
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 { 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, ReactNode, ComponentPropsWithoutRef } 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
|
|
|
@@ -94,4 +95,148 @@ interface UseSearchResult<T> {
|
|
|
94
95
|
*/
|
|
95
96
|
declare function useSearch<T>(options?: SearchOptions<T>): UseSearchResult<T>;
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
interface UseButtonResult extends ButtonState {
|
|
99
|
+
/** Spread onto the element named by `element`. */
|
|
100
|
+
props: {
|
|
101
|
+
onClick: (event: {
|
|
102
|
+
preventDefault(): void;
|
|
103
|
+
}) => void;
|
|
104
|
+
"aria-disabled": boolean;
|
|
105
|
+
"aria-busy": boolean;
|
|
106
|
+
"data-loading"?: "";
|
|
107
|
+
"data-cooldown"?: "";
|
|
108
|
+
/** Only on a real button: an anchor has no `disabled`. */
|
|
109
|
+
disabled?: boolean;
|
|
110
|
+
href?: string;
|
|
111
|
+
title?: string;
|
|
112
|
+
};
|
|
113
|
+
press: () => void;
|
|
114
|
+
reset: () => void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Button behaviour: disabled, loading, a cooldown and a keyboard shortcut collapsed into
|
|
118
|
+
* one `available`, plus the attributes that follow from it.
|
|
119
|
+
*
|
|
120
|
+
* ```tsx
|
|
121
|
+
* const { element: Tag, props, loading, cooldown } = useButton({
|
|
122
|
+
* cooldown: { ms: 30_000, key: "resend-code", storage: "local" },
|
|
123
|
+
* shortcut: "r",
|
|
124
|
+
* onPress: () => resendCode()
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* return <Tag {...props}>{loading ? "Sending" : cooldown ? `Wait ${Math.ceil(cooldown / 1000)}s` : "Resend"}</Tag>;
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
declare function useButton(options?: ButtonOptions): UseButtonResult;
|
|
131
|
+
|
|
132
|
+
/** An extra button inside the field. The built-ins are the same shape. */
|
|
133
|
+
interface FieldAction {
|
|
134
|
+
/** Stable id. Lands on `data-enigma-input-action`, and replaces a built-in of the same name. */
|
|
135
|
+
name: string;
|
|
136
|
+
/** Accessible name. Becomes `aria-label` and `title`. */
|
|
137
|
+
label: string;
|
|
138
|
+
icon: ReactNode;
|
|
139
|
+
onSelect: () => void;
|
|
140
|
+
/** Renders `aria-pressed`. Omit for actions that are not toggles. */
|
|
141
|
+
pressed?: boolean;
|
|
142
|
+
/** Default true. A false action is not rendered at all. */
|
|
143
|
+
visible?: boolean;
|
|
144
|
+
}
|
|
145
|
+
type BreachStatus = "idle" | "checking" | "safe" | "breached" | "error";
|
|
146
|
+
interface BreachState {
|
|
147
|
+
status: BreachStatus;
|
|
148
|
+
/** How many breaches the password appears in. 0 unless `status` is "breached". */
|
|
149
|
+
count: number;
|
|
150
|
+
/** Whatever the checker threw. The form decides whether that is worth showing. */
|
|
151
|
+
error: unknown;
|
|
152
|
+
}
|
|
153
|
+
type BreachChecker = (password: string, options: {
|
|
154
|
+
signal: AbortSignal;
|
|
155
|
+
}) => Promise<{
|
|
156
|
+
breached: boolean;
|
|
157
|
+
count: number;
|
|
158
|
+
}>;
|
|
159
|
+
interface InputProps extends Omit<ComponentPropsWithoutRef<"input">, "children"> {
|
|
160
|
+
/** The reveal toggle. Defaults to on for `type="password"` and off for everything else. */
|
|
161
|
+
reveal?: boolean;
|
|
162
|
+
revealLabels?: {
|
|
163
|
+
show?: string;
|
|
164
|
+
hide?: string;
|
|
165
|
+
};
|
|
166
|
+
/** Offer to generate a password. `true` for the defaults, or the generator's options. */
|
|
167
|
+
generate?: boolean | GeneratePasswordOptions;
|
|
168
|
+
generateLabel?: string;
|
|
169
|
+
/** Show what was generated. On by default - a password nobody can read is not usable. */
|
|
170
|
+
revealOnGenerate?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Also copy it to the clipboard. OFF by default and worth leaving off: the clipboard is
|
|
173
|
+
* shared with every other app on the machine and is not cleared.
|
|
174
|
+
*/
|
|
175
|
+
copyOnGenerate?: boolean;
|
|
176
|
+
onGenerate?: (password: string) => void;
|
|
177
|
+
/** Extra buttons, or a replacement for `reveal` / `generate` by name. */
|
|
178
|
+
actions?: FieldAction[];
|
|
179
|
+
/** Which end the buttons sit at. Position them yourself; this only orders the markup. */
|
|
180
|
+
position?: "start" | "end";
|
|
181
|
+
/** Score the password as it is typed, and render the meter under the field. */
|
|
182
|
+
strength?: boolean | EstimateOptions;
|
|
183
|
+
onStrengthChange?: (report: PasswordStrengthReport) => void;
|
|
184
|
+
/**
|
|
185
|
+
* Check the password against a breach corpus - pass `checkPasswordBreach` from
|
|
186
|
+
* @enigmax/utils, or your own. It is a prop rather than a built-in because it makes a
|
|
187
|
+
* network request, and that is not a decision a field should take on its own.
|
|
188
|
+
*/
|
|
189
|
+
breach?: BreachChecker;
|
|
190
|
+
/** Quiet time before a check fires, in ms. Default 500. */
|
|
191
|
+
breachDelay?: number;
|
|
192
|
+
onBreachChange?: (state: BreachState) => void;
|
|
193
|
+
/** Props for the element wrapping the field, its buttons and the meter. */
|
|
194
|
+
wrapperProps?: ComponentPropsWithoutRef<"div">;
|
|
195
|
+
/** Props for the row holding the field and its buttons - this is what you position. */
|
|
196
|
+
fieldProps?: ComponentPropsWithoutRef<"div">;
|
|
197
|
+
/**
|
|
198
|
+
* Classes for the parts you cannot reach with a ref, which is what Tailwind needs.
|
|
199
|
+
* `className` still goes to the `<input>` itself, where you would expect it.
|
|
200
|
+
*/
|
|
201
|
+
classNames?: {
|
|
202
|
+
actions?: string;
|
|
203
|
+
action?: string;
|
|
204
|
+
strength?: PasswordStrengthProps["classNames"];
|
|
205
|
+
};
|
|
206
|
+
/** Rendered inside the wrapper, after the meter. Your error message goes here. */
|
|
207
|
+
children?: ReactNode;
|
|
208
|
+
}
|
|
209
|
+
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
210
|
+
interface PasswordStrengthProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
211
|
+
/** The password to score. Ignored when `report` is given. */
|
|
212
|
+
value?: string;
|
|
213
|
+
/** A report you already have, e.g. from `<Input onStrengthChange>`. */
|
|
214
|
+
report?: PasswordStrengthReport;
|
|
215
|
+
userInputs?: string[];
|
|
216
|
+
/** Bars to draw. Five, so each score has one of its own. */
|
|
217
|
+
segments?: number;
|
|
218
|
+
/** Your own wording, worst first. */
|
|
219
|
+
labels?: readonly string[];
|
|
220
|
+
/** Show the top warning under the bars. On by default; it is the useful half. */
|
|
221
|
+
showWarning?: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Classes for the inner parts. The score is on the ROOT, so a segment colours itself
|
|
224
|
+
* with a group variant - `group-data-[score=0]/strength:bg-red-600` and so on.
|
|
225
|
+
*/
|
|
226
|
+
classNames?: {
|
|
227
|
+
track?: string;
|
|
228
|
+
segment?: string;
|
|
229
|
+
label?: string;
|
|
230
|
+
warning?: string;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The bars under a password field.
|
|
235
|
+
*
|
|
236
|
+
* Structure and state only - `data-score` on the root and `data-filled` per segment are
|
|
237
|
+
* where the colours attach. The component picks no colours, because red-through-green is a
|
|
238
|
+
* palette decision and this package does not own one.
|
|
239
|
+
*/
|
|
240
|
+
declare function PasswordStrength({ value, report, userInputs, segments, labels, showWarning, classNames, ...props }: PasswordStrengthProps): ReactNode;
|
|
241
|
+
|
|
242
|
+
export { type BreachChecker, type BreachState, type BreachStatus, ButtonOptions, 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 };
|