--- 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 ``` ## 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/migration.md --- # Migrating to 2.11 The 2.11 release unifies the whole input family onto one internal shell (`CoarInputFrame`) and a single field-padding token. **Visually almost nothing changes** — padding and sizing values were preserved — and the public props and slots of the text / number / password / select family are unchanged. For most apps the migration is **one search-and-replace or nothing at all**. The checklist below is ordered by how likely it is to affect you. ## TL;DR | If you… | …then you need to | | --- | --- | | Use the **date / time pickers** with a `label`, `hint`, or `error="message"` | Wrap them in [`CoarFormField`](/components/form-field) (see below) — a consistency fix that brings them in line with every other input. **The only real code change.** | | Rely on the **auto clear ✕** on text / number / password / date inputs | Add the `clearable` prop where you want it. | | **Override `--coar-input-padding-x`** in your own CSS | Rename it to `--coar-field-padding-x`. | | **Use `--coar-spacing-2xs` / `-xxl` / `-xxxl`** in your own CSS | Switch `2xs → xxs`; pick another step for `xxl` / `xxxl`. | | None of the above | **Nothing — you're done.** | *** ## 1. Date & time pickers move onto `CoarFormField` This is really a **consistency fix**, not a feature removal. The date/time pickers were the only inputs that rendered their *own* label and below-field message instead of delegating to [`CoarFormField`](/components/form-field) like every other field — an oversight from when they were first built. They now follow the same pattern as the rest of the family, which also **fixes** a latent bug where a `CoarFormField`-wrapped picker didn't pick up the error border. Concretely: `CoarPlainDatePicker`, `CoarPlainDateTimePicker` and `CoarZonedDateTimePicker` no longer render their own label, required asterisk, or below-field hint/error message, and they no longer take `label` / `hint` props. Their `error` prop is now a **`boolean`** (it flips the red border + `aria-invalid`), matching `CoarTextInput` and the rest of the field family. Wrap them in [`CoarFormField`](/components/form-field) — exactly like every other input — to get the label, the required `*`, validation messages and the inline status icon. **Before:** ```vue ``` **After:** ```vue ``` Notes: * `error` on the **picker** is now a boolean — pass `!!errorMessage` (or any boolean). The human-readable message lives on `CoarFormField`. * New `id` prop on each picker (explicit input id; otherwise taken from the wrapping `CoarFormField`, otherwise auto-generated) for parity with the other inputs. ## 2. `clearable` is now opt-in (defaults to `false`) `CoarTextInput`, `CoarPasswordInput`, `CoarNumberInput` and the three date/time pickers previously defaulted `clearable` to `true`. They now default to `false`, in line with the library rule that **every boolean prop defaults `false`** (and matching `CoarSelect` / `CoarMultiSelect`, which were already `false`). Nothing errors — the inline clear ✕ simply no longer appears unless you ask for it. **Before** (✕ shown automatically): ```vue ``` **After** (add `clearable` where you want the ✕): ```vue ``` `@cocoar/vue-data-grid` is unaffected — its cell editors already pass `clearable` explicitly. ## 3. Renamed / removed CSS tokens These only affect you if you **override Cocoar design tokens** in your own stylesheet. If you consume the components as-is, skip this section. ### Renamed | Old | New | | --- | --- | | `--coar-input-padding-x` | `--coar-field-padding-x` | `--coar-field-padding-x` (`12px`) is the single source of truth for form-field horizontal padding. It is intentionally **off** the spacing scale and decoupled from it, so tuning `--coar-spacing-*` no longer moves field padding. Each control size scales it by `--coar-component-{xs,s,m,l}-scale`. ### Removed | Removed | Use instead | | --- | --- | | `--coar-spacing-2xs` (was a duplicate of `xxs`, both `2px`) | `--coar-spacing-xxs` | | `--coar-spacing-xxl` (`48px`, unused in the library) | a remaining step, e.g. `--coar-spacing-xl` (`32px`) | | `--coar-spacing-xxxl` (`64px`, unused in the library) | a remaining step | The spacing scale is now: `3xs 1 · xxs 2 · xs 4 · s 8 · m 16 · l 24 · xl 32`. *** ## What did **not** change * **Public props & slots** of `CoarTextInput`, `CoarPasswordInput`, `CoarNumberInput`, `CoarSelect`, `CoarMultiSelect`, `CoarTagSelect` — including the `prefix` / `suffix` / `leading` / `trailing` slots. The move onto the internal `CoarInputFrame` shell is invisible to consumers. * **Visual output** — padding, radius and sizing values were preserved. Fields may differ by a sub-pixel at most; nothing reflows. * **Toggle, listbox and segmented controls** — `CoarCheckbox`, `CoarSwitch`, `CoarRadioGroup`, `CoarListbox`, `CoarDualListbox`, `CoarSegmentedControl` got token-consistency polish only, no API change. (`CoarSwitch` and `CoarRadioGroup` additionally **gain** an `xs` size — additive.) --- --- 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.16.0 **Point events, visually honest.** A timed event without `end` (a "point event" — think *call at 14:00*) previously rendered over the +30-minute layout default in the exact same card as a real 30-minute meeting, so *call at 14:00* and *standup 14:00–14:30* were indistinguishable in Day / Week. The card now says what it knows: the start time — and nothing more. ### Added * **`@cocoar/vue-calendar` — distinguishable rendering for point events in the time grid.** A timed event without `end` keeps its +30-minute slot geometry (layout math untouched — the card geometry of the web and SwiftUI ports stays identical) but renders with a solid start edge in the event color exactly on the start time, a semi-transparent card body (fill and leading bar mixed toward transparent; the **title stays fully opaque** — the body is tap target + label carrier, not a duration statement), and **no resize handles** (there is no `end` to grab; moving still works). The start edge is suppressed when the card is clipped at the top of the visible window, since the top edge then isn't the start time. Month and Agenda render point events unchanged — they draw no duration geometry. Two new scheme-invariant theme tokens: `--coar-calendar-point-edge-height` (default `3px`) and `--coar-calendar-point-body-opacity` (default `0.38`), value-matched to the SwiftUI port (`Cocoar.Calendar.iOS` `0eef2e0`), so web and iOS point events look alike. See the updated [Day](/components/calendar/day-view) / [Week](/components/calendar/week-view) pages. *** ## 2.15.0 **Calendar dark mode, out of the box** — plus a ghost-card layout fix. The calendar grid previously stayed white in a dark app shell because the `--coar-calendar-*` tokens only existed as hardcoded light fallbacks; the package now ships a full dark palette, value-identical to the SwiftUI port (`Cocoar.Calendar.iOS`), so web and iOS render the same dark calendar. ### Added * **`@cocoar/vue-calendar` — dark-mode values for the grid tokens.** The package stylesheet now defines every `--coar-calendar-*` token for dark mode, activating on the `.dark-mode` class (the Cocoar convention, same as `@cocoar/vue-ui`) **or** the `[data-theme="dark"]` attribute. The values mirror the SwiftUI port's `CalendarTheme.dark` (derived from the vue-ui dark primitives): surfaces `#18181b` / weekend `#212125` / other-month `#131316`, today-tint lifted to ~14% accent (4% is invisible on dark), grid lines `#2c2c30`, borders `#3f3f46`. Two new tokens join the set — `--coar-calendar-agenda-divider` (agenda row separator, near-invisible `#27272a` in dark; falls back to `--coar-calendar-border` in light) and `--coar-calendar-event-default-bg` (fill of events without a `meta.color`: dark accent `#1e3a8a` so the light title text stays readable; falls back to `--coar-color-accent-soft`). The calendar-local text tokens `--coar-text-base` / `--coar-text-subtle` (names not defined by `@cocoar/vue-ui`) get dark values too, so labels flip with the surfaces. Event colors from `meta.color` are intentionally **not** remapped — they render raw in both modes. Light mode is byte-identical to before (light values remain per-usage fallbacks). See the updated [Theming section](/components/calendar/#theming). ### Fixed * **`@cocoar/vue-calendar` — timed events without `end` no longer render ghost cards on following days.** `layoutDayEvents` applied the default 30-minute duration **after** projecting the start onto the day, so on any day after the event's start day the "before this day" sentinel (−1) yielded a visible 0:00–0:29 interval — a phantom card on **every** later day of the visible window in Week / Day views. The default now only applies when the start actually falls on the laid-out day; end-less events are visible on their start day only. *Heads-up for the SwiftUI port: `TimeGridLayout.swift` replicates the old behaviour fixture-true — regenerate the layout fixtures and port the fix.* *** ## 2.14.0 **Diagrams in Markdown.** A ` ```mermaid ` fenced code block now renders as a diagram, via a new pluggable fence-renderer seam, a standalone `@cocoar/vue-mermaid` renderer and the thin `@cocoar/vue-markdown-mermaid` adapter. The Markdown packages carry **zero** dependency on Mermaid — installing and registering the adapter is the opt-in, and a fence with no registered renderer stays a readable code block, so the Markdown stays portable. ### Added * **`@cocoar/vue-markdown` — fenced-code-block renderer registry.** A new open, language-keyed `FenceRegistry` (`MARKDOWN_FENCE_RENDERERS_KEY`, `resolveFenceRenderer`) lets a consumer render a specific fence language (e.g. `mermaid`) with a rich component instead of a plain code block. `` gains a `fenceRenderers` prop (provide/inject, like `embeds`); an unregistered language still renders as a normal, syntax-highlighted code block. Additive — existing behaviour is unchanged. * **`@cocoar/vue-mermaid` — new standalone diagram component.** `` renders a Mermaid diagram from a source string — **markdown-agnostic**, usable anywhere. Lazy-loaded Mermaid (its own chunk), client-only, `securityLevel: 'strict'` (author diagram text is sanitized), waits for `document.fonts.ready` (so labels aren't clipped by pre-font-load measurements), and an error-with-source fallback for invalid diagrams. **Cocoar-themed** — design tokens map onto Mermaid's `themeVariables`, colors normalized to sRGB. **Zoom & pan** (`zoomable`): fixed-height viewport with +/−/⤢ controls, Ctrl/⌘+wheel zoom, drag-to-pan and double-click reset — plain wheel and one-finger touch still scroll the page, so a diagram never traps scrolling. Import styles via `@cocoar/vue-mermaid/styles`. * **`@cocoar/vue-markdown-mermaid` — new opt-in fence adapter.** Registers `@cocoar/vue-mermaid` as the `mermaid` fence renderer for `@cocoar/vue-markdown`. Ships `mermaidFenceRenderers` (ready-to-spread) and `createMermaidFenceRenderers({ zoomable })`. ### Fixed * **`@cocoar/vue-map` — the package now exposes its stylesheet.** `@cocoar/vue-map/styles` (`./styles` export) ships the ~5 KB of `.coar-map*` component styles (pins, legend, layout). Previously there was no way to import them from the published package, and they are **not** part of `@cocoar/vue-ui`'s stylesheet — so a consumer using the built package got an unstyled map. Leaflet's own CSS is still loaded lazily by the component. *** ## 2.13.0 Optional-time date pickers: a small **clock toggle beside the field** lets one input be *either* a plain date *or* a date with time — no more separate "add time" checkbox. ### Added * **`@cocoar/vue-ui` — `` + ``.** A date picker whose time is **optional**. A clock toggle sits **beside** the field (left or right via `togglePosition`); off enters a `Temporal.PlainDate`, on enters a date with time — a `Temporal.ZonedDateTime` (zoned variant) or `Temporal.PlainDateTime` (plain variant). One `v-model` carries the union value and `v-model:withTime` is the toggle; the two stay in sync (a non-null value's own type *is* the mode), so you read the result straight off its Temporal type. Flipping the clock converts the current value rather than clearing it — adding a time keeps the date (and, zoned, attaches the `timeZone` or the user's zone); removing it drops back to the date. They're thin **compositions** over the existing `CoarPlainDatePicker` / `CoarPlainDateTimePicker` / `CoarZonedDateTimePicker` (all common props pass through: size, disabled, readonly, required, error, clearable, locale, min/max, minuteStep, and the zoned timezone props). The toggle icon switches between a clock and a struck-through clock with a state-aware tooltip + `aria-pressed`; its roundness tracks `--coar-input-radius` so it always matches the adjacent field. See the new **[Date · optional time](/components/date-or-time-picker)** page. * **`@cocoar/vue-ui` — new built-in `clock-off` icon** (a clock with a diagonal strike), available to the icon system like any other built-in. *** ## 2.12.0 Custom **embeds** for the markdown stack, plus a new standalone **map** package — viewer and **visual editor**. Authors write a `:::key{props}` directive and the shared stack renders a consumer-registered Vue component — read-only in the [``](/components/markdown) viewer, **live and editable** in [``](/components/markdown-editor) — with a lossless round-trip to plain Markdown. The embedded component has **zero dependency on markdown**: it's a plain component with normal props that a consumer registers from the outside, so the markdown packages never depend on it. To place those inserts, the editor's sidebar `tools` becomes an **ordered, groupable layout**. The new `@cocoar/vue-map` ships both [``](/components/map/) (display) and [``](/components/map/editor) (author points/routes via `v-model:data`), each markdown-agnostic by design. See the new **[Custom Embeds](/components/markdown-embeds)** page. Everything is opt-in behind the `cocoar` flavor. ### Added * **`@cocoar/vue-map` — new package: standalone interactive map (``).** A data-driven Leaflet map for Vue 3 that's **completely independent of markdown** (and of any embedding layer) — it takes resolved `MapData` + `MapConfig` and knows nothing about ids, fetching, or `:::` directives. Renders category-colored stop pins, named shape-point dots and a route polyline; hover tooltips, click popups, an opt-in legend (`show-legend`, then shown when ≥ 2 categories are present), caption, and `viewport`/auto-fit. An interactive bridge lets a consumer-built list drive the map and stay in sync: `v-model:selected`, a `point-click` event, and exposed `focusPoint(index)` / `highlightPoint(index)` methods (`fallbackEntries(...)` rows carry the matching point `index`). Leaflet (JS + CSS) is imported **lazily** on mount, with a crawlable no-JS `
    ` fallback until it hydrates. Untrusted point text is escaped (popups built from DOM text nodes). A consumer can register it as a markdown embed (`{ map: { viewer: MapEmbedWrapper } }`) where the wrapper resolves its id → `MapData` — but that wiring lives entirely in the consumer. *(Google basemaps are out of scope for now; a Google basemap is skipped in favour of the default.)* * **`@cocoar/vue-map` — visual map editor (``).** The write counterpart of ``, controlled via `v-model:data` (it never mutates the prop — every edit emits a fresh `MapData`). Click an empty spot to add a point (type-aware: `route`/`multi` append a stop, `single` moves the one point), drag any marker to move it with the route polyline following **live**, click a marker to edit its `label`/`note`/`category`/`icon` and toggle `stop`↔`shape` in a popup (anchored over the marker, flips below near the top edge, closable via **×** / `Esc` / click-away, fully overridable via the **`#point-form`** slot), reorder route waypoints, delete, and **click the route line to insert a waypoint exactly there** (snapped to the nearest segment). Adding a point does **not** auto-open the popup, and clicking empty space while a point is selected **dismisses** it rather than adding — so rapid placement stays fluid and "click away to close" works as expected. `readonly` freezes editing. A consumer toolbar can drive the same edits through exposed `addPoint` / `updatePoint` / `removePoint` / `reorder` / `captureViewport` (+ `focusPoint` / `highlightPoint`) methods. The immutable, Leaflet-free editing ops (`addPointForType`, `movePoint`, `updatePoint`, `removePoint`, `reorderPoint`, `insertOnSegment`, `setViewport`, `nearestSegment`, `normalizeLatLng`) are exported too. Built on the same lazy-Leaflet, markdown-agnostic, XSS-safe foundation as ``; tree-shakeable so viewer-only consumers don't pull in the editing weight. The editor's built-in property popup is rendered with the **Cocoar UI** form controls (`CoarTextInput` / `CoarSelect` / `CoarButton`); `@cocoar/vue-ui` is an **optional `peerDependency`** of `@cocoar/vue-map`, so the editor opts into the design system while viewer-only `` consumers aren't required to install it. See the new **[Map Editor](/components/map/editor)** page. * **`@cocoar/vue-map` — ready-made point list (``).** A drop-anywhere companion list so apps don't rebuild it. Controlled (`v-model:data` / `v-model:selected`) and decoupled from the map — it emits `focus` / `highlight` events you wire to the map's `focusPoint` / `highlightPoint`. Opt-in `reorderable` adds **drag-and-drop sorting** (a per-row handle, built on Cocoar UI's `useDragDrop`) and `removable` adds a per-row delete; both keep the selection on the same logical point through the change. Lists every point (including unnamed `shape` vertices), with a `#row` slot to override row content. Works next to `` (full editing) or a read-only `` (pure navigator). New exports `selectionAfterReorder` / `selectionAfterRemove` keep custom lists consistent. * **`@cocoar/vue-markdown-core` — `:::key{props}` custom-embed directive.** A standalone directive line parses to a generic `embed` node (`{ key, props }`) and serializes back losslessly (`parse → serialize → parse` is a fixed point; canonical forms like `:::map{id=}` are byte-stable). Exposes `parseEmbedDirective`, `serializeEmbedDirective` and `toEmbedProps`. The parser is registry-agnostic — any key round-trips, registered or not. Vue-free, like the rest of the core. * **`@cocoar/vue-markdown` — embed registry + viewer rendering.** New `embeds` prop on `` (and an app-wide `MARKDOWN_EMBEDS_KEY` provide) maps a directive key to a component via `EmbedRegistry` / `EmbedDefinition` (`{ viewer, editor?, insert? }`). A registered key renders through your component (props passed straight through); an unregistered key degrades to a labelled placeholder instead of vanishing. XSS-safe by construction — attribute values are bound as Vue props/text, never `innerHTML`. * **`@cocoar/vue-markdown-editor` — live, editable embeds.** Pass the same `embeds` registry and the editor folds `:::key{props}` into an atomic block whose NodeView mounts the registered component **live**. When the entry supplies an `editor` component it's mounted editable via a single typed `controller` prop (`EmbedEditorProps` / `EmbedEditorController` — `controller.patch(...)` writes attribute changes back to the Markdown); otherwise the read-only `viewer` is shown. Gated behind a new `embeds` flavor capability (on in `cocoar`, the default). * **`@cocoar/vue-markdown-editor` — groupable, ordered toolbar layout.** `tools` now accepts `CoarMarkdownEditorToolEntry[]`: built-in tool ids, custom-embed inserts addressed as `embed:` (icon/label from the registry's `insert`), `{ flyout }` submenu groups (mixing built-in and embed items), and `'divider'`s. Array order is the toolbar order, so embeds and built-ins can be placed and grouped anywhere. ### Changed * **`@cocoar/vue-markdown-editor` — `tools` is now an ordered layout (breaking).** A `tools` array previously acted as an order-insensitive whitelist that filtered a fixed sequence; it is now an ordered layout (array order = toolbar order). Flat built-in arrays remain valid and the default (omit `tools`) is unchanged, so apps passing tools in canonical order are unaffected — list tools in the order you want otherwise. `tools` is typed `CoarMarkdownEditorToolEntry[]` (a superset of `CoarMarkdownEditorTool[]`). * **`@cocoar/vue-markdown-editor` — the default `cocoar` flavor now recognises `:::key{props}` embeds.** A standalone `:::word{…}` line folds to an embed node (rendered as a placeholder when its key isn't registered) instead of staying literal text. Use a stricter `flavor` (`'commonmark'` / `'gfm'`, or a capability object without `embeds`) to keep such lines as plain text. *** ## 2.11.0 A library-wide consistency pass for the input & form-control family. The whole field family — text / number / password / select / multi-select / tag-select and the three date-time pickers — now sits on one internal shell (`CoarInputFrame`), so box, border, radius, surface, states and field padding all come from a single place. The non-typing form controls (checkbox, switch, radio, listbox, dual-listbox, segmented control) are aligned to the same input + semantic-error tokens, and the Theme Editor is rebuilt around the new token model. It is **mostly visual-neutral** — padding and sizing values were preserved. The breaking changes are narrow and concentrated: the date/time pickers move their label & validation onto `CoarFormField` (a consistency fix), `clearable` now defaults to `false`, and a few design tokens are renamed/removed. See the **[Migrating to 2.11 guide](/guide/migration)** — most apps need a one-line change or nothing at all. Underpinning it is a spacing-scale cleanup and a dedicated field-padding token. The spacing scale had drifted: two dead steps, a duplicate, and an off-scale value (`12px`) faked with `calc(--spacing-s + --spacing-xs)` — which implicitly coupled input padding to two scale steps, so tuning `--coar-spacing-s` silently shifted every text field. A usage census (workhorses `s`/`xs` ≈ 70% of all references; `xxl`/`xxxl` unused; `2xs` a duplicate of `xxs`) drove the change. ### Removed * **`@cocoar/vue-ui` — dead / duplicate spacing tokens (breaking).** `--coar-spacing-2xs` (a duplicate of `--coar-spacing-xxs`, both `2px`), `--coar-spacing-xxl` (`48px`) and `--coar-spacing-xxxl` (`64px`) are removed — the latter two had zero usages across the library. The scale is now `3xs 1 · xxs 2 · xs 4 · s 8 · m 16 · l 24 · xl 32`. Consumers referencing the removed tokens should switch `2xs → xxs` and pick a remaining step for `xxl`/`xxxl`. * **`@cocoar/vue-ui` — `--coar-input-padding-x` (breaking, renamed).** Replaced by `--coar-field-padding-x` (see below). * **`@cocoar/vue-ui` — date-picker `label` / `hint` props + own label/message rendering (breaking).** `CoarPlainDatePicker`, `CoarPlainDateTimePicker` and `CoarZonedDateTimePicker` no longer render their own label, required-asterisk or below-field error/hint message. Wrap them in [`CoarFormField`](/components/form-field) for a label, the required `*`, validation messages and the inline status icon — exactly like every other input. The pickers now `inject` the field contract (id / error / `aria-describedby`), which also **fixes** a latent bug where a `CoarFormField`-wrapped picker didn't pick up the error border. See the `error` prop change below. ### Added * **`@cocoar/vue-ui` — `--coar-field-padding-x` (`12px`) + `--coar-field-padding-x-tight` (`calc(--coar-field-padding-x / 2)`).** A dedicated component token for form-field horizontal padding, intentionally **off** the spacing scale (`12px` sits in the scale's `8→16` gap) and **decoupled** from it — tuning `--coar-spacing-*` no longer moves field padding. Single source of truth for text / password / number / select fields (the consumer multiplies by `--coar-component-density`). The Theme Editor's "Inputs → Field padding" control (with its opt-in override switch) now drives this token. * **`@cocoar/vue-ui` — `xs` size for `CoarSwitch` and `CoarRadioGroup`.** Both gain an extra-small `size="xs"` to match `CoarCheckbox` (which already had one): a `24×14` track with a `10px` thumb for the switch, a `14px` control with a `5px` dot for the radio, both labelled at `--coar-component-xs-font-size`. Adds `--coar-switch-xs-track-width` / `-track-height` / `-thumb-size` tokens. Additive — `s` / `m` / `l` are unchanged. ### Changed * **`@cocoar/vue-ui` — `CoarTextInput` / `CoarPasswordInput` field padding.** Now read `--coar-field-padding-x` (value unchanged at `12px`). `CoarPasswordInput` no longer re-derives the same value inline with `calc(--spacing-s + --spacing-xs)` (which had drifted from the shared token) and its size-variant toggle paddings use `--coar-field-padding-x` / `-tight` instead of off-scale `calc()` sums. * **`@cocoar/vue-ui` — field horizontal padding now scales per control size.** The whole field family (text / password / number / select / multi-select / tag-select) derives its horizontal padding as `--coar-field-padding-x × per-size-scale × density`, so xs/s/m/l no longer share a single flat value (e.g. at base `12px`: xs `8.1` · s `9.6` · m `12` · l `14.4`). You still tune **one** knob (`--coar-field-padding-x`, the `m` base) and every size scales proportionally — e.g. bump it for pill-shaped inputs and the smaller sizes follow. Adds `--coar-component-{xs,s,m,l}-scale` (unitless, height-derived: `0.675 / 0.8 / 1 / 1.2`) as a reusable per-size multiplier. Also unifies select/number field padding onto `--coar-field-padding-x` (they previously used a flat `--coar-spacing-s`, so the Theme Editor's Field-padding control now affects them too). * **`@cocoar/vue-ui` — Theme Editor.** Dropped the dead `XXL` spacing slider; renamed the "Horizontal padding" control to "Field padding" (writes `--coar-field-padding-x`). * **`@cocoar/vue-ui` — date-picker `error` prop is now `boolean` (breaking).** `CoarPlainDatePicker` / `CoarPlainDateTimePicker` / `CoarZonedDateTimePicker` `error` changed from `string` (an error message rendered below the field) to `boolean` (flips the red border + `aria-invalid`), matching `CoarTextInput` & the rest of the field family. The message itself moves to the wrapping `CoarFormField`. To migrate: `` → ``. New `id` prop (explicit input id; else taken from `CoarFormField`, else auto) for parity with the other inputs. * **`@cocoar/vue-ui` — list-selection controls aligned to the input token contract.** `CoarListbox` and `CoarDualListbox` now render their list panels with the input border + radius tokens (`--coar-border-input`, `--coar-input-radius`, capped at the field pill-end via `min(…, --coar-component-m-height / 2)` so a tall list doesn't bow into a stadium at full radius — the same cap the select dropdown panels use) and gain a distinct disabled surface (`--coar-surface-input-disabled`), matching the field family. `CoarDualListbox`'s transfer buttons are rebuilt on `CoarButton` (`secondary`, icon-only) instead of hand-rolled buttons, so they inherit the shared focus ring, sizing and disabled state (dropping an off-spec `opacity: 0.35` and hardcoded `32×28` dimensions). Visual refinement only — no API change. * **`@cocoar/vue-ui` — toggle controls aligned to the input + semantic-error token contract.** `CoarCheckbox`, `CoarSwitch` and `CoarRadioGroup` now key their box / track / control chrome off the shared input tokens and the correct semantic-error layers: the checkbox box radius uses `--coar-input-radius` (was `--coar-radius-xs`); checkbox + switch error states use `--coar-border-semantic-error-bold` / `--coar-background-semantic-error-bold` instead of the text-layer token (which fixes a too-dark error tint in dark mode); the radio control adopts `--coar-border-input` / `--coar-surface-input` / `--coar-border-input-hover` (was the `neutral-primary` tokens plus an accent hover) and `--coar-component-{size}-font-size` labels, and its disabled opacity is unified to `0.6` to match the rest of the toggle family. Action controls (`CoarButton` / `CoarSegmentedControl`) intentionally keep their `0.5` disabled look. Visual refinement only — no API change. * **`@cocoar/vue-ui` — `clearable` now defaults to `false` (breaking).** `CoarTextInput`, `CoarPasswordInput`, `CoarNumberInput`, `CoarPlainDatePicker`, `CoarPlainDateTimePicker` and `CoarZonedDateTimePicker` previously defaulted `clearable` to `true`; it's now `false`, in line with the library rule that every boolean prop defaults `false` (and matching `CoarSelect` / `CoarMultiSelect`, which were already `false`). Add `clearable` where you want the inline clear ✕. Also fixed: the right-aligned controls (number + the 3 date-pickers) used to render a hidden clear button unconditionally, reserving an empty slot on the **left**; the clear affix is now omitted entirely when not clearable, so a non-clearable control has no reserved gap. `@cocoar/vue-data-grid` is unaffected — its cell editors pass `clearable` explicitly. *** ## 2.10.0 A batch of improvements from the Tellify (CityDiary) build. For the media-library: inline node **creation** and an app-internal **drop target** for `CoarTree`, plus a programmatic **move**, optimistic **create**, and a **browse-only** mode for `@cocoar/vue-file-explorer-core` — so consumers can drop the workarounds they had built around the gaps. For the blog editor: `@cocoar/vue-markdown-editor` gains real **image** support (URL / paste / drag-drop upload / custom picker), full **table** editing (create, align, delete, and Notion-style hover handles with **drag-to-reorder**), and a **`flavor`** portability switch. Everything is additive and opt-in. ### Added * **`@cocoar/vue-ui` — `CoarTree` inline node creation.** Opt in with `creatable` (mirrors `renamable`) and call `api.startCreate(parentId, { kind?, initialName?, position? })` to insert a transient, focused **draft row** at its target position — `parentId: null` for the root, otherwise the parent auto-expands and the draft renders nested under it. Commit (Enter / blur) fires `@create` with `{ parentId, name, kind }`; Escape or an empty name fires `@create-cancel`. Same input + 200 ms blur-grace timer as inline rename, so it survives the context-menu-close focus race. The draft lives outside the visible-row model (selection / keyboard / DnD never see it) and works virtualized and at the empty-tree root. Optional `#draft` slot overrides the default folder/file icon. For async validation (e.g. a duplicate-name 409), a builder `onCreate` may return a `Promise` — the tree keeps the draft open + focused (name intact) until it settles, dropping it on success and reopening on reject; prop/event-form consumers get the same retry by re-calling `startCreate(parentId, { initialName })`. Everything works in prop-mode too (`startCreate` is on the component template ref). Removes the consumer workaround of a floating `` rendered outside the tree. * **`@cocoar/vue-ui` — `CoarTree` app-internal drop target (`acceptsData` / `@data-drop`).** Accept a non-OS, in-app drag — a card dragged out of a grid, a palette chip — by listing the MIME type(s) it carries in `acceptsData` (prop or builder `.acceptsData([...])`). A drop fires `@data-drop` / `onDataDrop` with `{ node: T | null, position, dataTransfer }` (`node: null` = the background), reusing the existing drop highlight, auto-expand-on-hover and `before`/`inside`/`after` position logic. Internal node drags (`@node-move`) are never re-delivered as data drops. Lets consumers drop hand-rolled `dragover`/`drop` listeners on the row slot for grid-card → folder moves. * **`@cocoar/vue-file-explorer-core` — programmatic `move(id, newParentId, position?)`.** `useFileExplorer` now exposes a plain move (same optimistic-update + rollback as a tree drag) for sources that aren't a `CoarTreeNodeMoveEvent` — a "move to folder" `