diff --git a/src/uix/eidos/components/color-field/README.md b/src/uix/eidos/components/color-field/README.md new file mode 100644 index 000000000..0bcc9b952 --- /dev/null +++ b/src/uix/eidos/components/color-field/README.md @@ -0,0 +1,84 @@ +# Eidos ColorField + +Segmented color input. Soma owns format switching (hex/rgb/hsl), channel +editing, alpha, ARIA spinbuttons and validation. Eidos adds size, +variant, intent color and recipe styling on top. + +## Baseline + +Air no tenía un `ColorField` segmentado en la rama anterior — sólo un +`ColorPicker` con saturación + hue + alpha. La referencia local real +es Soma `color-field`, que es **único en UIX**: ningún framework +externo (Ark, Chakra, Bits) ofrece edición canal-a-canal de un color +como segmentos accesibles. La receta Eidos sólo envuelve la superficie +Soma con tamaños, variante visual y paleta de intents. Referencias +externas para format switcher / alpha: Ark UI Color Picker, Chakra +Color Picker, Bits UI Color Picker. + +## Superficie + +```svelte + + Brand color + + {#snippet children({ segments })} + {#each segments as { part, value } (part + value)} + {value} + {/each} + + {/snippet} + + +``` + +Partes públicas: `Label`, `Input`, `Segment`, `FormatSelect`, `HiddenInput`. + +## Comparativa + +| Capacidad | Soma/Eidos | Ark UI | Chakra UI | Bits UI | +| --- | --- | --- | --- | --- | +| Input segmentado (canal a canal) | Sí | No | No | No | +| Format switch hex/rgb/hsl | Sí | Sí | Sí | Sí | +| Canal alpha | Sí (opt-in) | Sí | Sí | Sí | +| `allowedFormats` whitelist | Sí | No | No | No | +| Validación custom | Sí | Sí | Sí | Sí | +| Segmentos readonly | Sí | No | No | No | +| Input hidden para forms | Sí | Vía picker | Vía picker | Vía picker | +| Integración con Field | Sí | — | — | — | +| Variante / tamaño / paleta visual | Sí, Eidos | CSS externo | CSS externo | CSS externo | + +## Decisiones + +- No se exporta `Provider` ni `Root` público. La forma Eidos es + `` con hijos attached. +- Eidos no implementa saturación/área, hue slider ni swatches. Eso + pertenece a `ColorPicker`, no a `ColorField`. +- `ColorValue` mantiene `hex / rgb / hsl / hsv` en sincronía. Al editar + un canal el provider escribe contra el formato autoritativo (sin + redondeo cruzado HSV ↔ RGB). +- `format` es bindable y drives qué segmentos se renderizan. + `allowedFormats` controla qué opciones expone `FormatSelect`. Cuando + `allowedFormats.length === 1`, el select queda visualmente bloqueado + (`data-locked`). +- `enableAlpha={false}` quita el segmento alpha; `readonlySegments` lo + permite mantener visible pero no editable. +- `size`, `variant` y `color` pertenecen a Eidos. Resto (value, + placeholder, format, allowedFormats, enableAlpha, readonlySegments, + validación) pertenece a Soma. + +## Gaps + +| Gap | Disposición | Detalle | +| --- | --- | --- | +| ColorPicker (saturación + hue + alpha + swatches) | **implementar** | Commit independiente. ColorField cubre el caso "type the hex"; ColorPicker cubre el caso "explore". | +| Tests browser-level del flujo completo (Playwright) | **implementar** | Cobertura visual + interacciones (canal Arrow/Backspace, format switch). Se hace en una pasada conjunta de tests. | +| Eye dropper API | **diferir** | Sólo en navegadores con `EyeDropper`. Pasa al picker. | +| Documentación per-prop exhaustiva | **diferir** | Cubierto por la tabla de la demo (`API` tab). | + +## Referencias + +- Soma ColorField: `src/uix/soma/components/color-field/README.md` +- Ark UI Color Picker: https://ark-ui.com/docs/components/color-picker +- Chakra UI Color Picker: https://chakra-ui.com/docs/components/color-picker +- Bits UI Color Picker: https://bits-ui.com/docs/components/color-picker +- WAI-ARIA APG Spinbutton: https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/ diff --git a/src/uix/eidos/components/color-field/color-field-format-select.svelte b/src/uix/eidos/components/color-field/color-field-format-select.svelte new file mode 100644 index 000000000..a37dc04c0 --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field-format-select.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/color-field/color-field-hidden-input.svelte b/src/uix/eidos/components/color-field/color-field-hidden-input.svelte new file mode 100644 index 000000000..f12d27586 --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field-hidden-input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/uix/eidos/components/color-field/color-field-input.svelte b/src/uix/eidos/components/color-field/color-field-input.svelte new file mode 100644 index 000000000..ddaf750e5 --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field-input.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(args: ColorFieldInputSnippetProps)} + {@render bodyContent?.(args)} + {/snippet} + diff --git a/src/uix/eidos/components/color-field/color-field-label.svelte b/src/uix/eidos/components/color-field/color-field-label.svelte new file mode 100644 index 000000000..253b04e84 --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field-label.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/color-field/color-field-segment.svelte b/src/uix/eidos/components/color-field/color-field-segment.svelte new file mode 100644 index 000000000..640d68aac --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field-segment.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/color-field/color-field.css b/src/uix/eidos/components/color-field/color-field.css new file mode 100644 index 000000000..eccb3395a --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field.css @@ -0,0 +1,228 @@ +[data-color-field] { + --_color-field-height: var(--color-field-height-md); + --_color-field-px: var(--color-field-px-md); + --_color-field-gap: var(--color-field-gap-md); + --_color-field-segment-px: var(--color-field-segment-px-md); + --_color-field-font-size: var(--color-field-font-size-md); + --_color-field-swatch-size: var(--color-field-swatch-size-md); + --_color-field-accent-border: var(--color-primary-border); + --_color-field-accent-track: var(--color-primary-track); + --_color-field-accent-text: var(--color-primary-text); + + display: inline-flex; + flex-direction: column; + gap: var(--color-field-stack-gap); + inline-size: 100%; + min-inline-size: 0; + color: var(--color-field-color); + font-family: var(--color-field-font-family); + font-size: var(--_color-field-font-size); + line-height: var(--color-field-line-height); +} + +[data-color-field][data-size='xs'] { + --_color-field-height: var(--color-field-height-xs); + --_color-field-px: var(--color-field-px-xs); + --_color-field-gap: var(--color-field-gap-xs); + --_color-field-segment-px: var(--color-field-segment-px-xs); + --_color-field-font-size: var(--color-field-font-size-xs); + --_color-field-swatch-size: var(--color-field-swatch-size-xs); +} + +[data-color-field][data-size='sm'] { + --_color-field-height: var(--color-field-height-sm); + --_color-field-px: var(--color-field-px-sm); + --_color-field-gap: var(--color-field-gap-sm); + --_color-field-segment-px: var(--color-field-segment-px-sm); + --_color-field-font-size: var(--color-field-font-size-sm); + --_color-field-swatch-size: var(--color-field-swatch-size-sm); +} + +[data-color-field][data-size='lg'] { + --_color-field-height: var(--color-field-height-lg); + --_color-field-px: var(--color-field-px-lg); + --_color-field-gap: var(--color-field-gap-lg); + --_color-field-segment-px: var(--color-field-segment-px-lg); + --_color-field-font-size: var(--color-field-font-size-lg); + --_color-field-swatch-size: var(--color-field-swatch-size-lg); +} + +[data-color-field][data-size='xl'] { + --_color-field-height: var(--color-field-height-xl); + --_color-field-px: var(--color-field-px-xl); + --_color-field-gap: var(--color-field-gap-xl); + --_color-field-segment-px: var(--color-field-segment-px-xl); + --_color-field-font-size: var(--color-field-font-size-xl); + --_color-field-swatch-size: var(--color-field-swatch-size-xl); +} + +[data-color-field][data-color='secondary'] { + --_color-field-accent-border: var(--color-secondary-border); + --_color-field-accent-track: var(--color-secondary-track); + --_color-field-accent-text: var(--color-secondary-text); +} + +[data-color-field][data-color='neutral'] { + --_color-field-accent-border: var(--color-neutral-border); + --_color-field-accent-track: var(--color-neutral-track); + --_color-field-accent-text: var(--color-neutral-text); +} + +[data-color-field][data-color='affirm'] { + --_color-field-accent-border: var(--color-affirm-border); + --_color-field-accent-track: var(--color-affirm-track); + --_color-field-accent-text: var(--color-affirm-text); +} + +[data-color-field][data-color='fulfill'] { + --_color-field-accent-border: var(--color-fulfill-border); + --_color-field-accent-track: var(--color-fulfill-track); + --_color-field-accent-text: var(--color-fulfill-text); +} + +[data-color-field][data-color='risk'] { + --_color-field-accent-border: var(--color-risk-border); + --_color-field-accent-track: var(--color-risk-track); + --_color-field-accent-text: var(--color-risk-text); +} + +[data-color-field][data-color='threat'] { + --_color-field-accent-border: var(--color-threat-border); + --_color-field-accent-track: var(--color-threat-track); + --_color-field-accent-text: var(--color-threat-text); +} + +[data-color-field][data-color='loss'] { + --_color-field-accent-border: var(--color-loss-border); + --_color-field-accent-track: var(--color-loss-track); + --_color-field-accent-text: var(--color-loss-text); +} + +[data-color-field-label] { + color: var(--color-field-label-color); + font: inherit; + font-weight: var(--color-field-label-font-weight); +} + +[data-color-field-input] { + display: inline-flex; + align-items: center; + gap: var(--_color-field-gap); + min-inline-size: 0; + inline-size: 100%; + min-block-size: var(--_color-field-height); + padding-inline: var(--_color-field-px); + border: var(--color-field-border-width) solid var(--color-field-border); + border-radius: var(--color-field-radius); + background: var(--color-field-bg); + color: inherit; + transition: + background var(--color-field-transition-duration) var(--color-field-transition-ease), + border-color var(--color-field-transition-duration) var(--color-field-transition-ease), + box-shadow var(--color-field-transition-duration) var(--color-field-transition-ease); +} + +[data-color-field][data-variant='outline'] [data-color-field-input] { + background: transparent; +} + +[data-color-field][data-variant='ghost'] [data-color-field-input] { + border-color: transparent; + background: transparent; + padding-inline: 0; +} + +[data-color-field-input]:has([data-color-field-segment]:focus-visible) { + border-color: var(--_color-field-accent-border); + box-shadow: var(--color-field-focus-shadow); +} + +[data-color-field][data-invalid] [data-color-field-input], +[data-color-field-input][data-invalid] { + border-color: var(--color-field-border-invalid); +} + +[data-color-field][data-disabled], +[data-color-field-input][data-disabled] { + opacity: var(--color-field-disabled-opacity); +} + +[data-color-field-segment] { + display: inline-flex; + align-items: center; + justify-content: center; + min-inline-size: var(--color-field-segment-min-width); + block-size: var(--color-field-segment-height); + padding-inline: var(--_color-field-segment-px); + border-radius: var(--color-field-segment-radius); + color: var(--color-field-segment-color); + font: inherit; + outline: none; + white-space: nowrap; + transition: + background var(--color-field-transition-duration) var(--color-field-transition-ease), + color var(--color-field-transition-duration) var(--color-field-transition-ease), + box-shadow var(--color-field-transition-duration) var(--color-field-transition-ease); +} + +[data-color-field-segment][data-segment='literal'] { + min-inline-size: auto; + padding-inline: 0; + color: var(--color-field-literal-color); +} + +[data-color-field-segment][data-placeholder] { + color: var(--color-field-placeholder-color); +} + +[data-color-field-segment]:focus-visible { + background: var(--_color-field-accent-track); + color: var(--_color-field-accent-text); + box-shadow: var(--color-field-segment-focus-shadow); +} + +[data-color-field-segment][data-invalid] { + color: var(--color-field-segment-invalid-color); +} + +[data-color-field-segment][data-readonly] { + background: var(--color-field-segment-readonly-bg); + color: var(--color-field-segment-readonly-color); + text-decoration: var(--color-field-segment-readonly-decoration); + text-underline-offset: 2px; + cursor: default; +} + +[data-color-field-segment][data-disabled] { + cursor: not-allowed; + opacity: var(--color-field-disabled-opacity); +} + +[data-color-field-format-select] { + margin-inline-start: auto; + border: none; + border-radius: var(--color-field-format-select-radius); + padding-inline: var(--color-field-format-select-px); + background: var(--color-field-format-select-bg); + color: var(--color-field-format-select-color); + font: inherit; + font-size: var(--_color-field-font-size); + cursor: pointer; +} + +[data-color-field-format-select]:focus-visible { + box-shadow: var(--color-field-segment-focus-shadow); +} + +[data-color-field-format-select][data-disabled], +[data-color-field-format-select][data-locked] { + cursor: not-allowed; + opacity: var(--color-field-disabled-opacity); +} + +@media (prefers-reduced-motion: reduce) { + [data-color-field-input], + [data-color-field-segment] { + transition: none; + } +} diff --git a/src/uix/eidos/components/color-field/color-field.svelte b/src/uix/eidos/components/color-field/color-field.svelte new file mode 100644 index 000000000..cda844f56 --- /dev/null +++ b/src/uix/eidos/components/color-field/color-field.svelte @@ -0,0 +1,31 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/color-field/index.ts b/src/uix/eidos/components/color-field/index.ts new file mode 100644 index 000000000..7326de29c --- /dev/null +++ b/src/uix/eidos/components/color-field/index.ts @@ -0,0 +1,41 @@ +import ColorFieldComponent from './color-field.svelte' +import Label from './color-field-label.svelte' +import Input from './color-field-input.svelte' +import Segment from './color-field-segment.svelte' +import FormatSelect from './color-field-format-select.svelte' +import HiddenInput from './color-field-hidden-input.svelte' + +type ColorFieldNamespace = typeof ColorFieldComponent & { + Label: typeof Label + Input: typeof Input + Segment: typeof Segment + FormatSelect: typeof FormatSelect + HiddenInput: typeof HiddenInput +} + +const ColorField = ColorFieldComponent as ColorFieldNamespace +ColorField.Label = Label +ColorField.Input = Input +ColorField.Segment = Segment +ColorField.FormatSelect = FormatSelect +ColorField.HiddenInput = HiddenInput + +export { ColorField } + +export default ColorField + +export type { + ColorFieldProps, + ColorFieldLabelProps as LabelProps, + ColorFieldInputProps as InputProps, + ColorFieldSegmentProps as SegmentProps, + ColorFieldFormatSelectProps as FormatSelectProps, + ColorFieldHiddenInputProps as HiddenInputProps, + InputSnippetProps, + ColorFieldSize, + ColorFieldVariant, + ColorFieldColor, + ColorValidator, + ColorOnInvalid, + ColorInvalidReason +} from './types' diff --git a/src/uix/eidos/components/color-field/types.ts b/src/uix/eidos/components/color-field/types.ts new file mode 100644 index 000000000..882694cb2 --- /dev/null +++ b/src/uix/eidos/components/color-field/types.ts @@ -0,0 +1,35 @@ +import type { + FormatSelectProps, + HiddenInputProps, + InputProps, + InputSnippetProps as ColorFieldInputSnippetProps, + LabelProps, + ProviderProps, + SegmentProps, + ColorValidator, + ColorOnInvalid, + ColorInvalidReason +} from '$soma/components/color-field' +import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types' + +export type ColorFieldSize = Extract +export type ColorFieldVariant = ControlVariant +export type ColorFieldColor = ColorRole + +export type ColorFieldProps = ProviderProps & { + /** Visual size for input height, segment padding and text. @default 'md' */ + size?: ResponsiveProp + /** Visual treatment of the segmented input. @default 'surface' */ + variant?: ColorFieldVariant + /** Intent palette for focus affordance. @default 'primary' */ + color?: ColorFieldColor +} + +export type ColorFieldLabelProps = LabelProps +export type ColorFieldInputProps = InputProps +export type ColorFieldSegmentProps = SegmentProps +export type ColorFieldFormatSelectProps = FormatSelectProps +export type ColorFieldHiddenInputProps = HiddenInputProps + +export type { ColorFieldInputSnippetProps, ColorFieldInputSnippetProps as InputSnippetProps } +export type { ColorValidator, ColorOnInvalid, ColorInvalidReason } diff --git a/src/uix/eidos/components/time-field/README.md b/src/uix/eidos/components/time-field/README.md new file mode 100644 index 000000000..a89252dfd --- /dev/null +++ b/src/uix/eidos/components/time-field/README.md @@ -0,0 +1,80 @@ +# Eidos TimeField + +Segmented time input. Soma owns the editable hour/minute/second segments, +ARIA spinbuttons, 12/24 hour cycle, day-period (AM/PM), timezone display +and validation. Eidos adds size, variant, intent color and recipe +styling on top. + +## Baseline + +Air no tenía un `TimeField` equivalente en la rama anterior. La +referencia local real es Soma `time-field`, que ya implementa la edición +segmentada (hour / minute / second / dayPeriod / timeZoneName), y la +receta Eidos solo envuelve esa superficie con tamaños, variante visual +y paleta de intents. Referencias externas: React Aria TimeField, Ark UI +Time Picker, Bits UI Time Field. + +## Superficie + +```svelte + + Start + + {#snippet children({ segments })} + {#each segments as { part, value } (part + value)} + {value} + {/each} + {/snippet} + + +``` + +Partes públicas: `Label`, `Input`, `Segment`, `HiddenInput`. + +## Comparativa + +| Capacidad | Soma/Eidos | React Aria | Ark UI | Bits UI | +| --- | --- | --- | --- | --- | +| Input segmentado | Sí | Sí | Sí | Sí | +| Hora/minuto/segundo | Sí | Sí | Sí | Sí | +| 12h / 24h cycle | Sí | Sí | Sí | Sí | +| AM/PM (dayPeriod) | Sí | Sí | Sí | Sí | +| Zona horaria visible | Sí (ZonedDateTime) | Sí | Sí | Sí | +| `granularity` (hour / minute / second) | Sí | Sí | Sí | Sí | +| `minValue` / `maxValue` | Sí | Sí | Sí | Sí | +| Validación custom | Sí | Sí | Sí | Sí | +| Segmentos readonly | Sí | No | No | Sí | +| Input hidden para forms | Sí | — | Sí | Sí | +| Integración con Field | Sí | Vía Form/TextField | Vía Field | Manual | +| Variante / tamaño / paleta visual | Sí, Eidos | CSS externo | CSS externo | CSS externo | + +## Decisiones + +- No se exporta `Provider` ni `Root` público. La forma Eidos es `` + con hijos attached. +- Eidos no implementa popover ni clock dial. Eso pertenece a `TimePicker`, + no a `TimeField`. +- `HiddenInput` se mantiene como parte pública por simetría con Soma; el + caso normal lo renderiza `TimeField.Input` cuando recibe `name`. +- `size`, `variant` y `color` pertenecen a Eidos porque sólo afectan + receta visual; `value`, `placeholder`, `granularity`, `hourCycle`, + `hideTimeZone`, `locale`, `dir`, validación y navegación de segmentos + pertenecen a Soma. +- El evento declarado por Morfo es `commit-set`; la edición continua de + segmentos no se modela como evento perceptivo por cada pulsación. + +## Gaps + +| Gap | Disposición | Detalle | +| --- | --- | --- | +| Tests browser-level del flujo completo (Playwright) | **implementar** | Cobertura visual + interacciones (Arrow/Backspace/dayPeriod toggle). Se hace en una pasada conjunta de tests. | +| API extendida sobre las referencias externas | **diferir** | Sólo si llega un caso de uso real. Mantener la superficie estable. | +| Documentación per-prop exhaustiva | **diferir** | Cubierto por la tabla de la demo (`API` tab). | + +## Referencias + +- Soma TimeField: `src/uix/soma/components/time-field/README.md` +- React Aria TimeField: https://react-spectrum.adobe.com/react-aria/TimeField.html +- Ark UI Time Picker: https://ark-ui.com/docs/components/time-picker +- Bits UI Time Field: https://bits-ui.com/docs/components/time-field +- WAI-ARIA APG Spinbutton: https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/ diff --git a/src/uix/eidos/components/time-field/index.ts b/src/uix/eidos/components/time-field/index.ts new file mode 100644 index 000000000..f987fdda6 --- /dev/null +++ b/src/uix/eidos/components/time-field/index.ts @@ -0,0 +1,34 @@ +import TimeFieldComponent from './time-field.svelte' +import Label from './time-field-label.svelte' +import Input from './time-field-input.svelte' +import Segment from './time-field-segment.svelte' +import HiddenInput from './time-field-hidden-input.svelte' + +type TimeFieldNamespace = typeof TimeFieldComponent & { + Label: typeof Label + Input: typeof Input + Segment: typeof Segment + HiddenInput: typeof HiddenInput +} + +const TimeField = TimeFieldComponent as TimeFieldNamespace +TimeField.Label = Label +TimeField.Input = Input +TimeField.Segment = Segment +TimeField.HiddenInput = HiddenInput + +export { TimeField } + +export default TimeField + +export type { + TimeFieldProps, + TimeFieldLabelProps as LabelProps, + TimeFieldInputProps as InputProps, + TimeFieldSegmentProps as SegmentProps, + TimeFieldHiddenInputProps as HiddenInputProps, + InputSnippetProps, + TimeFieldSize, + TimeFieldVariant, + TimeFieldColor +} from './types' diff --git a/src/uix/eidos/components/time-field/time-field-hidden-input.svelte b/src/uix/eidos/components/time-field/time-field-hidden-input.svelte new file mode 100644 index 000000000..094f99900 --- /dev/null +++ b/src/uix/eidos/components/time-field/time-field-hidden-input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/uix/eidos/components/time-field/time-field-input.svelte b/src/uix/eidos/components/time-field/time-field-input.svelte new file mode 100644 index 000000000..ca8c3de2a --- /dev/null +++ b/src/uix/eidos/components/time-field/time-field-input.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(args: TimeFieldInputSnippetProps)} + {@render bodyContent?.(args)} + {/snippet} + diff --git a/src/uix/eidos/components/time-field/time-field-label.svelte b/src/uix/eidos/components/time-field/time-field-label.svelte new file mode 100644 index 000000000..19b45e844 --- /dev/null +++ b/src/uix/eidos/components/time-field/time-field-label.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/time-field/time-field-segment.svelte b/src/uix/eidos/components/time-field/time-field-segment.svelte new file mode 100644 index 000000000..e62cce607 --- /dev/null +++ b/src/uix/eidos/components/time-field/time-field-segment.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/time-field/time-field.css b/src/uix/eidos/components/time-field/time-field.css new file mode 100644 index 000000000..f89e9fa01 --- /dev/null +++ b/src/uix/eidos/components/time-field/time-field.css @@ -0,0 +1,201 @@ +[data-time-field] { + --_time-field-height: var(--time-field-height-md); + --_time-field-px: var(--time-field-px-md); + --_time-field-gap: var(--time-field-gap-md); + --_time-field-segment-px: var(--time-field-segment-px-md); + --_time-field-font-size: var(--time-field-font-size-md); + --_time-field-accent-border: var(--color-primary-border); + --_time-field-accent-track: var(--color-primary-track); + --_time-field-accent-text: var(--color-primary-text); + + display: inline-flex; + flex-direction: column; + gap: var(--time-field-stack-gap); + inline-size: 100%; + min-inline-size: 0; + color: var(--time-field-color); + font-family: var(--time-field-font-family); + font-size: var(--_time-field-font-size); + line-height: var(--time-field-line-height); +} + +[data-time-field][data-size='xs'] { + --_time-field-height: var(--time-field-height-xs); + --_time-field-px: var(--time-field-px-xs); + --_time-field-gap: var(--time-field-gap-xs); + --_time-field-segment-px: var(--time-field-segment-px-xs); + --_time-field-font-size: var(--time-field-font-size-xs); +} + +[data-time-field][data-size='sm'] { + --_time-field-height: var(--time-field-height-sm); + --_time-field-px: var(--time-field-px-sm); + --_time-field-gap: var(--time-field-gap-sm); + --_time-field-segment-px: var(--time-field-segment-px-sm); + --_time-field-font-size: var(--time-field-font-size-sm); +} + +[data-time-field][data-size='lg'] { + --_time-field-height: var(--time-field-height-lg); + --_time-field-px: var(--time-field-px-lg); + --_time-field-gap: var(--time-field-gap-lg); + --_time-field-segment-px: var(--time-field-segment-px-lg); + --_time-field-font-size: var(--time-field-font-size-lg); +} + +[data-time-field][data-size='xl'] { + --_time-field-height: var(--time-field-height-xl); + --_time-field-px: var(--time-field-px-xl); + --_time-field-gap: var(--time-field-gap-xl); + --_time-field-segment-px: var(--time-field-segment-px-xl); + --_time-field-font-size: var(--time-field-font-size-xl); +} + +[data-time-field][data-color='secondary'] { + --_time-field-accent-border: var(--color-secondary-border); + --_time-field-accent-track: var(--color-secondary-track); + --_time-field-accent-text: var(--color-secondary-text); +} + +[data-time-field][data-color='neutral'] { + --_time-field-accent-border: var(--color-neutral-border); + --_time-field-accent-track: var(--color-neutral-track); + --_time-field-accent-text: var(--color-neutral-text); +} + +[data-time-field][data-color='affirm'] { + --_time-field-accent-border: var(--color-affirm-border); + --_time-field-accent-track: var(--color-affirm-track); + --_time-field-accent-text: var(--color-affirm-text); +} + +[data-time-field][data-color='fulfill'] { + --_time-field-accent-border: var(--color-fulfill-border); + --_time-field-accent-track: var(--color-fulfill-track); + --_time-field-accent-text: var(--color-fulfill-text); +} + +[data-time-field][data-color='risk'] { + --_time-field-accent-border: var(--color-risk-border); + --_time-field-accent-track: var(--color-risk-track); + --_time-field-accent-text: var(--color-risk-text); +} + +[data-time-field][data-color='threat'] { + --_time-field-accent-border: var(--color-threat-border); + --_time-field-accent-track: var(--color-threat-track); + --_time-field-accent-text: var(--color-threat-text); +} + +[data-time-field][data-color='loss'] { + --_time-field-accent-border: var(--color-loss-border); + --_time-field-accent-track: var(--color-loss-track); + --_time-field-accent-text: var(--color-loss-text); +} + +[data-time-field-label] { + color: var(--time-field-label-color); + font: inherit; + font-weight: var(--time-field-label-font-weight); +} + +[data-time-field-input] { + display: inline-flex; + align-items: center; + gap: var(--_time-field-gap); + min-inline-size: 0; + inline-size: 100%; + min-block-size: var(--_time-field-height); + padding-inline: var(--_time-field-px); + border: var(--time-field-border-width) solid var(--time-field-border); + border-radius: var(--time-field-radius); + background: var(--time-field-bg); + color: inherit; + transition: + background var(--time-field-transition-duration) var(--time-field-transition-ease), + border-color var(--time-field-transition-duration) var(--time-field-transition-ease), + box-shadow var(--time-field-transition-duration) var(--time-field-transition-ease); +} + +[data-time-field][data-variant='outline'] [data-time-field-input] { + background: transparent; +} + +[data-time-field][data-variant='ghost'] [data-time-field-input] { + border-color: transparent; + background: transparent; + padding-inline: 0; +} + +[data-time-field-input]:has([data-time-field-segment]:focus-visible) { + border-color: var(--_time-field-accent-border); + box-shadow: var(--time-field-focus-shadow); +} + +[data-time-field][data-invalid] [data-time-field-input], +[data-time-field-input][data-invalid] { + border-color: var(--time-field-border-invalid); +} + +[data-time-field][data-disabled], +[data-time-field-input][data-disabled] { + opacity: var(--time-field-disabled-opacity); +} + +[data-time-field-segment] { + display: inline-flex; + align-items: center; + justify-content: center; + min-inline-size: var(--time-field-segment-min-width); + block-size: var(--time-field-segment-height); + padding-inline: var(--_time-field-segment-px); + border-radius: var(--time-field-segment-radius); + color: var(--time-field-segment-color); + font: inherit; + outline: none; + white-space: nowrap; + transition: + background var(--time-field-transition-duration) var(--time-field-transition-ease), + color var(--time-field-transition-duration) var(--time-field-transition-ease), + box-shadow var(--time-field-transition-duration) var(--time-field-transition-ease); +} + +[data-time-field-segment][data-segment='literal'] { + min-inline-size: auto; + padding-inline: 0; + color: var(--time-field-literal-color); +} + +[data-time-field-segment][data-placeholder] { + color: var(--time-field-placeholder-color); +} + +[data-time-field-segment]:focus-visible { + background: var(--_time-field-accent-track); + color: var(--_time-field-accent-text); + box-shadow: var(--time-field-segment-focus-shadow); +} + +[data-time-field-segment][data-invalid] { + color: var(--time-field-segment-invalid-color); +} + +[data-time-field-segment][data-readonly] { + background: var(--time-field-segment-readonly-bg); + color: var(--time-field-segment-readonly-color); + text-decoration: var(--time-field-segment-readonly-decoration); + text-underline-offset: 2px; + cursor: default; +} + +[data-time-field-segment][data-disabled] { + cursor: not-allowed; + opacity: var(--time-field-disabled-opacity); +} + +@media (prefers-reduced-motion: reduce) { + [data-time-field-input], + [data-time-field-segment] { + transition: none; + } +} diff --git a/src/uix/eidos/components/time-field/time-field.svelte b/src/uix/eidos/components/time-field/time-field.svelte new file mode 100644 index 000000000..b791f41ef --- /dev/null +++ b/src/uix/eidos/components/time-field/time-field.svelte @@ -0,0 +1,29 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/time-field/types.ts b/src/uix/eidos/components/time-field/types.ts new file mode 100644 index 000000000..5e2a3efa8 --- /dev/null +++ b/src/uix/eidos/components/time-field/types.ts @@ -0,0 +1,28 @@ +import type { + InputProps, + InputSnippetProps as TimeFieldInputSnippetProps, + LabelProps, + ProviderProps, + SegmentProps +} from '$soma/components/time-field' +import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types' + +export type TimeFieldSize = Extract +export type TimeFieldVariant = ControlVariant +export type TimeFieldColor = ColorRole + +export type TimeFieldProps = ProviderProps & { + /** Visual size for input height, segment padding and text. @default 'md' */ + size?: ResponsiveProp + /** Visual treatment of the segmented input. @default 'surface' */ + variant?: TimeFieldVariant + /** Intent palette for focus affordance. @default 'primary' */ + color?: TimeFieldColor +} + +export type TimeFieldLabelProps = LabelProps +export type TimeFieldInputProps = InputProps +export type TimeFieldSegmentProps = SegmentProps +export type TimeFieldHiddenInputProps = Record + +export type { TimeFieldInputSnippetProps, TimeFieldInputSnippetProps as InputSnippetProps } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 40ed00f41..4cf8d8597 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -868,6 +868,119 @@ --date-field-transition-duration: var(--duration-fast); --date-field-transition-ease: var(--ease-default); --date-field-disabled-opacity: 0.56; + --time-field-stack-gap: var(--space-1-5); + --time-field-height-xs: var(--control-height-xs); + --time-field-height-sm: var(--control-height-sm); + --time-field-height-md: var(--control-height-md); + --time-field-height-lg: var(--control-height-lg); + --time-field-height-xl: var(--control-height-xl); + --time-field-px-xs: var(--space-2); + --time-field-px-sm: var(--space-2-5); + --time-field-px-md: var(--space-3); + --time-field-px-lg: var(--space-3-5); + --time-field-px-xl: var(--space-4); + --time-field-gap-xs: var(--space-0-5); + --time-field-gap-sm: var(--space-0-5); + --time-field-gap-md: var(--space-1); + --time-field-gap-lg: var(--space-1-5); + --time-field-gap-xl: var(--space-2); + --time-field-segment-px-xs: var(--space-0-5); + --time-field-segment-px-sm: var(--space-0-5); + --time-field-segment-px-md: var(--space-1); + --time-field-segment-px-lg: var(--space-1-5); + --time-field-segment-px-xl: var(--space-2); + --time-field-font-family: var(--font-ui); + --time-field-font-size-xs: var(--font-size-xs); + --time-field-font-size-sm: var(--font-size-xs); + --time-field-font-size-md: var(--font-size-sm); + --time-field-font-size-lg: var(--font-size-sm); + --time-field-font-size-xl: var(--font-size-md); + --time-field-line-height: var(--leading-ui); + --time-field-radius: var(--radius-md); + --time-field-border-width: var(--border-width); + --time-field-border: var(--color-border-default); + --time-field-border-invalid: var(--color-risk-border); + --time-field-bg: var(--color-surface-default); + --time-field-color: var(--color-content-primary); + --time-field-label-color: var(--color-content-secondary); + --time-field-label-font-weight: var(--font-weight-medium); + --time-field-placeholder-color: var(--color-content-muted); + --time-field-literal-color: var(--color-content-muted); + --time-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); + --time-field-segment-min-width: 1.5ch; + --time-field-segment-height: calc(var(--_time-field-height) - var(--space-2)); + --time-field-segment-radius: var(--radius-sm); + --time-field-segment-color: var(--color-content-primary); + --time-field-segment-invalid-color: var(--color-risk-text); + --time-field-segment-readonly-bg: var(--color-surface-overlay); + --time-field-segment-readonly-color: var(--color-content-secondary); + --time-field-segment-readonly-decoration: underline dotted var(--color-content-muted); + --time-field-segment-focus-shadow: inset 0 0 0 var(--border-width) var(--_time-field-accent-border); + --time-field-transition-duration: var(--duration-fast); + --time-field-transition-ease: var(--ease-default); + --time-field-disabled-opacity: 0.56; + --color-field-stack-gap: var(--space-1-5); + --color-field-height-xs: var(--control-height-xs); + --color-field-height-sm: var(--control-height-sm); + --color-field-height-md: var(--control-height-md); + --color-field-height-lg: var(--control-height-lg); + --color-field-height-xl: var(--control-height-xl); + --color-field-px-xs: var(--space-2); + --color-field-px-sm: var(--space-2-5); + --color-field-px-md: var(--space-3); + --color-field-px-lg: var(--space-3-5); + --color-field-px-xl: var(--space-4); + --color-field-gap-xs: var(--space-0-5); + --color-field-gap-sm: var(--space-0-5); + --color-field-gap-md: var(--space-1); + --color-field-gap-lg: var(--space-1-5); + --color-field-gap-xl: var(--space-2); + --color-field-segment-px-xs: var(--space-0-5); + --color-field-segment-px-sm: var(--space-0-5); + --color-field-segment-px-md: var(--space-1); + --color-field-segment-px-lg: var(--space-1-5); + --color-field-segment-px-xl: var(--space-2); + --color-field-font-family: var(--font-ui); + --color-field-font-size-xs: var(--font-size-xs); + --color-field-font-size-sm: var(--font-size-xs); + --color-field-font-size-md: var(--font-size-sm); + --color-field-font-size-lg: var(--font-size-sm); + --color-field-font-size-xl: var(--font-size-md); + --color-field-line-height: var(--leading-ui); + --color-field-radius: var(--radius-md); + --color-field-border-width: var(--border-width); + --color-field-border: var(--color-border-default); + --color-field-border-invalid: var(--color-risk-border); + --color-field-bg: var(--color-surface-default); + --color-field-color: var(--color-content-primary); + --color-field-label-color: var(--color-content-secondary); + --color-field-label-font-weight: var(--font-weight-medium); + --color-field-placeholder-color: var(--color-content-muted); + --color-field-literal-color: var(--color-content-muted); + --color-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); + --color-field-segment-min-width: 1.5ch; + --color-field-segment-height: calc(var(--_color-field-height) - var(--space-2)); + --color-field-segment-radius: var(--radius-sm); + --color-field-segment-color: var(--color-content-primary); + --color-field-segment-invalid-color: var(--color-risk-text); + --color-field-segment-readonly-bg: var(--color-surface-overlay); + --color-field-segment-readonly-color: var(--color-content-secondary); + --color-field-segment-readonly-decoration: underline dotted var(--color-content-muted); + --color-field-segment-focus-shadow: inset 0 0 0 var(--border-width) var(--_color-field-accent-border); + --color-field-swatch-size-xs: calc(var(--control-height-xs) - var(--space-2)); + --color-field-swatch-size-sm: calc(var(--control-height-sm) - var(--space-2)); + --color-field-swatch-size-md: calc(var(--control-height-md) - var(--space-2)); + --color-field-swatch-size-lg: calc(var(--control-height-lg) - var(--space-2)); + --color-field-swatch-size-xl: calc(var(--control-height-xl) - var(--space-2)); + --color-field-swatch-radius: var(--radius-sm); + --color-field-swatch-border: var(--color-border-subtle); + --color-field-format-select-color: var(--color-content-secondary); + --color-field-format-select-bg: transparent; + --color-field-format-select-radius: var(--radius-sm); + --color-field-format-select-px: var(--space-1); + --color-field-transition-duration: var(--duration-fast); + --color-field-transition-ease: var(--ease-default); + --color-field-disabled-opacity: 0.56; --calendar-gap: var(--space-3); --calendar-padding-sm: var(--space-3); --calendar-padding-md: var(--space-4); diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 2bd319ab1..c9b0fd86f 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -89,6 +89,8 @@ @import './components/date-field/date-field.css'; @import './components/date-picker/date-picker.css'; @import './components/date-range-picker/date-range-picker.css'; +@import './components/time-field/time-field.css'; +@import './components/color-field/color-field.css'; @import './components/slider/slider.css'; @import './components/toolbar/toolbar.css'; @import './components/tag-group/tag-group.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 56c24208f..a50d84a08 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -635,6 +635,125 @@ export const THEME_BASE_RECIPE_TOKENS = { 'transition-ease': 'var(--ease-default)', 'disabled-opacity': '0.56' }, + 'time-field': { + 'stack-gap': 'var(--space-1-5)', + 'height-xs': 'var(--control-height-xs)', + 'height-sm': 'var(--control-height-sm)', + 'height-md': 'var(--control-height-md)', + 'height-lg': 'var(--control-height-lg)', + 'height-xl': 'var(--control-height-xl)', + 'px-xs': 'var(--space-2)', + 'px-sm': 'var(--space-2-5)', + 'px-md': 'var(--space-3)', + 'px-lg': 'var(--space-3-5)', + 'px-xl': 'var(--space-4)', + 'gap-xs': 'var(--space-0-5)', + 'gap-sm': 'var(--space-0-5)', + 'gap-md': 'var(--space-1)', + 'gap-lg': 'var(--space-1-5)', + 'gap-xl': 'var(--space-2)', + 'segment-px-xs': 'var(--space-0-5)', + 'segment-px-sm': 'var(--space-0-5)', + 'segment-px-md': 'var(--space-1)', + 'segment-px-lg': 'var(--space-1-5)', + 'segment-px-xl': 'var(--space-2)', + 'font-family': 'var(--font-ui)', + 'font-size-xs': 'var(--font-size-xs)', + 'font-size-sm': 'var(--font-size-xs)', + 'font-size-md': 'var(--font-size-sm)', + 'font-size-lg': 'var(--font-size-sm)', + 'font-size-xl': 'var(--font-size-md)', + 'line-height': 'var(--leading-ui)', + radius: 'var(--radius-md)', + 'border-width': 'var(--border-width)', + border: 'var(--color-border-default)', + 'border-invalid': 'var(--color-risk-border)', + bg: 'var(--color-surface-default)', + color: 'var(--color-content-primary)', + 'label-color': 'var(--color-content-secondary)', + 'label-font-weight': 'var(--font-weight-medium)', + 'placeholder-color': 'var(--color-content-muted)', + 'literal-color': 'var(--color-content-muted)', + 'focus-shadow': + '0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', + 'segment-min-width': '1.5ch', + 'segment-height': 'calc(var(--_time-field-height) - var(--space-2))', + 'segment-radius': 'var(--radius-sm)', + 'segment-color': 'var(--color-content-primary)', + 'segment-invalid-color': 'var(--color-risk-text)', + 'segment-readonly-bg': 'var(--color-surface-overlay)', + 'segment-readonly-color': 'var(--color-content-secondary)', + 'segment-readonly-decoration': 'underline dotted var(--color-content-muted)', + 'segment-focus-shadow': 'inset 0 0 0 var(--border-width) var(--_time-field-accent-border)', + 'transition-duration': 'var(--duration-fast)', + 'transition-ease': 'var(--ease-default)', + 'disabled-opacity': '0.56' + }, + 'color-field': { + 'stack-gap': 'var(--space-1-5)', + 'height-xs': 'var(--control-height-xs)', + 'height-sm': 'var(--control-height-sm)', + 'height-md': 'var(--control-height-md)', + 'height-lg': 'var(--control-height-lg)', + 'height-xl': 'var(--control-height-xl)', + 'px-xs': 'var(--space-2)', + 'px-sm': 'var(--space-2-5)', + 'px-md': 'var(--space-3)', + 'px-lg': 'var(--space-3-5)', + 'px-xl': 'var(--space-4)', + 'gap-xs': 'var(--space-0-5)', + 'gap-sm': 'var(--space-0-5)', + 'gap-md': 'var(--space-1)', + 'gap-lg': 'var(--space-1-5)', + 'gap-xl': 'var(--space-2)', + 'segment-px-xs': 'var(--space-0-5)', + 'segment-px-sm': 'var(--space-0-5)', + 'segment-px-md': 'var(--space-1)', + 'segment-px-lg': 'var(--space-1-5)', + 'segment-px-xl': 'var(--space-2)', + 'font-family': 'var(--font-ui)', + 'font-size-xs': 'var(--font-size-xs)', + 'font-size-sm': 'var(--font-size-xs)', + 'font-size-md': 'var(--font-size-sm)', + 'font-size-lg': 'var(--font-size-sm)', + 'font-size-xl': 'var(--font-size-md)', + 'line-height': 'var(--leading-ui)', + radius: 'var(--radius-md)', + 'border-width': 'var(--border-width)', + border: 'var(--color-border-default)', + 'border-invalid': 'var(--color-risk-border)', + bg: 'var(--color-surface-default)', + color: 'var(--color-content-primary)', + 'label-color': 'var(--color-content-secondary)', + 'label-font-weight': 'var(--font-weight-medium)', + 'placeholder-color': 'var(--color-content-muted)', + 'literal-color': 'var(--color-content-muted)', + 'focus-shadow': + '0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', + 'segment-min-width': '1.5ch', + 'segment-height': 'calc(var(--_color-field-height) - var(--space-2))', + 'segment-radius': 'var(--radius-sm)', + 'segment-color': 'var(--color-content-primary)', + 'segment-invalid-color': 'var(--color-risk-text)', + 'segment-readonly-bg': 'var(--color-surface-overlay)', + 'segment-readonly-color': 'var(--color-content-secondary)', + 'segment-readonly-decoration': 'underline dotted var(--color-content-muted)', + 'segment-focus-shadow': 'inset 0 0 0 var(--border-width) var(--_color-field-accent-border)', + 'swatch-size-xs': 'calc(var(--control-height-xs) - var(--space-2))', + 'swatch-size-sm': 'calc(var(--control-height-sm) - var(--space-2))', + 'swatch-size-md': 'calc(var(--control-height-md) - var(--space-2))', + 'swatch-size-lg': 'calc(var(--control-height-lg) - var(--space-2))', + 'swatch-size-xl': 'calc(var(--control-height-xl) - var(--space-2))', + 'swatch-radius': 'var(--radius-sm)', + 'swatch-border': 'var(--color-border-subtle)', + 'format-select-color': 'var(--color-content-secondary)', + 'format-select-bg': 'transparent', + 'format-select-radius': 'var(--radius-sm)', + 'format-select-px': 'var(--space-1)', + 'transition-duration': 'var(--duration-fast)', + 'transition-ease': 'var(--ease-default)', + 'disabled-opacity': '0.56' + }, calendar: { gap: 'var(--space-3)', 'padding-sm': 'var(--space-3)', diff --git a/src/uix/morfo/components/color-field.ts b/src/uix/morfo/components/color-field.ts index 62cba1301..38b9d0481 100644 --- a/src/uix/morfo/components/color-field.ts +++ b/src/uix/morfo/components/color-field.ts @@ -5,6 +5,7 @@ export const colorFieldMorfo = { name: 'ColorField', kebab: 'color-field', scope: ['soma'], + apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/', texts: { label: '#?components.color-field.label|Color field', input: '#?components.color-field.input|Color value', diff --git a/src/uix/morfo/components/time-field.ts b/src/uix/morfo/components/time-field.ts index 79fe896d6..d2886517c 100644 --- a/src/uix/morfo/components/time-field.ts +++ b/src/uix/morfo/components/time-field.ts @@ -5,6 +5,7 @@ export const timeFieldMorfo = { name: 'TimeField', kebab: 'time-field', scope: ['soma'], + apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/', texts: { label: '#?components.time-field.label|Time field', input: '#?components.time-field.input|Time field' diff --git a/web/routes/uix/+layout@.svelte b/web/routes/uix/+layout@.svelte index 8713bf327..7fb399380 100644 --- a/web/routes/uix/+layout@.svelte +++ b/web/routes/uix/+layout@.svelte @@ -147,6 +147,8 @@ { slug: '/uix/components/date-field', label: 'Date field' }, { slug: '/uix/components/date-picker', label: 'Date picker' }, { slug: '/uix/components/date-range-picker', label: 'Date range picker' }, + { slug: '/uix/components/time-field', label: 'Time field' }, + { slug: '/uix/components/color-field', label: 'Color field' }, { slug: '/uix/components/stepper', label: 'Stepper' }, { slug: '/uix/components/tag-group', label: 'Tag group' }, { slug: '/uix/components/tags-input', label: 'Tags input' }, diff --git a/web/routes/uix/components/color-field/+page.svelte b/web/routes/uix/components/color-field/+page.svelte new file mode 100644 index 000000000..0466f5b06 --- /dev/null +++ b/web/routes/uix/components/color-field/+page.svelte @@ -0,0 +1,626 @@ + + +
+
+
Forms · ColorField
+

