Changelog
All notable changes to the Cocoar Design System (Vue) will be documented in this file.
The format is based on Keep a Changelog. Versions are calculated automatically by GitVersion.
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/CoarZonedDateTimeViewdisplay components: read-only Temporal-typed date / date-time / zoned-date-time displays paired with the existing picker family. Each viewer mirrors its picker'sformatValuelogic exactly (sameuseDatePickerBasefor locale + date-format resolution, samecoarFormatPlainDate+coarFormatTime+coarFormatTimezoneLabelhelpers) so a read-only display and the editor's resting state look identical. Props:value,locale?,dateFormat?,placeholder?,size?;CoarPlainDateTimeViewaddsuse24Hour?: boolean | 'auto'(defaults to locale detection);CoarZonedDateTimeViewaddsdisplayTimeZone?: string(project all values into a single zone),showTimeZone?: boolean(toggle the trailingGMT+1-style label), anduse24Hour?. Cross-realm-safe type checks: each viewer usesSymbol.toStringTaginstead ofinstanceof 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/polyfillpath under pnpm's isolated dependency tree. Reactive locale:useDatePickerBasealready tracks the consumer-appuseL10n().languageref, 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-uisuite 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 viatoLocaleStringwith date-style: medium;plainDateTimeadds time-style: short;zonedDateTimeadds 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 areTemporal.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:afterGuiAttachedfocuses 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 viagetValue()on Tab / Enter / click-outside. The legacycol.date(field, config?)shortcut (display-only, acceptsDate | string) is unchanged for back-compat.@js-temporal/polyfillis 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-columnswith three live demos (PlainDate task scheduling, PlainDateTime reminders, ZonedDateTime cross-zone meetings).@cocoar/vue-data-grid—col.multiSelect(field, s => …)andcol.tagSelect(field, s => …)column shortcuts: two new factory methods for multi-value cells. Both store the cell value asT[]and share one renderer (CoarMultiSelectCellRenderer) — comma-separated label lookup by default,.display('chips')opts into one<CoarTag>per value.col.multiSelect()editor wraps<CoarMultiSelect>(checkbox-list dropdown,.searchable()/.showSelectAll()/.clearable()available);col.tagSelect()editor wraps<CoarTagSelect>(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 toString(value)for unknown labels). Both editors auto-open viaafterGuiAttachedand use focus-preservation (capture-phasemousedownlistener thatpreventDefaults on.coar-overlay-hosttargets) so the dropdown stays open while the user toggles options; unlikecol.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 vias.options(row => …). Configurators (MultiSelectColumnConfigurator,TagSelectColumnConfigurator) export from the package root;MultiSelectCellEditorConfigis 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-selectwith live demo (both variants side-by-side).@cocoar/vue-ui—CoarOtpInputcomponent: 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 regularCoarTextInputfor 6-digit codes (typing fast, then Enter or mouse-to-OK). Fires acomplete(value)event the moment the last cell fills so consumers can auto-submit without an OK button.lengthprop (default6) 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 viainputmode;maskrenders cells as<input type="password">for shared-screen contexts. Sizes match the form-input family (xs/s/m/l) and the component picks uperror+requiredautomatically when wrapped inCoarFormFieldvia the existing FORM_FIELD_INJECTION_KEY pattern. First cell carriesautocomplete="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 matchtypefirst. Two optional hooks fine-tune per-character behaviour beyond the built-in classes:transform: (char) => stringrewrites or drops a character before commit (e.g.c => c.toUpperCase()so users can type lowercase claim codes), andaccept: (char) => booleanadds a per-char reject predicate that ANDs withtype(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"witharia-label="Verification code, N digits", each cell hasaria-label="Digit i of N",aria-invalidpropagates 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 atpackages/ui/src/components/otp-input/; typesCoarOtpInputProps,CoarOtpInputSize,CoarOtpInputTypeexported from the package root. Docs page at/components/otp-inputwith five live demos.@cocoar/vue-calendar—onEventHover/onEventHoverLeavehandlers: fire onpointerenter/pointerleaveover any event element in any view (day / week / workWeek / month / agenda / timeline). Payload mirrorsonEventClick:{ event, native: PointerEvent }.native.currentTargetis the event-element DOM node — pass it directly touseOverlay({ anchor: { kind: 'element', element: native.currentTarget } })from@cocoar/vue-uito 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 withsetTimeout(..., 200)if needed). Companion playground demo at/calendar-popoverwires 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 usebox-sizing: border-boxso the 1px right border doesn't add to the cell's flex width — bars (absolutely positioned atleft = 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#barslot. 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. UniformrowHeightmakes the visible-range mathO(1)(no per-row measurement); slot renderers only fire for visible rows. Companion playground page at/calendar-timeline-perfbenches 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 ismeta.__recurrence.seriesIdfor recurring events,event.idfor standalone — automatic, consumer code unchanged. New<CoarTimelineView>component,useTimelineView()composable, and the previously-reserved'timeline'value inCalendarViewis 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 CSSposition: 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/grabbingaffordance, pointer-capture so pan continues when cursor leaves the element,touch-action: nonefor 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 atlayoutTimeline(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)withclippedStart/clippedEndflags. 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-ganttpackage that builds on the same primitive. New i18n keycoar.calendar.view.timeline(default label "Timeline"). Added to the defaultavailableViewsso 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<CoarWorkWeekView>component,useWorkWeekView()composable, and the'workWeek'value joinsCalendarViewafter'week'. Configure the working-day set viabuilder.workDays(MaybeRefOrGetter<readonly DayOfWeek[]>)— 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-rangeViewWindowstays the full Mon–Sun span (soeventsLoader/seriesLoadersee 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 defaultavailableViewslist so the shell's view-switcher gets a "Work week" button automatically (consumers hide it by setting their ownavailableViews). New i18n keycoar.calendar.view.workWeek(default label "Work week"). Pure helperworkWeekDates(date, firstDayOfWeek, workDays)exposed fromcore/index.tsfor consumer-side use. Dedicated docs page at/components/calendar/work-week-viewdocuments 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-stubexpandSeriesthat 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<RecurringSeries<TMeta>[]>)binds a reactive in-memory series source (mutating the array re-expands), andbuilder.seriesLoader((window: ViewWindow) => RecurringSeries[] | Promise<RecurringSeries[]>)binds a calendar-managed per-window async loader (cached by${view}|${timezone}|${start}|${end}likeeventsLoader). Both compose withevents()/eventsLoader()—api.getVisibleEvents()returns the merged set.series()andseriesLoader()are mutually exclusive (calling one clears the other), independent of the events pair. Expansion happens lazily per visible window, never speculatively — a series withFREQ=DAILYfrom year 2000 doesn't pay 25 years of expansion to render today's calendar.@cocoar/vue-calendar/recurrence— public subpath: standaloneexpandSeries(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 bundledrrule-temporaladapter ~4 KB; both lazy-loaded on first call). Re-exportsRecurringSeries,RecurrenceExpansionWindow,RecurrencePatterntypes and thegetRecurrenceMeta(event)provenance accessor.@cocoar/vue-calendar/recurrence-rrule-temporal— bundled engine adapter: pure-JS, Temporal-native, wrapsrrule-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 inrecurrence/. Apps with extreme volume or specialized needs (alternative parsers, backend-delegated expansion) implement theRecurrenceEngineinterface in consumer code and register viabuilder.recurrenceEngine(custom)— no library change required.@cocoar/vue-calendar—builder.recurrenceEngine(engineOrFactory): override the bundled engine per builder. Accepts aRecurrenceEngineinstance or a() => RecurrenceEnginefactory (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<TMeta>public type: Temporal-typeddtstart(ZonedDateTimefor timed,PlainDatefor all-day), RFC-5545rrulestring, optionalduration({ minutes?, hours? }for timed;{ days? }for all-day — D2 day-count semantics, noPeriod), optionalrdate/exdatearrays of the matching Temporal type. ISO strings, nativeDate, floatingTemporal.PlainDateTimerejected at the boundary; mixedZonedDateTime/PlainDateinrdate/exdateagainst the series'dtstartshape throws with the series id named.@cocoar/vue-calendar—SeriesLoader<TMeta>type: mirrorsEventsLoader. Re-exported from the package root for ergonomic single-import.@cocoar/vue-calendar— per-occurrence provenance: every expandedCalendarEventcarriesmeta.__recurrence: { seriesId, recurrenceId, source: 'rrule' | 'rdate' }, read via the new publicgetRecurrenceMeta(event)accessor exported from@cocoar/vue-calendar/recurrence. Returnsnullfor non-recurring events.recurrenceIdmatches RFC-5545RECURRENCE-IDsemantics — the original wallclock slot — so future single-instance edits (override or cancel one occurrence) are addressable without changing theCalendarEventshape. The__prefix marks the key as library-managed; consumer code reads it through the accessor, not directly.@cocoar/vue-calendar—DstPolicyenforced 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 viaTemporal.PlainDateTime.toZonedDateTimewith 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'throwsDstResolutionErroron 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). ThedetectDstSituationprimitive moved from private tocore/index.tsexport 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-CoarSelectinside the<CoarZonedDateTimePicker>panel, which itself lives in an AG Grid cell editor) closed the outer panel mid-interaction. Root cause:focusoutevents fired on the editor root withrelatedTargetpointing to the nested overlay element, bubbled up through the cell DOM, and triggered AG Grid'sstopEditingWhenCellsLoseFocuscommit → editor unmounted → both overlays destroyed. The existingmousedowncapture-phase guard (preventDefaultfor.coar-overlay-hosttargets) was insufficient because some element types still emit a focus shift despite the prevented default. New shared composableusePopupEditorFocusGuard(rootRef)installs a second guard: afocusoutlistener on the editor root that callsstopPropagationwhen therelatedTargetis 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-uiviewer components: the three renderers (CoarPlainDateCellRenderer,CoarPlainDateTimeCellRenderer,CoarZonedDateTimeCellRenderer) previously did all formatting inline usingtoLocaleString+instanceof Temporal.Xchecks against their own polyfill copy. Two symptoms surfaced from real-world use: (1) editing azonedDateTimecell wouldn't update the renderer's display — the picker (in@cocoar/vue-ui) constructedTemporal.ZonedDateTimeinstances against its own polyfill copy, the renderer'sinstanceof Temporal.ZonedDateTimefrom its own copy rejected them, and the renderer fell through to empty string. (2) Locale changes only partially propagated —toLocaleStringreacts 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 fromuseDatePickerBase, 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<CoarPlainDateView>/<CoarPlainDateTimeView>/<CoarZonedDateTimeView>, which use cross-realm-safeSymbol.toStringTagchecks and the sameuseDatePickerBasereactive 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-onlydisplayTimeZoneconfig oncol.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, andsidebarFlyoutPresetwere declared incomponents/overlay/overlay-presets.tsand re-exported by the internalcomponents/overlay/index.ts, but missing from the public package barrel — only 7 of the 12 presets were reachable. Consumer code callingimport { popoverPreset } from '@cocoar/vue-ui'crashed withSyntaxError: 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, andCoarTagSelectCellEditorpreviously onlyclick()-ed the trigger inafterGuiAttachedto auto-open the dropdown — focus stayed on the AG Grid cell wrapper. The trigger's@keydownhandler (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 calltrigger.focus()after the click.tabindex="0"on the trigger (already present) makes the focus call valid. For.searchable(),CoarSelect.onTriggerClickschedules anextTickfocus on the inline<input>— that input's own@keydowndelegates to the same handler, so search-mode keyboard navigation continues to work.@cocoar/vue-calendar— views read the merged event source (was: onlystate.events):<CoarMonthView>,<CoarTimeGrid>,<CoarAgendaView>previously readstate.events ? toValue(state.events) : []directly, bypassing the loader cache (silent foreventsLoader-mode consumers) and — once Phase 4 landed — the series cache (recurring events counted ingetVisibleEvents()but never rendered). All three views now read viaprops.builder.api.getVisibleEvents(). Caught during browser hand-test via chrome-devtools on the/calendar-recurrenceplayground 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:layoutMonthGriddedupes events byevent.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 togetRecurrenceMeta(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: theRecurrenceEngineinterface lives atsrc/recurrence/types.tswith structured wire types (EngineRequest/EngineResponseuse plain numeric components + per-endpointtzid, no string round-trips). The bundled adapter atsrc/recurrence-rrule-temporal/is the only place that importsrrule-temporal. ESLintno-restricted-importsrule enforces the topology:rrule-temporaloutsiderecurrence-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 viabuilder.recurrenceEngine(...).@cocoar/vue-calendar— race-safe series expansion:_inFlightSeriesKeys: Set<string>keyed bywindowKeyblocks 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 whenstate.seriestransitions fromnullto 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—expandSerieslazy-imported by the builder: dynamicimport('../recurrence/index')in_runSeriesExpansionkeeps the recurrence chunk out of the main bundle for apps that never use series.dist/index.jsstays 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 fourDstPolicyvalues against spring-forward + fall-back inEurope/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 withevents(), 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 inapps/docs/components/calendar/coar-calendar.md: covers theRecurringSeriesshape and Temporal-only contract, the two source modes (series()reactive andseriesLoader()cached),getRecurrenceMeta()provenance access, the standaloneexpandSeries(series, window, dstPolicy, engine?)entry, theRecurrenceEngineinterface for custom engines (with SSR factory form), and theQuartz.NET 3.18.0interop note — since Quartz now ships native RFC-5545 RRULE triggers (WithRecurrenceSchedule), the samerrulestring +dtstart+ IANA zone round-trips between Cocoar's frontend display and a Quartz backend job scheduler without a translation layer. Companion live demodemos/CalendarRecurrence.vue(DST-policy switcher, reactive add-series button, provenance click-inspect).playground(calendar)— new/calendar-recurrencedemo 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,recurrenceEnginesetters 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<preview>-embedded demos inapps/docs/components/calendar/coar-calendar.md("Popovers and tooltips" section,demos/CalendarPopover.vue) andapps/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: documentscol.multiSelect()andcol.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: documentscol.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 withtimezoneFilter).docs(ui)— new "Date Views" page at/components/date-views: documentsCoarPlainDateView,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-realmSymbol.toStringTagsafety note.docs(ui)— new "OTP Input" page at/components/otp-input: documentsCoarOtpInputwith five live demos (Basic Usage, Length, Type & Mask, Custom filtering withtransform/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 existingcellRendererConfigfor swapping in custom editors — the config is wrapped undercellEditorParams.configso 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 vianode.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<CoarTextInput>, 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'seventKey(printable key seeds the input), value pre-selected viaafterGuiAttachedso 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): existingcol.number(field, config?)keeps the legacy config-object form (renderer only). New callback form bundlesCoarNumberCellEditorautomatically — Maskito-driven locale-aware parsing means1.234,56inde-ATand1,234.56inen-USboth 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) andCoarSelectCellEditor(auto-opens the dropdown viaafterGuiAttached). Selection auto-commits — picking an option is the edit, no separate Tab/Enter needed. Configurator:options(static array OR(row) => CoarSelectOption<T>[]for row-aware menus),clearable,searchable,placeholder,searchPlaceholder,size. Dropdown teleports to<body>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<CoarCheckbox>renderer + interactive<CoarCheckboxCellEditor>. 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, fixedmin-height,margin-tophack) inside the grid cell.- Configurator-callback pattern: new
CheckboxColumnConfigurator,TextColumnConfigurator,NumberColumnConfigurator,SelectColumnConfiguratorclasses provide thes => 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—CoarNumberInputclear button no longer surfaces on focus whenclearable={false}: the.coar-number-input-clear--hiddenmodifier (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 tov-iflikeCoarTextInput) because the clear button sits to the LEFT of the input — adisplay: nonehide would shift the input on appear/disappear, hurting layout stability.@cocoar/vue-data-grid—CoarSelectCellEditorcommits 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'sstopEditingWhenCellsLoseFocusfired 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 syntheticHTMLElement.click()which bypasses focus/blur flow. Fix is a capture-phase documentmousedownlistener installed while the editor is mounted: when the click target sits inside a Coaroverlay-host,preventDefault()blocks the focus shift. The click event still fires, CoarSelect updates the model, the watch picks it up and triggersstopEditing—getValue()then returns the new value. Outside-clicks (everywhere else) still cause focus loss → AG Grid commits/cancels normally.
Internal
@cocoar/vue-data-grid— newconfigurators/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-ignorefilter onapps/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 overTMeta:useCalendarDnd,useMonthDnd,useTimeGridDndnow accept aTMeta extends Record<string, unknown>type parameter, propagating event meta types throughCalendarEvent<TMeta>,EventDropPayload<TMeta>, snapshot types, and the keyboard-drag state. Default staysRecord<string, unknown>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—useViewWindowaccepts a{ view }override: standalone sub-views (<CoarMonthView />,<CoarDayView />,<CoarWeekView />,<CoarAgendaView />) pass their intended view via the new optional 2nd argument, which setsbuilder.state.viewso the same builder composed viauseDayView()/useMonthView()etc. (which never setview) renders correctly when handed to a sub-view component. Replaces the parallelonMountedhacks 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—canDropvalidators receivedisplayZone: theCanDropTargetshape advertiseddisplayZone: stringbut 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 readtarget.displayZonedirectly instead of closing overstate.timezoneseparately.
Changed
@cocoar/vue-calendar—EventLayoutCtx.kinddiscriminator 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 universalstate.eventRendererkeep working unchanged; renderers that explicitly switched onctx.layout.kindshould now match the corrected values.
Fixed
@cocoar/vue-fragment-parser—vue-routerpeer range widened to^4.5.0 || ^5.0.0: consumer apps already onvue-router@5.xgot a peer-dependency warning because the published range was still capped at^4.5.0. The package only usesuseRoute/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— externalv-modelupdates no longer dropped during Milkdown init: a parent that initialisedv-modelsynchronously to a placeholder and then assigned the real value asynchronously insideonMounted(typical store-load pattern) saw the editor stay locked on the placeholder. The watcher inEditorImplbailed silently whengetInstance()returnednullduring 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'sloadingref flushes it viareplaceAllonce init completes. Found while migratingcocoar-policyknowledge docs to event sourcing.@cocoar/vue-calendar—<CoarCalendar>honorsprefers-reduced-motion:smoothScrollBodyTowas readingwindow.value.matchMedia(...)where the localwindowconst shadowed the global by binding to aViewWindowcomputed (a{ start, end, timezone }POJO). The match-media check therefore always returnedundefinedand the animated scroll always fired, ignoring the user's reduced-motion preference. Reaches the browser'swindowviaglobalThisnow; users withprefers-reduced-motion: reduceget instant scroll onscrollToTime/scrollToDate.@cocoar/vue-calendar— phantom-event stubs use real Temporal objects: the placeholder events passed tophantomandinvalidvariants of<CoarTimeGridEvent>/<CoarTimeGridAllDayBar>/<CoarMonthPill>/<CoarMonthBar>were typed asCalendarEventbut constructed with stringstart('1970-01-01'/'1970-01-01T00:00:00Z') — direct C1 violation in internal code. Now constructed withTemporal.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-dtsbuild 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.kindalignment,Propsinterface inlining to bypass vue-tsc's TS4025 on script-setup-generic SFCs, slot signatures marked optional sov-if="$slots.foo"no longer trips TS2774, etc.). Newpnpm typecheckscript per package wired through a turbo task (vue-tsc --noEmit) and added as a CI step between Lint and Test in bothci-developandci-pr-validationworkflows. Future TS regressions now fail the build instead of being logged and ignored. @cocoar/vue-markdown-editor— first component-level test:CoarMarkdownEditor.test.tsmounts the editor with@vue/test-utils+ happy-dom +CoarOverlayPluginand 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-editordeduplicated to^0.55.1across the workspace:apps/docsandapps/playgroundwere still on^0.54.0while@cocoar/vue-script-editor's peer-range demanded^0.55.1— for0.xversions a caret pins the minor, so the two ranges did not overlap and the lockfile carried bothmonaco-editor@0.54.0and@0.55.1side-by-side. Apps bumped to^0.55.1; the duplicate is gone.- Lint cleanup — zero workspace-wide warnings:
vue/one-component-per-fileandvue/require-prop-typesdisabled in*.test.{ts,js}/**/__tests__/**(multi-component test harnesses are the standard Vue test pattern, not a smell);vue/attribute-hyphenationdisabled at the Vue-file level (Vue'saria-*/data-*fall-through-attribute treatment skips kebab→camel coercion, so binding:ariaRowIndexon 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) andCoarMarkdownEditor.vue(innerEditorImpl+Toolbarshare theMilkdownProvidercontext).
1.16.0
Added
@cocoar/vue-calendar— new package: Vue 3 calendar component built aroundTemporal. Day / Week / Month / Agenda views, a top-level<CoarCalendar>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 isTemporal.ZonedDateTime(timed events) orTemporal.PlainDate(all-day events) — never strings,Date,PlainDateTime, orInstant. Eight architecture invariants (C1–C8) drawn from the "Time in Software, Done Right" article series are enforced structurally: Temporal-only public surface (C1), single drop pipeline throughapplyMoveToEvent(C2), per-endpoint source zones preserved across every drag mode including cross-zone events (C3), explicitDstPolicyargument on every wall-time → instant conversion (C4), display zone vs source zone surfaced separately on drop payloads (C5), independentlocale/dateStyle/timeStyle/hour12decisions merged through a singlebuildFormatOptions(C6), reactivity-by-reads not setup-capture (C7), andRecurringSeriesas a first-class type with a typed throwing-stubexpandSeriesuntil the recurrence engine lands (C8). FlatCalendarBuilder— every setter (timeRange,slotDuration,maxEventsPerCell,agendaLengthDays, …) lives on the same builder. UniversaleventRenderer((ctx) => ...)withctx.layout.kinddiscriminator ('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 helpersparseScheduledTime/formatScheduledTime/parsePlainDatemirror the Article-8{ local, timeZoneId }shape that .NET / NodaTime backends and PostgreSQLlocal_start text + time_zone_id textstorage natively speak.@cocoar/vue-calendar—Temporalre-export:import { Temporal } from '@cocoar/vue-calendar'for consumers that don't want a direct@js-temporal/polyfilldependency.@cocoar/vue-calendar— default event renderers surface C3 / C5 zone semantics: a shared decoration layer (<CoarEventDecorations>, 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: <iana>" (Article 3 — render the user's clock without hiding the source). Suppressed on multi-day bars whenclippedStartso only the visible head decorates. The same logic ships as a public helpergetEventZoneHints(event, displayZone) → { isUtcAnchored, sourceZone }for custom renderers. Three i18n keys:coar.calendar.event.utcLabel,coar.calendar.event.utcGlobalHint,coar.calendar.event.crossZoneHint.<CoarDisplayZoneSwitcher>— drop-in display-zone selector exported from@cocoar/vue-calendar: wraps<CoarSelect>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:optionsoverride for full IANA / domain-specific lists.v-modelis the IANA id string the consumer passes intobuilder.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 atsrc/core/__tests__/timezone/pins every C1–C8 invariant; component tests cover the shell + each sub-view + the drop pipeline integration;useViewWindowtests pin the C5 single-writer invariant; zone-hint helper covered by 7 dedicated tests.
Docs
- New "Calendar" sidebar group under Components — overview page,
<CoarCalendar>(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: newtextColortool exposes a palette + native color-input picker in the floating toolbar and the sidebar (fixed mode). Selection-basedtext_colorProseMirror mark, full markdown round-trip as<span style="color: …">…</span>. 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),textColorplugin bundle,textColorMarkandtextColorRemarkfor advanced setups.@cocoar/vue-markdown-core— color-span sanitizer + parser fold: sharedsanitizeColor/sanitizeColorStyle/parseColorSpanOpen/isColorSpanClose/serializeColorSpanOpen/serializeColorSpanClosehelpers. 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); rejectsvar(--token),url(…),expression(…), multi-declaration styles, foreign attributes, control characters. NewcolorSpanMarkdownNodeTypewithattrs.color; the parser folds matched<span>open/close pairs in inline children into a single node (depth-aware nesting). Serializer flat-maps colorSpan back tohtmlopener + children + closer.@cocoar/vue-markdown—colorSpanrenderer:DefaultColorSpanregistered indefaultMarkdownRenderers. Re-validates the color attribute viasanitizeColorat render time (defence in depth) — invalid values strip the inline style and fall through to plain text. NewcolorSpanColorhelper exported fromhelpers.ts.
Changed
@cocoar/vue-markdownshared stylesheet — editor↔viewer parity: rendering rules now cover both the viewer's class-based DOM (<div class="coar-markdown-list-item">…) and the editor's PM-managed bare-element DOM (<li>no class). Block-spacing and typography selectors extended with.coar-markdown .ProseMirror > :where(…)so the editor's.ProseMirror-wrapped blocks pick up the same vertical rhythm. New bare-element fallbacks for:where(blockquote, ul, ol, li, code:not(pre code), a). User-agent margin on<p>inside<li>/<td>/<th>zeroed (PM wraps cell/list content in<p>— without the reset every editor row was one line taller than its viewer pendant). Table zebra rule rewritten with:nth-child(<n> of :not([data-is-header]))so Milkdown'str[data-is-header](which lives inside<tbody>, unlike the viewer's<thead>) is excluded from the alternation index — the first data row reads as "row 1" in both panes. Bare<blockquote>margin reset to0so the browser's user-agent40pxmargin doesn't indent editor blockquotes deeper than viewer blockquotes.@cocoar/vue-markdowntask-list rendering:DefaultListItemno longer emits a native<input type="checkbox">+ wrapping<div class="coar-markdown-list-item-content">. It now mirrors the editor's PM-emitted attributes —data-item-type="task"+data-checked="true|false"on the<li>directly — and the visual checkbox is a::beforepseudo-element on the<li>(cocoar-style filled square + check). Strikethrough on completed items moved to the shared stylesheet so editor and viewer render identically.@cocoar/vue-markdownshared stylesheet —--coar-markdown-heading-block-startlowered fromvar(--coar-spacing-xxxl, 4rem)tovar(--coar-spacing-xl, 2rem). The previous 4rem/64px gap before every heading read as "blank lines" in both viewer and editor; 2rem/32px still marks sections clearly without pushing four lines of whitespace into view.@cocoar/vue-markdown-editorstyles trimmed: deleted local typography overrides (h1/h2/h3/p/ul/ol/li/blockquote/code/table/th/td/a/strong) plus the task-list checkbox CSS — all now live in@cocoar/vue-markdown/stylesand apply uniformly. Editor<strong>was rendering atfont-weight: 600while the viewer used the browser'sbolder(700); both now read 700.
Fixed
@cocoar/vue-markdown-editor— color picker no longer flashes at (0,0): the previous manualTeleport+getBoundingClientRectpositioning rendered the popover at the initial{ left: '0px', top: '0px' }style ref before reactivity flushed the measured anchor coordinates. Replaced withuseOverlay().open({ spec: menuPreset, anchor: { kind: 'element', element: trigger } })so the overlay service measures + positions atomically before paint.@cocoar/vue-markdown-editor— color picker now closes on outside click: the previous custommousedownhandler exempted only.coar-md-color-picker, so clicks anywhere on the page outside that selector closed the picker via the floating-toolbar hide path — but the picker had no escape-key dismissal, no scroll-close, and was inconsistent with other Cocoar overlays. Now driven bymenuPresetwhich givesoutsideClick: true+escapeKey: true+scroll.strategy: 'close'for free.
1.14.0
Added
@cocoar/vue-ui—CoarSidebarsupports all four edges: newsideprop accepts'left' | 'right' | 'top' | 'bottom'(default'left');top/bottomswitch the layout to a horizontal toolbar (items in a row, scrolls horizontally) whileleft/rightkeep the classic vertical column. Tooltip placement, flyout direction, the active-state indicator border edge, and the collapsed dimension (width vs. height) all derive fromsideautomatically.CoarSidebarItemandCoarSidebarGroupinject the side via a newSIDEBAR_SIDE_KEYto adapt their own internals — child group flyouts open right (left side), left (right), down (top), or up (bottom). Items inside a horizontal sidebar areflex-shrink: 0so the row genuinely overflows rather than squishing, which is what triggers the OverlayScrollbars horizontal scrollbar. The deprecatedpositionprop still works for backwards compatibility but maps internally toside.@cocoar/vue-ui— collapsed sidebar dimensions auto-scale withsize: previous4remdefault for--coar-sidebar-collapsed-widthwas generous for nav rails but too wide for icon-only formatting toolbars. Per-size fallbacks now resolve to2.25rem(s),2.75rem(m),3.25rem(l) for both the collapsed width (vertical) and the new collapsed height (horizontal). Setting--coar-sidebar-collapsed-width/--coar-sidebar-collapsed-heightexplicitly still overrides the fallback — the variable becomes defined in the cascade andvar()returns the inherited value instead of the per-size literal, so app-level / wrapper-level overrides keep working unchanged (the@cocoar/vue-markdown-editorwrapper relied on this and continues to win at2.25rem).@cocoar/vue-ui— expanded-group children render as visually nested: items inside a<CoarSidebarGroup>'s expand panel get tighter padding, smaller font, ascale(0.85)icon with reduced opacity, and an explicit opacity fade on the label. Rule applies in all four orientation × collapsed combinations, so children read as nested whether the sidebar is vertical or horizontal, collapsed or expanded — important for horizontal expand mode where children sit inline with their parents on the same row and indent alone is no longer a cue. Hover background stays at full strength because the opacity is on the icon and label individually, not the item.@cocoar/vue-markdown-editor—toolbarPositionextended to all four edges: type widened from'left' | 'right'to'left' | 'right' | 'top' | 'bottom', so the formatting toolbar can sit above or below the editor as a horizontal strip. Editor root'sflex-directionflips betweenrow(left/right) andcolumn(top/bottom), and the toolbar/editor border edge follows the chosen side. The wrap now sets both--coar-sidebar-collapsed-widthand--coar-sidebar-collapsed-heightso the toolbar stays at2.25remin either orientation. Existing'left'/'right'consumers are unaffected — same default, same visuals.
Fixed
@cocoar/vue-ui—vTooltipdirective value type now allowsfalse/null/undefined: every collapsed-aware component (CoarSidebarItem,CoarSidebarGroup,CoarMultiSelect, …) returned a falsy value from itstooltipConfigcomputed to mean "do not render a tooltip" — which the directive's runtime already handled correctly but its TypeScript signature did not (string | TooltipOptionsonly).vue-tscflagged the call sites, even though the runtime was fine. Type widened tostring | TooltipOptions | false | null | undefinedandgetOptionsnow returns{ content: '', disabled: true }for falsy bindings instead of castingfalsetoTooltipOptions(was a latent runtime smell — primitive-boolean property reads happened to returnundefined, butstate.optswas lying about its type).
1.13.1
Fixed
@cocoar/vue-data-grid— wrapper column inner renderer config was shadowed by the wrapper config: factory-created column renderers (col.tag(),col.tree(),col.date(),col.number(),col.currency(),col.icon()) all read their own configuration viaparams.colDef.cellRendererParams.config, but AG Grid hands the outer (wrapped) colDef to the renderer — so wrapped inner renderers were receiving the wrapper's config instead of their own, droppingvariantMap,showChildCount,decimals,currencyCode,includeTime, and every other inner-renderer option. Tree columns stayed mostly functional (expand/collapse + indentation come fromgrid.context.coarTree, notcellRendererParams), butshowChildCountwas lost; tag, date, number, currency, and icon columns fell back to their defaults entirely when wrapped.WrapperCellRenderernow rebuilds the inner'sparams.colDef.cellRendererParamsso nested factory columns see exactly what they would see unwrapped. New probe test replicates the factory renderers' read path to prevent regression.
1.13.0
Added
@cocoar/vue-markdown-editor— new package: WYSIWYG Markdown editor for Vue 3 based on Milkdown (Kit approach), styled with the Cocoar Design System. Markdown-first round-trip (lossless) —v-modelis the persistence format, no JSON intermediate. Shares the same remark stack as@cocoar/vue-markdown-coreand<CoarMarkdown>. Three toolbar modes:floating(default, appears on text selection, teleported to<body>),fixed(CoarSidebarcollapsed strip with flyout submenus), andboth.toolbarPosition('left'|'right') controls sidebar side. Reactive active-state highlights on every button (Bold lights up when the cursor is in**bold**, Bullet List when inside a list, etc.) — driven by Milkdown'sselectionUpdatedplugin hook with aqueueMicrotaskdefer so the listener reads the freshly-committedview.stateinstead of the pre-apply snapshot.@cocoar/vue-markdown-editor— 18 toolbar tools, configurable viatoolswhitelist:bold,italic,strikethrough,inlineCode,headings(flyout H1–H6 + paragraph),bulletList,orderedList,taskList,indent,outdent,blockquote,horizontalRule,codeBlock,table,tableOps,clearFormatting,undo,redo.toolsundefined → all 18 render;toolsset → only the listed in canonical order, with auto-cleanup of orphan dividers. ConstantCOAR_MARKDOWN_EDITOR_ALL_TOOLSexported for consumer-side filtering. Migration mapping from richtext editors that exposedfont-size/align/font/color/underlineis documented in the docs page (those have no Markdown representation and are intentionally not exposed;font-sizemaps toheadingsfor typographic hierarchy).@cocoar/vue-markdown-editor— list & task semantics: list-toggle button cycles "in same → lift / in other → switch / outside → wrap" (clicking Bullet inside a Bullet item un-lists it; clicking Ordered inside a Bullet swaps the type). Task list items render with proper checkboxes via CSS::before(filled accent + strikethrough text when checked, hollow box when open); clicking the checkbox toggles thecheckedattribute round-tripping through Markdown as- [x]/- [ ]. Indent (sinkListItem) is disabled outside any list; Outdent (liftListItem) is disabled at the top list level — leaving the list is the list-button's job.@cocoar/vue-markdown-editor— sidebar context-aware table operations: when the cursor lands inside a table cell, the sidebar grows by 5 items (Insert Row Above/Below, Insert Column Left/Right, Delete Cell). Lets users edit table structure infixedtoolbar mode without falling back to the floating toolbar.@cocoar/vue-markdown-editor—CoarFormFieldintegration:disabled,error,id,aria-describedbypropagate automatically when the editor is wrapped in<CoarFormField>(sameFORM_FIELD_INJECTION_KEYinjection used byCoarTextInput/CoarScriptEditor). Direct props win over the injected context. Editor wrapper carriesaria-invalid,aria-disabled,aria-readonly,aria-required,data-nameso screen readers and form tooling see the right state.@cocoar/vue-markdown-editor— code-block view/edit toggle (NodeView): code blocks render asCoarCodeBlock(Prism-highlighted, language label, same look as the viewer) when the cursor sits elsewhere; switching to plain editable mode +CoarSelectfor the language when the cursor moves inside. Toggle is driven by a custom ProseMirror NodeView + a small companion plugin that watchesselectionUpdatedsoTextSelection(the natural cursor placement) flips the mode — PM's ownselectNode/deselectNodeonly fire forNodeSelection. Header dimensions / background / border-radius mirrorCoarCodeBlockso the toggle is visually flush.@cocoar/vue-markdown-editor—./stylessubpath export (mirrors@cocoar/vue-data-grid): consumers@import "@cocoar/vue-markdown-editor/styles"to pull the bundled CSS. Same fix applied to@cocoar/vue-script-editor, which was missing the subpath export since 1.9.0.@cocoar/vue-markdown— shared rendering registry: the viewer (<CoarMarkdown>) and the editor (@cocoar/vue-markdown-editor) consume the same Vue component map for every node type, so a code block, table, blockquote, etc. looks identical whether the user is reading or writing. The registry is exposed asMarkdownViewerRenderers(a typed map ofMarkdownNodeType → Component), with the Cocoar defaults available asdefaultMarkdownRenderers. Apps override slots per-instance via<CoarMarkdown :renderers="{...}" />or app-wide viaapp.provide(MARKDOWN_RENDERERS_KEY, ...). Resolution order: prop > inject > default. The recursive<RenderNode>dispatcher and therenderMarkdownNodeshelper are exported for consumers writing custom complex renderers.@cocoar/vue-markdown— shared block stylesheet at@cocoar/vue-markdown/styles. Headings, paragraphs, lists, blockquotes, tables, inline code, links, etc. all live in one CSS file that both the viewer and the editor pull in via thecoar-markdownouter class. The editor adds a deeper-specificity layer for compactness (smaller heading sizes for an editing surface) on top of the shared baseline.- Markdown table styling alignment: Markdown tables in the editor inherit the same shared stylesheet rules as the viewer's tables — zebra rows, header surface, padding, border. The viewer's
DefaultTableno longer wraps in<CoarTable>(was relying on:deep()scoped CSS that wouldn't reach the editor's contenteditable); both viewer and editor now emit a plain<table class="coar-markdown-table">that the shared stylesheet drives. Visual parity without a NodeView wrapper for tables. @cocoar/vue-data-grid— Wrapper Column: new.wrap(inner)factory on the column builder that decorates any column with left and/or right cell-body slots, ideal for status indicators, action icons, and inline badges without writing a customcellRenderer. The inner builder stays a realCoarGridColumnBuilder— sort, filter, quickFilter,valueFormatter,valueGetter, comparator,editable, and even existingcellRenderers (tag, date, number, currency, tree) all continue to work untouched; only thecellRenderergets wrapped. Example:col.wrap(col.field('name').header('Name').flex(1).sortable().option('editable', true)).left({ icon: (r) => r.starred ? 'star' : null, onClick: toggleStar }).right({ component: UnreadBadge, params: (r) => ({ count: r.unread }), show: (r) => r.unread > 0 }). Slots accept three shapes: icon shorthand ({ icon, source, size, color, tooltip, onClick, show }), component (any Vue component; automatically receivesrow: TDataas a prop, with optionalparams(row)to add/override props), and text ({ text, tooltip }). Each accessor (icon/color/text/tooltip) can be a static value or a per-row function. Pass an array to stack multiple items in the same slot — each with its ownshow()gate,onClick, and tooltip — e.g. two right-side icons forisCritical+awaitingFeedbackplus a third component slot that renders a tag or icon based on apriorityfield. SlotonClickhandlers callevent.stopPropagation()automatically so they don't trigger row-click / cell-click. Edit mode is untouched: AG Grid swaps the renderer for the editor on double-click (wrapper disappears during editing, reappears on Escape/commit). Wrapper slots are intentionally not in the tab order — they're visual hints, Tab navigates cell-to-cell. Ships withWrapperCellRenderer,CoarGridWrapperColumnBuilder, and fully typedWrapperSlotConfig/WrapperIconSlotConfig/WrapperComponentSlotConfig/WrapperTextSlotConfig/WrapperCellRendererConfigexports.- 17 new Lucide icons in the core registry:
bold,italic,strikethrough,heading,pilcrow,list-ordered,text-quote,square-code,table,table-cells-merge,table-cells-split,columns,rows,between-horizontal-start,between-horizontal-end,between-vertical-start,between-vertical-end,indent-increase,indent-decrease,eraser. Available to all consumers via the standard icon name lookup.
Changed
@cocoar/vue-markdownpackage surface: the viewer is no longer a thin wrapper. The package now hosts the registry, the default renderers, the recursive dispatcher, the helpers, and the shared CSS — all next to<CoarMarkdown>. Consumer-facing imports are unchanged (CoarMarkdownis still the top-level export); new exports (defaultMarkdownRenderers,MARKDOWN_RENDERERS_KEY,MarkdownViewerRenderers,RenderNode, …) sit alongside it. Internal-onlyMarkdownBlockNode.vue/MarkdownInlineNode.vue/helpers.tswere removed — their logic moved into the per-type default renderers indefault-renderers.ts.
Fixed
- Playground — markdown editor body font fell back to bare
sans-serif:App.vuereferenced a non-existent design tokenvar(--coar-font-family, sans-serif)(the actual token is--coar-body-base-family). The fallback chain bottomed out at the bare keyword for everything inside the playground's.appwrapper, including the markdown editor's body text. Updated to use the correct token; Poppins now inherits cleanly through the editor area.
Docs
- New "Markdown Editor" component page marked as Preview, with three live demos (basic
v-model, sidebar mode, in-form integration withCoarFormField), Architecture Notes section explaining why Milkdown over TipTap/Crepe and why@milkdown/components/table-blockis intentionally not used (CellSelection is ProseMirror-internal and doesn't fireselectionchange), Restricting the Toolbar section with the migration mapping table, "Code blocks — view / edit toggle" section documenting the toggle UX and supported languages, full Props/Events reference, and a TODO list for the deferred work (link-insert UI, image upload, placeholder, custom table edge-handles). /components/markdown— Custom renderers section with worked examples (per-instance override, app-wideprovide, registry contract, why the registry matters cross-package).
Internal
@cocoar/vue-markdown-editortest coverage: 12 Vitest unit tests for the pure helpers (isToolEnabled,decideListToggleAction) extracted totoolbar-helpers.ts, plus 23 Playwright E2E tests against the playground covering mounting, floating-toolbar visibility, mark commands via sidebar, full-set / minimal / no-tables tool whitelisting, indent + outdent (including the disabled-state gating), bullet-list wrap on plain text, clear-formatting (mark stripping + heading→paragraph), task-checkbox toggle in both directions, readonly mode, and the code-block view/edit toggle including language-selector → markdown-source round-trip.@cocoar/vue-markdowntest coverage: 9 viewer unit tests (7 existing for rendering + 2 new for therenderersprop override path).- Dependabot — 10 of 13 alerts cleared.
pnpm update+pnpm dedupeliftedhappy-dom,flatted,picomatch,minimatch, andbrace-expansionto patched versions. The remaining 3 alerts arevite 5issues that come in transitively viavitepress 1.6.4(which pinsvite 5as a peer). An attempt to globally overrideviteto^8was reverted because vitepress 1 is incompatible with rolldown-vite (the engine vite 8 ships with). The remaining alerts are dev-tooling only — no production runtime impact for consumers — and will close out when vitepress 2 (currently alpha-only) ships stable.
1.12.2
Fixed
@cocoar/vue-script-editor—extraLibswere invisible to the TypeScript worker, silently resolving toanyon hover:useMonacoEditorconfigured the shared TS/JS compiler options withnoResolve: true(added in 1.11.0 alongside thelib: ['es2024']fix). WithnoResolveset, the TS language worker skips pullingaddExtraLib-registered.d.tsfiles into compilation — so an identifier declared exclusively in an extraLib was accepted syntactically but resolved toany.noResolveis now removed from the shared options; library targeting stays constrained to['es2024'], so the original reason for the flag (keep the default DOM / WebWorker / WSH libs out of IntelliSense) is unaffected. Consumers usingextraLibsimmediately see correct hover types and IntelliSense with no code change.@cocoar/vue-script-editor—script-modeswallowed legitimateCannot find nameerrors:SCRIPT_MODE_DIAGNOSTIC_CODESadded TS code2304to Monaco'sdiagnosticCodesToIgnorealongside the structural wrapper artefacts (1375top-level await,2695unused comma-LHS,1108top-level return,7027unreachable code,1208isolatedModules).2304is a genuine semantic error — not a wrapper artefact — and suppressing it masked thenoResolvebug above: undeclared identifiers rendered asanywith no squiggle. Code2304is now removed from the suppression set; the other five codes remain. Possible visible change for consumers: code that previously compiled silently underscript-modewith unresolved identifiers now shows red squiggles. Register the missing names viaextraLibs, or extenddiagnosticCodesToIgnoredirectly onmonaco.languages.typescript.*Defaultsto restore the old behavior.
1.12.1
Fixed
CoarDataGrid— flex columns collapsed to ~36px when tree-mode grid started with emptyrowData: the flex-recalc workaround that re-appliescolumnDefsafter first data arrives (introduced in 1.5.3 for the flat-data codepath) was missing from#setTreeRowDataOnGrid, and the one-shot flag#flexAppliedwas consumed prematurely in both codepaths by the initial empty set that Vue's{ immediate: true }watcher fires on mount. Net effect: atreeDataRef(ref([]))grid mounted with zero rows, and by the time real rows arrived the workaround had already fired (against an empty grid) and no further re-flex ever happened. Flex columns kept whatever width AG Grid had assigned at mount — in narrow-at-mount scenarios that bottoms out around 36px, clipping cell content and breaking PlaywrighttoBeVisible()checks. The workaround is now applied in the tree codepath too, and the flag only flips onceresult.length > 0, so the one-shot is reserved for the actual 0→N transition. Consumers withtreeData(...)+flex(1)columns + initially-empty row refs get the correct flex layout without a manual resize.
1.12.0
Fixed
- Overlay widgets inside modals — stacking + tree-aware dismissal:
CoarSubFlyout,CoarContextMenu, the Select family (CoarSelect/CoarMultiSelect/CoarTagSelect), andCoarSidebarGroup(inmode="flyout") used to mount their panels via their own<Teleport to="body">with a hardcodedz-index: var(--coar-z-overlay)= 1000. Inside a dialog rendered through the overlay-service atz-index: 1002, those panels landed behind the dialog and clicks on them were treated as outside-the-dialog → the dialog closed. Each widget now opens its panel viaoverlay.open({ parent: useOverlayParent() }), so the service stacks it above the ancestor dialog (1000 + instance.id * 2) and treats clicks inside it as clicks inside the parent tree. All public APIs are unchanged — the migration is purely internal. - Overlay-service parent linking was a no-op when called from a descendant overlay:
useOverlayParent()returned anOverlayInstance, butOverlayOpenOptions.parentwas typedOverlayRef; the service looked up__instanceIdonly, silently failing for every instance-shaped value. The widened lookup now resolves either shape. Before the fix,instance.parentstayednulland the tree-aware click-outside never closed child branches — clicks outside a popover in a dialog used to leave the popover lingering until a hover-out timer fired. After the fix, one outside click cascades through the entire branch. - Select dropdown
transform: translate3d()containing-block trap: the dropdowns used to position themselves with a transform, which CSS-spec-wise creates a containing block for everyposition: fixeddescendant. Floating widgets rendered from inside a dropdown landed at the dropdown's offset instead of the viewport. The service's overlay host uses plaintop/left, so the trap is no longer reachable through a select. - Select dropdown scroll behavior:
selectPreset.scroll.strategychanged from'close'to'reposition'— dropdowns now follow the trigger on scroll instead of closing abruptly, matching the pre-migration (useSelectDropdown) behavior.
Changed
- Overlay service — parent type widened:
OverlayOpenOptions.parentnow acceptsOverlayRef | OverlayInstance. Existing callers that passed anOverlayRefcontinue to work; descendants usinguseOverlayParent()no longer have to cast. selectPreset.a11ydropped (was{ role: 'listbox' }): the select panels already render an innerrole="listbox"element that carries the referenced id andaria-multiselectable. Declaring the role on the outlet host duplicated the semantic element and misplaced the aria attribute.
Internal
- 6 new panel components:
CoarPopoverPanel(pre-existing),CoarTooltipPanel(pre-existing),CoarSubFlyoutPanel,CoarContextMenuPanel,CoarSelectDropdownPanel,CoarMultiSelectDropdownPanel,CoarTagSelectDropdownPanel,CoarSidebarFlyoutPanel. Each is the lean visual shell the overlay-service mounts; the owning component keeps its state machine (hover/click/keyboard/cascade) and forwards reactive state via props or closures. - 3 new overlay presets:
subFlyoutPreset,contextMenuPreset,sidebarFlyoutPreset. - Legacy helper removed:
packages/ui/src/components/select/useSelectDropdown.ts(dead after all three select variants migrated). - Playground diagnostic view extended:
/overlay-stackingnow covers 8 scenarios (popover, tooltip, submenu, context menu, nested popover→tooltip, nested popover→popover, all three selects, sidebar flyout with nested groups). Used for chrome-devtools-driven verification of every migration.
1.11.0
Added
CoarListbox— new component: single-column list primitive with multi-select highlight (Ctrl/Shift/Keyboard), search (three layers of control —searchFields,searchBy,filterWith), grouping with sticky or non-sticky headings, custom item rendering viaitemComponents(kind → component) or#item/#item-<kind>slots, per-item imperative API for inline actions,displayOnlymode for static rosters with ARIArole="list", and both native keyboard nav (arrows,Home/End,Space,Ctrl+A,Enter) anditem-click/item-dblclick/item-activateevents. Fully generic overT(CoarListboxOption<T>); ships its own Kitchen-Sink-grade prop/event/slot surface.CoarDualListbox— new component: composes twoCoarListboxinstances + move buttons for the classic "available ↔ selected" pattern. Managesv-model: T[](right column), forwards search / sort / grouping / custom-render / drag-drop / virtual props to both sides, exposesmoveRight/moveLeft/moveAllRight/moveAllLeft/clearHighlightvia template ref, and bubblesitem-remove/item-actionfrom custom renderers with aside: 'available' \| 'selected'annotation. Drag-drop between the two columns via one prop (drag-drop).CoarListboxItemApi<T>— imperative handle for custom item renderers: every component registered viaitemComponentsand every#item/#item-<kind>slot now receives anapiprop withhighlight()/unhighlight()/toggleHighlight()/activate()/remove()/action(name, payload?).removeandactionbubble up asitem-removeanditem-actionevents on the listbox — consumers update their ownoptionsarray. Powers inline trash buttons, ×-to-remove in the selected column of a DualListbox, per-row context-menu actions.- Drag & drop — first-class feature on
CoarListboxandCoarDualListbox. Three layers of permission:drag-group(coarse name matching),drag-id+drag-accept(directional whitelists for asymmetric flows like box1→box2→box3 with no back-edges), andcan-drag/can-dropcallbacks for per-item source control and runtime target validation. Selection-aware: dragging a highlighted item carries the whole highlighted set. Visual feedback viaisDragOverwithdropEffect='none'cursor when a drop is refused.items-add/items-removeevents fire synchronously on drop so there's no "duplicated items" frame between source and target re-renders.CoarDualListboxauto-wires an internal drag group whendrag-dropis set. - Virtual scrolling on
CoarListboxandCoarDualListbox(virtualprop): renders only the rows inside the viewport + overscan. Supports mixed per-row heights (items vs. group headings), search/filter, custom components, drag-drop, and keyboard nav (scrollToIndexfollows the focus). Tested with a 10,000-entry IPrincipal directory demo. useVirtualList— new exported composable (@cocoar/vue-ui): the framework-agnostic primitive behind virtual mode. Fixed or per-indexitemSize, configurableoverscan,scrollToIndex(i, align?). Cumulative-offset table (O(log n) per scroll), reactive on count / size changes,ResizeObserverfallback for environments without one. Usable standalone in any Vue component that scrolls — not tied to the listbox.useDragDrop— new exported composable (@cocoar/vue-ui): the generic primitive behind listbox drag-drop. Same group / accept / canDrop / canDrag semantics, same cross-surface registry that carries live object identity through DataTransfer. Ships a module-level registry (registerDrag/getDrag/getActiveDrag/deleteDrag+DRAG_MIMEconstant) for advanced integrations. Reach for it when building any other Vue component that needs the same drag semantics — no need to reimplement.- Boolean-prop convention:
displayOnly,hideSearch,hideMoveAll,hideCounts,sortSelectedBySource— all new boolean props defaultfalse, matching the library-wide "features are opt-in" rule. Where a feature should feel on-by-default (e.g. search onCoarDualListbox), the prop name is inverted so the default can stayfalse.
Fixed
- Monaco
libconfiguration for Jint-backed runtimes (@cocoar/vue-script-editor):useMonacoEditornow callssetCompilerOptions({ lib: ['es2024'], target: ES2024, allowNonTsExtensions: true, noResolve: true })on both TS and JS defaults on first mount. Previously Monaco fell back to its defaultlib = ['es5', 'dom', 'webworker.importscripts', 'scripthost'], autocompleting ~5485 browser / WSH / WebWorker APIs that don't exist in Jint —fetch,document,localStorage,WScript,importScripts, etc. — luring users into code that crashes at runtime. After the fix IntelliSense only surfaces standard ECMAScript APIs Jint actually runs; host-specific globals are layered back in explicitly viaextraLibs. The enum value for the script target is resolved with fallback (ES2024 ?? ES2023 ?? … ?? ES2020) so the fix works across Monaco versions.
Docs
- New component pages: Listbox, Dual Listbox — each with 5–7 live demos (basic, display-only, grouped, custom item component, directional DnD, virtual 10k, inline remove button, drag-drop columns) plus full API tables for props, events, slots, exposed methods.
- New Utilities pages:
useVirtualList(with a standalone 50k-log-line demo built from plain<div>s — no listbox in sight) anduseDragDrop(with a 3-column custom Kanban board, no listbox). Cross-linked from the component pages so consumers can discover the primitives. - Script Editor — "Runtime lib configuration" section: explains the default Monaco lib set vs. what Jint provides, documents the applied override, and explains how to provide a different lib set for non-Jint scenarios.
Internal
@cocoar/vue-ui/composablesmodule: new home for reusable composables. CurrentlyuseVirtualList,useDragDrop, and thedragRegistryprimitives; wired intoCoarListboxinternally so there is one source of truth for virtual-scrolling math and DnD semantics across the library.- Test coverage: +79 unit tests for the new surface (CoarListbox: 38, CoarDualListbox: 17, useVirtualList: 13, useDragDrop: 10, CoarScriptEditor: 1 for the Monaco fix). Full UI suite 1142/1142; script-editor 92/92.
1.9.0
Added
@cocoar/vue-script-editor— new package: Monaco-based code editor for Vue 3 with TypeScript, JavaScript, and JSON support. Peer-deps onmonaco-editor.v-modelis the persistence format,extraLibsfor TypeScript type injection (IntelliSense on domain types), Cocoar light/dark Monaco themes with reactiveautodetection that tracks.dark-modeclass on<html>/<body>(Cocoar convention),data-themeattribute, or OSprefers-color-scheme.getEditor()/getModel()escape hatches for Monaco APIs not covered by props.- Constrained mode (
// @lockedline protection): any line of the source containing// @lockedis protected against edits, deletion, and line-merging. Users can't touch the marker or its line; everything else is freely editable — including the file top, so TypeScript's Auto-Import quickfix works naturally. Markers stay inv-model, so the editor value round-trips through persistence with no extra schema. Powered byChangeGuard(inclusive overlap check + multi-cursor atomic rollback viaeditor.trigger('undo')),CursorGuard(snap away from locked interiors),DiagnosticsFilter(hides TS error markers that fall on locked lines caused by in-progress bodies), and per-mount auto-feature policy (formatOnType,formatOnPaste,linkedEditingdisabled to prevent cross-boundary reformats). - Authoring mode (
authoringprop): suspends enforcement so template authors can edit locked lines and markers themselves. Markers render at full size with a warm accent colour to signal enforcement is off. Toggle back to resume enforcement with the current marker state. @rejectevent: emits{ reason, range? }when a guard rolls back an illegal edit — hookable for toast / shake / line-highlight feedback.- Pure helpers (no editor mount required):
scanLockedLines,computeProtectedRanges,hasLockedMarkers,getEditableSegments,getSlots,getSlot,editIsProtected,snapOffsetAwayFromLocked,countLockedLines,isEverySegmentNonEmpty,validateSource. Use for submit-gating, server-side validation, or tests.SLOT_MARKER_PATTERNis exported as a regex source string so server-side parsers (e.g. a C# Jint host) can mirror the same matching. - Named slots (
@slot:NAME): attribute placed on a// @lockedline that names the editable segment which follows.getSlots(source)returns a{ slotName: bodyContent }dictionary,getSlot(source, name)returns a single body (orundefinedwhen the template does not declare it). Lets a consumer identify per-region fill state without knowing segment positions — ideal for templates where the user may fill in 0..N of several named function bodies and the runtime needs to decide which ones to invoke. Slot markers survive line shifts (e.g. auto-import at file top) because they're anchored to their locked line, not a fixed line number. First-wins on duplicate names;LockedLine.slotNameexposes the parsed name for custom tooling. - Form integration (
CoarFormField):CoarScriptEditornow auto-inheritsid,error,describedBy, anddisabledfromCoarFormFieldthe same wayCoarTextInputdoes. New props:disabled,error,placeholder,required,autofocus,id,name,height(CSS string or number). New events:focused,blurred. New exposed method:focus(). variant: 'editor' \| 'inline': compact form-field preset that turns off line numbers, gutter, folding, glyph margin, and context menu, and switches to tight padding + word-wrap + hover/focus ring matchingCoarTextInput.'editor'(default) keeps the existing full-chrome IDE look.lineNumbers: boolean: explicit toggle that overrides the variant default ('editor'→ on,'inline'→ off). When line numbers are off a small decoration column stays visible so the text is not flush with the border.scriptMode: boolean: suppresses the diagnostic codes Monaco emits for "script body" code — top-levelreturn/await/export, implicit any on injected globals, and unreachable-code warnings. Global side-effect ontypescriptDefaults/javascriptDefaults; documented in the form-integration section of the Script Editor docs.preamble: string: hidden + auto-locked prefix providing per-editor type context (e.g."declare const query: TodoQuery;"). Rendered invisibly above the user script viasetHiddenAreas, protected from cursor/paste/edit by an internal preamble guard, and stripped from the emittedmodelValueso it never round-trips through persistence.- Bundled
Cascadia Codefont:@cocoar/vue-ui/fontsnow also loads Cascadia Code (weights 400, 600, 700). BothCoarCodeBlockandCoarScriptEditornow prefer it over the previous Consolas/Monaco stack, with the same stack as fallback. Monaco getsfontLigatures: trueso!=,=>,===, and friends render as combined glyphs. Consumers who import@cocoar/vue-ui/fontsget the upgrade for free; consumers who do not (or who ship their own font stylesheet) fall back to Consolas/Monaco as before.
Docs
- New "Script Editor" component page: full guide with 6 live demos (basic TS, extraLibs, JSON, read-only + minimap, constrained mode with authoring toggle, and a form-integration demo with
CoarFormField+ preamble + extraLibs). Covers worker setup for SPA and SSR (VitePress / Nuxt / Astro),theme="auto"signal priority, JSON-schema configuration via Monaco escape hatch, security notes on untrustedextraLibs.content, and the full API reference with events and exposed methods. - Form-integration section in the Script Editor page: explains
preamblevsextraLibswith a decision table, documents the diagnostic codesscriptModesuppresses, and walks through thevariant="inline"form-field look.
Fixed
- Overlay system — fixed-positioned descendants inside modals: the
.coar-overlay-hostpositioned itself viatransform: translate3d(...), which CSS spec-wise creates a containing block for everyposition: fixeddescendant. Any component inside a dialog/menu/popover that relies onposition: fixedfor its own popups (Monaco's IntelliSense, floating tooltips, portal-style widgets) rendered at the overlay's offset instead of the viewport. Switched overlay positioning to plaintop/left— stacking isolation is still provided byposition: fixed+ numericz-index, and fixed descendants now resolve against the viewport as expected. Transparent to all existing Cocoar components.
Internal
- Playwright E2E infrastructure: first end-to-end test suite in the repo, wired into
apps/playground. Covers constrained-mode guards (executeEdits+ keyboard flows), undo/redo granularity, paste-across-boundary, multi-cursor mixed-zone edits, authoring toggle, diagnostics filter, language switching, and editor-in-modal IntelliSense positioning. 54 unit tests + 31 E2E tests total for the new package.
1.8.0
Added
- Select sorting (
sortGroups,sortOptions): Two new props onCoarSelect,CoarMultiSelect, andCoarTagSelectto control the display order of groups and options. Both accept presets ('asc','desc','none') or a custom comparator function.sortOptionsworks with and without groups — it sorts all options when ungrouped, or within each group when grouped. Defaults are backwards-compatible:sortGroups='asc'(alphabetical, as before),sortOptions='none'(input order, as before). New types:CoarSelectSortGroups,CoarSelectSortOptions<T>.
Fixed
- SubFlyout menu close chain: Clicking a
CoarMenuIteminside aCoarSubFlyoutnow closes the entire menu hierarchy (submenu + parent context menu). Previously, only the immediate submenu panel closed — the rootCoarContextMenustayed open, requiring consumers to manually callmenu.close()in every handler.
Docs
- Select sorting section: New "Sorting" section on the Select docs page with interactive
SortingDemo(side-by-side grouped vs. ungrouped). All three playground demos (Select, MultiSelect, TagSelect) now includesortGroupsandsortOptionscontrols. Props table updated with the new props.
1.7.0
Added
- CoarSelect / CoarMultiSelect — inline search: When
searchableis set, the trigger becomes an inline text input while the dropdown is open. Type to filter options in real-time. Space, Home, and End keys work correctly inside the search field. - CoarMultiSelect — selection tooltip: When 2+ values are selected, hovering the trigger shows a tooltip listing all selected labels.
- Select option grouping: Options with a
groupproperty are now rendered under sticky group headers. Groups are sorted alphabetically; ungrouped options appear first. Works in all three variants (CoarSelect, CoarMultiSelect, CoarTagSelect). - CoarMenu —
#header/#footerslots: Fixed header and footer areas that stay in place while the menu content scrolls. Render only when the slot is provided. - CoarMenuHeading —
stickyprop: Opt-in sticky positioning so section headings stay visible while scrolling through long menus.
Fixed
- Tooltip not closing in collapsed sidebar: Pointer-initiated focus (click/tap) no longer pins tooltips open via the
focusreason. Only keyboard focus (Tab) keeps tooltips open until focus moves away. This fixes tooltips staying visible in the collapsed sidebar until clicking elsewhere. - CoarTagSelect — Space key in search: Space now types a space character in the tag input instead of triggering option selection.
Changed
- Select search UX: Replaced the dropdown search box with an inline search input in the trigger for CoarSelect and CoarMultiSelect, matching the pattern already used by CoarTagSelect. All three variants now use a consistent search approach.
Docs
- Select playground demos: Interactive playgrounds for CoarSelect, CoarMultiSelect, and CoarTagSelect with toggleable props (searchable, clearable, grouped, disabled, readonly, error, size, appearance).
- Select API table: Documented missing props (searchable, clearable, readonly, appearance, compareWith, dropdownPosition).
- Menu scrollable demo: Updated with header (filter input), footer ("New project" action), and sticky headings toggle.
1.6.6
Changed
resetPersistedState(bucket?): Now accepts an optional bucket parameter to reset only a specific width bucket (defaults to the current bucket). Previously reset all buckets.resetPersistedStates(): New method that resets all persisted column states across all buckets for a grid (the previousresetPersistedState()behavior).
1.6.5
Added
- Column state persistence (
persistColumnState): New builder method to persist column widths, order, visibility, and sort in IndexedDB. Grid width is rounded to configurable buckets (default: 100px) so different container sizes (monitor switch, sidebar collapse) each keep their own column layout. When no exact bucket exists, the nearest saved state is applied. - Live column sync: Multiple grids sharing the same persistence key synchronize column changes instantly — resize, reorder, or hide a column in one grid and all others update immediately. Useful for comparison views with different filters on the same data structure.
cleanupColumnStates(maxAgeDays): Removes stale column state entries from IndexedDB that haven't been read or written within the specified number of days. Call once at application startup to prevent unbounded growth of persisted data.
Changed
- Dependency upgrades: Vite 7→8, vue 3.5.32, vue-router 4→5, vitest 4.1, lucide-static 0.x→1.x, @vitejs/plugin-vue 6.0.5, eslint 10.2, typescript-eslint 8.58, maskito 5.2, turbo 2.9, overlayscrollbars 2.15, happy-dom 20.8, prettier 3.8.2, vitepress 1.6.4, mermaid 11.14, @js-temporal/polyfill 0.5.1, path-to-regexp 8.4.
1.6.4
Changed
- Form field label styling: Labels now use
body-captiontokens (family,size,weight) instead ofbody-small-bold/component-m-label-font-sizefor a more compact, consistent appearance across all form controls. - Tab padding: Reduced tab button padding from
spacing-m / spacing-ltospacing-s / spacing-mfor tighter layout.
Fixed
- Date picker height mismatch:
CoarPlainDatePicker,CoarPlainDateTimePicker, andCoarZonedDateTimePickerreserved space for the hint/error message even when none was set, making them taller than other form controls (e.g.CoarTextInput). The message element is now conditionally rendered viav-if, and the fixedheight/min-height+visibility: hiddenworkaround has been removed.
Removed
CoarLabelcomponent: Removed the standaloneCoarLabelcomponent, its tests, exports, and documentation page. The component was unused by any input control or consumer app — labels are rendered directly byCoarFormFieldand the individual picker components.
1.6.3
Added
- Tag custom colors (
variantFn): NewvariantFnoption onTagCellRendererConfigfor dynamic tag styling. The function receives the raw cell value and can return:- A
TagVariantstring ('success','error', …) for predefined variants - A CSS color string (
'#dc2626') — used as text+border color, background auto-calculated viacolor-mix(in oklch)for consistent light/dark mode appearance - A
TagColorobject ({ bg, border?, text? }) for full control undefinedto fall back tovariantMap
- A
Fixed
- Empty tag rendering:
TagCellRendererno longer renders empty tags when a label is"",undefined, ornull.
1.6.2
Added
- Locale-aware column renderers:
date(),number(), andcurrency()column factory methods now use cell renderer components with the localization system (useL10n()), updating reactively on locale change. Replaces the previousvalueFormatter-based approach.date(field, config?)— formats viafmtDate(), supports{ includeTime: true }number(field, config?)— formats viafmtNumber(), supports{ decimals: number }currency(field, config?)— formats viafmtCurrency(), supports{ currencyCode: string }
- Locale switcher in docs: VitePress nav bar now includes a
CoarSelect-based locale switcher (en-US,en-GB,de-DE,de-AT,fr-FR,ja-JP) for live-testing locale-dependent rendering.
Changed
date()replaceslocalDate(): Thedate()factory method now uses theDateCellRenderercomponent (previously only available vialocalDate()).localDate()has been removed.number()signature: Changed fromnumber(field, decimals)tonumber(field, config?)withNumberCellRendererConfig.currency()signature: Changed fromcurrency(field, currencyCode)tocurrency(field, config?)withCurrencyCellRendererConfig.
Fixed
- TagCellRenderer variant matching:
variantMapnow matches against the raw cell value instead of the formatted value, sovalueFormatterno longer breaks variant resolution. - IconCellRenderer valueFormatter support: Icon name now uses
valueFormattedwhen available, keeping the raw value for sorting/filtering. - Currency symbol resolution:
formatCurrency()now resolves unknown currency symbols viaIntl.NumberFormatinstead of falling back to the raw currency code (e.g.€instead ofEUR). - ja-JP date format: Added
yyyy/mm/dddate pattern andzeroPadflag toCoarDateFormatData. Japanese dates now correctly render as2022/3/15instead of2022-03-15. - Localization plugin init:
setLanguage()is now called on plugin install, so locale data is available immediately without requiring a manual language switch.
1.6.1
Fixed
- Data Grid dark mode: Custom grid header (
CoarGridHeader) now inherits--ag-header-foreground-colorso text and sort icons render correctly in dark mode instead of staying black.
1.6.0
Added
- Sidebar navigation components: New dedicated components for sidebar navigation, replacing the pattern of using
CoarMenu/CoarMenuIteminsideCoarSidebar:CoarSidebarItem: Navigation item withicon,label,activestate, and automatic tooltip in collapsed mode. No menu cascade/close logic — designed purely for persistent navigation.CoarSidebarGroup: Expandable/flyout section with two modes:mode="expand"(default): Animated inline panel (grid-based 0fr→1fr). Plus/minus icon indicator.mode="flyout": Floating panel positioned next to the sidebar viaTeleport. Chevron icon indicator. Supports nested flyouts with parent-child cascade (hovering child keeps parent open).
CoarSidebarHeading: Section title that becomes a small spacer when collapsed (visual separation preserved without text).CoarSidebarDivider: Simple visual separator line.CoarSidebarSpacer: Vertical spacing component withheightandgrowprops.
- Flyout mode (
mode="flyout") onCoarSidebarGroup: Opens a floating panel next to the sidebar instead of expanding inline. Flyout panels are teleported to<body>and positioned viacomputeOverlayCoordinates. Click-to-open by default, with optionalopen-on-hoverprop for hover-triggered opening (200ms delay). Close-on-leave has a 300ms grace period so users can move to the panel without it closing. - Icon-only flyout (
icon-onlyprop onCoarSidebarGroup): Flyout items show as a vertical column of icons without labels, with tooltips on hover. Useful for compact action palettes. Nested flyout and expand groups inside icon-only flyouts automatically inherit the icon-only display. - Open on hover (
open-on-hoverprop onCoarSidebarGroup): Opt-in hover-to-open behavior for flyout groups. Opens after 200ms hover delay, closes after 300ms leave delay. Touch-friendly default remains click-to-open. - Nested flyouts: Flyout groups can be nested inside other flyouts. Parent-child cascade via
provide/injectkeeps parent panels open while interacting with children. Click-outside detection checks all flyout panels to prevent premature closing. - Expand in flyout: Expand groups work inside flyout panels, including icon-only flyouts where children render as centered icons without labels or indentation.
- Sidebar
sizeprop: Controls icon size —'s'(16px),'m'(20px, default),'l'(24px). Propagated to children via injection. - Sidebar collapsed UX:
CoarSidebarnow provides its collapsed state to children viainject. Sidebar items automatically show right-aligned tooltips when collapsed. Smooth width transition on collapse/expand. Group triggers show icon badge (plus for expand, chevron for flyout) in collapsed mode. - Sidebar scoped slots:
#header,#footer, and the default slot now receive{ collapsed }so parent components can adapt their content (e.g. full logo vs. icon-only). - Sidebar CSS tokens: New design tokens for sidebar items —
--coar-sidebar-item-padding,--coar-sidebar-item-hover,--coar-sidebar-item-active-color,--coar-sidebar-item-active-bg,--coar-sidebar-group-indent, etc. - Force expand tree (Data Grid): New
builder.forceExpanded(ref)method. When the ref istrue, all tree parents are expanded and chevron toggle is disabled. When it switches back tofalse, the previous open-state is restored.
Changed
CoarSidebarcollapsed prop: Now emitsupdate:collapsedfor optionalv-model:collapsedtwo-way binding. One-waycollapsedprop still works as before.- Sidebar footer padding:
--coar-sidebar-footer-paddingchanged fromvar(--coar-spacing-m)to0so footer items stretch to full width like content items.
Fixed
- Tooltip empty rectangle:
vTooltipdirective'supdatedhook now handles falsy values (false,''). Previously, switching tooltip config from an object tofalseleft a visible empty rectangle. - Tooltip z-index: Tooltip z-index increased to
calc(var(--coar-z-overlay,1000) + 1)so tooltips render above flyout panels.
1.5.5
Added
- Custom data filter: New
builder.customFilter((data, searchText) => filteredData | null)method onCoarGridBuilder. Filters the entire data array before passing it to AG Grid, replacing the per-row quick filter. This enables filter logic that depends on related rows — e.g. keeping all siblings in a tree when any one matches the search. Returningnullfrom the callback falls back to the default quick filter for that evaluation, allowing dynamic switching between custom and standard filtering. - Pipeline update triggers: New
builder.updateOn(...sources)method. Re-runs the data pipeline (filtering, tree flattening) when the given reactive sources change. Works with all pipeline modes (tree, flat+search, flat reactive) — useful whencustomFilterorquickFilterFndepends on external state like toggle flags.
1.5.4
Fixed
- Modal/Dialog centering: Overlays now stay centered when their content grows after initial render (e.g. async data loading). Previously,
modalPresetanddialogPresetskipped installing aResizeObserverbecause their scroll strategy isnoop— the overlay kept its initial position even as content changed size, resulting in more space above than below.
1.5.3
Added
- Scrollable Menu:
CoarMenunow uses overlay scrollbars (v-scrollbar) and scrolls automatically when content exceeds available height CoarSubFlyout: Renamed fromCoarSubmenuItemfor consistency withCoarSubExpand. Old name remains as deprecated alias for backwards compatibility.- Context Menu flyout demo: New docs example showing flyout submenus inside context menus (status, priority selectors)
Fixed
- Context Menu flyout click: Clicking a menu item inside a flyout submenu now correctly triggers the click handler. Previously the context menu closed before the handler fired because the teleported flyout panel was treated as "outside" the menu.
- Cell text overflow: Removed
display: flexfrom.ag-cell— flex containers don't clip plain text children withoverflow: hidden, causing text to bleed into adjacent cells. AG Grid handles vertical centering internally. - Scroll position reset: Grid no longer jumps to top when data updates. Column definitions are now re-applied only once (on first data set) instead of on every update.
- Toolbar padding: Toolbar only has padding when
borderedorelevatedis set. Without those, only a gap between toolbar and grid is shown. - Empty toolbar visibility: Toolbar is hidden when no slots have content and search is disabled (CSS
:has()selector).
1.5.2
Added
- Unified toolbar:
CoarDataGridnow has built-in toolbar with#toolbar-left,#toolbar-rightslots andshow-searchprop — replaces the need forCoarDataGridPanel. Toolbar appears automatically when search is enabled or any slot is used. Search input fills available space (flex: 1), actions are pushed to the right. - Appearance props:
borderedandelevatedprops onCoarDataGridfor border and elevation shadow. When toolbar is active, it gets padding while the grid sits flush. - Data Grid styles export:
@cocoar/vue-data-grid/stylesnow works without a Vite alias — added./stylesto the package exports map.
Changed
CoarDataGridPaneldeprecated: UseCoarDataGridwithshow-searchand#toolbar-rightslot instead.CoarDataGridPanelremains as a thin wrapper for backwards compatibility.- Event handler composition:
onRowClicked,onRowDoubleClicked,onCellClicked,onCellDoubleClickednow use#composeHandler(multiple handlers are chained, not overwritten). onGridReadyisolation: User'sbuilder.onGridReady()handler no longer conflicts with internal grid initialization._bind()always runs first, then the user handler.
Fixed
- Grid render flicker: Fixed visible Layout Shift where columns animated from left to right on initial render. Root cause: AG Grid animates the
leftCSS property. Fix:suppressColumnMoveAnimationandtransition: noneon cells. - Flex columns with
rowDataRef: Fixedflex()andautoSize('fitGridWidth')not filling available width when usingrowDataRef(). Column definitions are re-applied after data arrives to force a fresh flex layout pass. - Empty toolbar-right gap: Fixed gap visible on the right side when no
#toolbar-rightslot content is provided.
1.5.1
Fixed
- Fragment parser bundle:
vue,vue-router, and@cocoar/vue-uiwere embedded in the bundle instead of externalized, causinginjection "Symbol(route location)" not foundat runtime. Now correctly listed as rollup externals (bundle: 245 KB → 3.5 KB)
1.5.0
Added
- Quick Filter (Search): New
CoarDataGridSearchandCoarDataGridPanelcomponents for adding a search bar above the grid.CoarDataGridPanelcombines search + grid in one component with a#actionsslot for buttons. Search text filters row data before AG Grid using per-column configuration via.quickFilter() - Search highlighting:
.searchHighlight()on the builder enables the CSS Custom Highlight API to underline matching text in grid cells — no DOM manipulation, works with AG Grid virtualization - Tree Data:
.treeData({ children, rowId })enables hierarchical data with expand/collapse. Newcol.tree()column type renders indentation, animated chevron toggle, and child count. Search automatically expands matching branches - Row Drag & Drop:
.rowDragManaged()for flat list reordering with.getDisplayedRowData()to read the new order..onRowDragEnd()callback for persisting changes - Tree Drag & Drop: Drag rows between parents for reparenting.
.rowDragHighlight({ canDrop })provides visual feedback — blue outline on valid targets, red dashed outline on invalid targets. Drop on empty area moves to root level - Tree meta access:
builder.getTreeMeta(rowId)exposes depth, hasChildren, isExpanded, and childCount — useful for customcanDropvalidation (e.g., limiting nesting depth) - I18n column headers:
.header('Name', 'i18n.key')supports runtime language switching via@cocoar/vue-localizationwith automatic fallback when the package is not installed - Auto size:
.autoSize('fitGridWidth')and.autoSize('fitCellContents')for convenient column sizing - Fragment modal routing:
@cocoar/vue-fragment-parsernow includes Vue composables for deep-linkable modals via URL fragments —useFragmentNavigation()(open/close modals by changing URL hash,appendoption for multi-modal),useRoutedFragments()(reactive fragment parsing), anduseRoutedModals()(auto-open/close from URL). Two fragment types:type: 'dialog'(CoarDialog shell with header/title) andtype: 'modal'(raw overlay, full control). Supports browser back/forward and deep-linking.vue-routerand@cocoar/vue-uiare optional peer dependencies
Fixed
- Grid render flicker: Fixed visible Layout Shift where columns animated from left to right on initial render. Root cause: AG Grid animates the
leftCSS property when positioning cells. Fix:suppressColumnMoveAnimationandtransition: noneon cells - Flex columns with
rowDataRef: Fixedflex()andautoSize('fitGridWidth')not filling available width when usingrowDataRef(). Column definitions are re-applied after data arrives to force a fresh flex layout pass - Cell renderer scoped CSS: Removed
scopedfrom all AG Grid cell renderers (Tag, Icon, Date, Tree) — AG Grid doesn't apply Vue'sdata-v-*attributes, so scoped styles never matched - Cyclic dependency: Removed unused
@cocoar/vue-fragment-parserdependency from@cocoar/vue-ui - Turbo telemetry: Disabled Turborepo telemetry in all CI workflows
Docs
- Data Grid: Added interactive demos for Search, Tree Data, Row Drag & Drop, and Tree Drag & Drop with full API documentation including search highlighting, i18n headers, and auto size
- Icons: Added documentation for custom icon sources (SVG Map, HTTP Source, built-in overrides)
- Fragment Parser & Modal Routing: New documentation page with parser API, modal routing guide (composables, deep-linking, browser back), and step-by-step integration example
- Playground app: New
apps/playground/for testing features that require Vue Router (fragment routing, etc.) - Markdown: New documentation page for markdown parsing (
@cocoar/vue-markdown-core) and rendering (@cocoar/vue-markdown)
1.3.0
Added
- Label position:
CoarCheckboxandCoarRadioGroupnow supportlabelPosition="before" | "after"— place the label text before or after the control, matching the existingCoarSwitchAPI - Placeholder token: New
--coar-text-placeholderdesign token for consistent, clearly distinguishable placeholder styling across all input components
Fixed
- Placeholder color: Placeholder text in all input components (TextInput, PasswordInput, NumberInput, Select, MultiSelect, TagSelect, PlainDatePicker, PlainDateTimePicker) was too dark and looked like actual input — now uses
--coar-text-placeholder(gray-400) instead of--coar-text-neutral-tertiary(gray-700)
Docs
- Label Position demos: New interactive examples for Checkbox, RadioGroup, and Switch showing
labelPosition="before"vs"after" - API tables: Updated props documentation for Checkbox and RadioGroup with the new
labelPositionprop
1.2.0
Added
- Context Menu: New
useContextMenu()composable and<CoarContextMenu>component for right-click menus — handles positioning at cursor, viewport clamping, click-outside / Escape / scroll dismissal, and auto-close on item click - Data Grid context menu: Works with
onCellContextMenuandonViewportContextMenu— use separateuseContextMenu()instances for cell vs. viewport right-clicks - Docs: Context Menu documentation page with interactive demos (standalone, submenus, data grid integration)
1.1.0
Added
- Theming: oklch-based color system — set
--coar-accent,--coar-success,--coar-error,--coar-warning,--coar-infoand all 10-step shade scales (50–900) auto-calculate for both light and dark mode - CoarSidebar: New
variant('primary' | 'secondary'),elevated, andborderlessprops for visual customization - Kitchen Sink: Full-page component showcase at
/foundations/kitchen-sinkfor evaluating visual coherence - Theming guide: New docs page at
/guide/themingexplaining color customization
Fixed
- Packaging:
import '@cocoar/vue-ui/styles'no longer crashes in consumer apps — moved OverlayScrollbars CSS import fromstyles/all.css(bare-specifier, unresolvable by PostCSS in consumer context) intovScrollbar.ts(bundled by Vite intodist/index.css) - Fonts export:
@cocoar/vue-ui/fontsnow correctly points to compileddist/fonts.jsinstead of unpublishedsrc/fonts.ts - sideEffects: Fixed
./src/fonts.tsreference to./dist/fonts.jsfor correct tree-shaking
Changed
- Color primitives: Replaced hand-picked hex values with oklch-based calculations — colors are now perceptually uniform across lightness levels, eliminating washed-out shades in dark mode and "baby blue" tints in light mode
- Primary button: Now uses
accent-500(= exact brand color) instead ofaccent-700(darker shade) —--coar-accent: #1183CDmeans the primary button IS#1183CD - Build: Added
fonts.tsas second entry point, externalized@fontsource/*packages, removedvite-plugin-css-injected-by-js(not needed — hash mismatch was a misdiagnosis) - Sidebar variants:
primary= visually distinct from content (secondary background),secondary= same as content background
1.0.1
Fixed
- Temporal polyfill: Moved
@js-temporal/polyfillfrom optional peer dependency to regular dependency — fixesCould not resolve "@js-temporal/polyfill"errors in consuming apps using Vite
1.0.0
Initial release of the Cocoar Design System for Vue 3.
Components
- Form Controls: Button, TextInput, PasswordInput, NumberInput, Select, MultiSelect, TagSelect, Checkbox, RadioGroup, Switch
- Date & Time: PlainDatePicker, PlainDateTimePicker, ZonedDateTimePicker, TimePicker, ScrollableCalendar
- Navigation: Menu, Sidebar, Navbar, Breadcrumb, Tabs, Pagination
- Overlays: Dialog, Popover, Popconfirm, Toast, Tooltip
- Display: Avatar, Badge, Card, CodeBlock, Divider, Label, Link, Note, ProgressBar, Spinner, Table, Tag
- Utilities: Icon, Scrollbar
- Layout: FormField — wrapper for label, hint, and validation around any form control
- Transitions: Fade, Slide, Scale, Collapse — pre-built Vue
<Transition>wrappers using motion tokens - Data Grid: AG Grid wrapper (
@cocoar/vue-data-grid, separate package) - Markdown: Markdown viewer (
@cocoar/vue-markdown, separate package)
Design System
- Two-layer token system: primitives referenced by semantic tokens
- 6 token categories: Color, Typography, Spacing, Radius, Shadow, Motion
- Light and dark mode via
.dark-modeclass (no JS at render time) - CSS
@layercascade for predictable specificity - Tablet-first design: touch interaction with desktop information density
Localization
- All 57 built-in component strings (aria-labels, button text, placeholders) translatable via
useI18n()from@cocoar/vue-localization - English defaults — works without any configuration
- Locale-aware
firstDayOfWeekdetection viaIntl.Locale.getWeekInfo() - Date format pattern auto-detection from browser
IntlAPI
Responsive
- Date picker panels: viewport-clamped widths via CSS
min(), stacked layout below 540px - Overlay system:
shift+flippositioning,maxWidth: 'viewport'constraint - Typography scales across 3 breakpoints (1024px+, 768–1023px, <768px)
Architecture
- Monorepo: pnpm workspaces + Turborepo
- 8 packages:
vue-ui,vue-data-grid,vue-markdown,vue-markdown-core,vue-localization,vue-fragment-parser, docs, icons - Self-hosted fonts via
@fontsource(Poppins + Inter) —import '@cocoar/vue-ui/fonts' - Overlay service with plugin architecture (
CoarOverlayPlugin+CoarOverlayHost) - Overlay companion detection for teleported dropdowns (Select inside overlays)
- Temporal API for date/time components — native in Chrome/Firefox/Edge, optional polyfill for Safari
Accessibility
- ARIA attributes across all interactive components
- Keyboard navigation: roving tabindex in menus/tabs, arrow keys, Escape to close
- Focus management: focus trap in dialogs, focus restoration on close
prefers-reduced-motionrespected — all motion tokens collapse to 0ms- Screen reader support: live regions for toasts, semantic roles,
aria-describedbychains
Documentation
- VitePress docs with 47 pages, deployed to docs.cocoar.dev via Shelf
- Interactive demos with source code preview
- i18n keys documented on each component page
- Design principles, typography, colors, spacing, motion foundations
- Localization guide: l10n formatting, i18n translations, timezone providers
- Form validation examples with vee-validate + Zod
- Error handling guide
Bundle
@cocoar/vue-ui: 378 KB JS (86 KB gzip), 167 KB CSS (17 KB gzip)- Tree-shakeable: all dependencies externalized
@js-temporal/polyfillincluded as dependency (Temporal API for date/time components)