---
url: /guide/getting-started.md
---
# Getting Started
Set up the Cocoar Design System in your Vue 3 project in a few steps.
## 1. Install
```bash
pnpm add @cocoar/vue-ui
```
## 2. Import Fonts & Styles
Import fonts and styles in your app's entry point. Fonts are self-hosted via `@fontsource` — no external CDN needed.
```ts
// main.ts
import '@cocoar/vue-ui/fonts'; // Poppins + Inter (self-hosted)
import '@cocoar/vue-ui/styles'; // Design tokens + component styles
```
::: info Bring your own fonts?
The font import is optional. If you prefer a CDN or custom fonts, skip `@cocoar/vue-ui/fonts` and load them yourself. Components fall back to system fonts gracefully.
:::
## 3. Use Components
Import components directly — no global registration required. Tree-shaking is automatic.
```vue
Hello Coar
```
## 4. Dark Mode
Toggle dark mode by adding the `.dark-mode` class to the root element. All design tokens and components adapt automatically.
```ts
document.documentElement.classList.toggle('dark-mode', isDark);
```
## 5. Overlay System
For components that render overlays (Dialog, Toast, Popover, Tooltip), register the plugin once:
```ts
// main.ts
import { createApp } from 'vue';
import { CoarOverlayPlugin } from '@cocoar/vue-ui';
createApp(App)
.use(CoarOverlayPlugin)
.mount('#app');
```
And add the overlay host to your root layout:
```vue
```
## Date/Time Components
The date and time pickers use the [Temporal API](https://tc39.es/proposal-temporal/docs/) via `@js-temporal/polyfill`, which is included as a dependency of `@cocoar/vue-ui`. No extra install needed. When native Temporal support reaches all browsers, the polyfill can be dropped in a future major release.
## Additional Packages
Optional packages for extended functionality:
```bash
pnpm add @cocoar/vue-localization # i18n & timezone
pnpm add @cocoar/vue-data-grid # AG Grid wrapper
pnpm add @cocoar/vue-markdown # Markdown viewer
```
---
---
url: /guide/error-handling.md
---
# Error Handling
Cocoar UI components are designed to fail gracefully. Internal errors are either caught and recovered silently, or surfaced to the user through controlled feedback mechanisms. This guide explains the patterns used throughout the library and how to handle errors in your own application code.
## Library Philosophy: Fail Gracefully
Components never throw unhandled exceptions into your application. Instead they follow one of two patterns:
* **Silent fallback** — return a safe default (`null`, `'UTC'`, `false`) and continue
* **User feedback** — surface the error visibly via a Toast or state change
## Overlay Promises
`CoarDialog` and `CoarPopconfirm` return Promises. Always handle the rejection case:
```ts
import { useDialog } from '@cocoar/vue-ui';
const dialog = useDialog();
// ✅ Always add .catch()
dialog.confirm({
title: 'Delete item',
message: 'This cannot be undone.',
})
.then((confirmed) => {
if (confirmed) deleteItem();
})
.catch(() => {
// Dialog was closed unexpectedly (e.g. overlay destroyed before user responded)
});
```
With async/await:
```ts
try {
const confirmed = await dialog.confirm({ title: 'Delete item', message: '...' });
if (confirmed) await deleteItem();
} catch {
// Handle unexpected close
}
```
::: tip Popconfirm
`CoarPopconfirm` emits `@confirmed` and `@cancelled` events — no Promise handling needed there. Use it for simple inline confirmations, and reserve `useDialog()` for programmatic flows where error handling is more important.
:::
## Toast for Error Feedback
Use `useToast().error()` to surface errors to the user. Error toasts are persistent by default (duration `0`) — they stay until the user dismisses them, which is appropriate for errors that require attention.
```ts
import { useToast } from '@cocoar/vue-ui';
const toast = useToast();
async function saveData() {
try {
await api.save(payload);
toast.success('Saved successfully');
} catch (err) {
toast.error('Save failed', {
message: err instanceof Error ? err.message : 'Please try again.',
});
}
}
```
```ts
// With a retry action
toast.error('Connection lost', {
message: 'Could not reach the server.',
action: {
label: 'Retry',
callback: () => saveData(),
},
});
```
## Date and Time Parsing
Date parsing functions return `null` on failure instead of throwing. Always null-check the result before using it:
```ts
import { coarParsePlainDate } from '@cocoar/vue-ui';
const date = coarParsePlainDate(userInput);
if (date === null) {
// Input was invalid — show validation error
toast.error('Invalid date format');
return;
}
// date is a Temporal.PlainDate — safe to use
processDate(date);
```
The date picker components handle this internally — invalid input simply doesn't update the model value. Your `v-model` will remain `null` until the user enters a valid date.
## Timezone Fallbacks
Timezone utilities default to `'UTC'` when the browser API fails or the timezone identifier is unrecognised. This keeps date/time components functional even in restricted environments:
```ts
import { useTimezone } from '@cocoar/vue-localization';
const { timezone } = useTimezone();
// Always a valid IANA identifier — 'UTC' as last resort
```
## Async Operations in Overlays
When loading data inside a Dialog or Popover, manage loading and error states yourself:
```vue
```
## Pattern Summary
| Situation | Recommended pattern |
|-----------|---------------------|
| Dialog/Popconfirm result | `.then().catch()` or `try/await/catch` |
| API call in component | `try/catch` + `toast.error()` |
| Date input validation | Null-check return value of parse functions |
| Non-recoverable error | `toast.error()` with persistent duration (default) |
| Recoverable error | `toast.error()` with `action: { label: 'Retry', callback }` |
| Silent failures OK | Rely on library defaults (`null`, `'UTC'`, `false`) |
---
---
url: /guide/theming.md
---
# Theming
Cocoar uses an **oklch-based** color system. You set a few base colors, and the entire palette — including all shades for light and dark mode — is auto-calculated.
## Quick Start
Override the CSS custom properties on `:root` to match your brand:
```css
:root {
--coar-accent: #1183CD; /* Your brand color → primary buttons, links, focus rings */
}
```
That's it. All accent shades (50–900), in both light and dark mode, recalculate from this single value.
## Customizable Base Colors
| Variable | Default | Purpose |
|----------|---------|---------|
| `--coar-accent` | `#1183CD` | Brand/accent color — primary buttons, active states, links |
| `--coar-success` | `#1e8f48` | Success states — confirmations, positive feedback |
| `--coar-error` | `#d63b3b` | Error states — validation errors, destructive actions |
| `--coar-warning` | `#cc821f` | Warning states — caution, attention needed |
| `--coar-info` | `#5e6b84` | Info states — neutral informational context |
### Example: Red Brand
```css
:root {
--coar-accent: #C41E3A; /* Red brand */
--coar-error: #8B0000; /* Darker red so errors are distinguishable */
}
```
### Example: Purple Brand
```css
:root {
--coar-accent: #7C3AED;
}
```
## How It Works
Each base color generates a 10-step shade scale using **oklch relative color syntax**:
```css
/* You set this: */
--coar-accent: #1183CD;
/* The library calculates these: */
--coar-color-accent-50: oklch(from var(--coar-accent) 0.97 0.012 h); /* lightest */
--coar-color-accent-100: oklch(from var(--coar-accent) 0.92 0.035 h);
--coar-color-accent-200: oklch(from var(--coar-accent) 0.84 0.075 h);
/* ... */
--coar-color-accent-500: var(--coar-accent); /* = your exact color */
/* ... */
--coar-color-accent-900: oklch(from var(--coar-accent) 0.31 0.095 h); /* darkest */
```
The `h` (hue) is extracted from your color. Lightness and chroma follow a designed curve that keeps shades vibrant instead of washed out. `accent-500` is always your exact brand color.
This works because **oklch is perceptually uniform** — unlike HSL, a lightness of 0.5 in oklch looks equally "medium" for blue, red, and yellow.
## Fine-Tuning Individual Shades
If an auto-calculated shade doesn't look right for your specific color, override it:
```css
:root {
--coar-accent: #FF6600;
/* Auto-calculated 50 too warm? Override just that one: */
--coar-color-accent-50: #FFF5EB;
}
```
## Dark Mode
Dark mode shades are calculated from the same base variables — no need to set anything extra. The library uses a separate lightness/chroma curve designed for dark backgrounds:
* Low numbers (50–200): dark with a subtle color tint
* Mid range (300–500): vibrant and saturated
* High numbers (600–900): lighter for text on dark backgrounds
The primary button color (`accent-500`) stays identical in both modes.
## Browser Support
The oklch color system requires:
* Chrome 119+
* Firefox 128+
* Safari 18+
This covers all modern browsers. For older browsers, consider providing hex fallbacks for your specific brand color.
---
---
url: /guide/changelog.md
---
# Changelog
All notable changes to the Cocoar Design System (Vue) will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versions are calculated automatically by [GitVersion](https://gitversion.net/).
***
## 2.0.0
This release lands the calendar package's largest feature push since it shipped in 1.16.0 — the C8 recurrence pipeline is wired end-to-end, three previously-reserved or missing view IDs (`workWeek`, `timeline`) become real working components, and the event-interaction surface picks up hover handlers so consumers can wire their own popovers / tooltips via `@cocoar/vue-ui`'s overlay system. None of the changes are strictly breaking — the new shapes are additive — but the surface delta is large enough that a major bump is the honest signal. `@cocoar/vue-data-grid`, `@cocoar/vue-script-editor`, `@cocoar/vue-markdown-editor`, `@cocoar/vue-fragment-parser`, and `@cocoar/vue-ui` ride along on the same monorepo cadence; only one user-visible UI fix in this release (popover-preset export gap).
### Added
* **`@cocoar/vue-ui` — `CoarPlainDateView` / `CoarPlainDateTimeView` / `CoarZonedDateTimeView` display components**: read-only Temporal-typed date / date-time / zoned-date-time displays paired with the existing picker family. Each viewer mirrors its picker's `formatValue` logic exactly (same `useDatePickerBase` for locale + date-format resolution, same `coarFormatPlainDate` + `coarFormatTime` + `coarFormatTimezoneLabel` helpers) so a read-only display and the editor's resting state look identical. Props: `value`, `locale?`, `dateFormat?`, `placeholder?`, `size?`; `CoarPlainDateTimeView` adds `use24Hour?: boolean | 'auto'` (defaults to locale detection); `CoarZonedDateTimeView` adds `displayTimeZone?: string` (project all values into a single zone), `showTimeZone?: boolean` (toggle the trailing `GMT+1`-style label), and `use24Hour?`. **Cross-realm-safe type checks**: each viewer uses `Symbol.toStringTag` instead of `instanceof Temporal.X`, so a Temporal value created against one polyfill copy (e.g. inside `@cocoar/vue-ui`) still renders correctly when read by another package that resolves a different `@js-temporal/polyfill` path under pnpm's isolated dependency tree. **Reactive locale**: `useDatePickerBase` already tracks the consumer-app `useL10n().language` ref, so display updates on language change without consumer wiring. Use these anywhere you'd show a date without an editor — cards, dialogs, list rows, data-grid cells. 18 new unit tests across the three viewer components (rendering, format resolution, null handling, cross-realm duck-type acceptance, displayTimeZone projection). Total `@cocoar/vue-ui` suite 1211 tests across 60 files.
* **`@cocoar/vue-data-grid` — `col.plainDate()` / `col.plainDateTime()` / `col.zonedDateTime()` column shortcuts**: three new Temporal-typed column factories for date / date-time / zoned-date-time cells. Each pairs a locale-aware renderer (formats via `toLocaleString` with date-style: medium; `plainDateTime` adds time-style: short; `zonedDateTime` adds short zone-name suffix so cross-zone columns stay unambiguous at a glance) with an editor that wraps the matching picker from `@cocoar/vue-ui` (`CoarPlainDatePicker` / `CoarPlainDateTimePicker` / `CoarZonedDateTimePicker`). Cell values are `Temporal.PlainDate | null` / `Temporal.PlainDateTime | null` / `Temporal.ZonedDateTime | null` — strict typing, matching `@cocoar/vue-calendar`'s contract so dates round-trip between the grid and the calendar without conversion shims. Configurators expose the middle-tier picker surface: `.size()`, `.clearable()`, `.min()`, `.max()`, `.showWeekNumbers()`, `.highlightWeekends()`, `.markers()` (static or per-row function), `.locale()`. `col.zonedDateTime()` additionally exposes `.timeZone()` (default IANA zone for newly-created values; existing values keep their own zone) and `.timezoneFilter()` (wildcard patterns like `['Europe/*', 'America/*']`). Editor lifecycle matches the other Coar cell editors: `afterGuiAttached` focuses the trigger so the picker's keyboard handlers fire (arrow-key scrolls-page bug fixed at the source), focus-preservation prevents AG Grid from committing prematurely while the user navigates the body-teleported panel, commit happens via `getValue()` on Tab / Enter / click-outside. The legacy `col.date(field, config?)` shortcut (display-only, accepts `Date | string`) is unchanged for back-compat. `@js-temporal/polyfill` is now a peer dependency of `@cocoar/vue-data-grid`. 11 new factory tests; total data-grid suite 258 tests across 8 files. Docs page at `/components/data-grid/date-columns` with three live demos (PlainDate task scheduling, PlainDateTime reminders, ZonedDateTime cross-zone meetings).
* **`@cocoar/vue-data-grid` — `col.multiSelect(field, s => …)` and `col.tagSelect(field, s => …)` column shortcuts**: two new factory methods for multi-value cells. Both store the cell value as `T[]` and share one renderer (`CoarMultiSelectCellRenderer`) — comma-separated label lookup by default, `.display('chips')` opts into one `` per value. `col.multiSelect()` editor wraps `` (checkbox-list dropdown, `.searchable()` / `.showSelectAll()` / `.clearable()` available); `col.tagSelect()` editor wraps `` (chip-style trigger, dropdown shows only not-yet-selected, `.allowCreate()` accepts free-form values that round-trip into the array verbatim — the renderer falls back to `String(value)` for unknown labels). Both editors auto-open via `afterGuiAttached` and use focus-preservation (capture-phase `mousedown` listener that `preventDefault`s on `.coar-overlay-host` targets) so the dropdown stays open while the user toggles options; unlike `col.select()`'s auto-commit-per-pick, AG Grid only commits when the user finishes (click outside / Tab / Enter — `getValue()` returns the final array). Row-aware options work via `s.options(row => …)`. Configurators (`MultiSelectColumnConfigurator`, `TagSelectColumnConfigurator`) export from the package root; `MultiSelectCellEditorConfig` is the shared editor params type. 11 new factory tests; total data-grid suite 247 tests across 8 files. Docs page at `/components/data-grid/multi-select` with live demo (both variants side-by-side).
* **`@cocoar/vue-ui` — `CoarOtpInput` component**: N-cell input for 2FA / TOTP / SMS verification codes — the pattern where each digit lives in its own box, focus auto-advances on type, jumps back on Backspace-empty, and pastes spread across cells. Replaces the long-standing pain of using a regular `CoarTextInput` for 6-digit codes (typing fast, then Enter or mouse-to-OK). Fires a `complete(value)` event the moment the last cell fills so consumers can auto-submit without an OK button. `length` prop (default `6`) covers 4-digit PINs and 8-digit backup codes alike; `type: 'numeric' | 'alphanumeric' | 'text'` (default `'numeric'`) drives both keystroke filtering and the mobile keyboard hint via `inputmode`; `mask` renders cells as `` for shared-screen contexts. Sizes match the form-input family (`xs` / `s` / `m` / `l`) and the component picks up `error` + `required` automatically when wrapped in `CoarFormField` via the existing FORM\_FIELD\_INJECTION\_KEY pattern. First cell carries `autocomplete="one-time-code"` so iOS / Android offer the SMS-autofill chip on the right cell. Keyboard model: Type → fill + advance, Backspace on empty → jump-back-and-clear, Backspace on filled → clear, Delete → clear in place, Arrows + Home/End → navigate, Tab → exits the group (the OTP input is effectively one tab-stop). Paste handler accepts a multi-character clipboard payload starting at any cell and spreads it forward, stripping characters that don't match `type` first. Two optional hooks fine-tune per-character behaviour beyond the built-in classes: `transform: (char) => string` rewrites or drops a character before commit (e.g. `c => c.toUpperCase()` so users can type lowercase claim codes), and `accept: (char) => boolean` adds a per-char reject predicate that ANDs with `type` (e.g. `c => !/[O0lI1]/.test(c)` to block visually-ambiguous chars in printed claim codes). Both hooks fire on single-char input AND on every char of paste-spread — paste runs through the same sanitizer. ARIA: `role="group"` with `aria-label="Verification code, N digits"`, each cell has `aria-label="Digit i of N"`, `aria-invalid` propagates from the error state. 24 unit tests cover the full surface (cell rendering, v-model in/out, numeric filtering, Backspace jump-back, Delete clear, paste spread, complete event, disabled / readonly / error states). Lives at `packages/ui/src/components/otp-input/`; types `CoarOtpInputProps`, `CoarOtpInputSize`, `CoarOtpInputType` exported from the package root. Docs page at `/components/otp-input` with five live demos.
* **`@cocoar/vue-calendar` — `onEventHover` / `onEventHoverLeave` handlers**: fire on `pointerenter` / `pointerleave` over any event element in any view (day / week / workWeek / month / agenda / timeline). Payload mirrors `onEventClick`: `{ event, native: PointerEvent }`. `native.currentTarget` is the event-element DOM node — pass it directly to `useOverlay({ anchor: { kind: 'element', element: native.currentTarget } })` from `@cocoar/vue-ui` to anchor a popover. The library deliberately does NOT ship a built-in popover (consumers want different shape: title-only tooltip vs full action menu vs edit-in-place panel) and does NOT apply hover delay (wrap with `setTimeout(..., 200)` if needed). Companion playground demo at `/calendar-popover` wires the pattern end-to-end.
* **`@cocoar/vue-calendar` — `'timeline'` view (Gantt-lite)**: one-row-per-logical-event horizontal time-axis layout for project-plan rendering. Date-axis and day-grid cells use `box-sizing: border-box` so the 1px right border doesn't add to the cell's flex width — bars (absolutely positioned at `left = days × pixelsPerDay`) stay aligned with their date columns indefinitely (without the fix, bars drifted left by 1px per day, accumulating to 21px after three weeks). Default bar render is a coloured rectangle without inline text — the row label on the left is the title source-of-truth; bars are pure timeline geometry. Consumers wanting inline bar text use the `#bar` slot. **Row virtualization built in** — labels and bars only render for rows in the viewport + 8-row buffer; a 1000-task project plan costs ~30-40 DOM rows regardless of total count. Uniform `rowHeight` makes the visible-range math `O(1)` (no per-row measurement); slot renderers only fire for visible rows. Companion playground page at `/calendar-timeline-perf` benches the workload at 100 / 500 / 1 000 / 2 500 tasks. **Recurring series collapse to one row** with N bars (one per occurrence in the window) instead of N separate rows — a weekly standup with 26 occurrences renders as ONE "Standup ×26" row, not 26 stacked "Standup" entries. Grouping key is `meta.__recurrence.seriesId` for recurring events, `event.id` for standalone — automatic, consumer code unchanged. New `` component, `useTimelineView()` composable, and the previously-reserved `'timeline'` value in `CalendarView` is now live. Left pane lists event labels (`meta.title ?? event.id`); right pane renders each event as a bar positioned by `[start, end)` against the visible window. **Excel-style frozen-pane layout** — date axis sticks at top during vertical scroll, label column sticks at left during horizontal scroll, top-left corner sticks at both. Single scroll container with pure CSS `position: sticky` + CSS Grid; no JavaScript scroll-sync. **Click-and-drag pan mode** — drag anywhere on non-interactive area to scroll both axes at once (`cursor: grab` / `grabbing` affordance, pointer-capture so pan continues when cursor leaves the element, `touch-action: none` for matching tablet behavior). Bar clicks bypass the pan handler via interactive-element detection. Configurable density via four setters: `timelineRangeDays(n)` (default 60 days — also the prev/next step), `timelinePixelsPerDay(p)` (default 56 — sized so a localized "DD. Mon" label fits on one line), `timelineRowHeight(h)` (default 32), `timelineLabelWidth(w)` (default 200). Pure layout math at `layoutTimeline(events, options)` exported from the package root for custom implementations and tests; one row per event sorted by start asc + id-asc tie-break, bars clamped to `[windowStart, windowEnd)` with `clippedStart` / `clippedEnd` flags. Cross-zone timed events project into the display zone for visual layout (a meeting at 23:00 Tokyo viewed in Vienna lands on the correct visual day). Three slots (`label`, `bar`, `dateHeader`) override the defaults with the full row geometry available. Scope-bounded — task hierarchy, dependencies, critical-path computation, milestones, and resource lanes are out of scope here; those land in a future `@cocoar/vue-gantt` package that builds on the same primitive. New i18n key `coar.calendar.view.timeline` (default label "Timeline"). Added to the default `availableViews` so the shell's view-switcher exposes it. Dedicated docs page at `/components/calendar/timeline-view`.
* **`@cocoar/vue-calendar` — `'workWeek'` view**: working-days subset of the week view. New `` component, `useWorkWeekView()` composable, and the `'workWeek'` value joins `CalendarView` after `'week'`. Configure the working-day set via `builder.workDays(MaybeRefOrGetter)` — default Mon–Fri (`[1, 2, 3, 4, 5]` using the 0 = Sun … 6 = Sat convention), overridable for 6-day operations (Mon–Sat), 4-day weeks (Mon–Thu), or Middle-East Sun–Thu work weeks. The visible-range `ViewWindow` stays the full Mon–Sun span (so `eventsLoader` / `seriesLoader` see weekend events the same way they do for the week view); only the rendered columns differ. Navigation steps by 7 days (the workday filter is purely a render concern, not a navigation one — stepping by 5 would leave the cursor on a weekend on alternate clicks). Added to the default `availableViews` list so the shell's view-switcher gets a "Work week" button automatically (consumers hide it by setting their own `availableViews`). New i18n key `coar.calendar.view.workWeek` (default label "Work week"). Pure helper `workWeekDates(date, firstDayOfWeek, workDays)` exposed from `core/index.ts` for consumer-side use. Dedicated docs page at `/components/calendar/work-week-view` documents the working-day conventions, navigation semantics, and the window-vs-render-set distinction (loaders see weekends, columns don't render them); matches the per-view doc-page pattern established by Day / Week / Month / Agenda.
* **`@cocoar/vue-calendar` — recurring events end-to-end (Phase 4 of the C1–C8 architecture)**: the C8 typed-throwing-stub `expandSeries` that shipped with the package in 1.16.0 is now a working engine. Two new builder setters mirror the non-recurring event pipeline: `builder.series(MaybeRefOrGetter[]>)` binds a reactive in-memory series source (mutating the array re-expands), and `builder.seriesLoader((window: ViewWindow) => RecurringSeries[] | Promise)` binds a calendar-managed per-window async loader (cached by `${view}|${timezone}|${start}|${end}` like `eventsLoader`). Both compose with `events()` / `eventsLoader()` — `api.getVisibleEvents()` returns the merged set. `series()` and `seriesLoader()` are mutually exclusive (calling one clears the other), independent of the events pair. Expansion happens lazily per visible window, never speculatively — a series with `FREQ=DAILY` from year 2000 doesn't pay 25 years of expansion to render today's calendar.
* **`@cocoar/vue-calendar/recurrence` — public subpath**: standalone `expandSeries(series, window, dstPolicy, engine?)` function for non-builder use (server-side pre-expansion, custom views, deterministic tests). Async — engines are async by contract so worker-backed implementations fit the same shape. Lives at a subpath so apps that don't use recurrence don't pull the engine into their main bundle (the subpath chunk is ~7 KB; the bundled `rrule-temporal` adapter ~4 KB; both lazy-loaded on first call). Re-exports `RecurringSeries`, `RecurrenceExpansionWindow`, `RecurrencePattern` types and the `getRecurrenceMeta(event)` provenance accessor.
* **`@cocoar/vue-calendar/recurrence-rrule-temporal` — bundled engine adapter**: pure-JS, Temporal-native, wraps `rrule-temporal` (~1.5K LOC) for the canonical RFC-5545 RRULE feature set. No WASM, no worker, SSR-clean. Construction is cheap; dynamic-imported by the lazy default in `recurrence/`. Apps with extreme volume or specialized needs (alternative parsers, backend-delegated expansion) implement the `RecurrenceEngine` interface in consumer code and register via `builder.recurrenceEngine(custom)` — no library change required.
* **`@cocoar/vue-calendar` — `builder.recurrenceEngine(engineOrFactory)`**: override the bundled engine per builder. Accepts a `RecurrenceEngine` instance or a `() => RecurrenceEngine` factory (the factory form is the SSR escape — engines are constructed only on first client-side use). Intentionally NOT C7-reactive (mid-session swap has no sensible semantics — in-flight requests, worker lifecycle, cache coherency). Replacing the engine clears the resolved-engine cache and the series cache so the next visible-range read re-expands through the new engine.
* **`@cocoar/vue-calendar` — `RecurringSeries` public type**: Temporal-typed `dtstart` (`ZonedDateTime` for timed, `PlainDate` for all-day), RFC-5545 `rrule` string, optional `duration` (`{ minutes?, hours? }` for timed; `{ days? }` for all-day — D2 day-count semantics, no `Period`), optional `rdate` / `exdate` arrays of the matching Temporal type. ISO strings, native `Date`, floating `Temporal.PlainDateTime` rejected at the boundary; mixed `ZonedDateTime` / `PlainDate` in `rdate`/`exdate` against the series' `dtstart` shape throws with the series id named.
* **`@cocoar/vue-calendar` — `SeriesLoader` type**: mirrors `EventsLoader`. Re-exported from the package root for ergonomic single-import.
* **`@cocoar/vue-calendar` — per-occurrence provenance**: every expanded `CalendarEvent` carries `meta.__recurrence: { seriesId, recurrenceId, source: 'rrule' | 'rdate' }`, read via the new public `getRecurrenceMeta(event)` accessor exported from `@cocoar/vue-calendar/recurrence`. Returns `null` for non-recurring events. `recurrenceId` matches RFC-5545 `RECURRENCE-ID` semantics — the original wallclock slot — so future single-instance edits (override or cancel one occurrence) are addressable without changing the `CalendarEvent` shape. The `__` prefix marks the key as library-managed; consumer code reads it through the accessor, not directly.
* **`@cocoar/vue-calendar` — `DstPolicy` enforced uniformly across every recurring occurrence**: the same `'compatible' | 'reject' | 'earlier' | 'later'` union the drag pipeline uses (C4). After the engine returns instants, every timed rule-generated occurrence is re-resolved via `Temporal.PlainDateTime.toZonedDateTime` with the configured disambiguation, using the series' source zone — engine-swap invariance is structural, not advisory. Observable output depends only on `(intended wallclock, source zone, dstPolicy)`, never on which engine ran underneath. `'reject'` throws `DstResolutionError` on the first gap/overlap with the series id and offending wallclock in the message. All-day series and RDATE-originated occurrences pass through (no DST involvement, no library-imposed override of consumer intent). The `detectDstSituation` primitive moved from private to `core/index.ts` export so the drag pipeline and the recurrence pipeline share one source of truth.
### Fixed
* **`@cocoar/vue-data-grid` — popup cell editors survive nested-overlay interaction (zone-select inside date-picker panel)**: clicking a body-teleported overlay inside an already-open editor overlay (e.g. the timezone-`CoarSelect` inside the `` panel, which itself lives in an AG Grid cell editor) closed the outer panel mid-interaction. Root cause: `focusout` events fired on the editor root with `relatedTarget` pointing to the nested overlay element, bubbled up through the cell DOM, and triggered AG Grid's `stopEditingWhenCellsLoseFocus` commit → editor unmounted → both overlays destroyed. The existing `mousedown` capture-phase guard (`preventDefault` for `.coar-overlay-host` targets) was insufficient because some element types still emit a focus shift despite the prevented default. New shared composable `usePopupEditorFocusGuard(rootRef)` installs a second guard: a `focusout` listener on the editor root that calls `stopPropagation` when the `relatedTarget` is inside `.coar-overlay-host`. AG Grid never sees the cell-focus-loss for focus shifts into body-teleported overlay panels. Applied to all six popup-style cell editors: `CoarSelectCellEditor`, `CoarMultiSelectCellEditor`, `CoarTagSelectCellEditor`, `CoarPlainDateCellEditor`, `CoarPlainDateTimeCellEditor`, `CoarZonedDateTimeCellEditor`. The text + number editors don't use the guard — their inputs are in-cell, no body teleport.
* **`@cocoar/vue-data-grid` — date-column renderers refactored as thin wrappers around `@cocoar/vue-ui` viewer components**: the three renderers (`CoarPlainDateCellRenderer`, `CoarPlainDateTimeCellRenderer`, `CoarZonedDateTimeCellRenderer`) previously did all formatting inline using `toLocaleString` + `instanceof Temporal.X` checks against their own polyfill copy. Two symptoms surfaced from real-world use: (1) **editing a `zonedDateTime` cell wouldn't update the renderer's display** — the picker (in `@cocoar/vue-ui`) constructed `Temporal.ZonedDateTime` instances against its own polyfill copy, the renderer's `instanceof Temporal.ZonedDateTime` from its own copy rejected them, and the renderer fell through to empty string. (2) **Locale changes only partially propagated** — `toLocaleString` reacts to the BCP-47 language tag, but the pickers (and the rest of the date-time family) format via a locale-resolved date-format pattern from `useDatePickerBase`, so consumer locale switches that updated the format pattern bypassed the renderer's BCP-47-only formatter. Both bugs fixed at the source: renderers now embed the matching `` / `` / ``, which use cross-realm-safe `Symbol.toStringTag` checks and the same `useDatePickerBase` reactive resolution as the pickers. Cell editors got the same toStringTag fix for their initial-value type-check (same potential cross-polyfill failure mode on edit-mode entry). New renderer-only `displayTimeZone` config on `col.zonedDateTime()` (`.displayTimeZone('Europe/Vienna')`) projects every row into a single zone for cross-zone coordination views.
* **`@cocoar/vue-ui` — overlay-preset export gap closed**: `popoverPreset`, `datepickerPreset`, `subFlyoutPreset`, `contextMenuPreset`, and `sidebarFlyoutPreset` were declared in `components/overlay/overlay-presets.ts` and re-exported by the internal `components/overlay/index.ts`, but missing from the public package barrel — only 7 of the 12 presets were reachable. Consumer code calling `import { popoverPreset } from '@cocoar/vue-ui'` crashed with `SyntaxError: The requested module does not provide an export named 'popoverPreset'`. Caught when wiring the calendar popover demo. Five exports added to the public barrel; existing preset exports unchanged.
* **`@cocoar/vue-data-grid` — select cell editors focus the trigger on edit-mode entry**: `CoarSelectCellEditor`, `CoarMultiSelectCellEditor`, and `CoarTagSelectCellEditor` previously only `click()`-ed the trigger in `afterGuiAttached` to auto-open the dropdown — focus stayed on the AG Grid cell wrapper. The trigger's `@keydown` handler (Arrow Up / Arrow Down navigation, Enter commit) therefore never fired; arrow keys bubbled to the document and **scrolled the page** instead of moving through the option list. All three editors now call `trigger.focus()` after the click. `tabindex="0"` on the trigger (already present) makes the focus call valid. For `.searchable()`, `CoarSelect.onTriggerClick` schedules a `nextTick` focus on the inline `` — that input's own `@keydown` delegates to the same handler, so search-mode keyboard navigation continues to work.
* **`@cocoar/vue-calendar` — views read the merged event source** (was: only `state.events`): ``, ``, `` previously read `state.events ? toValue(state.events) : []` directly, bypassing the loader cache (silent for `eventsLoader`-mode consumers) and — once Phase 4 landed — the series cache (recurring events counted in `getVisibleEvents()` but never rendered). All three views now read via `props.builder.api.getVisibleEvents()`. Caught during browser hand-test via chrome-devtools on the `/calendar-recurrence` playground page: stats panel said 22 events visible, only 1 (the one-off) rendered as a pill.
* **`@cocoar/vue-calendar` — recurring occurrences no longer collapse to one pill in the month view**: `layoutMonthGrid` dedupes events by `event.id` — multiple expanded occurrences sharing the series id collapsed to a single rendered pill (12 of 13 weekly standups silently dropped). Each occurrence now carries a unique synthetic `${seriesId}__${recurrenceId}` id; series identity moved to `getRecurrenceMeta(event).seriesId`. Public contract reflects the unique-id shape; tests assert no duplicates in expansion output.
### Internal
* **`@cocoar/vue-calendar` — adapter-pattern topology with one bundled engine**: the `RecurrenceEngine` interface lives at `src/recurrence/types.ts` with structured wire types (`EngineRequest` / `EngineResponse` use plain numeric components + per-endpoint `tzid`, no string round-trips). The bundled adapter at `src/recurrence-rrule-temporal/` is the only place that imports `rrule-temporal`. ESLint `no-restricted-imports` rule enforces the topology: `rrule-temporal` outside `recurrence-rrule-temporal/**` is a lint error. The decision to ship one engine (not the planned rrule-rust + rrule-temporal pair) follows from the engine-baseline divergence found during cross-engine bake-off: rrule-rust's DST and per-RDATE-zone semantics differed from rrule-temporal's in ways that couldn't be hidden behind the post-processing normalizer without losing information. Apps that need rrule-rust performance plug their own adapter via `builder.recurrenceEngine(...)`.
* **`@cocoar/vue-calendar` — race-safe series expansion**: `_inFlightSeriesKeys: Set` keyed by `windowKey` blocks duplicate dispatches when `[SET_VISIBLE_RANGE]` and the post-flush series watcher both trigger expansion for the same window in close succession. Generation counter (`_seriesGeneration`) discards stale results on cache invalidation (`refresh()`, engine swap, dstPolicy change, source replacement). The initial-set watcher guard (skip when `state.series` transitions from `null` to a value, since `[SET_VISIBLE_RANGE]` is the canonical trigger for that case) prevents a self-DoS where setting series before setVisibleRange would leak in-flight chains.
* **`@cocoar/vue-calendar` — `expandSeries` lazy-imported by the builder**: dynamic `import('../recurrence/index')` in `_runSeriesExpansion` keeps the recurrence chunk out of the main bundle for apps that never use series. `dist/index.js` stays at ~133 KB (was 128 KB before Phase 4 — +5 KB for the new builder methods, no engine code).
* **`@cocoar/vue-calendar` — 32 new unit tests across 4 files**: `recurrence-public.test.ts` (expansion correctness for timed + all-day, source-zone preservation, EXDATE / RDATE behavior, provenance), `dst-resolve.test.ts` (all four `DstPolicy` values against spring-forward + fall-back in `Europe/Vienna`, RDATE pass-through, all-day pass-through, cross-zone Tokyo-in-Vienna), `recurrence-engine-setter.test.ts` (builder API + factory form), `series-pipeline.test.ts` (reactive series source, seriesLoader caching, mutual exclusivity, composition with `events()`, recurrence-engine swap invalidation, dstPolicy change invalidation, `refresh()` / `refreshRange()`, loading flag accounting). Total calendar suite: 634 unit tests across 44 files (was 602 baseline).
### Docs
* **`docs(calendar)` — new "Recurring events" section** in `apps/docs/components/calendar/coar-calendar.md`: covers the `RecurringSeries` shape and Temporal-only contract, the two source modes (`series()` reactive and `seriesLoader()` cached), `getRecurrenceMeta()` provenance access, the standalone `expandSeries(series, window, dstPolicy, engine?)` entry, the `RecurrenceEngine` interface for custom engines (with SSR factory form), and the `Quartz.NET 3.18.0` interop note — since Quartz now ships native RFC-5545 RRULE triggers (`WithRecurrenceSchedule`), the same `rrule` string + `dtstart` + IANA zone round-trips between Cocoar's frontend display and a Quartz backend job scheduler without a translation layer. Companion live demo `demos/CalendarRecurrence.vue` (DST-policy switcher, reactive add-series button, provenance click-inspect).
* **`playground(calendar)` — new `/calendar-recurrence` demo page**: same shape as the docs demo but with a larger surface area (multiple visible weeks, all-day yearly holiday, dynamic series mutation, "Jump to DST day" navigation shortcut). Used for chrome-devtools regression testing; landed both browser-only bugs in this release.
* **`docs(calendar)` — API reference table extended**: `series`, `seriesLoader`, `recurrenceEngine` setters added with full type signatures and mutual-exclusivity notes.
* **`docs(calendar)` — live popover + timeline demos mirrored from playground into VitePress**: the playground app (`apps/playground/`) isn't deployed publicly, so the popover-anchoring and timeline interaction patterns were invisible to consumers reading the docs. New ``-embedded demos in `apps/docs/components/calendar/coar-calendar.md` ("Popovers and tooltips" section, `demos/CalendarPopover.vue`) and `apps/docs/components/calendar/timeline-view.md` ("Live example" section, `demos/CalendarTimeline.vue`).
* **`docs(data-grid)` — new "Multi-Select & Tag-Select Columns" page** at `/components/data-grid/multi-select`: documents `col.multiSelect()` and `col.tagSelect()` side-by-side (when-to-use comparison, edit-mode flow, rendering modes, row-aware options, separate API tables, layered-overrides escape-hatch). Single live demo shows both variants in one grid (checkbox-list multi-select with chips display + tag-style trigger with allow-create). Sidebar entry under Data Grid.
* **`docs(data-grid)` — new "Date Columns" page** at `/components/data-grid/date-columns`: documents `col.plainDate()`, `col.plainDateTime()`, `col.zonedDateTime()` with a Temporal-only contract callout, edit-mode flow, per-shortcut API tables, row-aware markers example, and three live demos (PlainDate task scheduling with weekend highlights, PlainDateTime reminders, ZonedDateTime cross-zone meetings with `timezoneFilter`).
* **`docs(ui)` — new "Date Views" page** at `/components/date-views`: documents `CoarPlainDateView`, `CoarPlainDateTimeView`, `CoarZonedDateTimeView` — the read-only display siblings of the picker family. Single page covers all three with a head-to-head feature table, live demos (basic display, locale switching, 12h/24h, cross-zone projection, placeholder for null), and the cross-realm `Symbol.toStringTag` safety note.
* **`docs(ui)` — new "OTP Input" page** at `/components/otp-input`: documents `CoarOtpInput` with five live demos (Basic Usage, Length, Type & Mask, Custom filtering with `transform`/`accept`, Validation, Sizes), behavior table covering every keyboard interaction (auto-advance, Backspace-jump-back, Delete clear, paste-spread), full API table, accessibility notes, and the mobile SMS-autofill chip behavior. Sidebar entry under Form Controls.
***
## 1.18.0
### Added
* **`@cocoar/vue-data-grid` — cell-editing primitives**: three new chainable methods on the column / grid builders form the foundation for in-cell editing. `column.editable(value | row-predicate)` gates whether a cell enters edit-mode (boolean or `(row) => boolean`; predicate auto-handles missing row data). `column.cellEditorConfig(component, config)` mirrors the existing `cellRendererConfig` for swapping in custom editors — the config is wrapped under `cellEditorParams.config` so every editor gets the same access shape. `gridBuilder.onCellValueChanged(handler)` provides a single grid-level commit hook fired for both editor commits and renderer-driven mutations (e.g. checkbox toggles via `node.setDataValue`). New "Editing" doc page documents the AG-Grid edit-mode flow + Tab-through-edit-mode navigation.
* **`@cocoar/vue-data-grid` — `col.text(field, t => …)`**: text column whose editor is ``, fitted into the cell with form chrome stripped (no nested border, no extra focus ring — the AG Grid cell is the edit-mode frame). Replace-on-type via AG Grid's `eventKey` (printable key seeds the input), value pre-selected via `afterGuiAttached` so typing replaces. Configurator: `placeholder`, `maxLength`, `size`, `prefix`, `suffix`. Renderer uses AG Grid's default text rendering.
* **`@cocoar/vue-data-grid` — `col.number(field, n => …)`** *(overload)*: existing `col.number(field, config?)` keeps the legacy config-object form (renderer only). New callback form bundles `CoarNumberCellEditor` automatically — Maskito-driven locale-aware parsing means `1.234,56` in `de-AT` and `1,234.56` in `en-US` both yield the same numeric value. Configurator: `decimals` (renderer + editor), `min`/`max`/`step`/`stepperButtons`/`placeholder`/`size` (editor). Replace-on-type seeded via the digit / `.` / `,` / `-` key that started the edit.
* **`@cocoar/vue-data-grid` — `col.select(field, s => …)`**: select column with two cooperating components — `CoarSelectCellRenderer` (label-lookup; displays the label of the option matching the cell value) and `CoarSelectCellEditor` (auto-opens the dropdown via `afterGuiAttached`). Selection auto-commits — picking an option *is* the edit, no separate Tab/Enter needed. Configurator: `options` (static array OR `(row) => CoarSelectOption[]` for row-aware menus), `clearable`, `searchable`, `placeholder`, `searchPlaceholder`, `size`. Dropdown teleports to `` via Coar's overlay-host so it can extend past cell / grid boundaries without clipping.
* **`@cocoar/vue-data-grid` — `col.checkbox(field, c => …)`**: checkbox column with a read-only `` renderer + interactive ``. Renderer is always read-only by design (matches text/number/select); interactivity comes from edit-mode entered via double-click / Enter / F2, exactly like other editable column types. Inside edit-mode Space toggles, Tab commits and moves to the next editable cell (AG Grid's standard keyboard navigation), Escape cancels. Configurator: `label` (static or per-row), `indeterminate` (per-row tri-state), `size`. Vertical-centering CSS shim corrects CoarCheckbox's form-context layout (`align-items: flex-start`, fixed `min-height`, `margin-top` hack) inside the grid cell.
* **Configurator-callback pattern**: new `CheckboxColumnConfigurator`, `TextColumnConfigurator`, `NumberColumnConfigurator`, `SelectColumnConfigurator` classes provide the `s => s.options(...).clearable()`-style fluent API. Layered overrides via `.cellRenderer(MyOwn)` / `.cellEditorConfig(MyEditor, …)` continue to work as escape hatches (last-write-wins on the chain).
### Fixed
* **`@cocoar/vue-ui` — `CoarNumberInput` clear button no longer surfaces on focus when `clearable={false}`**: the `.coar-number-input-clear--hidden` modifier (`opacity: 0`) was outranked by the focused / hover overrides (`opacity: 1`, same selector specificity but defined later in the cascade), so the X appeared even on focused inputs explicitly opted out of clearing. Fix scopes the focused / hover rules with `:not(.coar-number-input-clear--hidden)` so they explicitly skip hidden buttons. Keeps the opacity-based hiding strategy (rather than switching to `v-if` like `CoarTextInput`) because the clear button sits to the LEFT of the input — a `display: none` hide would shift the input on appear/disappear, hurting layout stability.
* **`@cocoar/vue-data-grid` — `CoarSelectCellEditor` commits the selected option** (was: silently dropped under real user clicks): mousedown on a body-teleported dropdown option shifted focus away from the editor → AG Grid's `stopEditingWhenCellsLoseFocus` fired *before* CoarSelect's option-click handler could update the model → `getValue()` returned the OLD value. Caught by the user during hand-testing; the original verification used synthetic `HTMLElement.click()` which bypasses focus/blur flow. Fix is a capture-phase document `mousedown` listener installed while the editor is mounted: when the click target sits inside a Coar `overlay-host`, `preventDefault()` blocks the focus shift. The click event still fires, CoarSelect updates the model, the watch picks it up and triggers `stopEditing` — `getValue()` then returns the new value. Outside-clicks (everywhere else) still cause focus loss → AG Grid commits/cancels normally.
### Internal
* **`@cocoar/vue-data-grid` — new `configurators/` directory**: holds the per-column-type fluent configurator classes. Re-exported from the package root for consumers writing custom helpers.
* **Docs reorganisation**: dedicated "Data Grid" sidebar section replaces the single-item "Data" section. Currently five sub-pages (Overview, Editing, Text Column, Number Column, Select Column, Checkbox Column) with one live demo per page.
* **`docs(calendar)`**: marked the Calendar package as **Preview** in the sidebar to set expectations until v1.0 of `@cocoar/vue-calendar`.
* **`ci`**: docs-only changes now skip the full CI matrix (`paths-ignore` filter on `apps/docs/**` and `**/*.md`). Saves ~3 min per docs-only PR; full CI still runs whenever non-docs files are touched.
***
## 1.17.0
### Added
* **`@cocoar/vue-calendar` — DnD composables generic over `TMeta`**: `useCalendarDnd`, `useMonthDnd`, `useTimeGridDnd` now accept a `TMeta extends Record` type parameter, propagating event meta types through `CalendarEvent`, `EventDropPayload`, snapshot types, and the keyboard-drag state. Default stays `Record` so existing JS consumers are unaffected; TypeScript consumers writing custom DnD wiring can now keep their meta types end-to-end through the drop pipeline.
* **`@cocoar/vue-calendar` — `useViewWindow` accepts a `{ view }` override**: standalone sub-views (``, ``, ``, ``) pass their intended view via the new optional 2nd argument, which sets `builder.state.view` so the same builder composed via `useDayView()` / `useMonthView()` etc. (which never set `view`) renders correctly when handed to a sub-view component. Replaces the parallel `onMounted` hacks DayView and WeekView each carried; MonthView and AgendaView never had them and were silently broken in standalone mode (window computed for the wrong view, loader fetched the wrong range).
* **`@cocoar/vue-calendar` — `canDrop` validators receive `displayZone`**: the `CanDropTarget` shape advertised `displayZone: string` but both DnD composables silently dropped the field before invoking the validator. Now passed through. Article-5 / C5 conformance — consumers writing rules like "no drops in business hours of the user's local zone" can read `target.displayZone` directly instead of closing over `state.timezone` separately.
### Changed
* **`@cocoar/vue-calendar` — `EventLayoutCtx.kind` discriminator values aligned with templates**: type values are now `'positioned' | 'allDayBar' | 'monthPill' | 'monthBar'` (matching the layout-class names used by the views and the values templates have always emitted). The previous `'timed' | 'allDay' | 'monthBar' | 'monthPill'` was a documentation lie: templates never sent `'timed'` or `'allDay'`, so consumer event-renderers branching on those values were dead code at runtime. Renderers using the universal `state.eventRenderer` keep working unchanged; renderers that explicitly switched on `ctx.layout.kind` should now match the corrected values.
### Fixed
* **`@cocoar/vue-fragment-parser` — `vue-router` peer range widened to `^4.5.0 || ^5.0.0`**: consumer apps already on `vue-router@5.x` got a peer-dependency warning because the published range was still capped at `^4.5.0`. The package only uses `useRoute` / `useRouter` — both stable across vue-router 4 and 5 — so widening is safe. The monorepo itself runs on 5.x (playground + fragment-parser dev dep), the peer range was just lagging behind.
* **`@cocoar/vue-markdown-editor` — external `v-model` updates no longer dropped during Milkdown init**: a parent that initialised `v-model` synchronously to a placeholder and then assigned the real value asynchronously inside `onMounted` (typical store-load pattern) saw the editor stay locked on the placeholder. The watcher in `EditorImpl` bailed silently when `getInstance()` returned `null` during Milkdown's async init window, and the missed update was never replayed — so saving without further edits round-tripped the placeholder back to the API and overwrote the real document body. The watcher now buffers the latest external value while the editor isn't ready and a second watcher on Milkdown's `loading` ref flushes it via `replaceAll` once init completes. Found while migrating `cocoar-policy` knowledge docs to event sourcing.
* **`@cocoar/vue-calendar` — `` honors `prefers-reduced-motion`**: `smoothScrollBodyTo` was reading `window.value.matchMedia(...)` where the local `window` const shadowed the global by binding to a `ViewWindow` computed (a `{ start, end, timezone }` POJO). The match-media check therefore always returned `undefined` and the animated scroll always fired, ignoring the user's reduced-motion preference. Reaches the browser's `window` via `globalThis` now; users with `prefers-reduced-motion: reduce` get instant scroll on `scrollToTime` / `scrollToDate`.
* **`@cocoar/vue-calendar` — phantom-event stubs use real Temporal objects**: the placeholder events passed to `phantom` and `invalid` variants of `` / `` / `` / `` were typed as `CalendarEvent` but constructed with string `start` (`'1970-01-01'` / `'1970-01-01T00:00:00Z'`) — direct C1 violation in internal code. Now constructed with `Temporal.PlainDate.from(...)` / `Temporal.ZonedDateTime.from(...)`. Stubs are still "plumbing only, never observed at runtime" (the variants don't invoke their slots), so the change is purely a type-correctness fix.
### Internal
* **Workspace-wide TypeScript hygiene + CI hardening**: the `vite-plugin-dts` build emits TS errors to stdout but does not fail the build, so 144 silent type errors had accumulated across `@cocoar/vue-script-editor` (8), `@cocoar/vue-ui` (10), and `@cocoar/vue-calendar` (126) — all green CI runs were green-with-errors-in-the-log. Every error is fixed (Monaco 0.55 namespace migration, `useSlots()` typing in renderless wrappers, generic propagation through TMeta, `EventLayoutCtx.kind` alignment, `Props` interface inlining to bypass vue-tsc's TS4025 on script-setup-generic SFCs, slot signatures marked optional so `v-if="$slots.foo"` no longer trips TS2774, etc.). New `pnpm typecheck` script per package wired through a turbo task (`vue-tsc --noEmit`) and added as a CI step between Lint and Test in both `ci-develop` and `ci-pr-validation` workflows. Future TS regressions now fail the build instead of being logged and ignored.
* **`@cocoar/vue-markdown-editor` — first component-level test**: `CoarMarkdownEditor.test.ts` mounts the editor with `@vue/test-utils` + happy-dom + `CoarOverlayPlugin` and pins both the v-model race (external update before Milkdown ready) and the post-init external-update path. Test 1 fails deterministically without the v-model buffer fix.
* **`monaco-editor` deduplicated to `^0.55.1` across the workspace**: `apps/docs` and `apps/playground` were still on `^0.54.0` while `@cocoar/vue-script-editor`'s peer-range demanded `^0.55.1` — for `0.x` versions a caret pins the minor, so the two ranges did not overlap and the lockfile carried both `monaco-editor@0.54.0` and `@0.55.1` side-by-side. Apps bumped to `^0.55.1`; the duplicate is gone.
* **Lint cleanup — zero workspace-wide warnings**: `vue/one-component-per-file` and `vue/require-prop-types` disabled in `*.test.{ts,js}` / `**/__tests__/**` (multi-component test harnesses are the standard Vue test pattern, not a smell); `vue/attribute-hyphenation` disabled at the Vue-file level (Vue's `aria-*` / `data-*` fall-through-attribute treatment skips kebab→camel coercion, so binding `:ariaRowIndex` on a child must use camelCase to actually wire the prop). Two file-level disables for the deliberate multi-component layouts: `default-renderers.ts` (the entire markdown default registry in one place per the file-header comment) and `CoarMarkdownEditor.vue` (inner `EditorImpl` + `Toolbar` share the `MilkdownProvider` context).
***
## 1.16.0
### Added
* **`@cocoar/vue-calendar` — new package**: Vue 3 calendar component built around `Temporal`. Day / Week / Month / Agenda views, a top-level `` shell with prev / today / next navigation and a segmented-control view switcher, and standalone composables (`useDayView()` / `useWeekView()` / `useMonthView()` / `useAgendaView()`) for embedding a single view without the shell. Public surface is `Temporal.ZonedDateTime` (timed events) or `Temporal.PlainDate` (all-day events) — never strings, `Date`, `PlainDateTime`, or `Instant`. Eight architecture invariants (C1–C8) drawn from the ["Time in Software, Done Right" article series](https://dev.to/bwi/why-a-date-is-not-a-point-in-time-ad8) are enforced structurally: Temporal-only public surface (C1), single drop pipeline through `applyMoveToEvent` (C2), per-endpoint source zones preserved across every drag mode including cross-zone events (C3), explicit `DstPolicy` argument on every wall-time → instant conversion (C4), display zone vs source zone surfaced separately on drop payloads (C5), independent `locale` / `dateStyle` / `timeStyle` / `hour12` decisions merged through a single `buildFormatOptions` (C6), reactivity-by-reads not setup-capture (C7), and `RecurringSeries` as a first-class type with a typed throwing-stub `expandSeries` until the recurrence engine lands (C8). Flat `CalendarBuilder` — every setter (`timeRange`, `slotDuration`, `maxEventsPerCell`, `agendaLengthDays`, …) lives on the same builder. Universal `eventRenderer((ctx) => ...)` with `ctx.layout.kind` discriminator (`'positioned' | 'allDayBar' | 'monthPill' | 'monthBar'`) covers every variant. Drag-and-drop with mouse / touch / keyboard, cluster-aware lane sizing, virtualized agenda surface, multi-day bars across month rows, "+ N more" overflow expansion via per-cell kebab menu. Wire helpers `parseScheduledTime` / `formatScheduledTime` / `parsePlainDate` mirror the Article-8 `{ local, timeZoneId }` shape that .NET / NodaTime backends and PostgreSQL `local_start text + time_zone_id text` storage natively speak.
* **`@cocoar/vue-calendar` — `Temporal` re-export**: `import { Temporal } from '@cocoar/vue-calendar'` for consumers that don't want a direct `@js-temporal/polyfill` dependency.
* **`@cocoar/vue-calendar` — default event renderers surface C3 / C5 zone semantics**: a shared decoration layer (``, internal) inserts a small globe icon + tooltip + sr-only announcement on the default time-grid event card, month pill, month multi-day bar, and agenda event row. Two semantics, mutually exclusive: (1) `start.timeZoneId === 'UTC'` → globe + tooltip "Global event — same instant worldwide" (Article 5 — UTC-anchored events render the same instant for everyone, regardless of display zone); (2) `start.timeZoneId !== displayZone` (and is not UTC) → globe + accent dot + tooltip "Source zone: \" (Article 3 — render the user's clock without hiding the source). Suppressed on multi-day bars when `clippedStart` so only the visible head decorates. The same logic ships as a public helper `getEventZoneHints(event, displayZone) → { isUtcAnchored, sourceZone }` for custom renderers. Three i18n keys: `coar.calendar.event.utcLabel`, `coar.calendar.event.utcGlobalHint`, `coar.calendar.event.crossZoneHint`.
* **`` — drop-in display-zone selector** exported from `@cocoar/vue-calendar`: wraps `` with a curated 7-zone default list (Vienna / Berlin / London / New York / Los Angeles / Tokyo / UTC), automatically prepends the browser-detected zone if it isn't in the list, accepts an `:options` override for full IANA / domain-specific lists. `v-model` is the IANA id string the consumer passes into `builder.timezone(tz)`. Two i18n keys: `coar.calendar.zoneSwitcher.label`, `coar.calendar.zoneSwitcher.browserSuffix`.
### Internal
* **`@cocoar/vue-calendar` — 602 unit tests across 43 files**: timezone conformance suite at `src/core/__tests__/timezone/` pins every C1–C8 invariant; component tests cover the shell + each sub-view + the drop pipeline integration; `useViewWindow` tests pin the C5 single-writer invariant; zone-hint helper covered by 7 dedicated tests.
### Docs
* **New "Calendar" sidebar group** under Components — overview page, `` (composer) reference with full builder API, per-view pages (Day, Week, Month, Agenda) with standalone-usage examples, and a manual performance bench in the playground (`/calendar-perf-bench`) for eyeballing wheel-scroll smoothness, view-switch latency, and drag-frame stability against documented Tier-A targets.
***
## 1.15.0
### Added
* **`@cocoar/vue-markdown-editor` — text color**: new `textColor` tool exposes a palette + native color-input picker in the floating toolbar and the sidebar (fixed mode). Selection-based `text_color` ProseMirror mark, full markdown round-trip as `…`. Picker uses the shared overlay service (`menuPreset`): anchor-relative positioning, viewport flip, scroll-reposition, outside-click + escape dismissal — no bespoke layout. Active color shows as a thin indicator bar under the trigger icon. New exports: `COAR_TEXT_COLOR_PALETTE` (8 swatches), `textColor` plugin bundle, `textColorMark` and `textColorRemark` for advanced setups.
* **`@cocoar/vue-markdown-core` — color-span sanitizer + parser fold**: shared `sanitizeColor` / `sanitizeColorStyle` / `parseColorSpanOpen` / `isColorSpanClose` / `serializeColorSpanOpen` / `serializeColorSpanClose` helpers. Whitelist accepts hex (`#rgb`/`#rrggbb`/+alpha), `rgb()` / `rgba()` / `hsl()` / `hsla()` (legacy + modern syntax), and a small set of named CSS colors (`red`, `blue`, …, `transparent`, `currentcolor`); rejects `var(--token)`, `url(…)`, `expression(…)`, multi-declaration styles, foreign attributes, control characters. New `colorSpan` `MarkdownNodeType` with `attrs.color`; the parser folds matched `` open/close pairs in inline children into a single node (depth-aware nesting). Serializer flat-maps colorSpan back to `html` opener + children + closer.
* **`@cocoar/vue-markdown` — `colorSpan` renderer**: `DefaultColorSpan` registered in `defaultMarkdownRenderers`. Re-validates the color attribute via `sanitizeColor` at render time (defence in depth) — invalid values strip the inline style and fall through to plain text. New `colorSpanColor` helper exported from `helpers.ts`.
### Changed
* **`@cocoar/vue-markdown` shared stylesheet — editor↔viewer parity**: rendering rules now cover both the viewer's class-based DOM (`