ColorField

+

+ Segmented color input. Soma owns format switching (hex/rgb/hsl), channel editing, alpha, + ARIA spinbuttons and validation; Eidos adds size, variant, intent color and recipe styling. +

+
+ parts{compiled.parts.order.length} + events{events.length} + formats3 + sizes5 +
+
+ +
+
+
+ + Brand color + + {#snippet children({ segments }: InputSnippetProps)} + {#each segments as { part, value }, index (`${part}-${index}`)} + {value} + {/each} + + {/snippet} + + + +
+
+
+ trace + {#if trace.length === 0} + edit or play an event to see sema stamps + {:else} + {#each trace.slice(0, 3) as entry} + {entry.event} · {entry.family}{entry.intent + ? ' · ' + entry.intent + : ''} + {fmtTime(entry.at)} + {/each} + {/if} + + value + {valueLabel} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ soma owns value, channel editing, format + switching and validation. eidos owns only the + visual treatment. +

+ +
+ soma props · segmented value +
+
+ + + + + + + +
+ +
+ eidos props · visual treatment +
+
+ + + +
+ +
+
+ soma + headless · segment editing + ARIA + svelte +
+
{somaSnippet}
+
+
+
+ eidos + visual · root + attached parts + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API + reference comparison

+
+ + + + + + + + + + + + + +
FeatureArkChakraBitsUIXDecision
Segmented inputnononoyesSoma owns editable channel segments — unique to UIX.
Format switch (hex/rgb/hsl)yesyesyesyesFormatSelect is a public part.
Alpha channelyesyesyesyesOpt-in via enableAlpha.
ValidationyesyesyesyesCustom validate + incomplete reason.
Readonly segmentsnononoyesPer-channel lock (e.g. alpha).
Field integration———yesSoma integrates with Field context.
Visual variantsCSSCSSCSSyesEidos adds canonical control variants.
+
+
+ + + + + + + + + + + + +
PropOwnerDefaultNotes
value / placeholdersoma—Bindable ColorValue (hex + rgb + hsl + hsv all in sync).
formatsomahexBindable. Drives which segments render.
allowedFormatssoma['hex','rgb','hsl']Whitelist exposed by FormatSelect.
enableAlphasomatrueWhen false, hides the alpha segment entirely.
readonlySegmentssoma[]Lock individual channels.
validatesoma—Returns error message string(s) when invalid.
dirsomaservicesKeyboard navigation direction.
size / variant / coloreidosmd / surface / primaryVisual-only data attrs.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
PartMarkerElementRoleOptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.optional ? 'yes' : 'no'}
+
+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events + perceptual signature +

+

+ ColorField only declares commit events. Channel editing remains headless until the value + is set. +

+
+ + + + {#each events as action} + + + + + + + + + {/each} + +
NameFamilyVerbSequenceTargetPlay
{action.name}{action.semantic.family}{action.semantic.verb ?? '—'}{action.semantic.sequence ?? 'pre'}{action.target} + +
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Selectors live in src/uix/eidos/components/color-field/color-field.css and + consume Soma segment state plus Eidos data-size, + data-variant and data-color. +

+
+ + + + + + + + + +
SelectorOwnerPurpose
[data-color-field]morfoRoot color-field contract and state.
[data-color-field][data-size][data-variant][data-color]eidosVisual scale, control treatment and intent palette.
[data-color-field-input][data-format]morfoSegment group container reflecting active format.
[data-color-field-segment][data-segment][data-readonly]morfoPer-channel segment state.
[data-color-field-format-select][data-locked]morfoFormat switcher; locked when allowedFormats has 1 entry.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + + +
ConcernContract
GroupInput exposes role="group" and an accessible name.
ChannelsEditable channels expose role="spinbutton" with min/max/current value.
KeyboardArrow keys, Home/End, Backspace and digits handled by Soma.
Format selectNative <select> with localised option labels.
FormHidden input submits the formatted color string when name is set.
+
+
+ {/if} +
+ + diff --git a/web/routes/uix/components/time-field/+page.svelte b/web/routes/uix/components/time-field/+page.svelte new file mode 100644 index 000000000..4c254c27d --- /dev/null +++ b/web/routes/uix/components/time-field/+page.svelte @@ -0,0 +1,583 @@ + + +
+
+
Forms · TimeField
+

TimeField

+

+ Segmented time input. Soma owns hour/minute/second editing, ARIA spinbuttons, validation + and form submission; Eidos adds size, variant, intent color and recipe styling. +

+
+ parts{compiled.parts.order.length} + events{events.length} + variants3 + sizes5 +
+
+ +
+
+
+ + Start + + {#snippet children({ segments }: InputSnippetProps)} + {#each segments as { part, value }, index (`${part}-${index}`)} + {value} + {/each} + {/snippet} + + +
+
+
+ trace + {#if trace.length === 0} + edit or play an event to see sema stamps + {:else} + {#each trace.slice(0, 3) as entry} + {entry.event} · {entry.family}{entry.intent + ? ' · ' + entry.intent + : ''} + {fmtTime(entry.at)} + {/each} + {/if} + + value + {valueLabel} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ soma owns value, segment behavior and + validation. eidos owns only the visual + treatment. +

+ +
+ soma props · segmented value +
+
+ + + + + + + + +
+ +
+ eidos props · visual treatment +
+
+ + + +
+ +
+
+ soma + headless · segment editing + ARIA + svelte +
+
{somaSnippet}
+
+
+
+ eidos + visual · root + attached parts + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API + reference comparison

+
+ + + + + + + + + + + + + +
FeatureReact AriaArkBitsUIXDecision
Segmented inputyesyesyesyesSoma owns editable segments.
Granularity (hour/min/sec)yesyesyesyesDrives which segments render.
12 / 24 hour cycleyesyesyesyes12h adds dayPeriod (AM/PM) segment.
ValidationyesyesyesyesCustom validate + min/max.
Readonly segmentsnonoyesyesPer-segment lock without disabling input.
Field integrationform contextfieldmanualyesSoma integrates with Field context.
Visual variantsCSSCSSCSSyesEidos adds canonical control variants.
+
+
+ + + + + + + + + + + + +
PropOwnerDefaultNotes
value / placeholdersoma—Bindable TimeValue.
minValue / maxValuesoma—Validation bounds.
granularitysomaminutehour | minute | second.
hourCyclesomalocale12h adds dayPeriod segment; 24h hides it.
hideTimeZonesomafalseOnly affects ZonedDateTime values.
readonlySegmentssoma[]Lock individual segments (e.g. seconds).
locale / dirsomaservicesFormatting + keyboard direction.
size / variant / coloreidosmd / surface / primaryVisual-only data attrs.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
PartMarkerElementRoleOptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.optional ? 'yes' : 'no'}
+
+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events + perceptual signature +

+

+ TimeField only declares commit events. Segment editing remains headless until the value is set. +

+
+ + + + {#each events as action} + + + + + + + + + {/each} + +
NameFamilyVerbSequenceTargetPlay
{action.name}{action.semantic.family}{action.semantic.verb ?? '—'}{action.semantic.sequence ?? 'pre'}{action.target} + +
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Selectors live in src/uix/eidos/components/time-field/time-field.css and + consume Soma segment state plus Eidos data-size, + data-variant and data-color. +

+
+ + + + + + + + + +
SelectorOwnerPurpose
[data-time-field]morfoRoot time-field contract and state.
[data-time-field][data-size][data-variant][data-color]eidosVisual scale, control treatment and intent palette.
[data-time-field-input]morfoSegment group container.
[data-time-field-segment][data-placeholder][data-readonly]morfoIndividual segment state.
[data-time-field][data-event]eidosTransient perceptual event styling from Sema projection.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + + +
ConcernContract
GroupInput exposes role="group" and an accessible name.
SegmentsEditable segments expose role="spinbutton" with min/max/current value.
KeyboardArrow keys, Home/End, Backspace, digits and A/P (for dayPeriod) handled by Soma.
AnnouncerLive region announces segment changes via aria-valuetext.
FormHidden input submits the ISO time string when name is set.
+
+
+ {/if} +