diff --git a/scripts/component-audit.ts b/scripts/component-audit.ts index 0d38db83c..4f4bbe671 100644 --- a/scripts/component-audit.ts +++ b/scripts/component-audit.ts @@ -290,21 +290,61 @@ function checkMorfo(kebab: string, src: string, info: ComponentReport): CheckRes ) ); - // A-3.7: event/keyboard coverage heuristic + // A-3.7: event/keyboard coverage heuristic. + // + // Per the COMPONENT_GUIDE: "every distinct keyboard action that mutates + // state has a corresponding semantic event. Pure focus moves don't need + // an event." So we COUNT only the actions that mutate state — arrow + // navigation, page/month/year shifts, home/end inside a grid are pure + // focus moves and must not inflate the ratio. + const allKeyboardActions = [...src.matchAll(/action:\s*['"]([\w-]+)['"]/g)].map((m) => m[1]); + const focusMoveActions = new Set([ + 'next-day', + 'prev-day', + 'next-week', + 'prev-week', + 'next-month', + 'prev-month', + 'next-year', + 'prev-year', + 'first-day-of-week', + 'last-day-of-week', + 'focus-next', + 'focus-prev', + 'focus-first', + 'focus-last', + 'focus-up', + 'focus-down', + 'focus-left', + 'focus-right' + ]); + const mutatingKeyboardActions = allKeyboardActions.filter((a) => !focusMoveActions.has(a)); const keys = [...src.matchAll(KEYBOARD_KEY_RE)].length; + const mutatingKeyCount = mutatingKeyboardActions.length; if (info.interactive) { - // Heuristic: if keyboard keys >= 2× events AND >= 4 total keys, likely - // under-declared events - if (keys >= 4 && declaredEvents > 0 && keys >= declaredEvents * 2) { + // Only count MUTATING actions toward the ratio. A calendar with 10 + // keys / 2 mutating (Enter, Space) / 1 commit-select event passes + // because Enter and Space both map to the same `select` action. + if ( + mutatingKeyCount >= 4 && + declaredEvents > 0 && + mutatingKeyCount >= declaredEvents * 2 + ) { out.push( fail( 'A-3.7', 'error', - `${keys} keyboard keys declared but only ${declaredEvents} semantic events — likely under-declared` + `${mutatingKeyCount} mutating keyboard actions but only ${declaredEvents} semantic events — likely under-declared (total keys ${keys})` ) ); } else { - out.push(pass('A-3.7', 'error', `${keys} keys / ${declaredEvents} events`)); + out.push( + pass( + 'A-3.7', + 'error', + `${keys} keys (${mutatingKeyCount} mutating) / ${declaredEvents} events` + ) + ); } } diff --git a/src/uix/eidos/components/calendar/README.md b/src/uix/eidos/components/calendar/README.md index 36ab3139d..9e9650814 100644 --- a/src/uix/eidos/components/calendar/README.md +++ b/src/uix/eidos/components/calendar/README.md @@ -4,7 +4,7 @@ Visual wrapper for Soma Calendar. Calendar follows the compound Eidos surface: `Calendar` is the public root and parts hang from it (`Calendar.Day`, `Calendar.Grid`, etc.). There is no public `Provider` export. -## Baseline audit +## Baseline | Source | Relevant coverage | Decision | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | @@ -13,7 +13,7 @@ Visual wrapper for Soma Calendar. Calendar follows the compound Eidos surface: | Morfo current | 15 declared parts including `WeekNumber` contract, grid APG, day/cell state data. | Add semantic events because Calendar is interactive. `WeekNumber` remains contract-only until Soma exposes a component. | | Sema current | No calendar pack before this migration. | Add `calendarSema` for select/unselect and navigation. | -## Reference comparison +## Comparativa | Capability | Air | React Aria | Ark UI | Bits UI | shadcn-svelte | UIX decision | | --------------------- | ---------------------------------------- | -------------------------------------------------------------------------- | --------------------------------- | ----------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------- | @@ -87,10 +87,33 @@ before adding the Eidos recipe. ``` +## Decisiones + +- **No se expone `Calendar.Provider`** en Eidos. El componente raíz + monta el provider internamente y estampa `data-size` / `data-variant` + / `data-color`. Los consumidores manipulan estado vía props + (`bind:value`, `bind:placeholder`). +- **`commit-select` lleva `intent: 'affirm'`** porque seleccionar una + fecha es un compromiso evaluativamente positivo (suave). Un commit + destructivo correspondería a `commit-unselect` con `intent: 'neutral'`. +- **`shift-navigate` cubre todo el movimiento de mes/año** (prev/next, + selects, atajos de teclado). No se subdivide por dirección — la + perceptiva de Sema se modula con `target` y `sequence`, no con + variantes de verbo. +- **Las flechas de teclado son focus moves puros**: mueven el día + enfocado pero no mutan `value`. Por eso el morfo declara 10 + keyboards y sólo 3 eventos: Enter/Space (mutación) → `commit-select`, + los 8 restantes no necesitan evento propio. +- **`data-readonly` es estado, no atributo de morfo input**: el grid + marca el `aria-readonly` y la receta lo tinta con tono atenuado pero + permite seguir leyendo y navegando con teclado. + ## Gaps -| Gap | Owner | Decision | -| --------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| Public `Calendar.WeekNumber` part | Soma | Deferred. Morfo declares the part, but Soma currently exposes week numbers through `GridRow` snippet data instead of a DOM component. | -| Range selection | Soma | Deferred to `range-calendar` / date range components, not Eidos Calendar. | -| Popover date picker shell | Soma/Eidos date-picker | Deferred to `date-picker`; Calendar remains inline. | +| Gap | Disposición | Detalle | +| --- | --- | --- | +| Public `Calendar.WeekNumber` part | **diferir** | Morfo declara el part, pero Soma expone los números de semana a través del snippet data de `GridRow` en vez de un componente DOM. Requiere refactor en Soma antes de exponer en Eidos. | +| Range selection | **descartar** | Vive en `range-calendar` / componentes de range, no en Calendar. | +| Popover date picker shell | **descartar** | Vive en `date-picker`; Calendar es inline por diseño. | +| Modal + footer (clear / cancel / close) compartido con pickers | **diferir** | Calendar inline no lo necesita; los pickers lo aportan en su capa. Si llega un caso de Calendar como contenido de Drawer, se reevalúa. | +| Año/mes con scroll vertical estilo iOS | **diferir** | Patrón nuevo, no cubierto por ninguna referencia que mantengamos. Sin caso de uso real. | diff --git a/src/uix/eidos/components/calendar/calendar.css b/src/uix/eidos/components/calendar/calendar.css index e6317fbf2..86cd17d4d 100644 --- a/src/uix/eidos/components/calendar/calendar.css +++ b/src/uix/eidos/components/calendar/calendar.css @@ -344,6 +344,23 @@ opacity: var(--calendar-disabled-opacity); } +/* Read-only is informational: the grid still shows focus + selection + navigation, but day cells can't be reactivated. Use a softer dim and + block the pointer affordance on cells. */ +[data-calendar][data-readonly] { + cursor: default; +} + +[data-calendar][data-readonly] [data-calendar-day]:not([data-selected]) { + cursor: default; +} + +[data-calendar][data-readonly] [data-calendar-day]:not([data-selected]):hover { + background: var(--calendar-day-bg); + border-color: var(--calendar-day-border); + color: var(--calendar-day-color); +} + [data-calendar][data-event], [data-calendar-day][data-event], [data-calendar-prev-button][data-event],