feat(morfo,soma,eidos): chronos deja de tener partes que el morfo no conoce

C3 cierra con `more-link` y C4 con los seis marcadores que eidos estilizaba
sin declaracion. Los tres botones sin fuentes (`day-add`, `edit-action`,
`search-button`) comparten una clase; `undo`/`redo` se cablean sobre la del
nav; `timegrid` pasa a `time-view` porque estaba a un guion de la parte
`time-grid` que vive dentro; `peek-panel` y `day-peek` quedan como partes de
display puro con marcador inline. Cada una renderiza un `<button>` real
cuando el morfo lo declara: una `Region` que cae a `<div>` pondria el `type`
declarado sobre un no-boton.

La lupa deja de ser un adorno sin `onclick`: abre una paleta `Command.Dialog`
sobre TODOS los eventos, no un filtro de la vista, con `goToDate` nuevo en el
provider — la generalizacion de la que `goToday` era un caso particular. El
texto buscable viaja en `keywords` porque el scorer de Command lee value +
keywords y NUNCA los hijos renderizados, asi que se busca por titulo, por tipo
y por fecha.

Y tres etiquetas que mentian en silencio: `translationRef` / `commonRef`
normalizan la clave LITERAL y no consultan ningun mapa, asi que
`prevMonth` / `nextMonth` / `moreEvents` y `action.undo` / `action.redo` caian
al fallback ingles en una pagina `es`. Corregidas contra el catalogo, con
`common.buttons.undo` / `redo` anadidas.

Verificado: check 74 = base (delta cero) · 458 vitest · docs:check 0/621 ·
rtl:check 0/177 · eidos-lint chronos morfo-backed 32 -> 44, invalid 0 ·
navegador limpio en las tres vistas, con el A/B del undo medido
(18 jun -> drag -> 25 jun -> undo -> 18 jun -> redo -> 25 jun) y el salto de
la paleta (noviembre -> elegir Retro -> junio).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 56a96114f8
commit 968fff9cdf

@ -160,11 +160,11 @@ values live in its own `types.ts`.
| `marker` | `solid` · `soft` · `outline` |
| `tabs` | `line` · `surface` · `pills` · `segmented` |
## Shared strings (`common.*`, 38)
## Shared strings (`common.*`, 40)
The shared idlangref leaves (`commonLangs`, `src/uix/langs/common.ts`),
reachable via `v.commonRef(...)` or `#?common.{path}|Fallback` — reuse these
instead of re-declaring a close/cancel/clear label per component.
`buttons.cancel` · `buttons.clear` · `buttons.close` · `buttons.decrement` · `buttons.dismiss` · `buttons.edit` · `buttons.increment` · `buttons.next` · `buttons.prev` · `buttons.remove` · `buttons.reset` · `buttons.save` · `buttons.submit` · `calendar.prev-month` · `calendar.next-month` · `calendar.month-select` · `calendar.year-select` · `month-grid.prev-year` · `month-grid.next-year` · `year-grid.prev-page` · `year-grid.next-page` · `date.year` · `date.month` · `date.day` · `time.hour` · `time.minute` · `time.second` · `time.day-period` · `time.time-zone` · `time.am` · `time.pm` · `time.period.dawn` · `time.period.morning` · `time.period.afternoon` · `time.period.dusk` · `time.period.night` · `time.period.late-night` · `field.empty`
`buttons.cancel` · `buttons.clear` · `buttons.close` · `buttons.decrement` · `buttons.dismiss` · `buttons.edit` · `buttons.increment` · `buttons.next` · `buttons.prev` · `buttons.redo` · `buttons.remove` · `buttons.reset` · `buttons.save` · `buttons.submit` · `buttons.undo` · `calendar.prev-month` · `calendar.next-month` · `calendar.month-select` · `calendar.year-select` · `month-grid.prev-year` · `month-grid.next-year` · `year-grid.prev-page` · `year-grid.next-page` · `date.year` · `date.month` · `date.day` · `time.hour` · `time.minute` · `time.second` · `time.day-period` · `time.time-zone` · `time.am` · `time.pm` · `time.period.dawn` · `time.period.morning` · `time.period.afternoon` · `time.period.dusk` · `time.period.night` · `time.period.late-night` · `field.empty`

