@cocoar/vue-ui 3.1.0 → 3.2.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
- package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
- package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
- package/dist/components/data-list/data-list-builder.d.ts +288 -0
- package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
- package/dist/components/data-list/index.d.ts +12 -0
- package/dist/components/data-list/index.d.ts.map +1 -0
- package/dist/components/data-list/internal/compare.d.ts +14 -0
- package/dist/components/data-list/internal/compare.d.ts.map +1 -0
- package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
- package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
- package/dist/components/data-list/internal/search.d.ts +16 -0
- package/dist/components/data-list/internal/search.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
- package/dist/components/data-list/types.d.ts +185 -0
- package/dist/components/data-list/types.d.ts.map +1 -0
- package/dist/components/data-list/useDataListModel.d.ts +79 -0
- package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
- package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
- package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
- package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
- package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
- package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
- package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
- package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
- package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
- package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
- package/dist/composables/dragRegistry.d.ts +12 -0
- package/dist/composables/dragRegistry.d.ts.map +1 -1
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useDragDrop.d.ts +62 -6
- package/dist/composables/useDragDrop.d.ts.map +1 -1
- package/dist/composables/useVirtualList.d.ts +21 -1
- package/dist/composables/useVirtualList.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4508 -2039
- package/package.json +5 -3
- package/skills/cocoar-vue-ui/SKILL.md +244 -0
- package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
- package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
- package/skills/cocoar-vue-ui/references/components/button.md +252 -0
- package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +353 -0
- package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1645 -0
- package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
- package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
- package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +475 -0
- package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
- package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
- package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
- package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
- package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
- package/skills/cocoar-vue-ui/references/components/card.md +151 -0
- package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
- package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
- package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
- package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
- package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
- package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
- package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
- package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
- package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
- package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
- package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
- package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
- package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
- package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
- package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
- package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
- package/skills/cocoar-vue-ui/references/components/link.md +235 -0
- package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
- package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
- package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
- package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
- package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
- package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
- package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
- package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
- package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
- package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
- package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
- package/skills/cocoar-vue-ui/references/components/note.md +96 -0
- package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
- package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
- package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
- package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
- package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
- package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
- package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
- package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
- package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
- package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
- package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
- package/skills/cocoar-vue-ui/references/components/select.md +463 -0
- package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
- package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
- package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
- package/skills/cocoar-vue-ui/references/components/table.md +170 -0
- package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
- package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
- package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
- package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
- package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
- package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
- package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
- package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
- package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
- package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
- package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
- package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
- package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
- package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
- package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
- package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
- package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
- package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
- package/skills/cocoar-vue-ui/references/guide/getting-started.md +119 -0
- package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
- package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
- package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/calendar/month-view.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Month Views (Preview)
|
|
4
|
+
|
|
5
|
+
The shell's Month view follows the iOS structure: months scroll continuously and expose **Compact**, **Stacked**, **Details**, and **List** display choices. Continuous sections render only the 4–6 weeks the month actually needs; leading and trailing positions stay blank instead of repeating neighbour dates.
|
|
6
|
+
|
|
7
|
+
| Choice | Rendering |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Compact | 52 px base week rows; per-day events combine into a segmented colour capsule. |
|
|
10
|
+
| Stacked | 68 px base rows; compact individual event marks. |
|
|
11
|
+
| Details | 94 px base rows; titles, assignees, multi-day bars and row expansion. |
|
|
12
|
+
| List | Compact month selector plus the selected day's event list; stacked in narrow containers and side-by-side from 720 px. |
|
|
13
|
+
|
|
14
|
+
The regular Month choices use `<CoarContinuousMonthView>`. `<CoarMonthView>` remains exported as the lower-level single-month section for widgets and custom compositions.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const { builder } = useCalendar();
|
|
18
|
+
builder
|
|
19
|
+
.view('month')
|
|
20
|
+
.availableViews(['month', 'monthList'])
|
|
21
|
+
.monthDensity('compact')
|
|
22
|
+
.shadeWeekends(true);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Continuous month
|
|
26
|
+
|
|
27
|
+
Month navigation scrolls to the requested section. Scrolling updates the builder cursor when the next month's heading reaches the top, and event / recurrence loaders preload the adjacent months. The surface initially materializes 13 months and extends in either direction while keeping a bounded DOM window.
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<CoarContinuousMonthView :builder="builder" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Month List {#month-list}
|
|
34
|
+
|
|
35
|
+
`monthList` is a real serialized `CalendarView`, but `<CoarCalendar>` nests it under Month as the **List** display choice. Selecting a date in the small calendar updates the builder cursor and the adjacent daily event list.
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<CoarMonthListView :builder="builder" />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Single month section {#single-month-section}
|
|
42
|
+
|
|
43
|
+
`<CoarMonthView>` renders the fixed single-section grid used internally by the continuous composition. Multi-day events render as continuous **bars** across the rows they touch; single-day events render as **pills** inside cells. Use it directly when the host owns pagination or needs a compact embedded month.
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<CoarMonthView :builder="builder" />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Standalone usage
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { ref } from 'vue';
|
|
53
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
54
|
+
import {
|
|
55
|
+
CoarContinuousMonthView,
|
|
56
|
+
CoarMonthListView,
|
|
57
|
+
CoarMonthView,
|
|
58
|
+
useMonthView,
|
|
59
|
+
type CalendarEvent,
|
|
60
|
+
} from '@cocoar/vue-calendar';
|
|
61
|
+
|
|
62
|
+
const events = ref<CalendarEvent[]>([
|
|
63
|
+
{
|
|
64
|
+
id: 'devconf',
|
|
65
|
+
start: Temporal.PlainDate.from('2026-04-13'),
|
|
66
|
+
end: Temporal.PlainDate.from('2026-04-16'),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'standup',
|
|
70
|
+
start: Temporal.ZonedDateTime.from('2026-04-15T09:00:00[UTC]'),
|
|
71
|
+
end: Temporal.ZonedDateTime.from('2026-04-15T09:30:00[UTC]'),
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
const date = ref('2026-04-15');
|
|
75
|
+
|
|
76
|
+
const { builder, api } = useMonthView();
|
|
77
|
+
builder
|
|
78
|
+
.events(events)
|
|
79
|
+
.date(date)
|
|
80
|
+
.timezone('UTC')
|
|
81
|
+
.maxEventsPerCell(5)
|
|
82
|
+
.eventRenderer((ctx) => {
|
|
83
|
+
if (ctx.layout?.kind === 'monthPill') return h(MyPill, { event: ctx.event, pill: ctx.layout.layout });
|
|
84
|
+
if (ctx.layout?.kind === 'monthBar') return h(MyBar, { event: ctx.event, bar: ctx.layout.layout });
|
|
85
|
+
return undefined; // fall through to lib default for other variants
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<CoarMonthView :builder="builder" />
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Demo — `calendar/demos/MonthViewBasic.vue`**
|
|
94
|
+
|
|
95
|
+
```vue
|
|
96
|
+
<template>
|
|
97
|
+
<div style="height: 640px; border: 1px solid var(--coar-border-neutral-tertiary); border-radius: var(--coar-radius-xs); overflow: hidden;">
|
|
98
|
+
<CoarMonthView :builder="builder" />
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import { ref } from 'vue';
|
|
104
|
+
import {
|
|
105
|
+
CoarMonthView,
|
|
106
|
+
useMonthView,
|
|
107
|
+
Temporal,
|
|
108
|
+
type CalendarEvent,
|
|
109
|
+
} from '@cocoar/vue-calendar';
|
|
110
|
+
|
|
111
|
+
const date = ref(Temporal.PlainDate.from('2026-04-15'));
|
|
112
|
+
|
|
113
|
+
const pd = (iso: string) => Temporal.PlainDate.from(iso);
|
|
114
|
+
const zdt = (iso: string, tz = 'Europe/Vienna') =>
|
|
115
|
+
Temporal.ZonedDateTime.from(`${iso}[${tz}]`);
|
|
116
|
+
|
|
117
|
+
const events = ref<CalendarEvent[]>([
|
|
118
|
+
// Multi-day bars across week boundaries.
|
|
119
|
+
{
|
|
120
|
+
id: 'devconf',
|
|
121
|
+
start: pd('2026-04-13'),
|
|
122
|
+
end: pd('2026-04-16'),
|
|
123
|
+
meta: { title: 'DevConf — Vienna', color: '#7c3aed' },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'sven-ooo',
|
|
127
|
+
start: pd('2026-04-15'),
|
|
128
|
+
end: pd('2026-04-18'),
|
|
129
|
+
meta: { title: 'Sven — OOO', color: '#9ca3af' },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'team-offsite',
|
|
133
|
+
start: pd('2026-04-22'),
|
|
134
|
+
end: pd('2026-04-28'),
|
|
135
|
+
meta: { title: 'Team offsite', color: '#0891b2' },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'easter',
|
|
139
|
+
start: pd('2026-04-03'),
|
|
140
|
+
end: pd('2026-04-07'),
|
|
141
|
+
meta: { title: 'Easter break', color: '#84cc16' },
|
|
142
|
+
},
|
|
143
|
+
// Daily standups Mon–Fri.
|
|
144
|
+
...['2026-04-06', '2026-04-07', '2026-04-08', '2026-04-09', '2026-04-10'].map(
|
|
145
|
+
(d): CalendarEvent => ({
|
|
146
|
+
id: `standup-${d}`,
|
|
147
|
+
start: zdt(`${d}T09:00:00`),
|
|
148
|
+
end: zdt(`${d}T09:30:00`),
|
|
149
|
+
meta: { title: 'Standup', color: '#10b981' },
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
...['2026-04-13', '2026-04-14', '2026-04-15', '2026-04-16', '2026-04-17'].map(
|
|
153
|
+
(d): CalendarEvent => ({
|
|
154
|
+
id: `standup-w2-${d}`,
|
|
155
|
+
start: zdt(`${d}T09:00:00`),
|
|
156
|
+
end: zdt(`${d}T09:30:00`),
|
|
157
|
+
meta: { title: 'Standup', color: '#06b6d4' },
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
// Wed busy day — extra pills to trigger the per-cell scroll + kebab.
|
|
161
|
+
{
|
|
162
|
+
id: 'wed-design',
|
|
163
|
+
start: zdt('2026-04-15T11:00:00'),
|
|
164
|
+
end: zdt('2026-04-15T12:30:00'),
|
|
165
|
+
meta: { title: 'Design review', color: '#8b5cf6' },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'wed-pair',
|
|
169
|
+
start: zdt('2026-04-15T11:30:00'),
|
|
170
|
+
end: zdt('2026-04-15T13:00:00'),
|
|
171
|
+
meta: { title: 'Pair: calendar', color: '#f59e0b' },
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'wed-lunch',
|
|
175
|
+
start: zdt('2026-04-15T12:00:00'),
|
|
176
|
+
end: zdt('2026-04-15T13:00:00'),
|
|
177
|
+
meta: { title: 'Lunch with Anna', color: '#ef4444' },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'wed-1on1',
|
|
181
|
+
start: zdt('2026-04-15T15:00:00'),
|
|
182
|
+
end: zdt('2026-04-15T15:45:00'),
|
|
183
|
+
meta: { title: '1:1 with Bernhard', color: '#3b82f6' },
|
|
184
|
+
},
|
|
185
|
+
// Quarterly review crossing into May.
|
|
186
|
+
{
|
|
187
|
+
id: 'quarter-review',
|
|
188
|
+
start: pd('2026-04-29'),
|
|
189
|
+
end: pd('2026-05-02'),
|
|
190
|
+
meta: { title: 'Quarterly review', color: '#2563eb' },
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const { builder } = useMonthView();
|
|
195
|
+
builder
|
|
196
|
+
.events(events)
|
|
197
|
+
.date(date)
|
|
198
|
+
.timezone('Europe/Vienna')
|
|
199
|
+
.onEventDrop(({ event, next }) => {
|
|
200
|
+
const idx = events.value.findIndex((e) => e.id === event.id);
|
|
201
|
+
if (idx < 0) return;
|
|
202
|
+
events.value = [
|
|
203
|
+
...events.value.slice(0, idx),
|
|
204
|
+
{ ...event, start: next.start, ...(next.end ? { end: next.end } : {}) },
|
|
205
|
+
...events.value.slice(idx + 1),
|
|
206
|
+
];
|
|
207
|
+
});
|
|
208
|
+
</script>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Custom pills and bars
|
|
212
|
+
|
|
213
|
+
The month view exposes two slots — `#pill` for single-day events and `#multiDayBar` for multi-day events. Both receive `{ event, pill }` / `{ event, bar }` so you can branch on the layout payload (lane, col-span, clipping flags) when needed.
|
|
214
|
+
|
|
215
|
+
**Demo — `calendar/demos/MonthViewCustomPillBar.vue`**
|
|
216
|
+
|
|
217
|
+
```vue
|
|
218
|
+
<template>
|
|
219
|
+
<div>
|
|
220
|
+
<p class="hint">
|
|
221
|
+
Custom <code>#pill</code> + <code>#multiDayBar</code> slots —
|
|
222
|
+
pills get a leading time chip; bars get a trailing day-count chip.
|
|
223
|
+
</p>
|
|
224
|
+
<div style="height: 600px; border: 1px solid var(--coar-border-neutral-tertiary); border-radius: var(--coar-radius-xs); overflow: hidden;">
|
|
225
|
+
<CoarMonthView :builder="builder">
|
|
226
|
+
<template #pill="{ event }">
|
|
227
|
+
<span class="pill">
|
|
228
|
+
<span class="pill__time">{{ formatTime(event) }}</span>
|
|
229
|
+
<span class="pill__title">{{ title(event) }}</span>
|
|
230
|
+
</span>
|
|
231
|
+
</template>
|
|
232
|
+
<template #multiDayBar="{ event, bar }">
|
|
233
|
+
<span class="bar">
|
|
234
|
+
<span class="bar__title">{{ title(event) }}</span>
|
|
235
|
+
<span class="bar__chip">{{ bar.endCol - bar.startCol + 1 }}d</span>
|
|
236
|
+
</span>
|
|
237
|
+
</template>
|
|
238
|
+
</CoarMonthView>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</template>
|
|
242
|
+
|
|
243
|
+
<script setup lang="ts">
|
|
244
|
+
import { ref } from 'vue';
|
|
245
|
+
import {
|
|
246
|
+
CoarMonthView,
|
|
247
|
+
useMonthView,
|
|
248
|
+
Temporal,
|
|
249
|
+
isTimedEvent,
|
|
250
|
+
type CalendarEvent,
|
|
251
|
+
} from '@cocoar/vue-calendar';
|
|
252
|
+
|
|
253
|
+
const date = ref(Temporal.PlainDate.from('2026-04-15'));
|
|
254
|
+
|
|
255
|
+
const pd = (iso: string) => Temporal.PlainDate.from(iso);
|
|
256
|
+
const zdt = (iso: string, tz = 'Europe/Vienna') =>
|
|
257
|
+
Temporal.ZonedDateTime.from(`${iso}[${tz}]`);
|
|
258
|
+
|
|
259
|
+
const events = ref<CalendarEvent[]>([
|
|
260
|
+
{
|
|
261
|
+
id: 'devconf',
|
|
262
|
+
start: pd('2026-04-13'),
|
|
263
|
+
end: pd('2026-04-16'),
|
|
264
|
+
meta: { title: 'DevConf — Vienna', color: '#7c3aed' },
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'sven-ooo',
|
|
268
|
+
start: pd('2026-04-15'),
|
|
269
|
+
end: pd('2026-04-18'),
|
|
270
|
+
meta: { title: 'Sven — OOO', color: '#9ca3af' },
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'team-offsite',
|
|
274
|
+
start: pd('2026-04-22'),
|
|
275
|
+
end: pd('2026-04-28'),
|
|
276
|
+
meta: { title: 'Team offsite', color: '#0891b2' },
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'standup-mon',
|
|
280
|
+
start: zdt('2026-04-13T09:00:00'),
|
|
281
|
+
end: zdt('2026-04-13T09:30:00'),
|
|
282
|
+
meta: { title: 'Standup', color: '#10b981' },
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'review',
|
|
286
|
+
start: zdt('2026-04-15T11:00:00'),
|
|
287
|
+
end: zdt('2026-04-15T12:00:00'),
|
|
288
|
+
meta: { title: 'Design review', color: '#8b5cf6' },
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: 'lunch',
|
|
292
|
+
start: zdt('2026-04-15T12:00:00'),
|
|
293
|
+
end: zdt('2026-04-15T13:00:00'),
|
|
294
|
+
meta: { title: 'Lunch', color: '#ef4444' },
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'demo',
|
|
298
|
+
start: zdt('2026-04-17T15:00:00'),
|
|
299
|
+
end: zdt('2026-04-17T16:30:00'),
|
|
300
|
+
meta: { title: 'Client demo', color: '#dc2626' },
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
const { builder } = useMonthView();
|
|
305
|
+
builder
|
|
306
|
+
.events(events)
|
|
307
|
+
.date(date)
|
|
308
|
+
.timezone('Europe/Vienna')
|
|
309
|
+
.onEventDrop(({ event, next }) => {
|
|
310
|
+
const idx = events.value.findIndex((e) => e.id === event.id);
|
|
311
|
+
if (idx < 0) return;
|
|
312
|
+
events.value = [
|
|
313
|
+
...events.value.slice(0, idx),
|
|
314
|
+
{ ...event, start: next.start, ...(next.end ? { end: next.end } : {}) },
|
|
315
|
+
...events.value.slice(idx + 1),
|
|
316
|
+
];
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
function title(event: CalendarEvent): string {
|
|
320
|
+
return (event.meta as { title?: string } | undefined)?.title ?? event.id;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const timeFmt = new Intl.DateTimeFormat('en-US', {
|
|
324
|
+
hour: 'numeric',
|
|
325
|
+
minute: '2-digit',
|
|
326
|
+
timeZone: 'Europe/Vienna',
|
|
327
|
+
});
|
|
328
|
+
function formatTime(event: CalendarEvent): string {
|
|
329
|
+
// All-day events (PlainDate start) have no clock time to show.
|
|
330
|
+
if (!isTimedEvent(event)) return '';
|
|
331
|
+
// ZonedDateTime → epoch ms via toInstant() so we can hand it to Intl.
|
|
332
|
+
return timeFmt
|
|
333
|
+
.format(new Date(event.start.toInstant().epochMilliseconds))
|
|
334
|
+
.replace(' ', ' ');
|
|
335
|
+
}
|
|
336
|
+
</script>
|
|
337
|
+
|
|
338
|
+
<style scoped>
|
|
339
|
+
.hint {
|
|
340
|
+
margin: 0 0 12px;
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
color: var(--coar-text-subtle, #6b7280);
|
|
343
|
+
}
|
|
344
|
+
.hint code {
|
|
345
|
+
font-family: var(--coar-font-family-mono, monospace);
|
|
346
|
+
font-size: 12px;
|
|
347
|
+
background: var(--coar-background-neutral-tertiary, #f3f4f6);
|
|
348
|
+
padding: 1px 5px;
|
|
349
|
+
border-radius: 3px;
|
|
350
|
+
}
|
|
351
|
+
.pill {
|
|
352
|
+
display: inline-flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
gap: 4px;
|
|
355
|
+
width: 100%;
|
|
356
|
+
overflow: hidden;
|
|
357
|
+
}
|
|
358
|
+
.pill__time {
|
|
359
|
+
font-variant-numeric: tabular-nums;
|
|
360
|
+
font-weight: 700;
|
|
361
|
+
color: var(--coar-text-base, #1a1c1f);
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
}
|
|
364
|
+
.pill__title {
|
|
365
|
+
flex: 1 1 auto;
|
|
366
|
+
overflow: hidden;
|
|
367
|
+
text-overflow: ellipsis;
|
|
368
|
+
white-space: nowrap;
|
|
369
|
+
color: var(--coar-text-base, #1a1c1f);
|
|
370
|
+
}
|
|
371
|
+
.bar {
|
|
372
|
+
display: inline-flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
justify-content: space-between;
|
|
375
|
+
width: 100%;
|
|
376
|
+
gap: 6px;
|
|
377
|
+
}
|
|
378
|
+
.bar__title {
|
|
379
|
+
flex: 1 1 auto;
|
|
380
|
+
overflow: hidden;
|
|
381
|
+
text-overflow: ellipsis;
|
|
382
|
+
white-space: nowrap;
|
|
383
|
+
font-weight: 600;
|
|
384
|
+
color: var(--coar-text-base, #1a1c1f);
|
|
385
|
+
}
|
|
386
|
+
.bar__chip {
|
|
387
|
+
flex: 0 0 auto;
|
|
388
|
+
font-size: 10px;
|
|
389
|
+
font-weight: 700;
|
|
390
|
+
color: var(--coar-text-base, #1a1c1f);
|
|
391
|
+
background: rgba(0, 0, 0, 0.12);
|
|
392
|
+
padding: 0 6px;
|
|
393
|
+
border-radius: 999px;
|
|
394
|
+
}
|
|
395
|
+
</style>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Inside `<CoarCalendar>`
|
|
399
|
+
|
|
400
|
+
`<CoarCalendar>` and `<CoarMonthView>` consume the SAME `CalendarBuilder` instance — there's no sub-builder forking. Set month-specific config (e.g. `maxEventsPerCell`) directly on the composer's builder:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
const { builder } = useCalendar();
|
|
404
|
+
builder.maxEventsPerCell(5);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
When the active view is `month`, the same builder feeds `<CoarContinuousMonthView>` and resolves `monthDensity`. Selecting List switches the serialized view to `monthList` and mounts `<CoarMonthListView>`. View-specific settings simply have no effect outside their view.
|
|
408
|
+
|
|
409
|
+
## `useMonthView<TMeta>()`
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
function useMonthView<TMeta>(): {
|
|
413
|
+
builder: CalendarBuilder<TMeta>;
|
|
414
|
+
api: CalendarApi<TMeta>;
|
|
415
|
+
};
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Returns a fresh standalone builder + its imperative api. The builder type is the same `CalendarBuilder` used by `<CoarCalendar>` — `useMonthView()` is just a thin shorthand that pre-sets `view: 'month'`.
|
|
419
|
+
|
|
420
|
+
## Builder setters
|
|
421
|
+
|
|
422
|
+
Full reference: see [the composer's API reference](./coar-calendar.md#api-reference). Highlights that matter for the month view:
|
|
423
|
+
|
|
424
|
+
| Setter | Argument | Default | Notes |
|
|
425
|
+
|---|---|---|---|
|
|
426
|
+
| `firstDayOfWeek(d)` | `0..6 \| undefined` | locale-aware | `0` = Sunday, `1` = Monday, … |
|
|
427
|
+
| `monthDensity(d)` | `'compact' \| 'stacked' \| 'details'` | `'details'` | Presentation used by continuous Month. |
|
|
428
|
+
| `shadeWeekends(b)` | `MaybeRefOrGetter<boolean>` | `true` | Shades Saturday / Sunday cells and weekday headers. Set `false` for an unshaded appearance. |
|
|
429
|
+
| `maxEventsPerCell(n)` | `MaybeRefOrGetter<number>` | `3` | Pill cap hint. The library never truncates — pills always reach the DOM — but the collapsed-cell height reserves space for ~`n` pills before the cell starts to scroll. |
|
|
430
|
+
| `eventRenderer(r)` | `EventRenderer<TMeta>` | — | Universal renderer. Branch on `ctx.layout?.kind === 'monthPill' \| 'monthBar'` for variant-specific rendering — see the example above. |
|
|
431
|
+
| `dayHeaderRenderer(r)` | `DayHeaderRenderer` | — | Weekday-strip header (Mon / Tue / ...). |
|
|
432
|
+
|
|
433
|
+
## Per-cell expansion
|
|
434
|
+
|
|
435
|
+
Each cell has a kebab trigger (top-right of the day-number row, hover-reveal on desktop, always visible on touch). Clicking it opens a context menu with **Show more events** / **Show fewer events**, which expands or collapses the entire **row** (single-row mode — opening one collapses any other previously-expanded row). Right-click / long-press on the cell body opens the same menu at the pointer.
|
|
436
|
+
|
|
437
|
+
The collapsed cell uses a height that fits ~`maxEventsPerCell` pills + the multi-day-bar lane area; expanded rows grow to a fixed maximum so all overflowing pills are reachable via scroll.
|
|
438
|
+
|
|
439
|
+
## Drag and drop
|
|
440
|
+
|
|
441
|
+
- **Pills** — drag a single-day pill to another cell to shift its date. The library reflows the source cell as if the event were already gone, and renders a dashed-outline ghost pill at the target.
|
|
442
|
+
- **Bars** — drag a multi-day bar's body to shift the whole bar; drag the **left** or **right** edge handle to resize one side. Resize handles only appear on non-clipped edges (no point resizing from off-month).
|
|
443
|
+
- **Keyboard** — Tab to focus an event, Arrow keys to move ±1 day (Up / Down jump a full week-row). Shift + Arrow on an all-day event grows / shrinks the end side.
|
|
444
|
+
- **`canDrop`** — the universal drop validator on the builder; returning `false` paints a red dashed "invalid" ghost and silently swallows the drop on release.
|
|
445
|
+
|
|
446
|
+
## Imperative API
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
interface CalendarApi<TMeta> {
|
|
450
|
+
goTo(iso: string): void;
|
|
451
|
+
goToToday(): void;
|
|
452
|
+
next(): void; // ±1 month
|
|
453
|
+
prev(): void;
|
|
454
|
+
getVisibleRange(): ViewWindow | null;
|
|
455
|
+
getVisibleEvents(): CalendarEvent<TMeta>[];
|
|
456
|
+
refresh(): void;
|
|
457
|
+
refreshRange(start: string, end: string): void;
|
|
458
|
+
readonly loading: Readonly<Ref<boolean>>;
|
|
459
|
+
readonly visibleRange: Readonly<Ref<ViewWindow | null>>;
|
|
460
|
+
readonly gridReady: Readonly<Ref<boolean>>;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
`scrollToTime` / `scrollToDate` are not month operations. `api.goTo(...)`, `next()`, and `prev()` align the continuous surface to the requested month; a standalone `<CoarContinuousMonthView>` additionally exposes `scrollToMonth(...)` on its component ref.
|
|
465
|
+
|
|
466
|
+
## Month component props + slots
|
|
467
|
+
|
|
468
|
+
| Prop | Type | Description |
|
|
469
|
+
|---|---|---|
|
|
470
|
+
| `builder` | `CalendarBuilder` | **Required.** From `useMonthView()` (or share the one from `useCalendar()`). |
|
|
471
|
+
|
|
472
|
+
| Slot | Scope | Purpose |
|
|
473
|
+
|---|---|---|
|
|
474
|
+
| `pill` | `{ event, pill }` | Single-day pill renderer. `pill` is the `MonthCellPill` (event + visual order in the cell). |
|
|
475
|
+
| `multiDayBar` | `{ event, bar }` | Multi-day bar renderer. `bar` is the `MonthMultiDayBar` (lane / startCol / endCol / clipping flags). |
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/calendar/performance.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Performance baseline (Preview)
|
|
4
|
+
|
|
5
|
+
The calendar's hot paths — virtualization, drag-and-drop with auto-scroll,
|
|
6
|
+
2D scrolling — were instrumented via the [Long Animation Frame API][loaf]
|
|
7
|
+
during development. This page records the numbers that justified the
|
|
8
|
+
architectural choices, plus the targets you should expect on real
|
|
9
|
+
hardware.
|
|
10
|
+
|
|
11
|
+
[loaf]: https://developer.mozilla.org/docs/Web/API/Performance_API/Long_animation_frame_timing
|
|
12
|
+
|
|
13
|
+
## How the numbers are measured
|
|
14
|
+
|
|
15
|
+
Two perf signals are reported throughout this page. They answer different
|
|
16
|
+
questions and have very different reliabilities:
|
|
17
|
+
|
|
18
|
+
| Metric | What it measures | Authoritative? |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| **Long frames (5 s window)** | Count of actual ≥ 50 ms frames over the last 5 s, from the browser's own pipeline (Long Animation Frame API). | **Yes.** Cannot be fooled by rAF scheduling. |
|
|
21
|
+
| **Worst frame (5 s window)** | Duration in ms of the longest frame in the last 5 s. | **Yes.** Same source. |
|
|
22
|
+
| **rAF FPS mean / min** | How often the JS `requestAnimationFrame` callback fires. | **No.** Chromium can defer rAF callbacks one or two vsync ticks during wheel-scroll input dispatch without producing visual jank — the rAF counter dips to 30 even on a smooth page. |
|
|
23
|
+
|
|
24
|
+
**Rule of thumb:** if Long-frames count is `0` and Worst-frame is under
|
|
25
|
+
50 ms, the page is genuinely smooth — regardless of what rAF FPS shows.
|
|
26
|
+
|
|
27
|
+
The Long Animation Frame API was specified by the W3C precisely to
|
|
28
|
+
expose the distinction; it is the source of truth in the numbers below.
|
|
29
|
+
|
|
30
|
+
## Targets
|
|
31
|
+
|
|
32
|
+
| Surface | Tier A (laptop / desktop) | Tier B (CI / cheap hardware) |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| **1D virtualization (≤ 10 000 fixed-size items)** | 0 long frames during wheel-scroll; worst frame < 16 ms | ≤ 1 long frame; worst frame < 30 ms |
|
|
35
|
+
| **1D virtualization (variable-size, 10 000 items)** | 0 long frames during wheel + measurement flushes | ≤ 2 long frames; worst frame < 30 ms |
|
|
36
|
+
| **2D virtualization (1 000 × 1 000 cells)** | 0 long frames during diagonal wheel-scroll | ≤ 2 long frames; worst frame < 30 ms |
|
|
37
|
+
| **Drag with auto-scroll (200-item list)** | 0 long frames over a 2.6 s scripted drag-and-hold session | ≤ 3 long frames; worst frame < 150 ms |
|
|
38
|
+
| **Memory (idle, 10 000 items)** | < 60 MB | < 80 MB |
|
|
39
|
+
|
|
40
|
+
## Validation results
|
|
41
|
+
|
|
42
|
+
Measured on a Snapdragon X Elite X1E-78-100 dev box (Windows 11 ARM64,
|
|
43
|
+
Chrome ARM64), production build, against the components shipped in
|
|
44
|
+
`@cocoar/vue-calendar`.
|
|
45
|
+
|
|
46
|
+
### 1D virtualization, 10 000 fixed-size items
|
|
47
|
+
|
|
48
|
+
| Scenario | Long frames | Worst frame |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Idle | 0 | 0 ms |
|
|
51
|
+
| Wheel-scroll, mixed direction, ~ 30 s session | **0** | 0 ms |
|
|
52
|
+
| Bottom-jump from 10 000 items | **0** | 0 ms |
|
|
53
|
+
|
|
54
|
+
DOM cost: ≤ ~ 30 items mounted at any time. Bottom-jump still mounts
|
|
55
|
+
~ 14 items, never the full 10 000. Add / remove 1 000 items at runtime
|
|
56
|
+
updates the surface in place without remounting visible slots.
|
|
57
|
+
|
|
58
|
+
### 1D virtualization, 10 000 variable-size items
|
|
59
|
+
|
|
60
|
+
The variable-size path adds the `MeasurementCache` (Fenwick-tree-backed
|
|
61
|
+
prefix sums + interval search) and anchor-based scroll restoration.
|
|
62
|
+
Items mount with the `estimatedItemSize`, then `ResizeObserver` flushes
|
|
63
|
+
real heights inside one rAF.
|
|
64
|
+
|
|
65
|
+
| Scenario | Long frames | Worst frame |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Wheel-scroll + first-paint measurement flushes | **0** | 0 ms |
|
|
68
|
+
| Manual size-toggle above the viewport (anchor restoration) | **0** | 0 ms |
|
|
69
|
+
|
|
70
|
+
### 2D virtualization, 1 000 × 1 000 cells
|
|
71
|
+
|
|
72
|
+
| Scenario | Long frames | Worst frame | Cells in DOM |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| 30 ticks at 50 ms cadence (≈ real wheel) | **0** | 0 ms | 378 |
|
|
75
|
+
| 50 ticks at 30 ms cadence (synthetic burst) | 1 | 50 ms | 357 |
|
|
76
|
+
| 1D-variable regression under the same regime | **0** | 0 ms | 10 |
|
|
77
|
+
|
|
78
|
+
The single long frame in the synthetic burst (~ twice the cadence of
|
|
79
|
+
natural wheel input) lands exactly on the 50 ms LoAF threshold. Real-
|
|
80
|
+
user wheel-scroll does not produce it. Adding 2D had no effect on the
|
|
81
|
+
1D path.
|
|
82
|
+
|
|
83
|
+
### Drag with auto-scroll (200-item list)
|
|
84
|
+
|
|
85
|
+
A scripted session via Chrome DevTools:
|
|
86
|
+
|
|
87
|
+
1. `pointerdown` on row 5
|
|
88
|
+
2. 30-step diagonal drag down to the bottom hot zone (~ 600 ms)
|
|
89
|
+
3. **Hold at bottom hot zone for 800 ms** — auto-scroll down fires each frame
|
|
90
|
+
4. 30-step reverse drag back up to the top hot zone (~ 600 ms)
|
|
91
|
+
5. **Hold at top hot zone for 600 ms** — auto-scroll up fires
|
|
92
|
+
6. `pointerup`
|
|
93
|
+
|
|
94
|
+
Total session ~ 2.6 s. `surface.scrollTop` ended at 231 px from a start
|
|
95
|
+
of 0 — auto-scroll is real, not simulated.
|
|
96
|
+
|
|
97
|
+
| Metric | Value |
|
|
98
|
+
|---|---|
|
|
99
|
+
| Long frames over the session | **0** |
|
|
100
|
+
| Worst frame | **0 ms** (under the 50 ms LoAF threshold) |
|
|
101
|
+
|
|
102
|
+
### Math kernel microbenchmarks
|
|
103
|
+
|
|
104
|
+
The pure-function primitives in `packages/calendar/src/core/`:
|
|
105
|
+
|
|
106
|
+
| Operation | mean | hz |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `getVisibleRange1D` (variable, midpoint, overscan = 3) | 314 ns | 3.19 M / sec |
|
|
109
|
+
| `MeasurementCache.prefixSum` (10 k items, midpoint) | 75 ns | 13.26 M / sec |
|
|
110
|
+
| `MeasurementCache.indexAtOffset` (10 k items, midpoint) | 168 ns | 5.95 M / sec |
|
|
111
|
+
| `computeAnchorAdjustment` (10 k items, midpoint) | 89 ns | 11.24 M / sec |
|
|
112
|
+
|
|
113
|
+
The math kernel uses ≈ 0.002 % of a 60 fps frame budget at 10 k items.
|
|
114
|
+
The DOM composition pipeline is the bound, not the math.
|
|
115
|
+
|
|
116
|
+
## Architectural decisions the numbers locked in
|
|
117
|
+
|
|
118
|
+
- **Fenwick-tree variable-size cache.** `prefixSum` / `indexAtOffset` /
|
|
119
|
+
`set` all in O(log n). Linear scans were measurably worse at 10 k+
|
|
120
|
+
items in early prototypes; the tree paid for itself.
|
|
121
|
+
- **Transform-only positioning.** Items render at `transform: translate3d(0, y, 0)`
|
|
122
|
+
rather than re-layouting on scroll. Composite-only updates per frame.
|
|
123
|
+
- **Keyed v-for over the visible range, no recycling pool.** Vue's
|
|
124
|
+
diff turned out faster than a stable pool for typical slot content
|
|
125
|
+
in benchmarks. Heavy custom renderers (charts, video) might benefit;
|
|
126
|
+
the surface accepts a custom recycling pool when needed.
|
|
127
|
+
- **rAF-throttled drag.** Pointermove batches into a single rAF tick;
|
|
128
|
+
multiple moves between frames coalesce. Auto-scroll velocity is
|
|
129
|
+
computed in a pure function (`computeAutoScrollVelocity`) and
|
|
130
|
+
applied to `scrollTop` / `scrollLeft` per tick.
|
|
131
|
+
|
|
132
|
+
## Known accessibility gap — drop announcements
|
|
133
|
+
|
|
134
|
+
The calendar ships full keyboard-driven drag (Tab to focus, arrow keys
|
|
135
|
+
to move, Shift + arrow to resize, Enter to confirm, Escape to cancel),
|
|
136
|
+
but **non-sighted users currently get no audible confirmation that a
|
|
137
|
+
drop landed.** Sighted users see the event jump to the new slot;
|
|
138
|
+
screen-reader users hear nothing.
|
|
139
|
+
|
|
140
|
+
This is a known gap, not by-design. An earlier internal implementation
|
|
141
|
+
had a polite-live-region announcement that read e.g.
|
|
142
|
+
`"Daily Standup moved to Monday, 15. Juni 2026, 09:00"` after each
|
|
143
|
+
drop. The plumbing was lost during a reactivity refactor (the shell-
|
|
144
|
+
level handler that prepended the announcement before forwarding to
|
|
145
|
+
`state.onEventDrop` was bypassed when sub-views started reading
|
|
146
|
+
`state.onEventDrop` directly per the C7 read-on-every-call contract),
|
|
147
|
+
and shipping it cleanly requires plumbing the announcer into the
|
|
148
|
+
single drop pipeline (`useCalendarDnd`) so every drop path —
|
|
149
|
+
mouse, touch, keyboard — surfaces it once.
|
|
150
|
+
|
|
151
|
+
Estimated effort: 1 – 2 h. Tracked as a post-launch a11y task.
|
|
152
|
+
|
|
153
|
+
Until then, consumers who need this can wire their own announcement
|
|
154
|
+
inside `onEventDrop`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { useA11yAnnouncer } from '@cocoar/vue-calendar';
|
|
158
|
+
|
|
159
|
+
const announcer = useA11yAnnouncer();
|
|
160
|
+
builder.onEventDrop(({ event, next }) => {
|
|
161
|
+
const title = (event.meta as { title?: string })?.title ?? event.id;
|
|
162
|
+
announcer.announce(`${title} moved to ${next.start.toString()}`);
|
|
163
|
+
// …persist the change
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Reproduce locally
|
|
168
|
+
|
|
169
|
+
A manual smoke test surface ships with the playground:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
pnpm --filter @cocoar/playground dev
|
|
173
|
+
# open http://localhost:5188/calendar-perf-bench
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Pick `100 / 500 / 1 000 / 2 500` events from the segmented control,
|
|
177
|
+
drag, switch view, wheel-scroll. Generation-ms, in-data count, and
|
|
178
|
+
visible-window count are surfaced in the toolbar so you can sanity-check
|
|
179
|
+
the workload at a glance.
|
|
180
|
+
|
|
181
|
+
For automated perf gating (run the same scripted scenarios against a
|
|
182
|
+
production build under Playwright + Chromium DevTools), wire up your
|
|
183
|
+
own LoAF observer using the same approach documented in the
|
|
184
|
+
[Long Animation Frame API spec][loaf]. The patterns from this page
|
|
185
|
+
(scripted drag-and-hold, mixed-direction wheel, bottom-jump) are a good
|
|
186
|
+
starting fixture set.
|