+
+ Forms · PasswordField
+ PasswordField
+
+ Single-line password input with masked value, visibility toggle, CapsLock awareness, and an
+ optional composable strength meter. Anatomy follows Ark UI; CapsLockIndicator fills a
+ universal gap (10/10 surveyed libraries warn nothing when Caps Lock is on while typing).
+ Soma owns value, masking, autocomplete (current-password/new-password)
+ and Caps Lock detection.
+
+
+
+ parts{compiled.parts.order.length}
+
+
+ events{events.length}
+
+
+ keyboard1
+
+
+ sizes{sizes.length}
+
+
+ apg
+ textbox
+
+
+
+
+
+
+
+
+
(lastSubmit = v)}
+ >
+
+ {#if showCapsLockIndicator}
+
+ {/if}
+ {#if showVisibilityTrigger}
+
+ {/if}
+ {#if showStrengthMeter}
+
+ {/if}
+
+
+
+
+ trace
+ {#if trace.length === 0}
+ type, press Caps Lock, click the eye to see events
+ {:else}
+ {#each trace.slice(0, 3) as entry}
+
+ {entry.event}
+ · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}
+
+ {fmtTime(entry.at)}
+ {/each}
+ {/if}
+
+ length
+ {value.length} · strength {strength}/4
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if tab === 'live'}
+
+ {/if}
+
+ {#if tab === 'api'}
+
+ API reference
+
+ PasswordField composes through 5 parts. Root drives value + visibility + caps detection;
+ every visual part is optional (composition over visibility props).
+
+
+ Provider props
+
+
+ | Prop | Type | Notes |
+
+ | value | string | Bindable. Default ''. |
+ | visible | boolean | Bindable. Toggle plaintext / masked. Default false. |
+
+ | purpose |
+ 'current' | 'new' |
+
+ Drives the native autocomplete attribute. 'current' →
+ current-password (login). 'new' →
+ new-password (registration / change). Default 'current'.
+ |
+
+ | minLength / maxLength | number | Native constraints + mirrored ARIA. Used by HTML form validation. |
+ | disabled / readonly / required / invalid | boolean | OR-merged with parent <Field>. |
+ | placeholder | string? | Native placeholder. |
+ | name | string? | Form submission name. |
+ | aria-label | string? | Override translated 'Password' default. Ignored if inside Field.Label. |
+ | onValueChange | (v: string) => void | Fires on every keystroke. |
+ | onSubmit | (v: string) => void | Fires on Enter. |
+ | onVisibilityChange | (visible: boolean) => void | Fires after the eye toggle. |
+
+
+
+
+ Eidos visual props
+
+
+ | Prop | Type | Default |
+
+ | size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' |
+ | variant | ControlVariant | 'surface' |
+ | color | ColorRole | 'primary' |
+
+
+
+
+ Reference comparison
+
+
+
+ | Library | Built-in toggle | CapsLock | Strength meter |
+
+
+ | React Aria | no | no | no |
+ | Ark UI | yes | no | external |
+ | Chakra v3 | yes | no | sibling cmp |
+ | Mantine | yes | no | compose |
+ | PrimeReact | yes | no | built-in popup |
+ | Ant Design | yes (default) | no | no |
+ | shadcn/ui | no | no | no |
+ | Material UI | no | no | no |
+
+ | UIX |
+ yes |
+ yes (universal gap) |
+ composable |
+
+
+
+
+
+ {/if}
+
+ {#if tab === 'morfo'}
+
+ Morfo contract
+
+
+
+ | Field | Value |
+
+ | name | {passwordFieldMorfo.name} |
+ | kebab | {passwordFieldMorfo.kebab} |
+ | scope | {passwordFieldMorfo.scope.join(', ')} |
+ | apg | textbox |
+ | parts | {partsList.length} |
+ | events | {events.length} |
+
+
+
+
+ Parts
+
+
+
+
+ | kebab |
+ marker |
+ element |
+ role |
+ archetype |
+ optional |
+
+
+
+ {#each partsList as part (part.kebab)}
+
+ | {part.kebab} |
+ [{part.marker}] |
+ <{part.defaultElement}> |
+ {part.role ?? '—'} |
+ {part.archetype ?? '—'} |
+ {part.optional ? 'yes' : 'no'} |
+
+ {/each}
+
+
+
+
+ Events
+
+
+ | name | family | verb | target | intent |
+
+ {#each events as e (e.name)}
+
+ {e.name} |
+ {e.semantic?.family ?? '—'} |
+ {e.semantic?.verb ?? '—'} |
+ {e.semantic && 'target' in e.semantic ? e.semantic.target.target : '—'} |
+ {e.semantic && 'intent' in e.semantic && typeof e.semantic.intent === 'string' ? e.semantic.intent : '—'} |
+
+ {/each}
+
+
+
+
+ {/if}
+
+ {#if tab === 'sema'}
+
+
+ sema · events
+
+
+ Type a character to fire commit-input. Press Enter to commit
+ commit-submit. Click the eye to fire
+ shift-toggle-visibility. Press Caps Lock while the input is focused to fire
+ shift-caps-state. Use ▶ play to dispatch the event without the gesture.
+
+
+
+
+ | name | family | verb | sequence | intent | |
+
+ {#each events as action (action.name)}
+ {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
+ {@const effectiveIntent =
+ typeof intentDecl === 'string' ? intentDecl : undefined}
+
+ {action.name} |
+ {action.semantic.family} |
+ {action.semantic.verb ?? '—'} |
+ {action.semantic.sequence ?? '—'} |
+ {effectiveIntent ?? '—'} |
+
+
+ |
+
+ {/each}
+
+
+
+
+ {/if}
+
+ {#if tab === 'recipe'}
+
+ Eidos recipe
+
+ Recipe lives in src/uix/eidos/components/password-field/password-field.css.
+ Provider is the inline-flex container; tokens are private (--_password-field-*).
+ CapsLock indicator and StrengthMeter have their own data-* attribute states.
+
+
+
+ | Selector | Owner | Purpose |
+
+
+ [data-password-field] |
+ morfo |
+ Provider marker. Inline-flex container with border + focus ring. |
+
+
+ [data-password-field][data-size='xs'..'xl'] |
+ eidos |
+ Padding + font-size + radius scale. |
+
+
+ [data-password-field][data-color='primary'..'loss'] |
+ eidos |
+ Maps to focus accent token. |
+
+
+ [data-password-field][data-focused] |
+ morfo |
+ Focus ring via box-shadow color-mix. |
+
+
+ [data-password-field-input] |
+ morfo |
+ Input cell. Letter-spacing increased for legibility while masked. |
+
+
+ [data-password-field-visibility-trigger] |
+ morfo |
+ Eye toggle button. Hover background + focus outline. |
+
+
+ [data-password-field-caps-lock-indicator][data-active] |
+ morfo |
+ Risk-tinted pill, hidden when inactive. |
+
+
+ [data-password-field-strength-meter][data-strength='1'..'4'] |
+ morfo |
+ Active segments tint per strength bucket (loss / risk / fulfill / affirm). |
+
+
+
+
+
+ {/if}
+
+ {#if tab === 'a11y'}
+
+ Accessibility
+
+
+ | Concern | Contract |
+
+
+ | Role |
+
+ Native <input> with type="password" (or
+ "text" when visible). Implicit role="textbox".
+ |
+
+
+ | Label |
+
+ Defaults to translated 'Password' / 'Contraseña'. When inside a
+ <Field> the Field.Label wins via aria-labelledby.
+ |
+
+
+ | Autofill |
+
+ autocomplete="current-password" (login) or
+ "new-password" (registration / password change). Drives password
+ manager behavior.
+ |
+
+
+ | Visibility toggle |
+
+ <button type="button" aria-pressed={'{visible}'} aria-label="Show/Hide
+ password">. tabindex="-1" so it doesn't break the form's
+ natural tab order — the user toggles via mouse / programmatic focus instead.
+ |
+
+
+ | Caps Lock detection |
+
+ Soma listens to keydown/keyup on the input and reads
+ KeyboardEvent.getModifierState('CapsLock'). Indicator carries
+ role="status" + aria-live="polite" so screen readers
+ announce when Caps Lock turns on while typing.
+ |
+
+
+ | Strength meter |
+
+ role="meter" with aria-valuemin=0,
+ aria-valuemax=4, aria-valuenow. Visual segments are
+ aria-hidden; only the label is announced.
+ |
+
+
+ | Keyboard |
+
+ Enter fires commit-submit. No other keys hijacked —
+ selection / cursor movement / paste all behave natively.
+ |
+
+
+ | Disabled / readonly |
+
+ Mirrored to native attributes AND ARIA. The visibility trigger also disables when
+ the input is disabled.
+ |
+
+
+ | Reduced motion |
+ All transitions disabled under prefers-reduced-motion: reduce. |
+
+
+
+
+
+ {/if}
+
diff --git a/web/routes/uix/uix.css b/web/routes/uix/uix.css
index e64e066c2..2201665a2 100644
--- a/web/routes/uix/uix.css
+++ b/web/routes/uix/uix.css
@@ -1376,236 +1376,115 @@
font-weight: var(--uix-weight-medium);
}
-/* ── Responsive docs shell ─────────────────────────────────────────── */
-@media (max-width: 900px) {
- [data-uix-shell] {
- grid-template-columns: minmax(0, 1fr);
- }
-
- [data-uix-topbar] {
- max-inline-size: 100vw;
- overflow: hidden;
- padding-inline: var(--uix-space-3);
- gap: var(--uix-space-2);
- }
-
- [data-uix-brand],
- [data-uix-topbar-nav],
- [data-uix-topbar-tools] {
- min-inline-size: 0;
- }
-
- [data-uix-brand-version] {
- display: none;
- }
-
- [data-uix-topbar-nav] {
- margin-inline-start: 0;
- }
-
- [data-uix-topbar-nav] a {
- padding-inline: var(--uix-space-2);
- }
-
- [data-uix-topbar-tools] {
- flex: 1 1 auto;
- justify-content: flex-end;
- overflow-x: auto;
- overscroll-behavior-inline: contain;
- scrollbar-width: none;
- }
-
- [data-uix-topbar-tools]::-webkit-scrollbar {
- display: none;
- }
-
- [data-uix-rail] {
- display: none;
- }
-
- [data-uix-canvas] {
- grid-column: 1;
- padding: var(--uix-space-8) var(--uix-space-4) var(--uix-space-12);
- }
-
- [data-uix-canvas-inner],
- [data-uix-canvas-inner][data-with-toc='true'] {
- display: block;
- max-inline-size: none;
- }
-
- [data-uix-toc] {
- display: none;
- }
-}
-
-@media (max-width: 560px) {
- [data-uix-topbar-nav] a[href='https://github.com/'] {
- display: none;
- }
-
- [data-uix-canvas] {
- padding: var(--uix-space-6) var(--uix-space-3) var(--uix-space-10);
- }
-
- [data-uix-page-title] {
- font-size: var(--uix-text-4xl);
- }
-
- [data-uix-page-lede] {
- font-size: var(--uix-text-lg);
- }
-
- [data-uix-page-meta],
- [data-uix-tabs],
- [data-uix-code-head] {
- overflow-x: auto;
- overscroll-behavior-inline: contain;
- }
-
- [data-uix-tabs] {
- flex-wrap: nowrap;
- }
-
- [data-uix-tab],
- [data-uix-meta-pill],
- [data-uix-code-head] > * {
- flex: 0 0 auto;
- }
-
- [data-uix-stage-area] {
- place-items: stretch;
- }
-
- [data-uix-stage-trace] {
- align-items: flex-start;
- gap: var(--uix-space-2);
- }
-
- [data-uix-controls] {
- grid-template-columns: minmax(0, 1fr);
- padding: var(--uix-space-4);
- }
-
- [data-uix-part] {
- grid-template-columns: minmax(0, 1fr);
- gap: var(--uix-space-2);
- }
-}
-
-/* ── Responsive docs shell ─────────────────────────────────────────── */
-@media (max-width: 900px) {
- [data-uix-shell] {
- grid-template-columns: minmax(0, 1fr);
- }
-
- [data-uix-topbar] {
- max-inline-size: 100vw;
- overflow: hidden;
- padding-inline: var(--uix-space-3);
- gap: var(--uix-space-2);
- }
-
- [data-uix-brand],
- [data-uix-topbar-nav],
- [data-uix-topbar-tools] {
- min-inline-size: 0;
- }
-
- [data-uix-brand-version] {
- display: none;
- }
-
- [data-uix-topbar-nav] {
- margin-inline-start: 0;
- }
-
- [data-uix-topbar-nav] a {
- padding-inline: var(--uix-space-2);
- }
-
- [data-uix-topbar-tools] {
- flex: 1 1 auto;
- justify-content: flex-end;
- overflow-x: auto;
- overscroll-behavior-inline: contain;
- scrollbar-width: none;
- }
-
- [data-uix-topbar-tools]::-webkit-scrollbar {
- display: none;
- }
-
- [data-uix-rail] {
- display: none;
- }
-
- [data-uix-canvas] {
- grid-column: 1;
- padding: var(--uix-space-8) var(--uix-space-4) var(--uix-space-12);
- }
-
- [data-uix-canvas-inner],
- [data-uix-canvas-inner][data-with-toc='true'] {
- display: block;
- max-inline-size: none;
- }
-
- [data-uix-toc] {
- display: none;
- }
-}
-
-@media (max-width: 560px) {
- [data-uix-topbar-nav] a[href='https://github.com/'] {
- display: none;
- }
-
- [data-uix-canvas] {
- padding: var(--uix-space-6) var(--uix-space-3) var(--uix-space-10);
- }
-
- [data-uix-page-title] {
- font-size: var(--uix-text-4xl);
- }
-
- [data-uix-page-lede] {
- font-size: var(--uix-text-lg);
- }
-
- [data-uix-page-meta],
- [data-uix-tabs],
- [data-uix-code-head] {
- overflow-x: auto;
- overscroll-behavior-inline: contain;
- }
-
- [data-uix-tabs] {
- flex-wrap: nowrap;
- }
-
- [data-uix-tab],
- [data-uix-meta-pill],
- [data-uix-code-head] > * {
- flex: 0 0 auto;
- }
-
- [data-uix-stage-area] {
- place-items: stretch;
- }
-
- [data-uix-stage-trace] {
- align-items: flex-start;
- gap: var(--uix-space-2);
- }
-
- [data-uix-controls] {
- grid-template-columns: minmax(0, 1fr);
- padding: var(--uix-space-4);
- }
-
- [data-uix-part] {
- grid-template-columns: minmax(0, 1fr);
- gap: var(--uix-space-2);
- }
+/* ── Responsive docs shell: driven by ActiveDom breakpoints ────────── */
+[data-uix-docs][data-uix-compact='true'] [data-uix-shell] {
+ grid-template-columns: minmax(0, 1fr);
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar] {
+ max-inline-size: 100vi;
+ overflow: hidden;
+ padding-inline: var(--uix-space-3);
+ gap: var(--uix-space-2);
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-brand],
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar-nav],
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar-tools] {
+ min-inline-size: 0;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-brand-version] {
+ display: none;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar-nav] {
+ margin-inline-start: 0;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar-nav] a {
+ padding-inline: var(--uix-space-2);
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar-tools] {
+ flex: 1 1 auto;
+ justify-content: flex-end;
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+ scrollbar-width: none;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-topbar-tools]::-webkit-scrollbar {
+ display: none;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-rail] {
+ display: none;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-canvas] {
+ grid-column: 1;
+ padding: var(--uix-space-8) var(--uix-space-4) var(--uix-space-12);
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-canvas-inner],
+[data-uix-docs][data-uix-compact='true'] [data-uix-canvas-inner][data-with-toc='true'] {
+ display: block;
+ max-inline-size: none;
+}
+
+[data-uix-docs][data-uix-compact='true'] [data-uix-toc] {
+ display: none;
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-topbar-nav] a[href='https://github.com/'] {
+ display: none;
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-canvas] {
+ padding: var(--uix-space-6) var(--uix-space-3) var(--uix-space-10);
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-page-title] {
+ font-size: var(--uix-text-4xl);
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-page-lede] {
+ font-size: var(--uix-text-lg);
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-page-meta],
+[data-uix-docs][data-uix-phone='true'] [data-uix-tabs],
+[data-uix-docs][data-uix-phone='true'] [data-uix-code-head] {
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-tabs] {
+ flex-wrap: nowrap;
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-tab],
+[data-uix-docs][data-uix-phone='true'] [data-uix-meta-pill],
+[data-uix-docs][data-uix-phone='true'] [data-uix-code-head] > * {
+ flex: 0 0 auto;
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-stage-area] {
+ place-items: stretch;
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-stage-trace] {
+ align-items: flex-start;
+ gap: var(--uix-space-2);
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-controls] {
+ grid-template-columns: minmax(0, 1fr);
+ padding: var(--uix-space-4);
+}
+
+[data-uix-docs][data-uix-phone='true'] [data-uix-part] {
+ grid-template-columns: minmax(0, 1fr);
+ gap: var(--uix-space-2);
}