feat(fields): password-field composes Field via a real Control part (S6 — closes all fields)

Password is the one field with content BELOW the control (the strength meter), so
unlike the collapsed text fields it needs the full canonical Field structure:
provider STACK > control BOX + supporting content. Added a `<PasswordField.Control>`
part (morfo + soma provider class + component + exports/types + eidos wrapper/index)
that carries `fieldRole('control')`; the Provider takes `fieldRole('provider')` (the
stack) and the input `fieldRole('input')`.

- field.css owns the control box chrome (border/radius/bg/height/padding/focus-within/
  invalid/disabled) + the input reset. password-field.css keeps ONLY the affixes:
  visibility trigger, caps-lock indicator, strength meter, and the input's mask-tracking.
- REMOVED the 2-row grid + `::before` chrome hack (it existed only to avoid `:has()`
  layout flicker without a control element). Now the strength meter is a plain stack
  sibling BELOW the control — appears/disappears with no `:has()`, no flicker.
- No tokens aliasing the Field base.

This is the canonical composition (why A over the de-alias compromise: composability
IS the S6 canon; the "API change" — wrapping row 1 in <Control> — aligns password to
how Field itself works, it doesn't invent API). Contained: the demo is the consumer,
updated here.

Baseline unchanged (59). Verified: Provider stack (flex column), Control box paints
from field.css (36px/border/pad/radius), input + eye affix in a row, strength meter
8px below as a sibling, :focus-within ring. Closes S6 field composition — all 16
fields now compose field.css.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 43666f7ee4
commit 08ae76bcb5

@ -10,12 +10,14 @@
// <PasswordField.StrengthMeter strength={score} />
// </PasswordField>
import PasswordFieldComponent from './password-field.svelte';
import Control from './password-field-control.svelte';
import Input from './password-field-input.svelte';
import VisibilityTrigger from './password-field-visibility-trigger.svelte';
import CapsLockIndicator from './password-field-caps-lock-indicator.svelte';
import StrengthMeter from './password-field-strength-meter.svelte';
type PasswordFieldNamespace = typeof PasswordFieldComponent & {
Control: typeof Control;
Input: typeof Input;
VisibilityTrigger: typeof VisibilityTrigger;
CapsLockIndicator: typeof CapsLockIndicator;
@ -23,6 +25,7 @@ type PasswordFieldNamespace = typeof PasswordFieldComponent & {
};
const PasswordField = PasswordFieldComponent as PasswordFieldNamespace;
PasswordField.Control = Control;
PasswordField.Input = Input;
PasswordField.VisibilityTrigger = VisibilityTrigger;
PasswordField.CapsLockIndicator = CapsLockIndicator;
@ -34,6 +37,7 @@ export default PasswordField;
export type {
PasswordFieldProps,
PasswordFieldControlProps as ControlProps,
PasswordFieldInputProps as InputProps,
PasswordFieldVisibilityTriggerProps as VisibilityTriggerProps,
PasswordFieldCapsLockIndicatorProps as CapsLockIndicatorProps,

@ -0,0 +1,10 @@
<script lang="ts">
import * as PasswordField from '$soma/components/password-field';
import type { PasswordFieldControlProps } from './types';
let { ref = $bindable(null), children, ...rest }: PasswordFieldControlProps = $props();
</script>
<PasswordField.Control {...rest} bind:ref>
{@render children?.()}
</PasswordField.Control>

@ -1,238 +1,94 @@
/*
* PasswordField — single-line password input with visibility toggle,
* CapsLock indicator and optional strength meter.
* PasswordField — password input with visibility toggle, CapsLock indicator and
* optional strength meter.
*
* Mirrors search-field's chrome (border, radius, focus ring, padding,
* disabled opacity) so every text input in the system reads the same
* — only the affordances inside the control differ.
*
* Layout: CSS grid with TWO rows — row 1 holds the control affordances
* (input, caps indicator, visibility trigger), row 2 holds the optional
* StrengthMeter. The bordered chrome lives on a ::before pseudo-element
* that spans row 1 only. Row 2 auto-collapses to zero when the meter
* is not composed. This avoids the flicker that comes from toggling
* layout based on `:has()` selectors during reactive updates.
* password-field IS-A Field: the Provider is the field STACK (field.css `[data-field]`
* — flex column, gap), `<PasswordField.Control>` is the bordered box (field.css
* `[data-field-control]` — border / radius / bg / height / padding / focus-within /
* invalid / disabled, a flex ROW holding the input + caps + visibility affixes), the
* `<input>` is reset by `[data-field-input]`, and the StrengthMeter sits BELOW the
* control as a plain stack sibling. No 2-row grid / `::before` chrome hack — the meter
* just appears/disappears as a sibling (no `:has()` layout switch, no flicker). This
* recipe keeps ONLY the field-specific affixes; NO tokens aliasing the Field base.
*/
[data-password-field] {
--_password-field-height: var(--password-field-height-md);
--_password-field-padding-inline: var(--password-field-padding-inline-md);
--_password-field-gap: var(--password-field-gap-md);
--_password-field-font-size: var(--password-field-font-size-md);
--_password-field-trigger-size: var(--password-field-icon-size-md);
--_password-field-caps-font-size: var(--password-field-caps-font-size-md);
--_password-field-accent-border: var(--color-primary-border);
--_password-field-accent-track: var(--color-primary-track);
--_password-field-accent-text: var(--color-primary-text);
/* Chrome is driven through these vars so the ::before pseudo doesn't
* need its own state selectors — focused / invalid just rewrite them. */
--_password-field-border-color: var(--password-field-border);
--_password-field-bg: var(--password-field-bg);
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
grid-template-rows: var(--_password-field-height) auto;
column-gap: var(--_password-field-gap);
row-gap: var(--space-2);
align-items: center;
inline-size: 100%;
min-inline-size: 0;
color: var(--password-field-color);
font-family: var(--password-field-font-family);
font-size: var(--_password-field-font-size);
line-height: var(--password-field-line-height);
}
/* Bordered chrome spans row 1 only. Sits behind the controls (no
* pointer events) so input + buttons remain interactive.
*
* NO transitions on the pseudo. Per-keystroke validation can flip the
* `--_password-field-border-color` (when a form re-validates `onChange`)
* which would otherwise replay the focus/invalid transition on every
* letter the user types — the user perceives it as "the box redraws".
* Snap the chrome instantly to its target state instead. */
[data-password-field]::before {
content: '';
grid-column: 1 / -1;
grid-row: 1 / 2;
align-self: stretch;
justify-self: stretch;
min-block-size: var(--_password-field-height);
border: var(--password-field-border-width) solid var(--_password-field-border-color);
border-radius: var(--password-field-radius);
background: var(--_password-field-bg);
pointer-events: none;
}
/* Controls always live on row 1. The pseudo provides the chrome behind
* them, so the children stack on top with z-index 1. */
[data-password-field-input],
[data-password-field-visibility-trigger],
[data-password-field-caps-lock-indicator] {
grid-row: 1 / 2;
z-index: 1;
}
[data-password-field] [data-password-field-input] {
grid-column: 1 / 2;
padding-inline-start: var(--_password-field-padding-inline);
}
[data-password-field] [data-password-field-caps-lock-indicator] {
grid-column: 2 / 3;
}
[data-password-field] [data-password-field-visibility-trigger] {
grid-column: 3 / 4;
margin-inline-end: calc(var(--_password-field-padding-inline) - var(--_password-field-gap));
}
/* ── Color accents ─────────────────────────────────────────────────── */
/* ── Color accent — feeds the visibility trigger's hover only (focus is field.css's
color-agnostic `:focus-within` ring). Color-role refs, not Field-chrome aliases. */
[data-password-field][data-color='secondary'] {
--_password-field-accent-border: var(--color-secondary-border);
--_password-field-accent-track: var(--color-secondary-track);
--_password-field-accent-text: var(--color-secondary-text);
}
[data-password-field][data-color='neutral'] {
--_password-field-accent-border: var(--color-neutral-border);
--_password-field-accent-track: var(--color-neutral-track);
--_password-field-accent-text: var(--color-neutral-text);
}
[data-password-field][data-color='affirm'] {
--_password-field-accent-border: var(--color-affirm-border);
--_password-field-accent-track: var(--color-affirm-track);
--_password-field-accent-text: var(--color-affirm-text);
}
[data-password-field][data-color='fulfill'] {
--_password-field-accent-border: var(--color-fulfill-border);
--_password-field-accent-track: var(--color-fulfill-track);
--_password-field-accent-text: var(--color-fulfill-text);
}
[data-password-field][data-color='risk'] {
--_password-field-accent-border: var(--color-risk-border);
--_password-field-accent-track: var(--color-risk-track);
--_password-field-accent-text: var(--color-risk-text);
}
[data-password-field][data-color='threat'] {
--_password-field-accent-border: var(--color-threat-border);
--_password-field-accent-track: var(--color-threat-track);
--_password-field-accent-text: var(--color-threat-text);
}
[data-password-field][data-color='loss'] {
--_password-field-accent-border: var(--color-loss-border);
--_password-field-accent-track: var(--color-loss-track);
--_password-field-accent-text: var(--color-loss-text);
}
[data-password-field] {
--_password-field-accent-track: var(--color-primary-track);
--_password-field-accent-text: var(--color-primary-text);
}
/* ── Sizes ─────────────────────────────────────────────────────────── */
/* ── Sizes — only the AFFIX tokens (the box height/padding/font come from
field.css `[data-field][data-size]`). */
[data-password-field] {
--_password-field-trigger-size: var(--password-field-icon-size-md);
--_password-field-caps-font-size: var(--password-field-caps-font-size-md);
}
[data-password-field][data-size='xs'] {
--_password-field-height: var(--password-field-height-xs);
--_password-field-padding-inline: var(--password-field-padding-inline-xs);
--_password-field-gap: var(--password-field-gap-xs);
--_password-field-font-size: var(--password-field-font-size-xs);
--_password-field-trigger-size: var(--password-field-icon-size-xs);
--_password-field-caps-font-size: var(--password-field-caps-font-size-xs);
}
[data-password-field][data-size='sm'] {
--_password-field-height: var(--password-field-height-sm);
--_password-field-padding-inline: var(--password-field-padding-inline-sm);
--_password-field-gap: var(--password-field-gap-sm);
--_password-field-font-size: var(--password-field-font-size-sm);
--_password-field-trigger-size: var(--password-field-icon-size-sm);
--_password-field-caps-font-size: var(--password-field-caps-font-size-sm);
}
[data-password-field][data-size='lg'] {
--_password-field-height: var(--password-field-height-lg);
--_password-field-padding-inline: var(--password-field-padding-inline-lg);
--_password-field-gap: var(--password-field-gap-lg);
--_password-field-font-size: var(--password-field-font-size-lg);
--_password-field-trigger-size: var(--password-field-icon-size-lg);
--_password-field-caps-font-size: var(--password-field-caps-font-size-lg);
}
[data-password-field][data-size='xl'] {
--_password-field-height: var(--password-field-height-xl);
--_password-field-padding-inline: var(--password-field-padding-inline-xl);
--_password-field-gap: var(--password-field-gap-xl);
--_password-field-font-size: var(--password-field-font-size-xl);
--_password-field-trigger-size: var(--password-field-icon-size-xl);
--_password-field-caps-font-size: var(--password-field-caps-font-size-xl);
}
/* ── Variants ──────────────────────────────────────────────────────── */
[data-password-field][data-variant='outline'] {
--_password-field-bg: transparent;
}
[data-password-field][data-variant='ghost'] {
--_password-field-border-color: transparent;
--_password-field-bg: transparent;
}
[data-password-field][data-variant='ghost'] [data-password-field-input] {
padding-inline-start: 0;
}
[data-password-field][data-variant='ghost'] [data-password-field-visibility-trigger] {
margin-inline-end: 0;
}
/* ── State chrome ──────────────────────────────────────────────────── */
[data-password-field][data-focused] {
--_password-field-border-color: var(--password-field-border-focus);
}
/* Focus ring as a flush outline on the chrome pseudo-element (no offset → no gap),
so it survives forced-colors / HCM — replaces the old box-shadow ring. */
[data-password-field][data-focused]::before {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: 0;
}
[data-password-field][data-invalid] {
--_password-field-border-color: var(--password-field-border-invalid);
}
[data-password-field][data-disabled] {
opacity: var(--password-field-disabled-opacity);
}
/* ── Input ─────────────────────────────────────────────────────────── */
/* ── Input — reset owned by field.css `[data-field-input]`; keep only the mask
readability tweak (· glyphs read better with a touch of tracking). */
[data-password-field-input] {
min-inline-size: 0;
inline-size: 100%;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: var(--font-weight-medium);
outline: none;
/* Slightly wider tracking helps recognise the · placeholder mask. */
letter-spacing: var(--tracking-wide);
}
[data-password-field-input]::placeholder {
color: var(--password-field-placeholder-color);
letter-spacing: 0;
}
[data-password-field-input][data-disabled] {
cursor: not-allowed;
}
/* ── VisibilityTrigger ─────────────────────────────────────────────── */
/* ── VisibilityTrigger — flush affordance inside the control (no border competing
with the field's focus ring; subtle accent tint on hover). */
[data-password-field-visibility-trigger] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--_password-field-trigger-size);
block-size: var(--_password-field-trigger-size);
padding: 0;
/* Flush affordance inside the field — same treatment as the date-picker
* embedded trigger: no border (a hovered border competes with the field's
* own focus ring), just a subtle accent tint on hover. */
border: none;
border-radius: var(--password-field-trigger-radius);
background: var(--password-field-trigger-bg);
@ -240,9 +96,7 @@
cursor: pointer;
transition:
background var(--password-field-transition-duration) var(--password-field-transition-ease),
border-color var(--password-field-transition-duration) var(--password-field-transition-ease),
color var(--password-field-transition-duration) var(--password-field-transition-ease),
box-shadow var(--password-field-transition-duration) var(--password-field-transition-ease);
color var(--password-field-transition-duration) var(--password-field-transition-ease);
}
[data-password-field-visibility-trigger]:hover:not([data-disabled]) {
@ -256,8 +110,8 @@
}
[data-password-field-visibility-trigger][data-disabled] {
opacity: var(--password-field-disabled-opacity);
cursor: not-allowed;
opacity: var(--password-field-disabled-opacity);
}
[data-password-field-visibility-trigger] svg {
@ -265,11 +119,10 @@
block-size: 65%;
}
/* ── CapsLockIndicator ─────────────────────────────────────────────── */
/* Hidden by default. Only visible when caps lock is on. */
/* ── CapsLockIndicator — hidden until caps lock is on. */
[data-password-field-caps-lock-indicator] {
display: none;
flex: 0 0 auto;
align-items: center;
gap: 0.25em;
padding-inline: 0.4em;
@ -299,15 +152,13 @@
block-size: 1em;
}
/* ── StrengthMeter (composable; sits on row 2 of the root grid) ───── */
/* ── StrengthMeter — a plain stack sibling BELOW the control (Provider is a
field.css `[data-field]` column). Aligns its inline padding with the control's. */
[data-password-field-strength-meter] {
display: flex;
flex-direction: column;
gap: 0.25em;
grid-column: 1 / -1;
grid-row: 2 / 3;
padding-inline: var(--_password-field-padding-inline);
padding-inline: var(--_field-control-padding-inline);
}
[data-password-field-strength-meter-track] {
@ -320,8 +171,7 @@
block-size: var(--password-field-strength-segment-height);
border-radius: var(--password-field-strength-radius);
background: var(--password-field-strength-segment-bg);
transition:
background var(--password-field-transition-duration) var(--password-field-transition-ease);
transition: background var(--password-field-transition-duration) var(--password-field-transition-ease);
}
[data-password-field-strength-meter][data-strength='1']
@ -342,7 +192,7 @@
}
[data-password-field-strength-meter-label] {
font-size: calc(var(--_password-field-font-size) * 0.85); /* literal: meter caption proportional to the field size token */
font-size: calc(var(--_field-control-font-size) * 0.85); /* literal: meter caption proportional to the field size */
color: var(--password-field-placeholder-color);
font-variant-numeric: tabular-nums;
}

