import type { Morfo } from '../types'; import { v } from '../types'; export const monthGridMorfo = { name: 'MonthGrid', kebab: 'month-grid', scope: ['soma', 'sema', 'eidos'], // Pack shipped 2026-07-07 (checkpoint S3b — calendar-family coherence). expression: 'pack', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/grid/', texts: { label: '#?components.month-grid.label|Month grid', prevYear: '#?common.month-grid.prev-year|Previous year', nextYear: '#?common.month-grid.next-year|Next year' }, events: [ { name: 'commit-set', semantic: { family: 'commit', verb: 'set', target: v.partRef('cell'), intent: 'affirm', sequence: 'post' } }, { // Paginar al año anterior/siguiente. Per book cap. 27 §5, // `shift.navigate` cubre "Lista → detalle. Página A → página B" — // el usuario está en otro tramo del eje temporal. La variante // `step` mantiene la huella semántica de paso discreto. name: 'shift-navigate-step', semantic: { family: 'shift', verb: 'navigate', // Target is the `grid`, not the `provider`: the grid is the SUBJECT // of the reframing — it is the cell surface that swaps to another // year, while the header stays put and the arrow is only the // instrument. The provider would drag the header into the stamp. // Aligned with the rest of the calendar family so the // directional `shift` signature slides the grid in all four alike. target: v.partRef('grid'), sequence: 'post' } } ], direction: {}, focus: { kind: 'roving', parts: ['cell'], // One tabbable month cell (provider: `focused ? 0 : -1`, documented // there as "roving tabindex"), arrows move the stop on the month grid. orientation: 'grid', executor: { custom: '2D month-grid traversal (rows × columns of months) — executed by the shared 2D grid-walk primitive (soma/keyboard/grid-walk), not the 1D RovingFocusGroup' } }, parts: [ { name: 'Provider', kebab: 'provider', archetype: 'provider', kind: 'public', defaultElement: 'div', // No role — removed 2026-07-07 (checkpoint C4): the inner // `role='grid'` carries the pattern (family-wide decision). optional: false, data: [ { attr: 'data-readonly', severity: 'optional' }, { attr: 'data-disabled', severity: 'optional' } ], aria: [ { attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' }, // Provider emitted `boolToStr(disabled)` (always "true"/"false"), so // `propRef` + `ariaBoolean` lets the render bag emit the same string. { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ], keyboard: [ { key: 'ArrowRight', action: 'next-month' }, { key: 'ArrowLeft', action: 'prev-month' }, { key: 'ArrowDown', action: 'next-row' }, { key: 'ArrowUp', action: 'prev-row' }, { key: 'PageUp', action: 'prev-year' }, { key: 'PageDown', action: 'next-year' }, { key: 'Home', action: 'first-month' }, { key: 'End', action: 'last-month' }, { key: 'Enter', action: 'select' }, { key: ' ', action: 'select' } ] }, { name: 'Header', kebab: 'header', archetype: 'header', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }, { name: 'Heading', kebab: 'heading', archetype: 'header', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }, { name: 'PrevButton', kebab: 'prev-button', archetype: 'trigger', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [{ attr: 'data-disabled', severity: 'optional' }], attrs: [{ attr: 'type', value: v.literal('button') }], aria: [ { attr: 'aria-label', value: v.commonRef('month-grid.prev-year', 'Previous year'), severity: 'recommended' }, // Provider emitted `boolToStr(disabled)` (always "true"/"false"), so // `propRef` + `ariaBoolean` lets the render bag emit the same string. { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ] }, { name: 'NextButton', kebab: 'next-button', archetype: 'trigger', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [{ attr: 'data-disabled', severity: 'optional' }], attrs: [{ attr: 'type', value: v.literal('button') }], aria: [ { attr: 'aria-label', value: v.commonRef('month-grid.next-year', 'Next year'), severity: 'recommended' }, // Provider emitted `boolToStr(disabled)` (always "true"/"false"), so // `propRef` + `ariaBoolean` lets the render bag emit the same string. { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ] }, { name: 'Grid', kebab: 'grid', kind: 'public', defaultElement: 'div', role: 'grid', optional: false, data: [], aria: [ { attr: 'aria-labelledby', value: v.partRef('heading'), condition: { when: 'part-present', part: 'heading' }, severity: 'recommended' }, // Provider emitted `boolToStr(...)` (always "true"/"false") for both; // `propRef` + `ariaBoolean` lets the render bag emit the same string. { attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional', ariaBoolean: true }, { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ] }, { name: 'Cell', kebab: 'cell', // No archetype: display gridcell container, not a menu `item` (the // inner month button owns the interaction styling). kind: 'public', defaultElement: 'button', role: 'gridcell', optional: false, data: [ { attr: 'data-value' }, { attr: 'data-selected', severity: 'optional' }, { attr: 'data-current', severity: 'optional' }, { attr: 'data-disabled', severity: 'optional' }, { attr: 'data-focused', severity: 'optional' } ], aria: [ // Provider emitted `boolToStr(...)` (always "true"/"false") for both; // `propRef` + `ariaBoolean` lets the render bag emit the same string. { attr: 'aria-selected', value: v.propRef('selected'), severity: 'optional', ariaBoolean: true }, { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ] } ] } as const satisfies Morfo;