component StrongPassword { outputs { input(payload: { value: string; score: number; maximumScore: number; percent: number; level: string }) change(payload: { value: string; score: number; maximumScore: number; percent: number; level: string }) strength(payload: { value: string; score: number; maximumScore: number; percent: number; level: string }) } props { size: string = "default" color: string = "primary" label: string = "Password" name: string = "password" value: string = "" placeholder: string = "Create a strong password" autocomplete: string = "new-password" minLength: number = 8 specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?" requireLowercase: boolean = true requireUppercase: boolean = true requireNumber: boolean = true requireSpecialCharacter: boolean = true showRequirements: boolean = true presentation: string = "inline" hintText: string = "Use a unique password you do not use elsewhere." emptyLabel: string = "Enter a password" weakLabel: string = "Weak" fairLabel: string = "Fair" goodLabel: string = "Good" strongLabel: string = "Strong" disabled: boolean = false readonly: boolean = false required: boolean = false invalid: boolean = false validationMessage: string = "" class: string = "" } state password = value state detailsOpen: boolean = false functions { shared function hasMinimumLength() { return password.length >= Number(minLength) } shared function hasLowercase() { return password !== password.toUpperCase() } shared function hasUppercase() { return password !== password.toLowerCase() } shared function hasNumber() { return containsCharacterAt("0123456789", 0) } shared function hasSpecialCharacter() { return containsCharacterAt(specialCharactersSet, 0) } shared function containsCharacterAt(characters, index) { if (index >= characters.length) { return false } if (password.includes(characters[index])) { return true } return containsCharacterAt(characters, index + 1) } shared function score() { return (hasMinimumLength() ? 1 : 0) + (requireLowercase && hasLowercase() ? 1 : 0) + (requireUppercase && hasUppercase() ? 1 : 0) + (requireNumber && hasNumber() ? 1 : 0) + (requireSpecialCharacter && hasSpecialCharacter() ? 1 : 0) } shared function maximumScore() { return 1 + (requireLowercase ? 1 : 0) + (requireUppercase ? 1 : 0) + (requireNumber ? 1 : 0) + (requireSpecialCharacter ? 1 : 0) } shared function strengthPercent() { return Math.round(score() / maximumScore() * 100) } shared function strengthLevel() { if (!password) { return "empty" } if (strengthPercent() <= 25) { return "weak" } if (strengthPercent() <= 50) { return "fair" } if (strengthPercent() < 100) { return "good" } return "strong" } shared function strengthLabel() { if (strengthLevel() === "weak") { return weakLabel } if (strengthLevel() === "fair") { return fairLabel } if (strengthLevel() === "good") { return goodLabel } if (strengthLevel() === "strong") { return strongLabel } return emptyLabel } } view {
{#if presentation === "popover"}
Password strength {password ? strengthLabel() : emptyLabel}
{#if showRequirements}
  • At least {minLength} characters
  • {#if requireLowercase}
  • One lowercase letter
  • {/if} {#if requireUppercase}
  • One uppercase letter
  • {/if} {#if requireNumber}
  • One number
  • {/if} {#if requireSpecialCharacter}
  • One special character: {specialCharactersSet}
  • {/if}
{/if} {#if hintText}{hintText}{/if}
{/if}
{#if presentation !== "popover"}
Password strength {password ? strengthLabel() : emptyLabel}
{#if showRequirements} {/if} {#if hintText}{hintText}{/if}
{/if} {validationMessage}
} style { .wire-next--strong-password { position: relative; display: grid; width: 100%; gap: 0.6rem; color: var(--wire-color-text); } .wire-next--strong-password > label { font-size: 0.82em; font-weight: 700; } .wire-next__strong-password-anchor { position: relative; } .wire-next--strong-password input { width: 100%; min-height: 2.75em; padding: 0.68em 0.85em; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius-sm); outline: 0; color: var(--wire-color-text); background: var(--wire-color-surface); font: inherit; transition: border-color 160ms ease, box-shadow 160ms ease; } .wire-next--strong-password input:focus-visible { border-color: var(--wire-component-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-component-color) 16%, transparent); } .wire-next__strong-password-details { display: grid; gap: 0.65rem; } .wire-next__strong-password-summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem; color: var(--wire-color-muted); font-size: 0.75em; } .wire-next__strong-password-summary strong { color: var(--wire-color-text); } .wire-next__strong-password-meter { width: 100%; height: 0.42rem; overflow: hidden; border-radius: 999px; background: color-mix(in srgb, var(--wire-color-border) 72%, transparent); } .wire-next__strong-password-meter > span { display: block; width: 0; height: 100%; border-radius: inherit; background: var(--wire-color-danger); transition: width 180ms ease, background-color 180ms ease; } .wire-next--strong-password[data-strength="fair"] .wire-next__strong-password-meter > span { background: var(--wire-color-warning); } .wire-next--strong-password[data-strength="good"] .wire-next__strong-password-meter > span { background: var(--wire-component-color); } .wire-next--strong-password[data-strength="strong"] .wire-next__strong-password-meter > span { background: var(--wire-color-success); } .wire-next__strong-password-requirements { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.45rem 1rem; margin: 0; padding: 0; list-style: none; } .wire-next__strong-password-requirements li { display: flex; min-width: 0; align-items: flex-start; gap: 0.45rem; color: var(--wire-color-muted); font-size: 0.72em; line-height: 1.45; } .wire-next__strong-password-requirements li > span { position: relative; width: 0.9rem; height: 0.9rem; flex: 0 0 auto; margin-top: 0.08rem; border: 1px solid currentColor; border-radius: 50%; } .wire-next__strong-password-requirements li > span::after { position: absolute; top: 0.12rem; left: 0.29rem; width: 0.22rem; height: 0.42rem; border: solid var(--wire-color-surface); border-width: 0 0.1rem 0.1rem 0; content: ""; opacity: 0; transform: rotate(45deg); } .wire-next__strong-password-requirements li[data-met="true"] { color: var(--wire-color-success); } .wire-next__strong-password-requirements li[data-met="true"] > span { border-color: var(--wire-color-success); background: var(--wire-color-success); } .wire-next__strong-password-requirements li[data-met="true"] > span::after { opacity: 1; } .wire-next__strong-password-details > small { color: var(--wire-color-muted); font-size: 0.7em; line-height: 1.5; } .wire-next__strong-password-popover { position: absolute; top: calc(100% + 0.7rem); left: 0; z-index: 30; width: min(28rem, calc(100vw - 2rem)); padding: 1rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); opacity: 0; visibility: hidden; background: var(--wire-color-surface); box-shadow: var(--wire-shadow-2); transform: translateY(-0.35rem); transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease; } .wire-next__strong-password-popover[data-open="true"] { opacity: 1; visibility: visible; transform: translateY(0); } .wire-next__strong-password-popover-arrow { position: absolute; top: -0.38rem; left: 1.25rem; width: 0.7rem; height: 0.7rem; border-top: 1px solid var(--wire-color-border); border-left: 1px solid var(--wire-color-border); background: var(--wire-color-surface); transform: rotate(45deg); } @media (max-width: 560px) { .wire-next__strong-password-requirements { grid-template-columns: 1fr; } } } }