@cocoar/vue-ui 3.1.0 → 3.2.0-beta.11

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.
Files changed (180) hide show
  1. package/bin/cli.mjs +53 -0
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  3. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  5. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  6. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  7. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  9. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  10. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  11. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  12. package/dist/components/data-list/index.d.ts +12 -0
  13. package/dist/components/data-list/index.d.ts.map +1 -0
  14. package/dist/components/data-list/internal/compare.d.ts +14 -0
  15. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  17. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  18. package/dist/components/data-list/internal/search.d.ts +16 -0
  19. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  21. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  23. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  25. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  26. package/dist/components/data-list/types.d.ts +185 -0
  27. package/dist/components/data-list/types.d.ts.map +1 -0
  28. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  29. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  30. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  31. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  32. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  33. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  34. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  36. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  38. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  40. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  42. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  44. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  45. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  46. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  47. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  48. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  50. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  52. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  54. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  56. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  58. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  59. package/dist/composables/dragRegistry.d.ts +12 -0
  60. package/dist/composables/dragRegistry.d.ts.map +1 -1
  61. package/dist/composables/index.d.ts +2 -2
  62. package/dist/composables/index.d.ts.map +1 -1
  63. package/dist/composables/useDragDrop.d.ts +62 -6
  64. package/dist/composables/useDragDrop.d.ts.map +1 -1
  65. package/dist/composables/useVirtualList.d.ts +21 -1
  66. package/dist/composables/useVirtualList.d.ts.map +1 -1
  67. package/dist/index.css +1 -1
  68. package/dist/index.d.ts +4 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4508 -2039
  71. package/package.json +9 -3
  72. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  73. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  74. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  75. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  76. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +372 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1647 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +486 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  86. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  87. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  89. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  90. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  91. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  99. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  100. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  101. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  102. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  103. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  104. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  105. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  106. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  110. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  111. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  112. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  116. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  117. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  118. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  119. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  120. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  121. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  122. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  127. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  128. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  129. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  130. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  131. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  132. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  133. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  134. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  140. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  141. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  142. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  143. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  144. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  145. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  146. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  147. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  148. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  149. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  150. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  151. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  152. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  153. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  154. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  155. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  156. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  157. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  158. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  159. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  160. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  161. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  162. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  163. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  164. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  165. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  166. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  167. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  171. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  172. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  173. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  174. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  175. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  176. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  177. package/skills/cocoar-vue-ui/references/guide/getting-started.md +116 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  179. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  180. package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
@@ -0,0 +1,486 @@
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 | Base rows of at least 94 px — measured up when the host's pills are taller; titles, assignees, multi-day bars and a `+N` row for whatever does not fit. |
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>` | `2` | Single-day pills a **Details** cell shows before the rest fold into the `+N` row. Stacked and Compact use fixed limits (2 marks / 6 capsule segments) like iOS. |
430
+ | `monthMaxVisibleLanes(n)` | `MaybeRefOrGetter<number \| null>` | `2` | Multi-day lanes a week row shows. Bars past the cap leave the band and count into the `+N` of every day they cover; `null` lets the row grow with every lane. |
431
+ | `eventRenderer(r)` | `EventRenderer<TMeta>` | — | Universal renderer. Branch on `ctx.layout?.kind === 'monthPill' \| 'monthBar'` for variant-specific rendering — see the example above. |
432
+ | `dayHeaderRenderer(r)` | `DayHeaderRenderer` | — | Weekday-strip header (Mon / Tue / ...). |
433
+
434
+ ## Overflow — the `+N` row
435
+
436
+ Month rows have a fixed height per density plus the height of the week's multi-day lane band, and that band is capped too: a row shows at most `monthMaxVisibleLanes` lanes (default **2**). A multi-day bar past the cap leaves the band entirely — it is never clipped mid-row — and counts into the `+N` of every day it covers. `null` restores the unbounded band of the iOS port, where a week grows with every lane.
437
+
438
+ A Details cell renders its first `maxEventsPerCell` single-day pills (default **2**) and folds the rest, together with the folded bars covering the day, into one `+N` row in the subtle text colour. The Details base row is the iOS 94 px or, if the host's styling needs more, the measured height of the day-number row plus `maxEventsPerCell` pills plus the `+N` row — the view measures its own rendered cells (`useMonthMetrics`), so a `:deep()` override that makes pills or the day-number row taller never clips the marker. Details multi-day bars take the measured pill height, so bars and pills in one row are always the same size. Stacked shows 2 marks and Compact 6 capsule segments and, like iOS, cap silently — their marks carry no titles, so the day has to be opened either way. Cells never scroll, and there is no per-cell menu or row expansion.
439
+
440
+ On the web the `+N` row is a button (accessible name "N more events", `aria-haspopup="dialog"`). It opens a **day sheet** over that one cell: a small dialog aligned to the cell's top-left edge that lists every event of the day — the multi-day events covering it first (visible lanes, then folded ones), then the single-day pills — and scrolls when the list is long. The sheet opens downward and flips upward when the space below inside the scroll container is short; it never expands the row or the grid. Every pill in the sheet is a live pill with the grid's own wiring — drag it onto any other day, move it with the keyboard, double-click it for `onEventDoubleClick`. The sheet closes on Escape, its close control, a pointerdown outside it, another `+N`, or when the month changes; focus returns to the `+N` button.
441
+
442
+ A tap on the cell body still fires `onDateClick` (the `+N` button does not), so a host that prefers its own day surface keeps working unchanged.
443
+
444
+ In the grid itself hidden events are not in the DOM, so keyboard focus and drag-and-drop there reach only the visible pills; the sheet is where the rest become reachable. Drop onto a full cell works either way: a drag preview takes the last visible slot so the target is always visible.
445
+
446
+ ```ts
447
+ builder.monthDensity('details').maxEventsPerCell(3); // three titles, then +N
448
+ ```
449
+
450
+ ## Drag and drop
451
+
452
+ - **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.
453
+ - **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).
454
+ - **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.
455
+ - **`canDrop`** — the universal drop validator on the builder; returning `false` paints a red dashed "invalid" ghost and silently swallows the drop on release.
456
+
457
+ ## Imperative API
458
+
459
+ ```ts
460
+ interface CalendarApi<TMeta> {
461
+ goTo(iso: string): void;
462
+ goToToday(): void;
463
+ next(): void; // ±1 month
464
+ prev(): void;
465
+ getVisibleRange(): ViewWindow | null;
466
+ getVisibleEvents(): CalendarEvent<TMeta>[];
467
+ refresh(): void;
468
+ refreshRange(start: string, end: string): void;
469
+ readonly loading: Readonly<Ref<boolean>>;
470
+ readonly visibleRange: Readonly<Ref<ViewWindow | null>>;
471
+ readonly gridReady: Readonly<Ref<boolean>>;
472
+ }
473
+ ```
474
+
475
+ `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.
476
+
477
+ ## Month component props + slots
478
+
479
+ | Prop | Type | Description |
480
+ |---|---|---|
481
+ | `builder` | `CalendarBuilder` | **Required.** From `useMonthView()` (or share the one from `useCalendar()`). |
482
+
483
+ | Slot | Scope | Purpose |
484
+ |---|---|---|
485
+ | `pill` | `{ event, pill }` | Single-day pill renderer. `pill` is the `MonthCellPill` (event + visual order in the cell). |
486
+ | `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.