You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/components/time-range-field/README.md

7.0 KiB

<TimeRangeField> — eidos

Fecha de revisión: 2026-07-10 (dossier S4, re-auditoría 2026-07-07).

Entrada de rango horario segmento a segmento: dos endpoints que SON <TimeField> compuestos (mismo patrón que su gemelo <DateRangeField>), con validación compartida (minValue/maxValue/validate de rango) y una Label única que nombra el rango completo.

Superficie

<script lang="ts">
	import { TimeRangeField } from '$uix/eidos/components/time-range-field';
	import { Time } from '$libs/days';
	let value = $state({ start: new Time(9, 0), end: new Time(17, 30) });
</script>

<TimeRangeField bind:value variant="surface" size="md">
	<TimeRangeField.Label>Horario</TimeRangeField.Label>
	<TimeRangeField.Input type="start">
		{#snippet children({ segments })}
			{#each segments as seg}
				<TimeRangeField.Segment part={seg.part}>{seg.value}</TimeRangeField.Segment>
			{/each}
		{/snippet}
	</TimeRangeField.Input>
	<TimeRangeField.Input type="end">
		{#snippet children({ segments })}
			{#each segments as seg}
				<TimeRangeField.Segment part={seg.part}>{seg.value}</TimeRangeField.Segment>
			{/each}
		{/snippet}
	</TimeRangeField.Input>
</TimeRangeField>

Partes: root (provider, role="group"), Label (compartida, fila propia), Input ×2 (type='start'|'end' — cada uno monta un TimeField.Provider > TimeField.Input real), Segment (re-export del de time-field — misma receta).

  • Props eidos: variant / color / size (vocabulario de TimeField; los data-attrs cascados del root aterrizan en el ROOT del TimeField compuesto de cada endpoint).
  • Props headless (soma): value: TimeRange, onValueChange, onStartValueChange/onEndValueChange, placeholder, validate (validador de rango {start,end}), onInvalid (razón tipada TimeOnInvalid), minValue/maxValue, disabled, readonly, start/endReadonlySegments, required, granularity, hideTimeZone, hourCycle, locale, dir, errorMessageId.

Tokens públicos: --time-range-field-row-gap, --time-range-field-separator-{glyph,color}. Nada más a propósito — el chrome del endpoint es la API de Field/TimeField (ver Decisiones).

Baseline

Patrón WAI-ARIA Spinbutton por segmento (el que cita time-field single): https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/. Sin equivalente air.

Soma posee: composición de un TimeField real por endpoint (valor por slice, placeholder controlado compartido para que un endpoint no mueva el ancla del otro), validación de rango, flags mergeados con Field.Provider (OR).

Talla y tema

7 clave(s) pública(s) en lib/recipes/base.ts (bloque time-range-field). Es el contrato vivo: la pestaña Tokens de su demo lista estas mismas claves y las resuelve sobre el escenario.

Token (--time-range-field-…) Valor por defecto
column-gap var(--space-3)
row-gap var(--space-1-5)
label-fg var(--color-content-primary)
label-font-size var(--size-sm-font-size)
label-font-weight var(--font-weight-regular)
separator-fg var(--color-content-muted)
separator-glyph "'—'"

Comparativa

Lib TimeRangeField dedicado Endpoints segmentados Validación de rango compartida
React Aria No (TimeField single; el rango se compone a mano) ✓ (single) Manual
Bits UI No (DateRangeField sí; sin variante time) — —
Ark UI No — —
MUI X (Pro) ✓ (MultiInputTimeRangeField) ✓ ✓
Eidos (este) ✓ ✓ (TimeField real ×2) ✓ (validate de rango + min/max)

El patrón de referencia real es el gemelo interno DateRangeField — misma delegación per-endpoint, misma Label compartida, mismo separador.

Decisiones

  • Delegación by-design (expression: 'delegated', veredicto S11a): el morfo del rango declara 0 eventos. Los eventos sema (commit de segmentos) los emiten los DOS runtimes de TimeField compuestos, scoped por endpoint — ahí es donde pertenecen. El provider del rango no posee vocabulario propio.
  • Receta LAYOUT-ONLY: todo el chrome del endpoint — caja, borde, radio, padding, focus ring, invalid, disabled, segmentos — viene de field.css / time-field.css sobre el [data-field] propio de cada endpoint. Esta receta solo coloca grid (label + endpoints + separador). Cero tokens de chrome (mandato anti-alias S6).
  • Separador vía ::before anclado a la columna central del grid (:has([data-endpoint='end'])), glifo themeable (--time-range-field-separator-glyph, default —) — el consumidor no renderiza <span>—</span>.
  • Label compartida un paso por debajo del control (doctrina de label de Field); el root NO es [data-field], así que usa los valores foundation equivalentes.
  • Sin partes Description/Hint — es el primitivo field; la descripción pertenece al <Field> envolvente (igual que el gemelo).

Gaps

Gap Disposición Detalle
El data-invalid de RANGO no pinta (end < start valida en soma y marca el root, pero los endpoints solo reciben disabled/readonly/required — no el invalid de rango) implementar Espejo del gap del gemelo drf ("per-endpoint coloring"): propagar el invalid de rango a los endpoints o tintar desde el root. Mismo fix para ambos ranges (ficha F-3/F-1).
Sin evento de rango-completo (ambos endpoints fijados) diferir Decisión conjunta con date-range-field (ficha F-1 compartida); hoy el commit per-endpoint es el contrato.
Tests del wrapper de composición implementar Pasada SYS-2 (rango inválido, readonly segments por endpoint, placeholder compartido).

Referencias

Audit exceptions

  • R-1.5 exception: the focus ring lives on each composed endpoint — a REAL <TimeField> whose [data-field] / [data-field-control] gets field.css's :focus-within treatment; this recipe is layout-only (see the recipe header).
  • R-1.3 exception: readonly is forwarded to both endpoint TimeFields (readonly={provider.isReadonly}) and paints there via the shared Field layer; the layout-only recipe adds nothing.
  • R-1.4 exception: endpoint-level invalid (min/max) paints on each composed TimeField via the shared Field layer. RANGE-level invalid not painting is a tracked gap (see Gaps) shared with date-range-field.

Powered by TurnKey Linux.