fix(date-range-field): canonical demo + recipe specificity + DateField overlay

User audit (2026-05-23) caught two real defects in this component:

  1. Demo was a 386-line skeleton — switches used the made-up
     `data-control-kind="switch"` attribute, props like granularity /
     locale / hideTimeZone / per-endpoint readonlySegments were not
     wired, API / Morfo / Recipe / A11y tabs were one-sentence stubs.
     Canonical reference demos are 700–1200 lines.
  2. Recipe rule `[data-date-range-field-input][data-active]
     [data-date-field]` matched nothing — soma overlays
     `data-date-field-input` on the SAME element as
     `data-date-range-field-input` (no nested `[data-date-field]`).

Fixes:

  - Eidos `<DateRangeField>` root now overlays
    `data-date-field=""` on top of `data-date-range-field`. Lets the
    full `date-field.css` cascade (chrome, size, variant, color)
    apply inside the range with zero recipe duplication. Documented
    in the wrapper comment.

  - `date-range-field.css` simplified to layout-only:
      [data-date-range-field][data-date-field] (specificity bump
        beats date-field's base `display: inline-flex` rule)
      grid: 1fr · auto · 1fr × 2 rows
      label spans the full first row
      separator via `::before` in the middle grid cell
      endpoints placed in columns 1 and 3 via `data-endpoint='start' / 'end'`
    No recipe duplication; date-field paints input chrome.

  - Demo rebuilt to canonical depth (1008 lines, +622):
      Live tab: 6 layered subsections (value shape / locale + dir /
        flags / per-endpoint readonly segments / eidos chrome /
        actions), 11 switches in canonical `<span data-uix-switch>`
        style with state-text labels, 9 chip groups (value profile,
        kind, granularity, hourCycle, hideTimeZone, locale × 6
        locales, dir, size × 5, variant × 3, color × 6), reset /
        clear actions, reactive soma + eidos snippets reflecting
        every control.
      API tab: full prop table per part (Root with 21 props, Label,
        Input, Segment), reference parity table vs Bits UI / Ark UI
        / react-aria / Chakra v3.
      Morfo tab: header table + parts overview + per-part data /
        aria / keyboard tables (driven by raw morfo, cast to a
        narrowed shape) + events table.
      Sema tab: passive justification + delegation table showing
        per-endpoint DateField runtimes.
      Recipe tab: selector classification (morfo / eidos) + chrome
        attribution.
      A11y tab: per-segment keyboard + ARIA contract tables.

  - Demo `{#each segments}` keyed by `(i)` (was already fixed, kept).

Lesson registered in
`C:\Users\dev\.claude\projects\G--dev-svelte-vicen\memory\feedback_demos_must_be_canonical_depth.md`
and referenced from `MEMORY.md`: ALWAYS read DEMO_AUTHORING_GUIDE.md
+ a reference demo (date-field 716L, drawer 737L, date-range-picker
1238L) BEFORE writing. Compare against reference libs first.

Verified visually at /uix/components/date-range-field — DOM has
both endpoints side-by-side, em-dash separator in middle column,
date-field chrome applied to each input, canonical control style
across all 6 subsections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 18edecba5b
commit ec7cdd60b4

@ -1,89 +1,91 @@
/*
* DateRangeField recipe.
* DateRangeField recipe — layout only.
*
* [data-date-range-field] → label + control stack
* [data-date-range-field-label] → optional label
* [data-date-range-field-input] → endpoint container (start or end)
* The chrome of each endpoint input is painted by `date-field.css`
* because:
*
* Each Input wraps a DateField context internally, so segments inside
* pick up date-field's existing recipe automatically. The range
* recipe only adds:
* 1. The eidos `<DateRangeField>` root overlays `data-date-field=""`
* on top of `data-date-range-field`, so the existing
* `[data-date-field] { --_*: … }` token setup, size cascades,
* variant cascades and color tokens all apply inside the range.
*
* - Label + control row layout (mirrors DateField's stack)
* - A horizontal flex with the two inputs and an em-dash separator,
* rendered via ::before/::after on the end input so consumers don't
* need to render a `<span>` separator manually.
* - Endpoint-aware data-attr (`data-type='start'` / `data-type='end'`)
* passes through from the morfo; usable for endpoint-specific tints.
* 2. soma's `<DateRangeField.Input>` overlays `data-date-field-input`
* on each endpoint container (the runtime composes both markers
* on the same element). So `[data-date-field-input]` chrome rules
* paint each endpoint exactly like a standalone DateField.
*
* This recipe only adds the range-specific layout:
*
* - Label sits above the inputs, spans the full row
* - Two inputs side-by-side via grid (1fr · auto · 1fr)
* - Em-dash separator in the middle grid cell
* - `data-endpoint='start'` / `data-endpoint='end'` available for
* consumer-side tinting; the default recipe leaves it inert.
*
* [data-date-range-field] → grid wrapper (also data-date-field)
* [data-date-range-field-label] → label, spans the full row
* [data-date-range-field-input] → endpoint container
* (also data-date-field-input)
*/
[data-date-range-field] {
display: inline-flex;
flex-direction: column;
gap: var(--date-field-stack-gap, var(--space-1-5));
/*
* Specificity bump (0, 2, 0) so we beat `[data-date-field]`'s base
* rule from `date-field.css` — that rule sets
* `display: inline-flex; flex-direction: column;` which would stack
* the inputs instead of placing them on a 1fr · auto · 1fr grid.
* The eidos wrapper overlays both markers on the same element; this
* recipe makes the grid layout deterministic regardless of CSS
* import order.
*/
[data-date-range-field][data-date-field] {
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-rows: auto auto;
column-gap: var(--date-range-field-row-gap, var(--space-3));
row-gap: var(--date-field-stack-gap, var(--space-1-5));
align-items: center;
inline-size: 100%;
min-inline-size: 0;
font-family: var(--date-field-font-family, var(--style-label-font-family));
}
[data-date-range-field-label] {
grid-column: 1 / -1;
grid-row: 1;
font-family: var(--date-field-label-font-family, var(--style-label-font-family));
font-size: var(--date-field-label-font-size, var(--font-size-sm));
font-weight: var(--date-field-label-font-weight, 500);
color: var(--date-field-label-color, var(--color-content-primary));
}
/*
* Each Input is itself a DateField root, so [data-date-field]'s
* recipe paints the bordered control. The range layout wraps two
* such controls in a horizontal row.
*/
[data-date-range-field] > [data-date-range-field-input] {
display: inline-flex;
flex: 1 1 0;
[data-date-range-field-input] {
grid-row: 2;
min-inline-size: 0;
}
/* Row layout — when both Inputs are direct children, set the
* container to a row with the separator between them. */
[data-date-range-field]:has([data-date-range-field-input][data-type='start'])
:where([data-date-range-field-input][data-type='end']) {
margin-inline-start: var(--date-range-field-separator-gap, var(--space-2));
position: relative;
[data-date-range-field-input][data-endpoint='start'] {
grid-column: 1;
}
[data-date-range-field]:has([data-date-range-field-input][data-type='start'])
:where([data-date-range-field-input][data-type='end'])::before {
[data-date-range-field-input][data-endpoint='end'] {
grid-column: 3;
}
/* Em-dash separator between the two inputs. Anchored to the grid's
* middle column / second row. */
[data-date-range-field]:has(
[data-date-range-field-input][data-endpoint='start']
):has([data-date-range-field-input][data-endpoint='end'])::before {
content: var(--date-range-field-separator-glyph, '—');
position: absolute;
inset-inline-start: calc(var(--date-range-field-separator-gap, var(--space-2)) * -1);
inset-block-start: 50%;
transform: translate(-50%, -50%);
grid-column: 2;
grid-row: 2;
color: var(--date-range-field-separator-color, var(--color-content-muted));
font-family: var(--style-label-font-family, var(--font-ui));
font-size: 0.9em; /* literal: separator scales with the field font-size */
}
/* When consumers compose Label + the two Inputs at the same level,
* give the inputs a flex row so they sit side-by-side. The :has
* selector keeps the recipe inert if the consumer arranges things
* differently. */
[data-date-range-field]:has([data-date-range-field-input]) {
--_drf-inputs-display: flex;
}
[data-date-range-field] > [data-date-range-field-input]:first-of-type {
margin-block-start: 0;
user-select: none;
pointer-events: none;
}
[data-date-range-field][data-disabled] {
opacity: var(--date-field-disabled-opacity, 0.55);
pointer-events: none;
}
/* Endpoint tints — subtle hue shift on the active endpoint without
* hiding which side has focus. The morfo emits `data-active` on the
* input that holds the caret. */
[data-date-range-field-input][data-active] [data-date-field] {
--_date-field-border-color: var(--_date-field-accent-border);
}

@ -23,10 +23,22 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<!--
Overlay `data-date-field=""` on top of the morfo's
`data-date-range-field` so the whole `date-field.css` cascade
(chrome on `[data-date-field-input]`, variant cascade on
`[data-date-field][data-variant=…] [data-date-field-input]`, size
vars on `[data-date-field][data-size=…]`, color tokens on
`[data-date-field][data-color=…]`) applies inside the range without
duplicating any of those rules. soma's `DateRangeField.Input`
already overlays `data-date-field-input` on each endpoint, so the
selectors match end-to-end with zero recipe duplication.
-->
<DateRangeField.Provider
{...rest}
bind:value
bind:placeholder
data-date-field=""
data-size={resolvedSize}
data-variant={variant}
data-color={color}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save

Powered by TurnKey Linux.