diff --git a/continue.md b/continue.md index 52eecd484..63cebff79 100644 --- a/continue.md +++ b/continue.md @@ -11,7 +11,7 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente. ### Flujo obligatorio por componente 1. Leer Air en la rama anterior: - `git show gita/glm-5:src/uix/air/components/{name}/...` cuando exista. + `git show morfo-runtime:src/uix/air/components/{name}/...` cuando exista. Air es la primera baseline visual local. 2. Leer Morfo/Soma actuales del componente: - `src/uix/morfo/components/{name}.ts` @@ -343,7 +343,7 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente. hotspots. - Regla nueva para continuar Eidos componente por componente: - La primera referencia es Air en la rama anterior - (`glm-5:src/uix/air/components/{name}`) cuando exista. + (`morfo-runtime:src/uix/air/components/{name}`) cuando exista. - Despues se compara con Soma/Morfo actuales y con referentes externos relevantes (Radix/Radix Themes, Ark UI, Bits UI, shadcn-svelte y React Aria si aplica). @@ -359,7 +359,7 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente. `height`, `minHeight`, `maxHeight` en `Dialog.Content`, serializados como variables privadas del recipe. Soma sigue siendo dueno de comportamiento. - Segundo componente auditado con el nuevo protocolo: - - `Drawer`: comparado contra Air (`glm-5`), Soma/Morfo actuales, Vaul, + - `Drawer`: comparado contra Air (`morfo-runtime`), Soma/Morfo actuales, Vaul, shadcn-svelte y Ark UI Dialog como referencia de dialog/layer. - Decision: no se crea `Drawer.Positioner`; la geometria edge-anchored vive en `data-side` + `data-variant` y en la receta. @@ -370,7 +370,7 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente. variables privadas del recipe. El size canonico `sm/md/lg/full` sigue como preset principal. - Tercer componente auditado con el nuevo protocolo: - - `Popover`: Air no tenia equivalente en `glm-5`, por tanto la referencia + - `Popover`: Air no tenia equivalente en `morfo-runtime`, por tanto la referencia local pasa a ser Soma/Morfo y la comparativa externa Radix, Ark UI, Bits UI y shadcn-svelte. - Decision: no se crea `Popover.Positioner`; la posicion pertenece a Soma @@ -383,7 +383,7 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente. variables privadas. La receta usa transform-origin y available-height de Soma Floating. - Cuarto componente auditado con el nuevo protocolo: - - `Tooltip`: Air no tenia equivalente en `glm-5`; referencia local + - `Tooltip`: Air no tenia equivalente en `morfo-runtime`; referencia local Soma/Morfo, comparativa externa Radix, Ark UI, Bits UI y shadcn-svelte. - Decision: no se crea `Tooltip.Positioner` ni `Tooltip.ArrowTip`; Soma Floating y Soma Arrow son los duenos de esa geometria. @@ -1747,7 +1747,7 @@ a su `portalTo`. No usar nombres como `somaPortalTo` en `ActiveUix`. Eidos fuera de tests/guardias documentales. - Siguiente foco: 1. Continuar migracion componente por componente desde Soma a Eidos. - 2. Seguir el protocolo obligatorio: Air (`gita/glm-5`) -> Morfo/Soma -> + 2. Seguir el protocolo obligatorio: Air (`morfo-runtime`) -> Morfo/Soma -> Morfo/Sema -> referencias externas -> README -> wrapper/recipe/demo. 3. La tanda en curso venia por componentes relacionados con forms; el primer candidato natural no presente aun en Eidos es `form`. diff --git a/src/uix/eidos/component-visual-attrs.test.ts b/src/uix/eidos/component-visual-attrs.test.ts index 1afab1c13..f738bee88 100644 --- a/src/uix/eidos/component-visual-attrs.test.ts +++ b/src/uix/eidos/component-visual-attrs.test.ts @@ -13,6 +13,7 @@ const VISUAL_ATTRS = [ ['avatar/avatar-image.svelte', ['data-status']], ['avatar/avatar-fallback.svelte', ['data-status']], ['breadcrumb/breadcrumb.svelte', ['data-size']], + ['calendar/calendar.svelte', ['data-size', 'data-variant', 'data-color']], ['checkbox/checkbox.svelte', ['data-size', 'data-variant', 'data-color']], ['checkbox/checkbox-group.svelte', ['data-columns']], ['dialog/dialog-close.svelte', ['data-position', 'data-icon-only']], diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index 583d99590..f5108dc11 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -400,8 +400,8 @@ si un alias publico queda sin consumidor real, el test Ningun componente Eidos se declara cerrado solo por envolver Soma. Antes de implementar o revisar un componente: -1. Leer Air en la rama anterior (`glm-5:src/uix/air/components/{name}`) cuando - exista. Air es la primera baseline visual. +1. Leer Air en la rama anterior (`morfo-runtime:src/uix/air/components/{name}`) + cuando exista. Air es la primera baseline visual. 2. Leer Soma/Morfo actuales para separar comportamiento, ARIA, estado, traducciones y data-attrs de la superficie visual Eidos. 3. Auditar Morfo/Sema. El morfo no se considera correcto solo porque compile: @@ -455,7 +455,7 @@ su subdirectorio. --- -## Estado actual de la migración (2026-05-17) +## Estado actual de la migración (2026-05-20) La tanda 2026-05-17 reanuda la migración Soma -> Eidos por orden explícita. Los wrappers nuevos siguen el mismo criterio: envolver partes públicas de Soma, @@ -476,6 +476,7 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo. | accordion | ✅ multi-part | Item, Header, Trigger, Content | | avatar | ✅ multi-part | Image, Fallback (eidos-native) | | breadcrumb | ✅ multi-part | List, Item, Link, Separator, Ellipsis | +| calendar | ✅ multi-part | Header, Heading, Prev/Next, Month/YearSelect, Grid, Cell, Day | | icon | ✅ single-part | + 1697 lucide glyphs | | meter | ✅ multi-part | Indicator | | number-field | ✅ multi-part | Input, IncrementTrigger, DecrementTrigger, Scrubber | @@ -500,5 +501,4 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo. ### Orden recomendado para continuar 1. Componentes grandes sólo con tabla previa de migración: - `calendar`, `date-field`, `date-picker`, `date-range-picker`, - `time-field`, `time-picker`. + `date-picker`, `date-range-picker`, `time-field`, `time-picker`. diff --git a/src/uix/eidos/components/calendar/README.md b/src/uix/eidos/components/calendar/README.md new file mode 100644 index 000000000..36ab3139d --- /dev/null +++ b/src/uix/eidos/components/calendar/README.md @@ -0,0 +1,96 @@ +# Eidos Calendar + +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 + +| Source | Relevant coverage | Decision | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| Air `morfo-runtime:src/uix/air/components/calendar` | Full visual shell over Terra: root size, header, heading, prev/next, month/year selects, grid, cell/day states; direct Air sound on selection/navigation. | Keep the full part coverage and `size`. Do not copy direct sound; selection/navigation move to Morfo/Sema. | +| Soma current | Single/multiple value, placeholder navigation, min/max, disabled/unavailable/holiday matchers, fixed weeks, multi-month, week numbers, locale, RTL and live announcements. | Eidos wraps every public Soma part and adds only `size`, `variant`, `color`. | +| 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 + +| Capability | Air | React Aria | Ark UI | Bits UI | shadcn-svelte | UIX decision | +| --------------------- | ---------------------------------------- | -------------------------------------------------------------------------- | --------------------------------- | ----------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------- | +| Single date | yes | yes | yes | yes | yes | Soma owns. | +| Multiple dates | inherited Terra | RangeCalendar separate in React Aria; non-contiguous supported by API docs | `selectionMode="multiple"` | `type="multiple"` | via Bits Calendar | Soma supports `type="multiple"`. | +| Multiple months | yes | `visibleDuration` | `numOfMonths` | `numberOfMonths` | via Bits Calendar | Soma supports `numberOfMonths`; Eidos styles without assuming layout. | +| Fixed weeks | yes | display option | `fixedWeeks` | `fixedWeeks` | via Bits Calendar | Soma supports. | +| Month/year select | yes | documented as custom dropdowns | documented example | public `MonthSelect`/`YearSelect` | generated wrappers around Bits | Eidos exposes `MonthSelect` and `YearSelect`. | +| Min/max/unavailable | yes | `minValue`, `maxValue`, `isDateUnavailable` | `min`, `max`, `isDateUnavailable` | min/max/unavailable/disabled | via Bits Calendar | Soma supports selection and view bounds; invalid pairs surface as invalid + logger error. | +| Week numbers | no Air-specific part | not a first-class Calendar part | `showWeekNumbers` | documented separately through composition | not primary API | Soma exposes `showWeekNumbers` snippet data; visual support is included. | +| Holiday/weekend flags | Air styled some date states only | custom render state | custom machine state possible | custom render state possible | via custom code | Soma emits `data-holiday` and `data-weekend`; Eidos styles both. | +| Semantic feedback | Air called interaction sounds in wrapper | library-specific announcements | callbacks | callbacks | none in wrapper | Morfo/Sema owns `commit-select`, `commit-unselect`, `shift-navigate`. | + +Reference docs checked: React Aria Calendar, Ark UI Date Picker, Bits UI +Calendar, shadcn-svelte Calendar, Radix Primitives components list and WAI-ARIA +APG Date Picker Dialog/Grid. + +## Morfo / Sema + +| Event | Family | Target | Runtime source | Sema default | +| ----------------- | ---------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------- | +| `commit-select` | `commit.select` | `day` | `CalendarProvider.select(date, target)` after value changes | soft form commit + tap | +| `commit-unselect` | `commit.remove` | `day` | same, when a date is removed/cleared | subtle form commit + tap | +| `shift-navigate` | `shift.navigate` | provider fallback, actual button/select/day target when known | `prevPage`, `nextPage`, `setMonth`, `setYear`, keyboard month boundary | subtle ascending navigation sound | + +Calendar is not passive: a 0-event contract would hide committed selection and +visible-range navigation from the semantic layer. This migration closes that gap +before adding the Eidos recipe. + +## Eidos API + +```svelte + + + + {#snippet children({ months, weekdays })} + + Previous + + Next + + {#each months as month (month.value.toString())} + + + + {#each weekdays as day} + {day} + {/each} + + + + {#each month.weeks as week} + + {#each week as date} + + + + {/each} + + {/each} + + + {/each} + {/snippet} + +``` + +## 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. | diff --git a/src/uix/eidos/components/calendar/calendar-cell.svelte b/src/uix/eidos/components/calendar/calendar-cell.svelte new file mode 100644 index 000000000..340f66aca --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-cell.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(snippetProps: CalendarCellSnippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/calendar/calendar-day.svelte b/src/uix/eidos/components/calendar/calendar-day.svelte new file mode 100644 index 000000000..c7f9aa702 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-day.svelte @@ -0,0 +1,16 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: CalendarDaySnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/calendar/calendar-grid-body.svelte b/src/uix/eidos/components/calendar/calendar-grid-body.svelte new file mode 100644 index 000000000..cf3ebf591 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-grid-body.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-grid-head.svelte b/src/uix/eidos/components/calendar/calendar-grid-head.svelte new file mode 100644 index 000000000..803892aa8 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-grid-head.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-grid-row.svelte b/src/uix/eidos/components/calendar/calendar-grid-row.svelte new file mode 100644 index 000000000..c4f515f5f --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(snippetProps: CalendarGridRowSnippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/calendar/calendar-grid.svelte b/src/uix/eidos/components/calendar/calendar-grid.svelte new file mode 100644 index 000000000..e16063754 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-grid.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-head-cell.svelte b/src/uix/eidos/components/calendar/calendar-head-cell.svelte new file mode 100644 index 000000000..56805d300 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-head-cell.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-header.svelte b/src/uix/eidos/components/calendar/calendar-header.svelte new file mode 100644 index 000000000..fee1f19e7 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-header.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-heading.svelte b/src/uix/eidos/components/calendar/calendar-heading.svelte new file mode 100644 index 000000000..a3eb1ca6b --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-heading.svelte @@ -0,0 +1,16 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: CalendarHeadingSnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/calendar/calendar-month-select.svelte b/src/uix/eidos/components/calendar/calendar-month-select.svelte new file mode 100644 index 000000000..9c7e8194b --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-month-select.svelte @@ -0,0 +1,16 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: CalendarMonthSelectSnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/calendar/calendar-next-button.svelte b/src/uix/eidos/components/calendar/calendar-next-button.svelte new file mode 100644 index 000000000..ebf44eee3 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-next-button.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-prev-button.svelte b/src/uix/eidos/components/calendar/calendar-prev-button.svelte new file mode 100644 index 000000000..8b78df67e --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-prev-button.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/calendar/calendar-year-select.svelte b/src/uix/eidos/components/calendar/calendar-year-select.svelte new file mode 100644 index 000000000..68f0258b4 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar-year-select.svelte @@ -0,0 +1,16 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: CalendarYearSelectSnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/calendar/calendar.css b/src/uix/eidos/components/calendar/calendar.css new file mode 100644 index 000000000..ec4e74711 --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar.css @@ -0,0 +1,362 @@ +[data-calendar] { + --_calendar-padding: var(--calendar-padding-md); + --_calendar-control-size: var(--calendar-control-size-md); + --_calendar-day-size: var(--calendar-day-size-md); + --_calendar-font-size: var(--calendar-font-size-md); + --_calendar-accent-solid: var(--color-primary-solid); + --_calendar-accent-solid-hover: var(--color-primary-solid-hover); + --_calendar-accent-track: var(--color-primary-track); + --_calendar-accent-border: var(--color-primary-border); + --_calendar-accent-text: var(--color-primary-text); + + display: inline-flex; + flex-direction: column; + gap: var(--calendar-gap); + inline-size: max-content; + max-inline-size: 100%; + padding: var(--_calendar-padding); + border: var(--calendar-border-width) solid var(--calendar-border); + border-radius: var(--calendar-radius); + background: var(--calendar-bg); + box-shadow: var(--calendar-shadow); + color: var(--calendar-color); + font-family: var(--calendar-font-family); + font-size: var(--_calendar-font-size); + line-height: var(--calendar-line-height); +} + +[data-calendar][data-size='sm'] { + --_calendar-padding: var(--calendar-padding-sm); + --_calendar-control-size: var(--calendar-control-size-sm); + --_calendar-day-size: var(--calendar-day-size-sm); + --_calendar-font-size: var(--calendar-font-size-sm); +} + +[data-calendar][data-size='lg'] { + --_calendar-padding: var(--calendar-padding-lg); + --_calendar-control-size: var(--calendar-control-size-lg); + --_calendar-day-size: var(--calendar-day-size-lg); + --_calendar-font-size: var(--calendar-font-size-lg); +} + +[data-calendar][data-variant='outline'] { + background: var(--calendar-outline-bg); + box-shadow: var(--calendar-outline-shadow); +} + +[data-calendar][data-variant='ghost'] { + padding: 0; + border-color: transparent; + background: transparent; + box-shadow: none; +} + +[data-calendar][data-color='secondary'] { + --_calendar-accent-solid: var(--color-secondary-solid); + --_calendar-accent-solid-hover: var(--color-secondary-solid-hover); + --_calendar-accent-track: var(--color-secondary-track); + --_calendar-accent-border: var(--color-secondary-border); + --_calendar-accent-text: var(--color-secondary-text); +} + +[data-calendar][data-color='neutral'] { + --_calendar-accent-solid: var(--color-neutral-solid); + --_calendar-accent-solid-hover: var(--color-neutral-solid-hover); + --_calendar-accent-track: var(--color-neutral-track); + --_calendar-accent-border: var(--color-neutral-border); + --_calendar-accent-text: var(--color-neutral-text); +} + +[data-calendar][data-color='affirm'] { + --_calendar-accent-solid: var(--color-affirm-solid); + --_calendar-accent-solid-hover: var(--color-affirm-solid-hover); + --_calendar-accent-track: var(--color-affirm-track); + --_calendar-accent-border: var(--color-affirm-border); + --_calendar-accent-text: var(--color-affirm-text); +} + +[data-calendar][data-color='fulfill'] { + --_calendar-accent-solid: var(--color-fulfill-solid); + --_calendar-accent-solid-hover: var(--color-fulfill-solid-hover); + --_calendar-accent-track: var(--color-fulfill-track); + --_calendar-accent-border: var(--color-fulfill-border); + --_calendar-accent-text: var(--color-fulfill-text); +} + +[data-calendar][data-color='risk'] { + --_calendar-accent-solid: var(--color-risk-solid); + --_calendar-accent-solid-hover: var(--color-risk-solid-hover); + --_calendar-accent-track: var(--color-risk-track); + --_calendar-accent-border: var(--color-risk-border); + --_calendar-accent-text: var(--color-risk-text); +} + +[data-calendar][data-color='threat'] { + --_calendar-accent-solid: var(--color-threat-solid); + --_calendar-accent-solid-hover: var(--color-threat-solid-hover); + --_calendar-accent-track: var(--color-threat-track); + --_calendar-accent-border: var(--color-threat-border); + --_calendar-accent-text: var(--color-threat-text); +} + +[data-calendar][data-color='loss'] { + --_calendar-accent-solid: var(--color-loss-solid); + --_calendar-accent-solid-hover: var(--color-loss-solid-hover); + --_calendar-accent-track: var(--color-loss-track); + --_calendar-accent-border: var(--color-loss-border); + --_calendar-accent-text: var(--color-loss-text); +} + +[data-calendar-header] { + display: grid; + grid-template-columns: var(--_calendar-control-size) minmax(0, 1fr) var(--_calendar-control-size); + align-items: center; + gap: var(--calendar-header-gap); + inline-size: 100%; + min-inline-size: 0; +} + +[data-calendar-header-controls] { + display: flex; + align-items: center; + justify-content: center; + gap: var(--calendar-header-gap); + min-inline-size: 0; +} + +[data-calendar-heading] { + flex: 1 1 auto; + min-inline-size: 0; + color: var(--calendar-heading-color); + font-weight: var(--calendar-heading-font-weight); + text-align: center; +} + +[data-calendar-month-panel] { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--calendar-grid-gap); + min-inline-size: max-content; +} + +[data-calendar-month-panel] > [data-calendar-heading] { + flex: 0 0 auto; + font-size: var(--calendar-heading-font-size, 0.95em); + text-align: center; +} + +[data-calendar-prev-button], +[data-calendar-next-button] { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + inline-size: var(--_calendar-control-size); + block-size: var(--_calendar-control-size); + padding: 0; + border: var(--calendar-control-border-width) solid var(--calendar-control-border); + border-radius: var(--calendar-control-radius); + background: var(--calendar-control-bg); + color: var(--calendar-control-color); + cursor: pointer; + transition: + background var(--calendar-transition-duration) var(--calendar-transition-ease), + border-color var(--calendar-transition-duration) var(--calendar-transition-ease), + color var(--calendar-transition-duration) var(--calendar-transition-ease), + box-shadow var(--calendar-transition-duration) var(--calendar-transition-ease); +} + +[data-calendar-prev-button]:hover, +[data-calendar-next-button]:hover { + border-color: var(--calendar-control-border-hover); + background: var(--calendar-control-bg-hover); + color: var(--calendar-control-color-hover); +} + +[data-calendar-prev-button]:focus-visible, +[data-calendar-next-button]:focus-visible, +[data-calendar-month-select]:focus-visible, +[data-calendar-year-select]:focus-visible, +[data-calendar-day]:focus-visible { + outline: none; + box-shadow: var(--calendar-focus-shadow); +} + +[data-calendar-prev-button][data-disabled], +[data-calendar-next-button][data-disabled], +[data-calendar-month-select][data-disabled], +[data-calendar-year-select][data-disabled] { + cursor: not-allowed; + opacity: var(--calendar-disabled-opacity); +} + +[data-calendar-month-select], +[data-calendar-year-select] { + min-inline-size: 0; + block-size: var(--_calendar-control-size); + padding-inline: var(--calendar-select-px); + border: var(--calendar-control-border-width) solid var(--calendar-control-border); + border-radius: var(--calendar-control-radius); + background: var(--calendar-control-bg); + color: var(--calendar-control-color); + font: inherit; + cursor: pointer; + transition: + background var(--calendar-transition-duration) var(--calendar-transition-ease), + border-color var(--calendar-transition-duration) var(--calendar-transition-ease), + color var(--calendar-transition-duration) var(--calendar-transition-ease), + box-shadow var(--calendar-transition-duration) var(--calendar-transition-ease); +} + +[data-calendar-month-select] { + inline-size: calc(var(--_calendar-day-size) * 2.35); +} + +[data-calendar-year-select] { + inline-size: calc(var(--_calendar-day-size) * 2.15); +} + +[data-calendar-month-select]:hover, +[data-calendar-year-select]:hover { + border-color: var(--calendar-control-border-hover); + background: var(--calendar-control-bg-hover); + color: var(--calendar-control-color-hover); +} + +[data-calendar-grid] { + inline-size: max-content; + max-inline-size: 100%; + margin-inline: auto; + border-collapse: separate; + border-spacing: var(--calendar-grid-gap); + table-layout: fixed; +} + +[data-calendar-head-cell] { + inline-size: var(--_calendar-day-size); + block-size: var(--calendar-weekday-size); + color: var(--calendar-weekday-color); + font-size: var(--calendar-weekday-font-size); + font-weight: var(--calendar-weekday-font-weight); + text-align: center; +} + +[data-calendar-week-number] { + color: var(--calendar-weekday-color); + font-variant-numeric: tabular-nums; +} + +[data-calendar-week-number][scope='row'] { + block-size: var(--_calendar-day-size); + font-size: var(--calendar-weekday-font-size); + font-weight: var(--calendar-weekday-font-weight); +} + +[data-calendar-cell] { + padding: 0; + text-align: center; + vertical-align: middle; +} + +[data-calendar-day] { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: var(--_calendar-day-size); + block-size: var(--_calendar-day-size); + border: var(--calendar-day-border-width) solid var(--calendar-day-border); + border-radius: var(--calendar-day-radius); + background: var(--calendar-day-bg); + color: var(--calendar-day-color); + font: inherit; + font-weight: var(--calendar-day-font-weight); + cursor: pointer; + user-select: none; + transition: + background var(--calendar-transition-duration) var(--calendar-transition-ease), + border-color var(--calendar-transition-duration) var(--calendar-transition-ease), + color var(--calendar-transition-duration) var(--calendar-transition-ease), + box-shadow var(--calendar-transition-duration) var(--calendar-transition-ease), + opacity var(--calendar-transition-duration) var(--calendar-transition-ease); +} + +[data-calendar-day]:hover { + border-color: var(--calendar-day-border-hover); + background: var(--calendar-day-bg-hover); + color: var(--calendar-day-color-hover); +} + +[data-calendar-day][data-selected] { + border-color: var(--_calendar-accent-solid); + background: var(--_calendar-accent-solid); + color: var(--calendar-day-selected-color); +} + +[data-calendar-day][data-selected]:hover { + border-color: var(--_calendar-accent-solid-hover); + background: var(--_calendar-accent-solid-hover); +} + +[data-calendar-day][data-focused]:not([data-selected]) { + border-color: var(--_calendar-accent-border); + background: var(--_calendar-accent-track); + color: var(--_calendar-accent-text); +} + +[data-calendar-day][data-today]:not([data-selected]) { + border-color: var(--calendar-day-today-border); + color: var(--calendar-day-today-color); + font-weight: var(--calendar-day-today-font-weight); +} + +[data-calendar-day][data-outside-month] { + color: var(--calendar-day-outside-color); + opacity: var(--calendar-day-outside-opacity); +} + +[data-calendar-day][data-weekend]:not([data-selected]) { + color: var(--calendar-day-weekend-color); +} + +[data-calendar-day][data-holiday]:not([data-selected]) { + box-shadow: var(--calendar-day-holiday-shadow); +} + +[data-calendar-day][data-unavailable] { + text-decoration-line: line-through; + text-decoration-color: var(--calendar-day-unavailable-decoration); +} + +[data-calendar-day][data-disabled], +[data-calendar-day][data-unavailable] { + cursor: not-allowed; + opacity: var(--calendar-disabled-opacity); +} + +[data-calendar][data-invalid] { + border-color: var(--calendar-invalid-border); +} + +[data-calendar][data-disabled] { + opacity: var(--calendar-disabled-opacity); +} + +[data-calendar][data-event], +[data-calendar-day][data-event], +[data-calendar-prev-button][data-event], +[data-calendar-next-button][data-event], +[data-calendar-month-select][data-event], +[data-calendar-year-select][data-event] { + box-shadow: var(--calendar-event-shadow); +} + +@media (prefers-reduced-motion: reduce) { + [data-calendar-prev-button], + [data-calendar-next-button], + [data-calendar-month-select], + [data-calendar-year-select], + [data-calendar-day] { + transition: none; + } +} diff --git a/src/uix/eidos/components/calendar/calendar.svelte b/src/uix/eidos/components/calendar/calendar.svelte new file mode 100644 index 000000000..33c87f24c --- /dev/null +++ b/src/uix/eidos/components/calendar/calendar.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children(snippetProps: CalendarProviderSnippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/calendar/index.ts b/src/uix/eidos/components/calendar/index.ts new file mode 100644 index 000000000..a152a4f94 --- /dev/null +++ b/src/uix/eidos/components/calendar/index.ts @@ -0,0 +1,76 @@ +import CalendarComponent from './calendar.svelte' +import Header from './calendar-header.svelte' +import Heading from './calendar-heading.svelte' +import PrevButton from './calendar-prev-button.svelte' +import NextButton from './calendar-next-button.svelte' +import MonthSelect from './calendar-month-select.svelte' +import YearSelect from './calendar-year-select.svelte' +import Grid from './calendar-grid.svelte' +import GridHead from './calendar-grid-head.svelte' +import GridBody from './calendar-grid-body.svelte' +import GridRow from './calendar-grid-row.svelte' +import HeadCell from './calendar-head-cell.svelte' +import Cell from './calendar-cell.svelte' +import Day from './calendar-day.svelte' + +type CalendarNamespace = typeof CalendarComponent & { + Header: typeof Header + Heading: typeof Heading + PrevButton: typeof PrevButton + NextButton: typeof NextButton + MonthSelect: typeof MonthSelect + YearSelect: typeof YearSelect + Grid: typeof Grid + GridHead: typeof GridHead + GridBody: typeof GridBody + GridRow: typeof GridRow + HeadCell: typeof HeadCell + Cell: typeof Cell + Day: typeof Day +} + +const Calendar = CalendarComponent as CalendarNamespace +Calendar.Header = Header +Calendar.Heading = Heading +Calendar.PrevButton = PrevButton +Calendar.NextButton = NextButton +Calendar.MonthSelect = MonthSelect +Calendar.YearSelect = YearSelect +Calendar.Grid = Grid +Calendar.GridHead = GridHead +Calendar.GridBody = GridBody +Calendar.GridRow = GridRow +Calendar.HeadCell = HeadCell +Calendar.Cell = Cell +Calendar.Day = Day + +export { Calendar } + +export default Calendar + +export type { + CalendarProps, + CalendarHeaderProps as HeaderProps, + CalendarHeadingProps as HeadingProps, + CalendarPrevButtonProps as PrevButtonProps, + CalendarNextButtonProps as NextButtonProps, + CalendarMonthSelectProps as MonthSelectProps, + CalendarYearSelectProps as YearSelectProps, + CalendarGridProps as GridProps, + CalendarGridHeadProps as GridHeadProps, + CalendarGridBodyProps as GridBodyProps, + CalendarGridRowProps as GridRowProps, + CalendarHeadCellProps as HeadCellProps, + CalendarCellProps as CellProps, + CalendarDayProps as DayProps, + CalendarProviderSnippetProps, + CalendarCellSnippetProps, + CalendarDaySnippetProps, + CalendarHeadingSnippetProps, + CalendarMonthSelectSnippetProps, + CalendarYearSelectSnippetProps, + CalendarGridRowSnippetProps, + CalendarSize, + CalendarVariant, + CalendarColor +} from './types' diff --git a/src/uix/eidos/components/calendar/types.ts b/src/uix/eidos/components/calendar/types.ts new file mode 100644 index 000000000..c2feb1a43 --- /dev/null +++ b/src/uix/eidos/components/calendar/types.ts @@ -0,0 +1,62 @@ +import type { + CalendarCellProps as SomaCalendarCellProps, + CalendarCellSnippetProps, + CalendarDayProps as SomaCalendarDayProps, + CalendarDaySnippetProps, + CalendarGridBodyProps as SomaCalendarGridBodyProps, + CalendarGridHeadProps as SomaCalendarGridHeadProps, + CalendarGridProps as SomaCalendarGridProps, + CalendarGridRowProps as SomaCalendarGridRowProps, + CalendarGridRowSnippetProps, + CalendarHeadCellProps as SomaCalendarHeadCellProps, + CalendarHeaderProps as SomaCalendarHeaderProps, + CalendarHeadingProps as SomaCalendarHeadingProps, + CalendarHeadingSnippetProps, + CalendarMonthSelectProps as SomaCalendarMonthSelectProps, + CalendarMonthSelectSnippetProps, + CalendarNextButtonProps as SomaCalendarNextButtonProps, + CalendarPrevButtonProps as SomaCalendarPrevButtonProps, + CalendarProps as SomaCalendarProps, + CalendarProviderSnippetProps, + CalendarYearSelectProps as SomaCalendarYearSelectProps, + CalendarYearSelectSnippetProps +} from '$soma/components/calendar' +import type { DateValue } from '$libs/days' +import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types' + +export type CalendarSize = Extract +export type CalendarVariant = ControlVariant +export type CalendarColor = ColorRole + +export type CalendarProps = SomaCalendarProps & { + /** Visual size for day cells, nav buttons and select controls. @default 'md' */ + size?: ResponsiveProp + /** Visual treatment of the calendar surface. @default 'surface' */ + variant?: CalendarVariant + /** Intent palette used for selected days and focus affordances. @default 'primary' */ + color?: CalendarColor +} + +export type CalendarHeaderProps = SomaCalendarHeaderProps +export type CalendarHeadingProps = SomaCalendarHeadingProps +export type CalendarPrevButtonProps = SomaCalendarPrevButtonProps +export type CalendarNextButtonProps = SomaCalendarNextButtonProps +export type CalendarMonthSelectProps = SomaCalendarMonthSelectProps +export type CalendarYearSelectProps = SomaCalendarYearSelectProps +export type CalendarGridProps = SomaCalendarGridProps +export type CalendarGridHeadProps = SomaCalendarGridHeadProps +export type CalendarGridBodyProps = SomaCalendarGridBodyProps +export type CalendarGridRowProps = SomaCalendarGridRowProps & { week?: DateValue[] } +export type CalendarHeadCellProps = SomaCalendarHeadCellProps +export type CalendarCellProps = SomaCalendarCellProps +export type CalendarDayProps = SomaCalendarDayProps + +export type { + CalendarProviderSnippetProps, + CalendarCellSnippetProps, + CalendarDaySnippetProps, + CalendarHeadingSnippetProps, + CalendarMonthSelectSnippetProps, + CalendarYearSelectSnippetProps, + CalendarGridRowSnippetProps +} diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 66667f871..f49deabde 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -85,6 +85,7 @@ @import './components/search-field/search-field.css'; @import './components/select/select.css'; @import './components/combobox/combobox.css'; +@import './components/calendar/calendar.css'; @import './components/date-field/date-field.css'; @import './components/slider/slider.css'; @import './components/toolbar/toolbar.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 4b4a44259..cbe687330 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -579,6 +579,75 @@ export const THEME_BASE_RECIPE_TOKENS = { 'transition-ease': 'var(--ease-default)', 'disabled-opacity': '0.56' }, + calendar: { + gap: 'var(--space-3)', + 'padding-sm': 'var(--space-3)', + 'padding-md': 'var(--space-4)', + 'padding-lg': 'var(--space-5)', + radius: 'var(--radius-lg)', + 'border-width': 'var(--border-width)', + border: 'var(--color-border-subtle)', + 'invalid-border': 'var(--color-risk-border)', + bg: 'var(--color-surface-raised)', + 'outline-bg': 'var(--color-surface-default)', + 'outline-shadow': 'none', + color: 'var(--color-content-primary)', + shadow: 'var(--shadow-sm)', + 'font-family': 'var(--font-ui)', + 'font-size-sm': 'var(--font-size-xs)', + 'font-size-md': 'var(--font-size-sm)', + 'font-size-lg': 'var(--font-size-sm)', + 'line-height': 'var(--leading-ui)', + 'header-gap': 'var(--space-2)', + 'heading-color': 'var(--color-content-primary)', + 'heading-font-weight': 'var(--font-weight-semibold)', + 'control-size-sm': 'var(--control-height-xs)', + 'control-size-md': 'var(--control-height-sm)', + 'control-size-lg': 'var(--control-height-md)', + 'control-radius': 'var(--radius-md)', + 'control-border-width': 'var(--border-width)', + 'control-border': 'transparent', + 'control-border-hover': 'var(--color-border-subtle)', + 'control-bg': 'transparent', + 'control-bg-hover': 'var(--color-surface-overlay)', + 'control-color': 'var(--color-content-secondary)', + 'control-color-hover': 'var(--color-content-primary)', + 'select-min-width': '8ch', + 'select-px': 'var(--space-2)', + 'grid-gap': 'var(--space-0-5)', + 'weekday-size': 'var(--space-5)', + 'weekday-color': 'var(--color-content-muted)', + 'weekday-font-size': 'var(--font-size-xs)', + 'weekday-font-weight': 'var(--font-weight-medium)', + 'day-size-sm': 'var(--control-height-xs)', + 'day-size-md': 'var(--control-height-sm)', + 'day-size-lg': 'var(--control-height-md)', + 'day-radius': 'var(--radius-md)', + 'day-border-width': 'var(--border-width)', + 'day-border': 'transparent', + 'day-border-hover': 'var(--color-border-subtle)', + 'day-bg': 'transparent', + 'day-bg-hover': 'var(--color-surface-overlay)', + 'day-color': 'var(--color-content-primary)', + 'day-color-hover': 'var(--color-content-primary)', + 'day-font-weight': 'var(--font-weight-regular)', + 'day-selected-color': 'var(--color-content-on-solid)', + 'day-today-border': 'var(--color-border-strong)', + 'day-today-color': 'var(--color-content-primary)', + 'day-today-font-weight': 'var(--font-weight-semibold)', + 'day-outside-color': 'var(--color-content-muted)', + 'day-outside-opacity': '0.62', + 'day-weekend-color': 'var(--color-content-secondary)', + 'day-holiday-shadow': 'inset 0 -2px 0 var(--_calendar-accent-border)', + 'day-unavailable-decoration': 'var(--color-risk-border)', + 'focus-shadow': + '0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', + 'event-shadow': + '0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-accent-border)', + 'transition-duration': 'var(--duration-fast)', + 'transition-ease': 'var(--ease-default)', + 'disabled-opacity': '0.52' + }, pagination: { 'gap-sm': 'var(--space-1)', 'gap-md': 'var(--space-1-5)', diff --git a/src/uix/morfo/components/calendar.ts b/src/uix/morfo/components/calendar.ts index 5b0d6569f..c51885453 100644 --- a/src/uix/morfo/components/calendar.ts +++ b/src/uix/morfo/components/calendar.ts @@ -4,11 +4,42 @@ import { v } from '../types'; export const calendarMorfo = { name: 'Calendar', kebab: 'calendar', - scope: ['soma'], + scope: ['soma', 'sema'], apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/grid/', translations: { label: { es: 'Calendario', en: 'Calendar' } }, + events: [ + { + name: 'commit-select', + semantic: { + family: 'commit', + verb: 'select', + target: v.partRef('day'), + intent: 'affirm', + sequence: 'post' + } + }, + { + name: 'commit-unselect', + semantic: { + family: 'commit', + verb: 'remove', + target: v.partRef('day'), + intent: 'neutral', + sequence: 'post' + } + }, + { + name: 'shift-navigate', + semantic: { + family: 'shift', + verb: 'navigate', + target: v.partRef('provider'), + sequence: 'post' + } + } + ], parts: [ { name: 'Provider', diff --git a/src/uix/sema/components/calendar.ts b/src/uix/sema/components/calendar.ts new file mode 100644 index 000000000..635acb763 --- /dev/null +++ b/src/uix/sema/components/calendar.ts @@ -0,0 +1,48 @@ +import { semaSelector } from '$uix/morfo' +import { calendarMorfo } from '$uix/morfo/components/calendar' +import { soundTuning } from '../sounds' +import type { Sema } from '../sema-map' + +const onPart = ( + part: + | 'provider' + | 'prev-button' + | 'next-button' + | 'month-select' + | 'year-select' + | 'day', + matchers?: Parameters>[2] +) => semaSelector(calendarMorfo, part, matchers) + +export const calendarSema: Sema = { + name: 'calendar', + cascade: [ + { + selector: onPart('day', { eventName: 'commit-select' }), + sound: soundTuning('form.commit.soft'), + haptic: { kind: 'tap' } + }, + { + selector: onPart('day', { eventName: 'commit-unselect' }), + sound: soundTuning('form.commit.subtle'), + haptic: { kind: 'tap' } + }, + ...( + [ + 'provider', + 'prev-button', + 'next-button', + 'month-select', + 'year-select', + 'day' + ] as const + ).map((part) => ({ + selector: onPart(part, { eventName: 'shift-navigate' }), + sound: soundTuning('form.commit.subtle', { + gain: 0.18, + contour: 'ascending', + pitch: { op: 'add', value: -90 } + }) + })) + ] +} diff --git a/src/uix/soma/components/calendar/README.md b/src/uix/soma/components/calendar/README.md index 706cd5cb9..4ff31f591 100644 --- a/src/uix/soma/components/calendar/README.md +++ b/src/uix/soma/components/calendar/README.md @@ -1,6 +1,10 @@ # Calendar -A date-picker grid with single or multiple selection. Superior to every reference library (ark-ui, bits-ui, radix, react-aria) via built-in `MonthSelect`, `YearSelect`, ISO week numbers, a `holiday` matcher, weekend decoration, and navigation announcements. 14 parts, fully headless. +A date-picker grid with single or multiple selection. Soma owns the headless +model: `MonthSelect`, `YearSelect`, fixed weeks, multi-month grids, week-number +snippet data, holiday/weekend flags, RTL keyboard navigation and live navigation +announcements. It exposes 14 public components; Morfo also declares a future +`WeekNumber` part. ## Anatomy @@ -42,139 +46,148 @@ A date-picker grid with single or multiple selection. Superior to every referenc ## Parts -| Part | Element | Description | -| ------------- | ---------- | -------------------------------------------------------------- | -| `Provider` | `
` | Root context. Owns value, placeholder, navigation, keyboard. | -| `Header` | `
` | Structural wrapper for the title / controls row. | +| Part | Element | Description | +| ------------- | ---------- | ------------------------------------------------------------------ | +| `Provider` | `
` | Root context. Owns value, placeholder, navigation, keyboard. | +| `Header` | `
` | Structural wrapper for the title / controls row. | | `Heading` | `
` | Auto-localized heading (`"April 2026"`, or range for multi-month). | -| `PrevButton` | `