@ -527,8 +527,44 @@ habría dado verde con dos regiones sin comprobar.
`type`, y a diferencia del chip del mes no anida ningún control, así que aquí
el elemento nativo es el honesto.
**C3 va 8 de 9. Queda sólo `more-link`**, que vive en `chronos-day-peek.svelte`
(no en la vista), con fuentes `count` + `type` + `aria-label`.
**C3 CERRADO 9/9 (2026-08-10)** — `more-link` migrado. Clase propia
(`ChronosMoreLinkProvider` + componente), NO `Region`: el precedente es
`agenda-item` — renderiza un `<button>` real y `Region` cae a `<div>`, que
pondría el `type` declarado sobre un no-botón (el defecto que destapó el chip).
Cadena `Popover.Trigger → MoreLink → Button → un solo elemento`.
⚠️ El bag del popover va **el último** en el spread: el `aria-labelledby` de su
content cae al id del trigger, así que esa identidad tiene que ganar.
Medido en `/uix/components/chronos`: `data-count="6"` (nunca se había emitido),
`role`, `type` y `aria-label` del morfo conviviendo con `aria-haspopup` /
`aria-expanded` / `aria-controls` / `data-state` del popover en el MISMO nodo.
`check` 74 = base, DELTA CERO · 458✓.
⚠️ **CUARTA declaración falsa destapada al registrar** — y esta no es de chronos:
`v.translationRef('moreEvents')` **normaliza la clave literal**
(`compile.ts:normalizeTranslationRef` → `#?components.chronos.moreEvents`), NO
consulta el mapa `texts` del morfo. El catálogo la tiene como `more-events`, así
que el lookup falla y sale el FALLBACK inglés. Medido con la página en `es`:
```
today-button aria-label="Hoy" ← clave 'today', coincide con el catálogo
prev-button aria-label="Previous month" ← 'prevMonth' ≠ 'prev-month' → fallback
next-button aria-label="Next month" ← 'nextMonth' ≠ 'next-month' → fallback
more-link aria-label="Show more events" ← 'moreEvents' ≠ 'more-events' → fallback
day-add aria-label="Nuevo evento" ← eidos tr() con clave guionada: SÍ traduce
```
El guionado no es el problema: `tr()` lo resuelve. La regla real es que
`translationRef` exige **la clave del catálogo**, y un alias camelCase muere en
silencio. **`npm run translations:check` —el guard que lo cazaría— CASCA** antes
de comprobar nada (`SyntaxError` en `evalCatalogFile`, una `url:` multilínea de
algún catálogo), así que la clase entera lleva tiempo pasando desapercibida.
Censar los 166 morfos después de arreglar el script.
⚠️ **Label-in-Name (WCAG 2.5.3) en `more-link`**: el texto visible es
`{count} more…` (además SIN traducir, a diferencia del resto del fichero) y el
nombre accesible pasa a ser el `aria-label` del morfo, que no lleva el número.
Decisión de C4: o el `aria-label` se hace `propRef` con el conteo, o se retira y
nombra el contenido — pero entonces el texto visible tiene que traducirse.
`ChronosRegionProps` admite `class` y `style` explícitos: el morfo posee
`role`/aria, la receta posee la caja. Declarados uno a uno en vez de tragados
@ -567,30 +603,157 @@ navegación temporal, salir primero del mes de hoy.**
| --- | --- |
| C1 · partes que emiten | ✅ (6) — chronos con CERO `targetOverride` |
| C2 · singletons | ✅ (8) — medidos en las TRES vistas |
| C3 · repetidas | **8 / 9** — falta `more-link` |
| C4 · morfo | pendiente |
| C3 · repetidas | ✅ **9 / 9** |
| C4 · morfo | ✅ **7 / 7** |
| C5 · composición pública | pendiente |
**Por dónde seguir, en orden:**
1. **`more-link`** (`chronos-day-peek.svelte`, fuentes `count`/`type`/
`aria-label`) — cierra C3. Es un `<button>`: mirar si le vale `Region` o
necesita clase como `agenda-item`.
2. **C4 — decisiones de morfo.** Declarar las 5 partes reales que el CSS de
eidos estiliza y el morfo no conoce (`day-add`, `search-button`, `day-peek`,
`peek-panel`, `timegrid`); **quitar el `data-chronos-edit-action`**, que es un
gancho rociado sobre un `IconButton` compuesto dentro de un `Dialog`
compuesto y NO es parte (el SPEC dice que el morfo del editor lo aporta
Dialog); y resolver `undo-button` / `redo-button`, declarados y nunca
renderizados — o se cablean o se retiran.
3. **C5 — la composición pública**, que es el motivo por el que se anuló la
decisión de `exports.ts`: exponer las partes al consumidor con la forma de
Table/Calendar y recomponer `<Chronos {events} />` desde ellas.
#### C4 — hecho (2026-08-10)
Los 6 marcadores `data-chronos-*` que el morfo no conocía, resueltos así:
| Marcador | Verdicto | Cómo |
| --- | --- | --- |
| `day-add` | **PARTE pública** | Registrada: `ChronosDayAddProvider` + componente `<DayAdd>`; el `<IconButton>` de la vista se compone por `child`. El `aria-label` a mano DESAPARECE — sale del morfo. 42 instancias medidas |
| `timegrid` → **`time-view`** | **PARTE privada, RENOMBRADA** | Era el shell de semana/día, a UN guion de la parte `time-grid` que vive dentro: trampa. Marcador inline (parte de display puro), 4 reglas CSS renombradas |
| `peek-panel` · `day-peek` | **PARTES privadas** | Sin role ni aria: marcador inline, que es la forma sancionada para una región estructural que no debe ser class-hook |
| `edit-action` | **PARTE pública, registrada** | `ChronosActionProvider` + `<EditAction>` sobre el `IconButton` del editor. Ver decisión 1 |
| `search-button` | **PARTE pública, registrada** | Abre una paleta `Command.Dialog` sobre TODOS los eventos. Ver decisión 2 |
⚠️ **La premisa del plan era falsa en dos puntos, medidos**:
1. Decía «las 5 partes reales **que el CSS de eidos estiliza**». `search-button`
**no tiene ni una regla CSS ni un `onclick`** en todo el repo: una sola
aparición, el atributo. No es una parte por declarar, es una lupa que no
hace nada.
2. Suponía que estos marcadores estaban en rojo. `eidos-lint chronos` medido
antes y después (restaurando morfo+css de HEAD): **`invalid: 0` en ambos**.
Los clasificaba como *eidos-only*, no como inválidos. Lo que cambia es
**morfo-backed 32 → 43** (y eidos-only 53 → 42): el contrato pasa de señal
DOM sin declarar a declarado. Ese es el valor, no un guard en rojo.
⚠️ **`class-hooks: 115`** en chronos, sin cambio. La norma S1 de eidos-lint los
prohíbe (`component:audit` exige 0). Es la familia `.chronos-tg-*` /
`.chronos-peek-*` entera: deuda grande, PREEXISTENTE, y un eje propio.
**Y de paso, el arreglo de i18n medido** (§C3): con las claves del catálogo,
`prev`/`next`/`more-link` pasan de fallback inglés a **«Mes anterior» ·
«Mes siguiente» · «Ver más eventos»** en una página `es`.
Verificado: `check` **74 = base, DELTA CERO** · **458 ✓** · `contracts.test.ts`
los MISMOS 6 fallos ajenos (ninguna fila de chronos) · `docs:check` 0/621 ·
`rtl:check` 0/177 · navegador en las TRES vistas + el peek abierto, **consola
limpia en pestaña nueva**.
⚠️ **Dos trampas de verificación en este pase**:
- `prettier --check` sobre una copia del fichero FUERA del proyecto no valida
nada: no resuelve el plugin de svelte y **falla parseando**, lo que se lee
como «pasa» si sólo miras la lista de warns. `chronos-view.svelte` YA estaba
sucio en HEAD; lo descubrí sólo al comprobarlo in situ.
- `prettier --write` sobre él lo pasó a LF y produjo un diff de **719 líneas**
([[reference-git-crlf-wholefile-diff]]). Revertido y reaplicados los tres
cambios a mano: el diff real es **34 líneas**. No formatear ficheros que ya
estaban sucios.
#### C4 — las 3 decisiones, resueltas por el autor (2026-08-10)
**1 · `edit-action` → DECLARADA.** El plan decía retirarla; el autor decidió lo
contrario y es lo coherente: retirarla no era ejecutable (su única regla CSS la
posiciona en la esquina del diálogo, y cambiar el gancho por una clase es justo
lo que la norma S1 prohíbe), y el precedente del mismo día lo contradecía —
`more-link` dentro de un Popover compuesto y `day-add` dentro de un IconButton
compuesto SON partes. Alcanzar un componente compuesto con **tu propio attr con
namespace** es el patrón sancionado. Medido: `role` · `type` ·
`aria-label="Editar evento"` del morfo, la caja sigue a 20/20 px de la esquina
del diálogo (medido contra el rect real), y el click sigue abriendo la edición
(19 campos `data-readonly` → 0).
**2 · `search-button` → PARTE + PALETA sobre TODOS los eventos.** No había spec
en ninguna parte (ni `SPEC.md` ni `CONTINUE.md`; el README sólo lo mencionaba
como composición) y en soma/engine no había una línea de búsqueda, así que la
conducta la eligió el autor: paleta que busca en todo el almacén, no filtro de
la vista. Construido con **`Command.Dialog` del propio framework** — que YA
existe (paleta modal sobre el Dialog de eidos, con `mod+k` de serie) y que
estuve a punto de recomponer a mano: leer el barrel antes de escribir markup.
- Provider: **`goToDate(date)`**, la generalización de la que `goToday` era un
caso particular (mismo ancla rovinga, anuncio y emisión `shift-navigate`).
- El texto buscable viaja en **`keywords`**: el scorer de Command lee
`value` + `keywords` y **NUNCA** los hijos renderizados, y el `value` es el id
del evento. Por eso se busca por título, por tipo Y por fecha.
- Una serie recurrente sale UNA vez, igual que `openEdit`, que edita la serie.
Medido: `ctrl+k` ✓ · `meta+k` ✓ · el botón ✓ · 18 eventos listados con
«Título — 8 jun 2026 · Todo el día» · filtro por título («trip» → 1) y **por
fecha** («18 jun» → Retro) · vacío traducido · y el salto de marco:
**noviembre de 2026 → elegir «Retro» → junio de 2026** con el editor abierto.
⚠️ **Lo que NO se pudo verificar**: que la paleta se DESMONTE al elegir. Su
`data-state` pasa a `closed` (correcto), pero el nodo permanece porque **el
panel oculto congela las animaciones de salida** — medido: `document.hidden =
true` y `scale-out`/`fade-out` en `running` con `currentTime: 0`. Control: el
diálogo del editor tampoco cierra con Escape en ese estado (22 animaciones
congeladas). Es el entorno, no el código — pero queda por confirmar con el
panel visible.
**3 · `undo-button` / `redo-button` → CABLEADOS.** `ChronosNavPart` pasa de 3 a
5 partes (misma clase, el patrón de paginación de Calendar) y la barra los
renderiza en su propio `chronos-nav-group`. Medido de punta a punta:
```
Retro, 18 jun 2026 → drag → 25 jun → undo → 18 jun → redo → 25 jun
undo deshabilitado al arrancar (aria-disabled=true + data-disabled), habilitado tras el move
```
⚠️ **Y destapó la MISMA clase de fallo de traducción, tercera vez**: los dos
declaraban `v.commonRef('action.undo')` y **`common.action` no existe** — el
grupo es `common.buttons`. Añadidos `buttons.undo` / `buttons.redo`
(«Deshacer» / «Rehacer») y repuntadas las declaraciones. `docs:vocabularies`
regenerado (38 → 40 shared strings), que es lo que puso `docs:check` en rojo
hasta regenerarlo.
⚠️ **La abstracción llegó a sus dos consumidores**: `ChronosDayAddProvider` pasa
a `ChronosActionProvider` con parte `'day-add' | 'edit-action'` — mismo
contrato (botón real, role + type + aria-label traducido, CERO fuentes), el
patrón de `NavButton`. Los componentes siguen siendo dos con nombre propio
(`DayAdd`, `EditAction`), que es lo que C5 necesita para la API compuesta.
**Después: C5 — la composición pública**, que es el motivo por el que se anuló
la decisión de `exports.ts`: exponer las partes al consumidor con la forma de
Table/Calendar y recomponer `<Chronos {events} />` desde ellas.
Verificación al cierre: `check` **74 = base, DELTA CERO** sostenido en cada
lote · **458 ✓** (chronos + morfo + sema) · navegador limpio en las tres vistas.
Censo global de `targetOverride`: **148 → 89**.
#### ⚠️ TRES defectos de conducta medidos en el navegador (2026-08-10) — eje NUEVO
Salieron al responder dos preguntas del autor sobre la vista de mes. Ninguno es
de superficie perceptual; se anotan aquí para que no se pierdan.
1. **La 6ª fila fantasma.** `fixedWeeks` vale **`true` por defecto en chronos**
(`components/chronos.svelte:20`), al revés que Calendar / DatePicker /
RangeCalendar / DateRangePicker, que lo traen `false`. `buildMonthGrid`
rellena entonces hasta 42 celdas (`engine/grid.ts:67`), así que un mes que
cabe en 5 filas (junio de 2026: lun 1 jun → dom 5 jul) se inventa una 6ª
fila **entera del mes siguiente**. Decisión: alinear el defecto con la
familia, o mantener la altura estable creciendo las filas en vez de añadir
una semana.
2. **Una ocurrencia de evento recurrente NO se puede mover.** Su id es
sintético (`${serie}::${YYYY-MM-DD}`, `engine/recurrence.ts:145`) y
`moveEventToDay` la busca en `calendar.events`, que sólo guarda la SERIE →
`find` devuelve `undefined` y **sale sin hacer nada, en silencio**. Medido:
el destino se ilumina (`data-dragover`) y al soltar el chip sigue donde
estaba. Afecta igual a un día de este mes (2026-06-04) que a uno de fuera,
así que no es un problema de mes. Mismo agujero previsible en
`moveEventToDayTime` / resize / delete de una ocurrencia.
**Los eventos normales SÍ caen en las celdas de fuera de mes** — medido:
`Retro` → 2026-07-08 y `Launch` → 2026-07-09 aterrizan y se repintan allí.
3. **El chip pierde `data-event-id` tras un intento de arrastre.** El atributo
—`propRef` + `emit: 'value'`, parte del contrato C1— desaparece del nodo
mientras `data-color` y `aria-label`, del MISMO bag, siguen. El
`data-value` de DragDrop conserva el id. Tres chips distintos, reproducible.
Medido con eventos de puntero sintéticos: confirmar con un arrastre real
antes de diagnosticar.
#### ⚠️ Hallazgo — chronos estampa fuera del morfo
`commit-save` y `commit-delete` declaran `event-chip` y aterrizan en los botones

