feat(fields): mask-field composes Field via collapsed control identity (S6)

Same collapsed text-field pattern as the search-field pilot: mask-field is a single
inline-flex row box (input only, no affixes), so its Provider part collapses
`fieldRole('provider')` + `fieldRole('control')` and the `<input>` gets
`fieldRole('input')`. field.css owns the entire box chrome (border/radius/bg/height/
padding/hover/focus/invalid/disabled/variant/size) + the bare-input reset; mask-field.css
drops from 143 lines to a single rule — the input's `tabular-nums` (masked patterns are
digit-heavy, keep glyph columns aligned so fixed literals don't shift as the mask fills).

Baseline unchanged (59). Verified: box paints from field.css (36px/border/pad/radius,
flex row), input reset + tabular figures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 985685dffd
commit 81b7d460c0

@ -2,141 +2,16 @@
* MaskField — single-line masked input for fixed-literal patterns (phone,
* national id, credit card, IBAN, postal code, plate).
*
* Mirrors the shared text-input chrome (border, radius, focus ring, padding,
* disabled opacity) so every field in the system reads the same — the mask is
* a value affordance inside the control, not a structural change. The control
* stays a single flex row: the bordered box IS the provider, the borderless
* input fills it.
*
* No transitions on the chrome: an external `invalid` flip (e.g. a form
* re-validating onChange) would otherwise replay the border transition; the
* box snaps to its target state instead.
* mask-field IS-A Field control, collapsed onto one box (no label/segment stack):
* the box chrome (border / radius / bg / height / padding / hover / focus / invalid /
* disabled / variant / size) and the bare-input reset are ALL owned by field.css via
* the `data-field` + `data-field-control` + `data-field-input` identity (fieldRole).
* The mask is a value affordance inside the input, not a structural change — so the
* ONLY mask-specific rule is the input's tabular figures.
*/
[data-mask-field] {
--_mask-field-height: var(--mask-field-height-md);
--_mask-field-padding-inline: var(--mask-field-padding-inline-md);
--_mask-field-font-size: var(--mask-field-font-size-md);
/* Focus accent per data-color (foundation role tokens; default primary). */
--_mask-field-accent-border: var(--color-primary-border);
--_mask-field-border-color: var(--mask-field-border);
--_mask-field-bg: var(--mask-field-bg);
display: flex;
align-items: center;
inline-size: 100%;
min-inline-size: 0;
min-block-size: var(--_mask-field-height);
padding-inline: var(--_mask-field-padding-inline);
border: var(--mask-field-border-width) solid var(--_mask-field-border-color);
border-radius: var(--mask-field-radius);
background: var(--_mask-field-bg);
color: var(--mask-field-color);
font-family: var(--mask-field-font-family);
font-size: var(--_mask-field-font-size);
line-height: var(--mask-field-line-height);
}
/* ── Input ─────────────────────────────────────────────────────────── */
[data-mask-field-input] {
min-inline-size: 0;
inline-size: 100%;
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: var(--font-weight-medium);
outline: none;
/* Masked patterns are digit-heavy — keep glyph columns aligned. */
/* Masked patterns are digit-heavy — keep glyph columns aligned so the caret and
fixed literals don't shift as the mask fills. */
font-variant-numeric: tabular-nums;
}
[data-mask-field-input]::placeholder {
color: var(--mask-field-placeholder-color);
}
[data-mask-field-input][data-disabled] {
cursor: not-allowed;
}
/* ── Color accents (focus border tint) ─────────────────────────────── */
[data-mask-field][data-color='secondary'] {
--_mask-field-accent-border: var(--color-secondary-border);
}
[data-mask-field][data-color='neutral'] {
--_mask-field-accent-border: var(--color-neutral-border);
}
[data-mask-field][data-color='affirm'] {
--_mask-field-accent-border: var(--color-affirm-border);
}
[data-mask-field][data-color='fulfill'] {
--_mask-field-accent-border: var(--color-fulfill-border);
}
[data-mask-field][data-color='risk'] {
--_mask-field-accent-border: var(--color-risk-border);
}
[data-mask-field][data-color='threat'] {
--_mask-field-accent-border: var(--color-threat-border);
}
[data-mask-field][data-color='loss'] {
--_mask-field-accent-border: var(--color-loss-border);
}
/* ── Sizes ─────────────────────────────────────────────────────────── */
[data-mask-field][data-size='xs'] {
--_mask-field-height: var(--mask-field-height-xs);
--_mask-field-padding-inline: var(--mask-field-padding-inline-xs);
--_mask-field-font-size: var(--mask-field-font-size-xs);
}
[data-mask-field][data-size='sm'] {
--_mask-field-height: var(--mask-field-height-sm);
--_mask-field-padding-inline: var(--mask-field-padding-inline-sm);
--_mask-field-font-size: var(--mask-field-font-size-sm);
}
[data-mask-field][data-size='lg'] {
--_mask-field-height: var(--mask-field-height-lg);
--_mask-field-padding-inline: var(--mask-field-padding-inline-lg);
--_mask-field-font-size: var(--mask-field-font-size-lg);
}
[data-mask-field][data-size='xl'] {
--_mask-field-height: var(--mask-field-height-xl);
--_mask-field-padding-inline: var(--mask-field-padding-inline-xl);
--_mask-field-font-size: var(--mask-field-font-size-xl);
}
/* ── Variants ──────────────────────────────────────────────────────── */
[data-mask-field][data-variant='outline'] {
--_mask-field-bg: transparent;
}
[data-mask-field][data-variant='ghost'] {
--_mask-field-border-color: transparent;
--_mask-field-bg: transparent;
padding-inline: 0;
}
/* ── State chrome ──────────────────────────────────────────────────── */
[data-mask-field]:hover:not([data-disabled]):not([data-focused]) {
--_mask-field-border-color: var(--color-border-strong);
}
[data-mask-field][data-focused] {
--_mask-field-border-color: var(--_mask-field-accent-border);
/* Flush outline ring (no offset → no gap) so it survives forced-colors. */
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: 0;
}
[data-mask-field][data-invalid] {
--_mask-field-border-color: var(--mask-field-border-invalid);
}
[data-mask-field][data-disabled] {
opacity: var(--mask-field-disabled-opacity);
cursor: not-allowed;
}

@ -1,4 +1,8 @@
<script lang="ts">
// mask-field IS-A Field control (collapsed onto one box): field.css owns the box
// chrome / input reset / focus / invalid / disabled. mask-field.css keeps ONLY the
// mask-skeleton overlay — NO tokens aliasing the Field base.
import '../field/field.css';
import './mask-field.css';
import { ActiveEidos } from '$uix/eidos';
import * as MaskField from '$soma/components/mask-field';

@ -1,5 +1,6 @@
import type { Morfo } from '../types';
import { v } from '../types';
import { fieldRole } from '../field-role';
/**
* MaskField — a single-input character mask for free-form fixed-literal
@ -65,6 +66,11 @@ export const maskFieldMorfo = {
defaultElement: 'div',
optional: false,
data: [
// mask-field IS-A Field control (single-input row, no label/segment stack):
// the one box collapses BOTH the Field root identity + the control (row-box
// chrome from field.css `[data-field-control]`).
fieldRole('provider'),
fieldRole('control'),
{ 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' },
@ -87,6 +93,8 @@ export const maskFieldMorfo = {
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' }

Loading…
Cancel
Save

Powered by TurnKey Linux.