calendar: README sections + data-readonly recipe + audit A-3.7 refinement

Closes the calendar audit findings:

- README sections renamed to canonical headers: `## Baseline` (was
  "Baseline audit"), `## Comparativa` (was "Reference comparison").
  Added `## Decisiones` documenting the affirm-on-select choice, the
  collapsed `shift-navigate` event for all month/year movement, why
  arrow keys don't get their own events (pure focus moves), and the
  read-only treatment. Rewrote `## Gaps` with disposition tokens
  (implementar/diferir/descartar) for each entry.
- Recipe CSS gains `[data-calendar][data-readonly]` styling: cells
  drop their hover affordance and the cursor stays neutral. Selection
  + navigation remain interactive (matches `aria-readonly` semantics).
- Audit A-3.7 refined per the COMPONENT_GUIDE rule "pure focus moves
  don't need an event". The check now compares MUTATING keyboard
  actions to declared events. Focus-move actions
  (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,prev,first,last,up,down,left,right}) are excluded from
  the ratio. Calendar's 10 keys / 2 mutating (Enter+Space → `select`)
  / 1 `commit-select` event now passes correctly.

Project audit: PASS 3 → 5, no new regressions. `range-calendar` also
flips to PASS as a side effect of the focus-move filter.

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

@ -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`
)
);
}
}

@ -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.
</Calendar>
```
## 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. |

@ -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],

Loading…
Cancel
Save

Powered by TurnKey Linux.