import type { Morfo } from '../types'; import { v } from '../types'; /** * PasswordField — text input with masked value, visibility toggle and * CapsLock awareness. APG textbox pattern + ARIA Toggle Button for the * visibility trigger. * * Surface (consensus across Ark UI / Chakra v3 / Mantine / PrimeReact / * Ant Design — all dedicate a component for this): * * * * * * * * Distinctive UIX contribution: the `CapsLockIndicator` part fills a gap * present in 10/10 libraries surveyed — none warn the user when caps * lock is on while typing a password. Soma listens to keydown events, * reads `KeyboardEvent.getModifierState('CapsLock')`, and exposes the * boolean via `data-caps-active`. */ export const passwordFieldMorfo = { name: 'PasswordField', kebab: 'password-field', scope: ['soma', 'sema'], texts: { label: '#?components.password-field.label|Password', show: '#?components.password-field.show|Show password', hide: '#?components.password-field.hide|Hide password', 'caps-warning': '#?components.password-field.caps-warning|Caps Lock is on' }, apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/textbox/', events: [ { // Value committed (every keystroke). name: 'commit-input', semantic: { family: 'commit', verb: 'input', target: v.partRef('input'), intent: 'neutral', sequence: 'post' } }, { // User pressed Enter — consumer-defined submit hook. name: 'commit-submit', semantic: { family: 'commit', verb: 'submit', target: v.partRef('input'), intent: 'neutral', sequence: 'post' } }, { // Visibility toggled (mask ↔ plaintext). name: 'shift-toggle-visibility', semantic: { family: 'shift', verb: 'toggle', target: v.partRef('visibility-trigger'), sequence: 'post' } }, { // Caps Lock state changed while the input is focused. name: 'shift-caps-state', semantic: { family: 'shift', verb: 'navigate', target: v.partRef('caps-lock-indicator'), sequence: 'post' } } ], parts: [ { name: 'Provider', kebab: 'provider', archetype: 'provider', kind: 'public', defaultElement: 'div', optional: false, data: [ { 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' }, { attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }, { attr: 'data-focused', value: v.propRef('focused'), severity: 'optional' }, { attr: 'data-empty', value: v.propRef('empty'), severity: 'optional' }, { attr: 'data-visible', value: v.propRef('visible'), severity: 'optional' }, { attr: 'data-caps-active', value: v.propRef('capsActive'), severity: 'optional' } ], aria: [] }, { name: 'Input', kebab: 'input', archetype: 'input', kind: 'public', defaultElement: 'input', role: 'textbox', optional: false, data: [ { 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' } ], aria: [ { attr: 'aria-label', value: v.translationRef('#?components.password-field.label|Password'), severity: 'recommended' }, { attr: 'aria-invalid', value: v.literal('true'), severity: 'optional', condition: { when: 'prop-truthy', prop: 'invalid' } }, { attr: 'aria-required', value: v.literal('true'), severity: 'optional', condition: { when: 'prop-truthy', prop: 'required' } } ], keyboard: [{ key: 'Enter', action: 'submit' }] }, { name: 'VisibilityTrigger', kebab: 'visibility-trigger', archetype: 'trigger', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [ { attr: 'data-visible', value: v.propRef('visible'), severity: 'optional' }, { attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' } ], aria: [ { attr: 'type', value: v.literal('button') }, { // Toggle button pattern — pressed when password is visible. attr: 'aria-pressed', value: v.mapRef(v.propRef('visible'), { true: 'true', false: 'false' }), severity: 'recommended' }, { // Label flips between "Show" and "Hide" based on state. attr: 'aria-label', value: v.translationRef('#?components.password-field.show|Show password'), severity: 'recommended' } ] }, { // Live region that announces Caps Lock state while the input has // focus. `aria-live="polite"` so it doesn't interrupt typing flow. name: 'CapsLockIndicator', kebab: 'caps-lock-indicator', archetype: 'indicator', kind: 'public', defaultElement: 'span', role: 'status', optional: true, data: [ { attr: 'data-active', value: v.propRef('capsActive'), severity: 'optional' } ], aria: [{ attr: 'aria-live', value: v.literal('polite') }] }, { // Composable strength meter — consumer provides a 0–4 score. // UIX does not bundle a strength algorithm (zxcvbn etc.) to stay // dependency-free; consumer wires whichever library they trust. name: 'StrengthMeter', kebab: 'strength-meter', archetype: 'indicator', kind: 'public', defaultElement: 'div', role: 'meter', optional: true, data: [{ attr: 'data-strength', value: v.propRef('strength'), severity: 'optional' }], aria: [ { attr: 'aria-valuemin', value: v.literal('0') }, { attr: 'aria-valuemax', value: v.literal('4') }, { attr: 'aria-valuenow', value: v.propRef('strength') } ] } ] } as const satisfies Morfo;