@ -18,6 +18,7 @@
import { ActiveEidos } from '$uix/eidos';
import {
ChronosProvider,
MoreLink,
eventColorVars,
type ChronosEvent,
type TimedBlock
@ -132,16 +133,25 @@
<Popover>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="plain"
size="xs"
data-chronos-more-link
style="grid-column: {column}; grid-row: {row};"
>
{count} more…
</Button>
{#snippet child({ props: pop })}
<!-- Popover.Trigger → MoreLink → Button → one element: the marker,
`role` / `type`, the translated aria-label and `data-count` come
from the MORFO now instead of a hand-written attr. The popover's
bag goes LAST on purpose — its content's `aria-labelledby` falls
back to the trigger's id, so that identity has to win. -->
<MoreLink {count}>
{#snippet child({ props: link })}
<Button
{...link}
{...pop}
variant="plain"
size="xs"
style="grid-column: {column}; grid-row: {row};"
>
{count} more…
</Button>
{/snippet}
</MoreLink>
{/snippet}
</Popover.Trigger>
<Popover.Portal>

@ -2,6 +2,9 @@
import type { Snippet } from 'svelte';
import {
ChronosProvider,
EditAction,
SearchButton,
DayAdd,
DayCell,
EventChip,
AgendaItem,
@ -31,6 +34,7 @@
import { Dialog } from '$uix/eidos/components/dialog';
import { Field } from '$uix/eidos/components/field';
import { Select } from '$uix/eidos/components/select';
import { Command } from '$uix/eidos/components/command';
import { Button } from '$uix/eidos/components/button';
import { IconButton } from '$uix/eidos/components/icon-button';
import { Tooltip } from '$uix/eidos/components/tooltip';
@ -40,6 +44,8 @@
Search,
Plus,
Pencil,
Undo2,
Redo2,
TriangleAlert
} from '$uix/eidos/components/icon';
import ChronosWhenEditor from './chronos-when-editor.svelte';
@ -163,6 +169,37 @@
provider.deleteEvent(provider.editing.draft.id);
}
// ── Search palette ────────────────────────────────────────────────────────
let searchOpen = $state(false);
// Date (+ time range) of a result. A palette that spans every month has to
// say WHEN, not just how long — same `day / short month / year` format the
// toolbar subtitle uses.
function searchDateLabel(ev: ChronosEvent): string {
const day = getCachedDateFormat(locale, {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(toCalendarDate(ev.start).toDate(tz));
const range = provider.eventRangeLabel(ev);
return range ? `${day} · ${range}` : day;
}
// What the Command scorer matches on. It reads value + keywords ONLY (never
// the rendered children), and the value is the event id, so every searchable
// string has to travel here: title, type label and the date.
function searchKeywords(ev: ChronosEvent): string[] {
return [ev.title || untitled, provider.eventTypeLabel(ev), searchDateLabel(ev)].filter(Boolean);
}
// Jump to the chosen event's date and open it. The palette closes first so
// the editor dialog is not stacked on a dismissing one.
function openSearchResult(ev: ChronosEvent): void {
searchOpen = false;
provider.goToDate(toCalendarDate(ev.start));
provider.openEdit(ev);
}
// Drag-drop (framework DragDrop): drop an event chip on a day cell → move it.
function handleDrop(e: DropEvent): void {
const cell = (e.target as HTMLElement).closest('[data-date]') as HTMLElement | null;
@ -386,13 +423,44 @@
<span class="chronos-toolbar-spacer"></span>
<IconButton
aria-label={tr('#?components.chronos.search|Search')}
variant="surface"
data-chronos-search-button
>
<Search size="sm" />
</IconButton>
<!-- Search — a registered part that opens the palette below. -->
<SearchButton>
{#snippet child({ props: sb })}
<IconButton {...sb} variant="surface" onclick={() => (searchOpen = true)}>
<Search size="sm" />
</IconButton>
{/snippet}
</SearchButton>
<!-- Undo / redo — the engine's command stack, which the provider already
exposed (`canUndo` / `undo()`) while the toolbar never rendered them.
Same registered-part shape as the nav group. -->
<div class="chronos-nav-group">
<NavButton part="undo-button" disabled={!provider.canUndo}>
{#snippet child({ props: nav })}
<IconButton
{...nav}
variant="ghost"
disabled={!provider.canUndo}
onclick={() => provider.undo()}
>
<Undo2 size="sm" />
</IconButton>
{/snippet}
</NavButton>
<NavButton part="redo-button" disabled={!provider.canRedo}>
{#snippet child({ props: nav })}
<IconButton
{...nav}
variant="ghost"
disabled={!provider.canRedo}
onclick={() => provider.redo()}
>
<Redo2 size="sm" />
</IconButton>
{/snippet}
</NavButton>
</div>
<!-- Each nav button is a registered part composed onto the eidos control:
the morfo supplies role / type / the translated aria-label, and the
@ -541,19 +609,24 @@
{provider.formatDayNumber(date)}
</Region>
<!-- Create affordance — only on hover/focus; the cell itself no
longer captures clicks (drag/drop owns the cell surface). -->
<IconButton
data-chronos-day-add
aria-label={tr('#?components.chronos.new-event|New event')}
variant="ghost"
size="xs"
onclick={(e: MouseEvent) => {
e.stopPropagation();
provider.openCreate(date);
}}
>
<Plus size="sm" />
</IconButton>
longer captures clicks (drag/drop owns the cell surface).
The marker and the translated aria-label come from the
MORFO via the registered part, not a hand-written attr. -->
<DayAdd>
{#snippet child({ props: add })}
<IconButton
{...add}
variant="ghost"
size="xs"
onclick={(e: MouseEvent) => {
e.stopPropagation();
provider.openCreate(date);
}}
>
<Plus size="sm" />
</IconButton>
{/snippet}
</DayAdd>
</div>
</div>
{/snippet}
@ -667,7 +740,7 @@
onto all-day columns (framework DragDrop). -->
<DragDrop onDrop={handleTimeDrop}>
{#snippet children({ active })}
<div data-chronos-timegrid data-view={provider.view}>
<div data-chronos-time-view data-view={provider.view}>
<div class="chronos-tg-sticky">
<div class="chronos-tg-head">
<div class="chronos-tg-corner"></div>
@ -950,6 +1023,38 @@
{/if}
<!-- Editor — framework Dialog; composites carry their own focus (no Field.Control wrap). -->
<!-- Search palette — the framework's own `Command.Dialog` (modal palette over
the eidos Dialog, `mod+k` by default), never a hand-composed overlay. It
searches EVERY stored event, not just the visible frame: choosing one jumps
the calendar to its date and opens its editor. The searchable text travels
in `keywords` because the Command scorer reads value + keywords and NEVER
the rendered children, and the value is the event id. A recurring series is
listed once, like `openEdit`, which edits the series (v1: edit-all). -->
<Command.Dialog bind:open={searchOpen}>
<Command.Input placeholder={tr('#?components.chronos.search-placeholder|Search events…')} />
<Command.List>
<Command.Viewport>
<Command.Empty>
{tr('#?components.chronos.search-empty|No matching events.')}
</Command.Empty>
{#each provider.calendar.events as ev (ev.id)}
<Command.Item
value={ev.id}
keywords={searchKeywords(ev)}
onSelect={() => openSearchResult(ev)}
>
<Command.Item.Icon>
<span class="chronos-legend-swatch" style={swatchStyle(provider.resolveColor(ev))}
></span>
</Command.Item.Icon>
{ev.title || untitled}
<Command.Item.Shortcut>{searchDateLabel(ev)}</Command.Item.Shortcut>
</Command.Item>
{/each}
</Command.Viewport>
</Command.List>
</Command.Dialog>
<Dialog
open={provider.editing != null}
onOpenChange={(o) => {
@ -974,17 +1079,23 @@
<!-- View → edit toggle, pinned to the top-right corner at the same inset as
the footer buttons. Shown only for an existing event in view mode when
editing is permitted (perms / readonly / per-event). -->
editing is permitted (perms / readonly / per-event). A registered part:
the marker and the translated label come from the MORFO, and reaching
into the composed Dialog with our OWN namespaced attr is the sanctioned
shape (eidos-lint norm S1 forbids a class hook here). -->
{#if provider.showEditAction}
<IconButton
data-chronos-edit-action
aria-label={tr('#?components.chronos.edit-event|Edit event')}
variant="ghost"
size="sm"
onclick={() => provider.enableEditing()}
>
<Pencil size="sm" />
</IconButton>
<EditAction>
{#snippet child({ props: edit })}
<IconButton
{...edit}
variant="ghost"
size="sm"
onclick={() => provider.enableEditing()}
>
<Pencil size="sm" />
</IconButton>
{/snippet}
</EditAction>
{/if}
<div class="chronos-editor-fields">

@ -661,7 +661,7 @@
/* ── Time-grid (Week / Day) ─────────────────────────────────────────────── */
[data-chronos-timegrid] {
[data-chronos-time-view] {
--_chronos-hour-height: var(--space-12);
display: flex;
flex-direction: column;
@ -685,8 +685,8 @@
grid-template-columns: var(--space-12) 1fr;
border-block-end: var(--border-width) solid var(--color-border-subtle);
}
[data-chronos-timegrid][data-view='week'] .chronos-tg-head,
[data-chronos-timegrid][data-view='week'] .chronos-tg-allday {
[data-chronos-time-view][data-view='week'] .chronos-tg-head,
[data-chronos-time-view][data-view='week'] .chronos-tg-allday {
grid-template-columns: var(--space-12) repeat(7, minmax(0, 1fr));
}
.chronos-tg-dayhead {
@ -755,7 +755,7 @@
display: grid;
grid-template-columns: 1fr;
}
[data-chronos-timegrid][data-view='week'] .chronos-tg-cols {
[data-chronos-time-view][data-view='week'] .chronos-tg-cols {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.chronos-tg-col {

@ -18,10 +18,12 @@ export const commonLangs = {
increment: { es: 'Aumentar', en: 'Increment' },
next: { es: 'Siguiente', en: 'Next' },
prev: { es: 'Anterior', en: 'Previous' },
redo: { es: 'Rehacer', en: 'Redo' },
remove: { es: 'Eliminar', en: 'Remove' },
reset: { es: 'Restablecer', en: 'Reset' },
save: { es: 'Guardar', en: 'Save' },
submit: { es: 'Enviar', en: 'Submit' }
submit: { es: 'Enviar', en: 'Submit' },
undo: { es: 'Deshacer', en: 'Undo' }
},
calendar: {
'prev-month': { es: 'Mes anterior', en: 'Previous month' },

@ -19,6 +19,8 @@ export const chronosLangs = {
// ── Toolbar + views ───────────────────────────────────────────────────────
search: { es: 'Buscar', en: 'Search' },
'search-placeholder': { es: 'Buscar eventos…', en: 'Search events…' },
'search-empty': { es: 'No hay eventos que coincidan.', en: 'No matching events.' },
'week-n': { es: 'Semana {{n}}', en: 'Week {{n}}' },
'add-event': { es: 'Añadir evento', en: 'Add event' },
'new-event': { es: 'Nuevo evento', en: 'New event' },

@ -28,12 +28,19 @@ export const chronosMorfo = {
scope: ['soma', 'sema', 'eidos'],
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/grid/',
// Keys are the CATALOG keys, not prose aliases: `v.translationRef(key)`
// normalizes the key VERBATIM to `#?components.{kebab}.{key}` and never
// consults this map, so a camelCase alias (`prevMonth` vs the catalog's
// `prev-month`) misses the lookup and ships the English fallback in
// silence. Measured 2026-08-10 on an `es` page: `today` resolved, the three
// aliased ones did not.
texts: {
label: '#?components.chronos.label|Calendar',
prevMonth: '#?components.chronos.prev-month|Previous month',
nextMonth: '#?components.chronos.next-month|Next month',
'prev-month': '#?components.chronos.prev-month|Previous month',
'next-month': '#?components.chronos.next-month|Next month',
today: '#?components.chronos.today|Today',
moreEvents: '#?components.chronos.more-events|Show more events'
'more-events': '#?components.chronos.more-events|Show more events',
'new-event': '#?components.chronos.new-event|New event'
},
events: [
// Month navigation (prev / next / today / jump).
@ -202,7 +209,7 @@ export const chronosMorfo = {
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('prevMonth', 'Previous month'),
value: v.translationRef('prev-month', 'Previous month'),
severity: 'recommended'
},
{
@ -226,7 +233,7 @@ export const chronosMorfo = {
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('nextMonth', 'Next month'),
value: v.translationRef('next-month', 'Next month'),
severity: 'recommended'
},
{
@ -251,6 +258,27 @@ export const chronosMorfo = {
{ attr: 'aria-label', value: v.translationRef('today', 'Today'), severity: 'recommended' }
]
},
{
// Opens the search palette (a composed Command inside a Dialog). Its
// own contract is just the trigger's — the palette's anatomy belongs
// to Command and Dialog, which chronos does not re-declare.
name: 'SearchButton',
kebab: 'search-button',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('search', 'Search'),
severity: 'recommended'
}
]
},
{
name: 'UndoButton',
kebab: 'undo-button',
@ -262,7 +290,7 @@ export const chronosMorfo = {
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-label', value: v.commonRef('action.undo', 'Undo'), severity: 'recommended' },
{ attr: 'aria-label', value: v.commonRef('buttons.undo', 'Undo'), severity: 'recommended' },
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -282,7 +310,7 @@ export const chronosMorfo = {
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-label', value: v.commonRef('action.redo', 'Redo'), severity: 'recommended' },
{ attr: 'aria-label', value: v.commonRef('buttons.redo', 'Redo'), severity: 'recommended' },
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -422,6 +450,28 @@ export const chronosMorfo = {
data: [{ attr: 'data-value', value: v.propRef('value'), emit: 'value' }],
aria: []
},
{
// The per-cell "+" create affordance. A real part, not a sprayed hook:
// eidos styles it (reveal on cell hover / focus, hidden when the
// calendar is readonly or disabled) and it is the cell's only create
// control, so a consumer composing a cell composes this too.
name: 'DayAdd',
kebab: 'day-add',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('new-event', 'New event'),
severity: 'recommended'
}
]
},
// ── Event chip + resize handle ─────────────────────────────────────────
{
@ -477,17 +527,84 @@ export const chronosMorfo = {
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('moreEvents', 'Show more events'),
value: v.translationRef('more-events', 'Show more events'),
severity: 'recommended'
}
]
},
// ── Editor (a composed Dialog) ─────────────────────────────────────────
{
// View → edit toggle, pinned to the editor dialog's corner. The editor
// itself is a REUSED Dialog and chronos declares none of its parts —
// but this control is chronos's own, and eidos positions it, so it is
// a part: the sanctioned way to reach into a composed component is
// your OWN namespaced attr, not a class hook (eidos-lint norm S1).
// Same shape as `more-link` inside a composed Popover.
name: 'EditAction',
kebab: 'edit-action',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('edit-event', 'Edit event'),
severity: 'recommended'
}
]
},
// ── Day-peek (the "+N more" popover) ───────────────────────────────────
// Pure display parts: eidos styles them and nothing else reads them, so
// they carry no role / aria and the wrapper stamps the marker inline —
// the sanctioned shape for a structural region that is not a class hook.
{
// The peek's floating surface. It IS the composed `Popover.Content`;
// this marker is chronos's own namespaced hook on it (the sanctioned
// way to out-specify a composed component's recipe).
name: 'PeekPanel',
kebab: 'peek-panel',
kind: 'private',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
// The peek's body inside that panel: the day's all-day strip + the
// windowed hour column.
name: 'DayPeek',
kebab: 'day-peek',
kind: 'private',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
// ── Time-grid (Week / Day views) ───────────────────────────────────────
// `private`: contract-only. The eidos wrapper owns rendering; there is no
// public compound API (composition decision — reuse rides the pure engine,
// promoted to $libs on a 2nd consumer). These declare the ARIA grid + the
// keyboard contract the eidos delivers uniformly with the Month grid.
{
// The Week/Day SHELL — the scroll container that holds the sticky
// head, the all-day strip and the `time-grid` below. Named `time-view`
// and not `timegrid`: one hyphen away from the part below it is a trap,
// and this is the view, not the grid.
name: 'TimeView',
kebab: 'time-view',
kind: 'private',
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-view', values: ['week', 'day'], severity: 'optional' }],
aria: []
},
{
name: 'TimeGrid',
kebab: 'time-grid',

@ -31,9 +31,16 @@ root; the three view bodies are mutually exclusive.
| --- | --- |
| Root | `Provider` |
| Toolbar | `Toolbar` · `PrevButton` · `NextButton` · `TodayButton` · `UndoButton` · `RedoButton` · `Heading` |
| Month grid | `Grid` · `GridHead` · `HeadCell` · `GridBody` · `GridRow` · `DayCell` · `DayNumber` · `EventChip` · `EventResizeHandle` · `MoreLink` |
| Week / day | `TimeGrid` · `AllDayRow` · `DayColumn` · `TimeSlot` · `HourLabel` · `NowIndicator` |
| Month grid | `Grid` · `GridHead` · `HeadCell` · `GridBody` · `GridRow` · `DayCell` · `DayNumber` · `DayAdd` · `EventChip` · `EventResizeHandle` · `MoreLink` |
| Day peek (the `MoreLink` popover) | `PeekPanel` · `DayPeek` |
| Week / day | `TimeView` · `TimeGrid` · `AllDayRow` · `DayColumn` · `TimeSlot` · `HourLabel` · `NowIndicator` |
| Agenda | `Agenda` · `AgendaGroup` · `AgendaItem` |
| Editor (a composed Dialog) | `EditAction` |
`TimeView` is the week/day SHELL — the scroll container holding the sticky head,
the all-day strip and the `TimeGrid` (`role=grid`) inside it. The editor itself
is a reused Dialog and chronos declares none of its parts; `EditAction` is
chronos's own control within it.
## ARIA
@ -48,7 +55,7 @@ Chronos implements the WAI-ARIA
| HeadCell | `columnheader` |
| GridBody | `rowgroup` |
| DayCell · TimeSlot | `gridcell` |
| EventChip · MoreLink · AgendaItem · the five toolbar buttons | `button` |
| EventChip · MoreLink · DayAdd · EditAction · AgendaItem · the five toolbar buttons | `button` |
| AgendaGroup | `group` |
## Keyboard

@ -394,6 +394,18 @@ export class ChronosProvider {
this.announceNav();
this.emitRange();
}
/**
* Jump the frame to an arbitrary date — what a search result needs, and the
* generalisation `goToday` was a special case of. Same bookkeeping: roving
* anchor, announcement, and the `shift-navigate` emission via `emitRange`.
*/
goToDate(date: DateValue): void {
this.calendar.goTo(date);
this.focusedDate = toCalendarDate(date);
this.focusedSlot = null;
this.announceNav();
this.emitRange();
}
// ── Keyboard roving (month grid, ARIA grid pattern) ───────────────────────────
@ -1654,15 +1666,95 @@ export class ChronosDayCellProvider {
);
}
// ── Label-only action buttons ────────────────────────────────────────────────
/**
* Parts that are a real `<button>` whose WHOLE contract is the morfo's — role,
* the `type` literal and a translated `aria-label` — with no per-instance
* source at all: the per-cell "+" create affordance and the editor's
* view→edit toggle.
*
* A class rather than a `Region` for the same reason as the agenda item and
* the more-link: a div-defaulting Region would put the declared `type` on a
* non-button. One class for both (the NavButton pattern) because the shape is
* identical and the parts are two — the abstraction has its consumers.
*/
type ChronosActionPart = 'day-add' | 'edit-action' | 'search-button';
export class ChronosActionProvider {
readonly opts: WithRefOpts;
readonly runtimePart: SomaRuntimePart<typeof chronosMorfo, ChronosActionPart>;
readonly provider: ChronosProvider;
static create(part: ChronosActionPart, opts: WithRefOpts) {
return new ChronosActionProvider(part, opts);
}
private constructor(part: ChronosActionPart, opts: WithRefOpts) {
this.opts = opts;
this.provider = ChronosProvider.require();
this.runtimePart = this.provider.runtime.part(part, {
id: opts.id,
ref: opts.ref,
owner: this
});
}
// Morfo-declared: role, type, the translated aria-label.
readonly props = $derived.by(() => this.runtimePart.renderProps());
}
// ── "+N more" overflow link ──────────────────────────────────────────────────
/**
* A month cell's "+N more" link. Its own class rather than a `Region` with
* sources, for the same reason as the agenda item: it renders a real
* `<button>` — its morfo declares `role` plus the `type` literal — and a
* div-defaulting Region would put that `type` on a non-button, the exact
* mismatch the event chip surfaced.
*/
interface ChronosMoreLinkOpts extends WithRefOpts, ActiveProps<{ count: number }> {}
export class ChronosMoreLinkProvider {
readonly opts: ChronosMoreLinkOpts;
readonly runtimePart: SomaRuntimePart<typeof chronosMorfo, 'more-link'>;
readonly provider: ChronosProvider;
static create(opts: ChronosMoreLinkOpts) {
return new ChronosMoreLinkProvider(opts);
}
private constructor(opts: ChronosMoreLinkOpts) {
this.opts = opts;
this.provider = ChronosProvider.require();
this.runtimePart = this.provider.runtime.part('more-link', {
id: opts.id,
ref: opts.ref,
owner: this,
// `data-count` is a propRef: without this source the attr vanishes
// in silence, the way the time-grid lost its label in this pass.
props: { count: () => this.opts.count.current }
});
}
// Morfo-declared: role, type, the translated aria-label, data-count.
readonly props = $derived.by(() => this.runtimePart.renderProps());
}
// ── Navigation buttons ───────────────────────────────────────────────────────
/**
* Prev / Next / Today share one class: same contract (`role=button`, `type`,
* a translated `aria-label`, `aria-disabled` from a `disabled` source), same
* shape, different part. One class instead of three identical ones — the
* Calendar pagination pattern.
* Prev / Next / Today / Undo / Redo share one class: same contract
* (`role=button`, `type`, a translated `aria-label`, `aria-disabled` from a
* `disabled` source), same shape, different part. One class instead of five
* identical ones — the Calendar pagination pattern.
*/
type ChronosNavPart = 'prev-button' | 'next-button' | 'today-button';
type ChronosNavPart =
| 'prev-button'
| 'next-button'
| 'today-button'
| 'undo-button'
| 'redo-button';
interface ChronosNavButtonOpts extends WithRefOpts, ActiveProps<{ disabled: boolean }> {}

@ -0,0 +1,46 @@
<script lang="ts">
/**
* Soma `Chronos.DayAdd` — the "+" create affordance inside a day cell. A real
* `<button>` (its morfo declares `role: 'button'` plus the `type` literal),
* so the native element is the honest one here.
*/
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { ChronosActionProvider } from '../chronos-provider.svelte';
import type { ChronosActionProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'chronos-day-add'),
children,
child,
...restProps
}: ChronosActionProps = $props();
const state = ChronosActionProvider.create('day-add', {
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
// The morfo declares a translated `aria-label` here, so the resolved bag
// always carries one — `mergeProps` just cannot express that. Asserted at
// this single boundary, which is what lets `IconButton` (it requires a
// label) be composed without the view re-typing what the morfo owns.
const mergedProps = $derived(
mergeProps(restProps, state.props) as Record<string, unknown> & { 'aria-label': string }
);
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,47 @@
<script lang="ts">
/**
* Soma `Chronos.EditAction` — the view→edit toggle pinned in the editor
* dialog's corner. The editor IS a reused Dialog and chronos declares none
* of its parts, but this control is chronos's own and eidos positions it,
* so it is a part — the same shape as `MoreLink` inside a composed Popover.
*/
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { ChronosActionProvider } from '../chronos-provider.svelte';
import type { ChronosActionProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'chronos-edit-action'),
children,
child,
...restProps
}: ChronosActionProps = $props();
const state = ChronosActionProvider.create('edit-action', {
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
// The morfo declares a translated `aria-label` here, so the resolved bag
// always carries one — `mergeProps` just cannot express that. Asserted at
// this single boundary, which is what lets `IconButton` (it requires a
// label) be composed without the view re-typing what the morfo owns.
const mergedProps = $derived(
mergeProps(restProps, state.props) as Record<string, unknown> & { 'aria-label': string }
);
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,48 @@
<script lang="ts">
/**
* Soma `Chronos.MoreLink` — a month cell's "+N more" overflow link. A real
* `<button>` like the agenda item (its morfo declares `role: 'button'` plus
* the `type` literal, and it nests no control), so the native element is
* the honest one here.
*/
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { ChronosMoreLinkProvider } from '../chronos-provider.svelte';
import type { ChronosMoreLinkProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'chronos-more-link'),
count,
children,
child,
...restProps
}: ChronosMoreLinkProps = $props();
const state = ChronosMoreLinkProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
),
count: readableActive(() => count)
});
// Same guarantee as the nav buttons: the morfo declares a translated
// `aria-label` here, so the resolved bag always carries one — `mergeProps`
// just cannot express that. Asserted at this single boundary.
const mergedProps = $derived(
mergeProps(restProps, state.props) as Record<string, unknown> & { 'aria-label': string }
);
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,46 @@
<script lang="ts">
/**
* Soma `Chronos.SearchButton` — opens the search palette. Its contract is
* just the trigger's: the palette's anatomy belongs to the composed
* `Command` + `Dialog`, which chronos does not re-declare.
*/
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { ChronosActionProvider } from '../chronos-provider.svelte';
import type { ChronosActionProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'chronos-search-button'),
children,
child,
...restProps
}: ChronosActionProps = $props();
const state = ChronosActionProvider.create('search-button', {
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
// The morfo declares a translated `aria-label` here, so the resolved bag
// always carries one — `mergeProps` just cannot express that. Asserted at
// this single boundary, which is what lets `IconButton` (it requires a
// label) be composed without the view re-typing what the morfo owns.
const mergedProps = $derived(
mergeProps(restProps, state.props) as Record<string, unknown> & { 'aria-label': string }
);
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -10,6 +10,10 @@ export { default as ResizeHandle } from './components/chronos-resize-handle.svel
export { default as AgendaItem } from './components/chronos-agenda-item.svelte';
export { default as Region } from './components/chronos-region.svelte';
export { default as DayCell } from './components/chronos-day-cell.svelte';
export { default as DayAdd } from './components/chronos-day-add.svelte';
export { default as EditAction } from './components/chronos-edit-action.svelte';
export { default as SearchButton } from './components/chronos-search-button.svelte';
export { default as MoreLink } from './components/chronos-more-link.svelte';
export { default as NavButton } from './components/chronos-nav-button.svelte';
export {
ChronosProvider,
@ -18,6 +22,8 @@ export {
ChronosRegionProvider,
ChronosResizeHandleProvider,
ChronosDayCellProvider,
ChronosActionProvider,
ChronosMoreLinkProvider,
ChronosNavButtonProvider,
chronosAttrs
} from './internals';

@ -6,6 +6,8 @@ export {
ChronosRegionProvider,
ChronosResizeHandleProvider,
ChronosDayCellProvider,
ChronosActionProvider,
ChronosMoreLinkProvider,
ChronosNavButtonProvider,
chronosAttrs,
type ChronosOpts,

@ -187,7 +187,7 @@ export interface ChronosNavButtonProps {
ref?: HTMLElement | null;
id?: string;
/** Which navigation part this button IS. */
part: 'prev-button' | 'next-button' | 'today-button';
part: 'prev-button' | 'next-button' | 'today-button' | 'undo-button' | 'redo-button';
disabled?: boolean;
children?: Snippet;
/**
@ -242,6 +242,37 @@ export interface ChronosRegionProps {
[dataAttr: `data-${string}`]: unknown;
}
/**
* `Chronos.DayAdd` / `Chronos.EditAction` — the two label-only action buttons.
* Their whole contract is the morfo's, so they take no props of their own.
*/
export interface ChronosActionProps {
ref?: HTMLElement | null;
id?: string;
children?: Snippet;
/**
* Like the nav parts, the morfo declares a translated `aria-label` here, so
* the bag handed to a composed control is GUARANTEED to carry one.
*/
child?: Snippet<[{ props: Record<string, unknown> & { 'aria-label': string } }]>;
[dataAttr: `data-${string}`]: unknown;
}
/** `Chronos.MoreLink` — a month cell's "+N more" overflow link. */
export interface ChronosMoreLinkProps {
ref?: HTMLElement | null;
id?: string;
/** How many events the cell hides — lands on `data-count`. */
count: number;
children?: Snippet;
/**
* Like the nav parts, the morfo declares a translated `aria-label` here, so
* the bag handed to a composed control is GUARANTEED to carry one.
*/
child?: Snippet<[{ props: Record<string, unknown> & { 'aria-label': string } }]>;
[dataAttr: `data-${string}`]: unknown;
}
/** `Chronos.AgendaItem` — one event row of the Agenda view. */
export interface ChronosAgendaItemProps {
ref?: HTMLElement | null;

Loading…
Cancel
Save

Powered by TurnKey Linux.