@ -1,4 +1,9 @@
<script lang="ts">
// password-field IS-A Field: the Provider is the field STACK, `<PasswordField.Control>`
// is the bordered box (field.css `[data-field-control]` owns its chrome / focus /
// invalid / disabled), and the strength meter sits below as a sibling. password-field.css
// keeps ONLY the field-specific bits (caps indicator, strength meter, visibility affix).
import '../field/field.css';
import './password-field.css';
import { ActiveEidos } from '$uix/eidos';
import * as PasswordField from '$soma/components/password-field';

@ -1,5 +1,6 @@
import type {
ProviderProps,
ControlProps,
InputProps,
VisibilityTriggerProps,
CapsLockIndicatorProps,
@ -30,6 +31,7 @@ export type PasswordFieldProps = ProviderProps & {
color?: PasswordFieldColor;
};
export type PasswordFieldControlProps = ControlProps;
export type PasswordFieldInputProps = InputProps;
export type PasswordFieldVisibilityTriggerProps = VisibilityTriggerProps;
export type PasswordFieldCapsLockIndicatorProps = CapsLockIndicatorProps;

@ -4,6 +4,7 @@ import {
} from '$libs/secs';
import type { Morfo } from '../types';
import { v } from '../types';
import { fieldRole } from '../field-role';
/**
* PasswordField — text input with masked value, visibility toggle and
@ -112,6 +113,10 @@ export const passwordFieldMorfo = {
defaultElement: 'div',
optional: false,
data: [
// password-field IS-A Field: the Provider is the field STACK (control row +
// strength meter below), so it takes only the Field ROOT identity. The box
// chrome lives on the Control part (below), not here.
fieldRole('provider'),
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' },
{ attr: 'data-required', value: v.propRef('required'), severity: 'optional' },
@ -123,6 +128,25 @@ export const passwordFieldMorfo = {
],
aria: []
},
{
// The bordered CONTROL box (row 1): wraps the input + caps indicator +
// visibility trigger. Carries the Field control identity via
// `fieldRole('control')`, so field.css `[data-field-control]` paints its
// chrome (border / radius / bg / height / padding / focus / invalid /
// disabled). The strength meter sits BELOW it as a sibling in the Provider
// stack — no 2-row grid / `::before` hack needed.
name: 'Control',
kebab: 'control',
kind: 'public',
defaultElement: 'div',
optional: false,
data: [
fieldRole('control'),
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }
],
aria: []
},
{
name: 'Input',
kebab: 'input',
@ -132,6 +156,8 @@ export const passwordFieldMorfo = {
role: 'textbox',
optional: false,
data: [
// The bare <input> IS the Field input — field.css `[data-field-input]` resets it.
fieldRole('input'),
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' },
{ attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }

@ -0,0 +1,35 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { PasswordFieldControlProvider } from '../password-field-provider.svelte';
import type { PasswordFieldControlProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'password-field-control'),
children,
child,
...restProps
}: PasswordFieldControlProps = $props();
const state = PasswordFieldControlProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -1,4 +1,5 @@
export { default as Provider } from './components/password-field.svelte';
export { default as Control } from './components/password-field-control.svelte';
export { default as Input } from './components/password-field-input.svelte';
export { default as VisibilityTrigger } from './components/password-field-visibility-trigger.svelte';
export { default as CapsLockIndicator } from './components/password-field-caps-lock-indicator.svelte';
@ -9,6 +10,7 @@ export { default as StrengthMeter } from './components/password-field-strength-m
// re-export `$libs` types (contract guard E).
export type {
PasswordFieldProps as ProviderProps,
PasswordFieldControlProps as ControlProps,
PasswordFieldInputProps as InputProps,
PasswordFieldVisibilityTriggerProps as VisibilityTriggerProps,
PasswordFieldCapsLockIndicatorProps as CapsLockIndicatorProps,

@ -265,6 +265,38 @@ export class PasswordFieldProvider {
);
}
// ── Control (the bordered box: input + caps + visibility) ───────────────────
interface PasswordFieldControlOpts extends WithRefOpts {}
export class PasswordFieldControlProvider {
readonly opts: PasswordFieldControlOpts;
readonly runtimePart: SomaRuntimePart;
static create(opts: PasswordFieldControlOpts) {
return new PasswordFieldControlProvider(opts);
}
readonly provider: PasswordFieldProvider;
private constructor(opts: PasswordFieldControlOpts) {
this.opts = opts;
this.provider = PasswordFieldProvider.require();
this.runtimePart = this.provider.runtime.part('control', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
// data-password-field-control (kebab) + data-field-control (fieldRole) +
// data-disabled / data-invalid are morfo-declared and resolved by renderProps.
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps()
} as const)
);
}
// ── Input ──────────────────────────────────────────────────────────────────
interface PasswordFieldInputOpts extends WithRefOpts {}

@ -116,6 +116,12 @@ export type PasswordFieldProps = WithChild<
> &
Without<PrimitiveDivAttributes, { 'aria-label'?: string }>;
// ── Control (the bordered box: input + caps + visibility) ───────────────────
/** Props for `PasswordField.Control` — the bordered field box (row 1). */
export type PasswordFieldControlProps = WithChild<{ id?: string }> &
Without<PrimitiveDivAttributes, {}>;
// ── Input ──────────────────────────────────────────────────────────────────
/**

@ -119,9 +119,11 @@
maxLength !== undefined && ` maxLength={${maxLength}}`,
required && ' required',
'>',
' <PasswordField.Input placeholder="…" />',
showVisibilityTrigger && ' <PasswordField.VisibilityTrigger />',
showCapsLockIndicator && ' <PasswordField.CapsLockIndicator />',
' <PasswordField.Control>',
' <PasswordField.Input placeholder="…" />',
showVisibilityTrigger && ' <PasswordField.VisibilityTrigger />',
showCapsLockIndicator && ' <PasswordField.CapsLockIndicator />',
' </PasswordField.Control>',
'</PasswordField.Provider>'
]
.filter(Boolean)
@ -147,9 +149,11 @@
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
'>',
' <PasswordField.Input placeholder="…" />',
showVisibilityTrigger && ' <PasswordField.VisibilityTrigger />',
showCapsLockIndicator && ' <PasswordField.CapsLockIndicator />',
' <PasswordField.Control>',
' <PasswordField.Input placeholder="…" />',
showVisibilityTrigger && ' <PasswordField.VisibilityTrigger />',
showCapsLockIndicator && ' <PasswordField.CapsLockIndicator />',
' </PasswordField.Control>',
showStrengthMeter && ' <PasswordField.StrengthMeter />',
'</PasswordField>'
]
@ -210,13 +214,15 @@
{color}
onValueCommit={(v) => (lastSubmit = v)}
>
<PasswordField.Input />
{#if showCapsLockIndicator}
<PasswordField.CapsLockIndicator />
{/if}
{#if showVisibilityTrigger}
<PasswordField.VisibilityTrigger />
{/if}
<PasswordField.Control>
<PasswordField.Input />
{#if showCapsLockIndicator}
<PasswordField.CapsLockIndicator />
{/if}
{#if showVisibilityTrigger}
<PasswordField.VisibilityTrigger />
{/if}
</PasswordField.Control>
{#if showStrengthMeter}
<PasswordField.StrengthMeter />
{/if}

Loading…
Cancel
Save

Powered by TurnKey Linux.