@ -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 ;
}