@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,361 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/calendar/day-view.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# `<CoarDayView>` — Day View (Preview)
|
|
4
|
+
|
|
5
|
+
Time-grid surface with two display choices: **One day** renders one fixed column, while **Multi-day** derives 1–7 complete columns from the available container width. Multi-day all-day events that touch the visible range appear in the all-day band pinned under the day headers. Use it standalone via [`useDayView()`](#usedayview) when you need the surface without the [`<CoarCalendar>`](./coar-calendar.md) shell.
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<CoarDayView :builder="builder" />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Standalone usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { ref } from 'vue';
|
|
15
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
16
|
+
import {
|
|
17
|
+
CoarDayView,
|
|
18
|
+
useDayView,
|
|
19
|
+
type CalendarEvent,
|
|
20
|
+
} from '@cocoar/vue-calendar';
|
|
21
|
+
|
|
22
|
+
const events = ref<CalendarEvent[]>([
|
|
23
|
+
{
|
|
24
|
+
id: 'standup',
|
|
25
|
+
start: Temporal.ZonedDateTime.from('2026-04-15T09:00:00[UTC]'),
|
|
26
|
+
end: Temporal.ZonedDateTime.from('2026-04-15T09:15:00[UTC]'),
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
const date = ref('2026-04-15');
|
|
30
|
+
|
|
31
|
+
const { builder, api } = useDayView();
|
|
32
|
+
builder
|
|
33
|
+
.events(events)
|
|
34
|
+
.date(date)
|
|
35
|
+
.timezone('UTC')
|
|
36
|
+
.timeRange([8, 18])
|
|
37
|
+
.slotDuration(15)
|
|
38
|
+
.onTimeClick(({ time }) => console.log(time.toString()));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<CoarDayView :builder="builder" />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The Day view shares its builder type, `CalendarBuilder`, with the [Week view](./week-view.md) — they differ only in the days array the wrapper computes (Day uses `[date]`, Week uses `weekDates(date, fdow)`).
|
|
46
|
+
|
|
47
|
+
## One day and Multi-day
|
|
48
|
+
|
|
49
|
+
The shell presents both as variations under Day. Multi-day never squeezes partial columns into the available width: it calculates the number of full columns from `dayColumnMinWidth`, clamps the result to `1…7`, and honours `dayColumnCount` as its minimum.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
builder
|
|
53
|
+
.dayMode('multiDay')
|
|
54
|
+
.dayColumnCount(1)
|
|
55
|
+
.dayColumnMinWidth(220);
|
|
56
|
+
|
|
57
|
+
api.setDayMode('single');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For fixed seven-day and configured-workday grids, use [Week](./week-view.md) and [Work Week](./work-week-view.md). They share the same time-grid renderer.
|
|
61
|
+
|
|
62
|
+
**Demo — `calendar/demos/DayViewBasic.vue`**
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<template>
|
|
66
|
+
<div style="height: 600px; border: 1px solid var(--coar-border-neutral-tertiary); border-radius: var(--coar-radius-xs); overflow: hidden;">
|
|
67
|
+
<CoarDayView :builder="builder" />
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import { ref } from 'vue';
|
|
73
|
+
import {
|
|
74
|
+
CoarDayView,
|
|
75
|
+
useDayView,
|
|
76
|
+
Temporal,
|
|
77
|
+
type CalendarEvent,
|
|
78
|
+
} from '@cocoar/vue-calendar';
|
|
79
|
+
|
|
80
|
+
const date = ref(Temporal.PlainDate.from('2026-04-15'));
|
|
81
|
+
|
|
82
|
+
const pd = (iso: string) => Temporal.PlainDate.from(iso);
|
|
83
|
+
const zdt = (iso: string, tz = 'Europe/Vienna') =>
|
|
84
|
+
Temporal.ZonedDateTime.from(`${iso}[${tz}]`);
|
|
85
|
+
|
|
86
|
+
const events = ref<CalendarEvent[]>([
|
|
87
|
+
// All-day band: a multi-day OOO that touches today.
|
|
88
|
+
{
|
|
89
|
+
id: 'devconf',
|
|
90
|
+
start: pd('2026-04-13'),
|
|
91
|
+
end: pd('2026-04-16'),
|
|
92
|
+
meta: { title: 'DevConf — Vienna', color: '#7c3aed' },
|
|
93
|
+
},
|
|
94
|
+
// Three timed events with a 3-deep overlap cluster.
|
|
95
|
+
{
|
|
96
|
+
id: 'standup',
|
|
97
|
+
start: zdt('2026-04-15T09:00:00'),
|
|
98
|
+
end: zdt('2026-04-15T09:30:00'),
|
|
99
|
+
meta: { title: 'Daily standup', color: '#10b981' },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'design',
|
|
103
|
+
start: zdt('2026-04-15T11:00:00'),
|
|
104
|
+
end: zdt('2026-04-15T12:30:00'),
|
|
105
|
+
meta: { title: 'Design review', color: '#8b5cf6' },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'pair',
|
|
109
|
+
start: zdt('2026-04-15T11:30:00'),
|
|
110
|
+
end: zdt('2026-04-15T13:00:00'),
|
|
111
|
+
meta: { title: 'Pair: calendar', color: '#f59e0b' },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'lunch',
|
|
115
|
+
start: zdt('2026-04-15T12:00:00'),
|
|
116
|
+
end: zdt('2026-04-15T13:00:00'),
|
|
117
|
+
meta: { title: 'Lunch with Anna', color: '#ef4444' },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'one-on-one',
|
|
121
|
+
start: zdt('2026-04-15T15:00:00'),
|
|
122
|
+
end: zdt('2026-04-15T15:45:00'),
|
|
123
|
+
meta: { title: '1:1 with Bernhard', color: '#3b82f6' },
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const { builder } = useDayView();
|
|
128
|
+
builder
|
|
129
|
+
.events(events)
|
|
130
|
+
.date(date)
|
|
131
|
+
.timezone('Europe/Vienna')
|
|
132
|
+
// Apply drag/keyboard moves in place so the demo is interactive.
|
|
133
|
+
.onEventDrop(({ event, next }) => {
|
|
134
|
+
const idx = events.value.findIndex((e) => e.id === event.id);
|
|
135
|
+
if (idx < 0) return;
|
|
136
|
+
events.value = [
|
|
137
|
+
...events.value.slice(0, idx),
|
|
138
|
+
{ ...event, start: next.start, ...(next.end ? { end: next.end } : {}) },
|
|
139
|
+
...events.value.slice(idx + 1),
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
</script>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Working hours
|
|
146
|
+
|
|
147
|
+
Constrain the visible hour range via `timeRange([startHour, endHour])` and tighten drag-snap precision via `slotDuration(15)` for a 15-minute grid. Events outside the visible window are still in the data set but invisible.
|
|
148
|
+
|
|
149
|
+
**Demo — `calendar/demos/DayViewWorkingHours.vue`**
|
|
150
|
+
|
|
151
|
+
```vue
|
|
152
|
+
<template>
|
|
153
|
+
<div>
|
|
154
|
+
<p class="hint">
|
|
155
|
+
Visible hour range constrained to <code>[8, 18]</code>; slot
|
|
156
|
+
subdivision tightened to 15 min for finer drag-snap. The early
|
|
157
|
+
flight (5–7 AM) is still in <code>events</code> but lives off
|
|
158
|
+
the visible window.
|
|
159
|
+
</p>
|
|
160
|
+
<div style="height: 520px; border: 1px solid var(--coar-border-neutral-tertiary); border-radius: var(--coar-radius-xs); overflow: hidden;">
|
|
161
|
+
<CoarDayView :builder="builder" />
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<script setup lang="ts">
|
|
167
|
+
import { ref } from 'vue';
|
|
168
|
+
import {
|
|
169
|
+
CoarDayView,
|
|
170
|
+
useDayView,
|
|
171
|
+
Temporal,
|
|
172
|
+
type CalendarEvent,
|
|
173
|
+
} from '@cocoar/vue-calendar';
|
|
174
|
+
|
|
175
|
+
const date = ref(Temporal.PlainDate.from('2026-04-15'));
|
|
176
|
+
|
|
177
|
+
const zdt = (iso: string, tz = 'Europe/Vienna') =>
|
|
178
|
+
Temporal.ZonedDateTime.from(`${iso}[${tz}]`);
|
|
179
|
+
|
|
180
|
+
const events = ref<CalendarEvent[]>([
|
|
181
|
+
// Off-window early — invisible at timeRange [8, 18].
|
|
182
|
+
{
|
|
183
|
+
id: 'red-eye',
|
|
184
|
+
start: zdt('2026-04-15T05:00:00'),
|
|
185
|
+
end: zdt('2026-04-15T07:30:00'),
|
|
186
|
+
meta: { title: 'Early flight', color: '#84cc16' },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'standup',
|
|
190
|
+
start: zdt('2026-04-15T09:00:00'),
|
|
191
|
+
end: zdt('2026-04-15T09:15:00'),
|
|
192
|
+
meta: { title: 'Daily standup', color: '#10b981' },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'review',
|
|
196
|
+
start: zdt('2026-04-15T10:30:00'),
|
|
197
|
+
end: zdt('2026-04-15T11:30:00'),
|
|
198
|
+
meta: { title: 'Design review', color: '#8b5cf6' },
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'lunch',
|
|
202
|
+
start: zdt('2026-04-15T12:00:00'),
|
|
203
|
+
end: zdt('2026-04-15T13:00:00'),
|
|
204
|
+
meta: { title: 'Lunch', color: '#ef4444' },
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'deep-work',
|
|
208
|
+
start: zdt('2026-04-15T13:15:00'),
|
|
209
|
+
end: zdt('2026-04-15T16:45:00'),
|
|
210
|
+
meta: { title: 'Deep work', color: '#2563eb' },
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const { builder } = useDayView();
|
|
215
|
+
builder
|
|
216
|
+
.events(events)
|
|
217
|
+
.date(date)
|
|
218
|
+
.timezone('Europe/Vienna')
|
|
219
|
+
.timeRange({ startMinutes: 8 * 60, endMinutes: 18 * 60 })
|
|
220
|
+
.slotDuration(15)
|
|
221
|
+
.onEventDrop(({ event, next }) => {
|
|
222
|
+
const idx = events.value.findIndex((e) => e.id === event.id);
|
|
223
|
+
if (idx < 0) return;
|
|
224
|
+
events.value = [
|
|
225
|
+
...events.value.slice(0, idx),
|
|
226
|
+
{ ...event, start: next.start, ...(next.end ? { end: next.end } : {}) },
|
|
227
|
+
...events.value.slice(idx + 1),
|
|
228
|
+
];
|
|
229
|
+
});
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<style scoped>
|
|
233
|
+
.hint {
|
|
234
|
+
margin: 0 0 12px;
|
|
235
|
+
font-size: 13px;
|
|
236
|
+
color: var(--coar-text-subtle, #6b7280);
|
|
237
|
+
}
|
|
238
|
+
.hint code {
|
|
239
|
+
font-family: var(--coar-font-family-mono, monospace);
|
|
240
|
+
font-size: 12px;
|
|
241
|
+
background: var(--coar-background-neutral-tertiary, #f3f4f6);
|
|
242
|
+
padding: 1px 5px;
|
|
243
|
+
border-radius: 3px;
|
|
244
|
+
}
|
|
245
|
+
</style>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Point events (timed, no `end`)
|
|
249
|
+
|
|
250
|
+
A timed event without `end` keeps the default 30-minute slot geometry but renders distinguishably from a real 30-minute event: a solid start edge in the event color sits exactly on the start time, and the card body drops to ~38 % fill opacity — the title stays fully opaque. Resize handles are suppressed (there is no `end` to grab). Month and Agenda render point events unchanged. The look matches the SwiftUI port; tune it via `--coar-calendar-point-edge-height` / `--coar-calendar-point-body-opacity` (see [Theming](./index.md#theming)).
|
|
251
|
+
|
|
252
|
+
## Overlapping timed cards
|
|
253
|
+
|
|
254
|
+
Same cascade and compact-anatomy policy as the week view, tuned with `timedEventDetailMinWidth` — see [Overlapping timed cards](./week-view.md#overlapping-timed-cards).
|
|
255
|
+
## Inside `<CoarCalendar>`
|
|
256
|
+
|
|
257
|
+
`<CoarCalendar>` and `<CoarDayView>` consume the SAME `CalendarBuilder` instance — there's no sub-builder forking. Time-grid config goes directly on the composer's builder:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const { builder } = useCalendar();
|
|
261
|
+
builder
|
|
262
|
+
.timeRange({ startMinutes: 8 * 60, endMinutes: 18 * 60 })
|
|
263
|
+
.slotDuration(15);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
When the active view is day or week, the same builder feeds the embedded `<CoarTimeGrid>`. View-specific settings have no effect outside their view.
|
|
267
|
+
|
|
268
|
+
## `useDayView<TMeta>()`
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
function useDayView<TMeta>(): {
|
|
272
|
+
builder: CalendarBuilder<TMeta>;
|
|
273
|
+
api: CalendarApi<TMeta>;
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Returns a fresh standalone builder + its imperative api. The builder type is the same `CalendarBuilder` used by `<CoarCalendar>` — `useDayView()` is a thin shorthand that pre-sets `view: 'day'`.
|
|
278
|
+
|
|
279
|
+
## One model for every time grid
|
|
280
|
+
|
|
281
|
+
Day, Multi-day, Week and Work week render on the same surface and are described by the same four parameters. The classic views are presets; `builder.timeGridRange(...)` lets the Day view use any other combination.
|
|
282
|
+
|
|
283
|
+
| Parameter | Meaning | Values |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `anchor` | Where the first column stands | `'cursor'`, or `'weekStart'` (cursor snapped to the locale's first day of the week) |
|
|
286
|
+
| `span` | Days from the anchor | a number, or `'responsive'` (as many complete columns as the container width allows) |
|
|
287
|
+
| `filter` | Which of those days are drawn | `'all'`, or `'workDays'` (the builder's `workDays`; the loader window still covers the whole span) |
|
|
288
|
+
| `step` | How far `next` / `prev` / a swipe move the cursor | a number of days, or `'span'` (as many as are shown) |
|
|
289
|
+
|
|
290
|
+
| View | anchor | span | filter | step |
|
|
291
|
+
|---|---|---|---|---|
|
|
292
|
+
| Day, `dayMode('single')` | cursor | 1 | all | span |
|
|
293
|
+
| Day, `dayMode('multiDay')` | cursor | responsive | all | span |
|
|
294
|
+
| Week | weekStart | 7 | all | 7 |
|
|
295
|
+
| Work week | weekStart | 7 | workDays | 7 |
|
|
296
|
+
|
|
297
|
+
"Start on Monday, show five days, page by a week" is one more spec:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
builder.view('day').timeGridRange({ anchor: 'cursor', span: 5, filter: 'all', step: 7 });
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Week and Work week are fixed presets and ignore `timeGridRange`; `null` restores the `dayMode` presets. Because there is one resolver (`resolveTimeGridRange`, exported from the core subpath) and one surface underneath, every grid feature — touch paging, the empty-cell hooks, the all-day lane cap — behaves identically across the four.
|
|
304
|
+
|
|
305
|
+
## Touch paging
|
|
306
|
+
|
|
307
|
+
On touch devices a horizontal pan on the day columns moves the grid with the finger — header cells, all-day band and columns together, the hour axis stays put — and pages to the previous / next range on release past a quarter of the width or on a fast flick. Below that the grid settles back. A touch that never moves is a tap and reaches `onTimeClick` on release, so a swipe never starts with a stray slot click. Vertical pans stay native scrolling. Mouse and pen keep their click-on-press semantics.
|
|
308
|
+
|
|
309
|
+
While you drag, the **previous and next page** are drawn to the left and right of the current one — same columns, same events — so the gesture reads as paging between two visible pages; they are visual only and disappear once the grid settles. In loader mode the calendar pre-warms those two windows shortly after each page becomes visible (`builder.prefetchNeighbours(false)` switches that off; one extra fetch per neighbour), and `api.getEventsForWindow(window)` is the read behind it. In `events()` mode nothing needs fetching.
|
|
310
|
+
|
|
311
|
+
The **day-name strip** at the top is a paging handle for every pointer type: a mouse drag across the day names pages the grid the same way (grab cursor), because there is nothing else to drag or click up there. On the columns themselves the mouse keeps its click-on-press semantics.
|
|
312
|
+
|
|
313
|
+
`builder.swipeNavigation(false)` switches the gesture off. `prefers-reduced-motion` skips the settle animation and pages immediately. The same gesture is available on the Week and Work week grids.
|
|
314
|
+
|
|
315
|
+
## Builder setters
|
|
316
|
+
|
|
317
|
+
Full reference: see [the composer's API reference](./coar-calendar.md#api-reference). Highlights for the day view:
|
|
318
|
+
|
|
319
|
+
| Setter | Argument | Default | Notes |
|
|
320
|
+
|---|---|---|---|
|
|
321
|
+
| `timeRange(r)` | `MaybeRefOrGetter<{ startMinutes, endMinutes }>` | `{0, 1440}` | Visible hour range, in minutes from midnight. Events outside are still rendered into the all-day band when applicable. |
|
|
322
|
+
| `dayMode(m)` | `'single' \| 'multiDay'` | `'single'` | Fixed one-day column or width-aware multi-day range (the two Day presets of the time-grid model). |
|
|
323
|
+
| `timeGridRange(spec)` | `MaybeRefOrGetter<TimeGridRangeSpec \| null>` | `null` | Explicit anchor / span / filter / step for the Day view — see "One model for every time grid". |
|
|
324
|
+
| `dayColumnCount(n)` | `MaybeRefOrGetter<number>` | `1` | Minimum number of complete columns in Multi-day mode. |
|
|
325
|
+
| `dayColumnMinWidth(px)` | `MaybeRefOrGetter<number>` | `220` | Target width used to derive additional columns, capped at seven. |
|
|
326
|
+
| `slotDuration(d)` | `MaybeRefOrGetter<number>` | `30` | Slot subdivision (minutes). Also the snap step when dragging. |
|
|
327
|
+
| `pixelsPerHour(p)` | `MaybeRefOrGetter<number>` | `60` | Vertical density. `60` = 30 px per 30-min slot. |
|
|
328
|
+
| `eventRenderer(r)` | `EventRenderer<TMeta>` | — | Universal renderer. Branch on `ctx.layout?.kind === 'positioned'` (timed cards) vs `'allDayBar'` (all-day band). |
|
|
329
|
+
| `dayHeaderRenderer(r)` | `DayHeaderRenderer` | — | Per-day column header. |
|
|
330
|
+
|
|
331
|
+
## Imperative API
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
interface CalendarApi<TMeta> {
|
|
335
|
+
goTo(date: Temporal.PlainDate): void;
|
|
336
|
+
goToToday(): void;
|
|
337
|
+
next(): void; // ±1 day in day-view
|
|
338
|
+
prev(): void;
|
|
339
|
+
getVisibleRange(): ViewWindow | null;
|
|
340
|
+
getVisibleEvents(): CalendarEvent<TMeta>[];
|
|
341
|
+
scrollToTime(time: Temporal.PlainTime): void; // Day / Week only
|
|
342
|
+
scrollToDate(date: Temporal.PlainDate): void; // Agenda only
|
|
343
|
+
refresh(): void;
|
|
344
|
+
refreshRange(window: ViewWindow): void;
|
|
345
|
+
readonly loading: Readonly<Ref<boolean>>;
|
|
346
|
+
readonly visibleRange: Readonly<Ref<ViewWindow | null>>;
|
|
347
|
+
readonly gridReady: Readonly<Ref<boolean>>;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## `<CoarDayView>` props + slots
|
|
352
|
+
|
|
353
|
+
| Prop | Type | Description |
|
|
354
|
+
|---|---|---|
|
|
355
|
+
| `builder` | `CalendarBuilder` | **Required.** From `useDayView()` (or share the one from `useCalendar()`). |
|
|
356
|
+
|
|
357
|
+
| Slot | Scope | Purpose |
|
|
358
|
+
|---|---|---|
|
|
359
|
+
| `event` | `{ event, layout }` | Per-event renderer. `layout` is the `PositionedEvent` (lane / startMinutes / endMinutes / clipping flags). |
|
|
360
|
+
| `allDayEvent` | `{ event, layout }` | All-day band renderer. `layout` is the `AllDayBar` (lane / startCol / endCol / clipping flags). |
|
|
361
|
+
| `dayHeader` | `{ date, isToday, isWeekend }` | Per-day column header. |
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/calendar/index.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Calendar (Preview)
|
|
4
|
+
|
|
5
|
+
A Vue 3 calendar whose visible hierarchy follows the iOS calendar: **Year**, **Month**, **Day** and **Agenda**, plus fixed **Week** and **Work week** time grids for web applications. Month and Day expose their display choices as nested variations instead of flattening every renderer into the primary view switcher.
|
|
6
|
+
|
|
7
|
+
Events on the public surface use **`Temporal`** values directly (`PlainDate` for all-day, `ZonedDateTime` for timed) — there's no string parsing or implicit-zone guessing. Consumers convert their wire format (ISO strings, epoch ms, etc.) at the boundary; the calendar receives unambiguous values and runs all date math on them across DST boundaries.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
// Top-level shell + composer
|
|
12
|
+
CoarCalendar,
|
|
13
|
+
useCalendar,
|
|
14
|
+
// Sub-views — each usable standalone via a matching composable
|
|
15
|
+
CoarYearView,
|
|
16
|
+
CoarContinuousMonthView,
|
|
17
|
+
CoarMonthListView,
|
|
18
|
+
CoarMonthView, useMonthView,
|
|
19
|
+
CoarDayView, useDayView,
|
|
20
|
+
CoarWeekView, useWeekView,
|
|
21
|
+
CoarAgendaView, useAgendaView,
|
|
22
|
+
// Drop-in display-zone selector (writes to a string ref consumed by `builder.timezone(ref)`)
|
|
23
|
+
CoarDisplayZoneSwitcher,
|
|
24
|
+
// Helper for surfacing C3 / C5 zone semantics in custom renderers
|
|
25
|
+
getEventZoneHints,
|
|
26
|
+
// Public types
|
|
27
|
+
type CalendarEvent,
|
|
28
|
+
type CalendarView,
|
|
29
|
+
type ViewWindow,
|
|
30
|
+
type EventZoneHints,
|
|
31
|
+
} from '@cocoar/vue-calendar';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add @cocoar/vue-calendar
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The component styles ship as one stylesheet on the `./styles` subpath. Import it once, next to the design-system styles:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import '@cocoar/vue-ui/styles';
|
|
44
|
+
import '@cocoar/vue-calendar/styles';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Localization
|
|
48
|
+
|
|
49
|
+
Every visible label (`Today`, `Month`, `All day`, the a11y announcements, …) is looked up as a `coar.calendar.*` key through the host's `@cocoar/vue-localization` service, with English fallbacks inline. The package ships German and English catalogs plus a translation source, so a host registers them instead of maintaining the key list by hand:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createCoarLocalization } from '@cocoar/vue-localization';
|
|
53
|
+
import { createCalendarTranslationSource, calendarMessages } from '@cocoar/vue-calendar';
|
|
54
|
+
|
|
55
|
+
const localization = createCoarLocalization({ defaultLanguage: 'de-AT' });
|
|
56
|
+
localization.service.addTranslationSource(createCalendarTranslationSource());
|
|
57
|
+
app.use(localization);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Regional tags resolve to their base language (`de-AT` → `de`). A host source registered **after** the calendar's overrides per key, so app-specific wording still wins. `calendarMessages.en` / `.de` are the flat key → text maps for hosts that bundle one catalog file themselves. Date, time and weekday names are never in the catalog — they come from `Intl` (C6).
|
|
61
|
+
|
|
62
|
+
## Two ways to use the calendar
|
|
63
|
+
|
|
64
|
+
### As a single shell
|
|
65
|
+
|
|
66
|
+
[`<CoarCalendar>`](./coar-calendar.md) is the all-in-one component: header with prev / today / next, a primary view switcher, nested Month / Day display controls, and the body that dispatches to whichever view is active. **One** `useCalendar()` builder feeds it; switching views is just `api.setView('agenda')`.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const { builder, api } = useCalendar();
|
|
70
|
+
builder.events(events).date(date).timezone('Europe/Vienna');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<CoarCalendar :builder="builder" />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> **Tip: Use the shell when…**
|
|
78
|
+
>
|
|
79
|
+
> You want the complete view hierarchy, navigation chrome, and a consistent feel without writing custom layout. This is the path 90 % of consumers want.
|
|
80
|
+
|
|
81
|
+
### As a single sub-view
|
|
82
|
+
|
|
83
|
+
Each sub-view is exported and consumes its OWN `:builder` produced by a matching composable:
|
|
84
|
+
|
|
85
|
+
| View | Component | Composable | Builder |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| Year | [`<CoarYearView>`](./year-view.md) | `useCalendar()` + `.view('year')` | `CalendarBuilder` |
|
|
88
|
+
| Continuous Month | [`<CoarContinuousMonthView>`](./month-view.md) | `useCalendar()` + `.view('month')` | `CalendarBuilder` |
|
|
89
|
+
| Month List | [`<CoarMonthListView>`](./month-view.md#month-list) | `useCalendar()` + `.view('monthList')` | `CalendarBuilder` |
|
|
90
|
+
| Day | [`<CoarDayView>`](./day-view.md) | `useDayView()` | `CalendarBuilder` |
|
|
91
|
+
| Week | [`<CoarWeekView>`](./week-view.md) | `useWeekView()` | `CalendarBuilder` |
|
|
92
|
+
| Single Month Section | [`<CoarMonthView>`](./month-view.md#single-month-section) | `useMonthView()` | `CalendarBuilder` |
|
|
93
|
+
| Agenda | [`<CoarAgendaView>`](./agenda-view.md) | `useAgendaView()` | `CalendarBuilder` |
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const { builder } = useMonthView();
|
|
97
|
+
builder.events(events).date(cursor).timezone('Europe/Vienna');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<CoarMonthView :builder="builder" />
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Tip: Use a sub-view directly when…**
|
|
105
|
+
>
|
|
106
|
+
> You're building your own header / chrome, embedding the calendar in a larger layout, or only need one view forever. Sub-views skip the navigation header + view switcher entirely.
|
|
107
|
+
|
|
108
|
+
The standalone composables and the shell composer share the same universal config surface (events / locale / timezone / density / handlers / renderers), so a renderer or handler written once works in either context. The flat `CalendarBuilder` carries every view's config — there are no per-view sub-builders to reach into.
|
|
109
|
+
|
|
110
|
+
## Architecture invariants (C1–C8) {#invariants}
|
|
111
|
+
|
|
112
|
+
Eight non-negotiable invariants drawn from the ["Time in Software, Done Right"][articles] article series. They're enforced **structurally** — by the type surface, by the test suite, by the single drop pipeline — not by convention. Other docs pages reference them by id (e.g. "Article 3 / C5"); this is the canonical reference.
|
|
113
|
+
|
|
114
|
+
[articles]: https://dev.to/bwi/why-a-date-is-not-a-point-in-time-ad8
|
|
115
|
+
|
|
116
|
+
| Article | Invariant | What it means |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| 1, 2, 4, 8 | **C1** Temporal-only public surface | Strings, `Date`, `PlainDateTime`, `Instant` rejected at the events-watcher boundary by `validateCalendarEvent`. Only `Temporal.ZonedDateTime` (timed) or `Temporal.PlainDate` (all-day) cross the wire. |
|
|
119
|
+
| 4, 5 | **C2** Single drop pipeline | Exactly one function (`applyMoveToEvent`) converts a UI drop → new endpoints. Mouse, keyboard, touch all reach it once — same code path, same DST resolution, same payload shape. |
|
|
120
|
+
| 4 | **C3** Source zone preserved per-endpoint | Cross-zone events are first-class. A Tokyo→Vienna flight keeps both endpoints in their source zones across every drag mode; the library never collapses both ends to one zone. |
|
|
121
|
+
| 5 | **C4** DST disambiguation explicit | `DstPolicy` (`'compatible' \| 'reject' \| 'earlier' \| 'later'`) is a **required** parameter of every wall-time → instant conversion. No silent default — gap / overlap behaviour is always opted into. |
|
|
122
|
+
| 3, 4 | **C5** Display zone vs source zone separated | `EventDropPayload.target.displayZone` (the zone the user's eyes saw) and `next.start.timeZoneId` (where the event actually lives) are distinct fields. Switching display zone never destroys event intent. |
|
|
123
|
+
| 9 | **C6** Three independent display decisions | `locale`, `dateStyle`, `timeStyle`, `hour12` are independent setters, none derived from another. `buildFormatOptions(base, overrides)` is the only `Intl.DateTimeFormat` merge point. |
|
|
124
|
+
| spirit | **C7** Reactivity by reads, not setup-captures | Every consumer function (`canDrop`, `eventsLoader`, `eventRenderer`, `dayHeaderRenderer`) is read on every invocation — never captured at setup. Mutating the builder mid-session always takes effect on the next call. |
|
|
125
|
+
| 5 | **C8** Recurrence is a first-class type | `RecurringSeries` lives separately from `CalendarEvent`. RRULE / RDATE / EXDATE expand only for the visible window through the lazy bundled `rrule-temporal` engine, with occurrence provenance preserved for consumer-side series editing. |
|
|
126
|
+
|
|
127
|
+
The conformance test suite at `src/core/__tests__/timezone/` pins every invariant; CI fails if any of them slips.
|
|
128
|
+
|
|
129
|
+
## CalendarEvent shape
|
|
130
|
+
|
|
131
|
+
The library reads the layout-relevant fields (`start`, `end`, `id`); anything you put in `meta` is opaque to the engine and forwarded back to your renderer / slot. Use the generic `CalendarEvent<TMeta>` to keep your meta strongly typed.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
135
|
+
|
|
136
|
+
interface CalendarEvent<TMeta extends Record<string, unknown> = Record<string, unknown>> {
|
|
137
|
+
/** Stable id. For occurrences of a recurring event, this is the SERIES id. */
|
|
138
|
+
id: string;
|
|
139
|
+
/**
|
|
140
|
+
* `ZonedDateTime` for timed events, `PlainDate` for all-day events.
|
|
141
|
+
* The shape of `start` discriminates the event type — there is no
|
|
142
|
+
* separate `allDay` flag.
|
|
143
|
+
*/
|
|
144
|
+
start: Temporal.ZonedDateTime | Temporal.PlainDate;
|
|
145
|
+
/** Exclusive end. Must match `start`'s shape. Defaults to start + slot duration (timed) / start + 1 day (all-day). */
|
|
146
|
+
end?: Temporal.ZonedDateTime | Temporal.PlainDate;
|
|
147
|
+
/** Anything the consumer needs in their renderer. */
|
|
148
|
+
meta?: TMeta;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Construct events directly from `Temporal`:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
156
|
+
|
|
157
|
+
// All-day event:
|
|
158
|
+
{ id: 'devconf', start: Temporal.PlainDate.from('2026-04-13'), end: Temporal.PlainDate.from('2026-04-16') }
|
|
159
|
+
|
|
160
|
+
// Timed event in UTC:
|
|
161
|
+
{
|
|
162
|
+
id: 'standup',
|
|
163
|
+
start: Temporal.ZonedDateTime.from('2026-04-15T09:00:00[Europe/Vienna]'),
|
|
164
|
+
end: Temporal.ZonedDateTime.from('2026-04-15T09:30:00[Europe/Vienna]'),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Timed event in a specific zone:
|
|
168
|
+
{
|
|
169
|
+
id: 'vienna-call',
|
|
170
|
+
start: Temporal.ZonedDateTime.from('2026-06-15T10:00:00[Europe/Vienna]'),
|
|
171
|
+
end: Temporal.ZonedDateTime.from('2026-06-15T11:00:00[Europe/Vienna]'),
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The default event renderer reads `meta.title` and `meta.color` if present. Drop those in `meta` to skip writing a custom renderer for simple cases. Text on the coloured surface is black or white by contrast policy (`builder.eventTextContrast('wcag' | 'apca')`); `meta.textColor` (any CSS colour) overrides that choice for one event — tones on the fence are a design decision, not a computation.
|
|
176
|
+
|
|
177
|
+
## Display zone — switcher + on-card hints
|
|
178
|
+
|
|
179
|
+
Every event keeps its **source** zone (`start.timeZoneId`); the calendar renders it in whatever **display** zone the builder is configured with (`.timezone(ref)`). The two are kept separate by design — invariant C5 — so switching the display zone never destroys event intent. Two pieces of UI surface this distinction without writing any custom renderer:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { CoarDisplayZoneSwitcher, getEventZoneHints } from '@cocoar/vue-calendar';
|
|
183
|
+
|
|
184
|
+
const tz = ref(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
185
|
+
const { builder } = useCalendar();
|
|
186
|
+
builder.timezone(tz);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<CoarDisplayZoneSwitcher v-model="tz" />
|
|
191
|
+
<CoarCalendar :builder="builder" />
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`<CoarDisplayZoneSwitcher>` is a `<CoarSelect>` pre-populated with a curated short-list of common zones plus the browser-detected zone — pass `:options="..."` to swap in a domain-specific list (or `Intl.supportedValuesOf('timeZone')` for the full IANA roster).
|
|
195
|
+
|
|
196
|
+
The default event renderers also surface two zone semantics inline on every card:
|
|
197
|
+
|
|
198
|
+
- **Globe icon** when `start.timeZoneId === 'UTC'` — Article 5's "global event, same instant worldwide" (product launches, livestreams).
|
|
199
|
+
- **Cross-zone tag** (globe + accent dot) + tooltip when `start.timeZoneId` differs from the display zone — Article 3's fairness contract: we render the user's clock but don't hide where the event actually lives.
|
|
200
|
+
|
|
201
|
+
Both are rendered with `title=""` tooltips and an inline sr-only span for screen readers. The two are mutually exclusive — a UTC-anchored event in a non-UTC display gets only the global icon. Custom renderers can re-use the same logic via `getEventZoneHints(event, displayZone)`:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { getEventZoneHints } from '@cocoar/vue-calendar';
|
|
205
|
+
|
|
206
|
+
builder.eventRenderer((ctx) => {
|
|
207
|
+
const { isUtcAnchored, sourceZone } = getEventZoneHints(ctx.event, displayZone.value);
|
|
208
|
+
// …draw whatever icon / chip / accent suits your design.
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Reactive configuration
|
|
213
|
+
|
|
214
|
+
Every config setter accepts `MaybeRefOrGetter<T>` — pass a static value, a `ref()`, or a `() => …` getter and Vue tracks whatever shape you give it.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const { builder } = useCalendar();
|
|
218
|
+
|
|
219
|
+
builder.timezone('Europe/Vienna'); // static
|
|
220
|
+
builder.timezone(timezoneRef); // ref
|
|
221
|
+
builder.density(() => narrow.value ? 'compact' : 'comfortable'); // getter / computed
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This applies to the one flat builder shared by the shell and to every standalone sub-view builder.
|
|
225
|
+
|
|
226
|
+
## Theming
|
|
227
|
+
|
|
228
|
+
The calendar reads CSS custom properties at runtime, so themes apply the moment you set them on any ancestor — typically `:root`, the calendar's wrapper, or a per-instance class.
|
|
229
|
+
|
|
230
|
+
Two layers of tokens. **Calendar-specific** tokens are unique to this component and override only what would otherwise inherit from the design system. **Inherited DS tokens** are the shared `--coar-*` palette / type / radius scale every Cocoar component uses; they're listed here for completeness so consumers know what to set if they're embedding the calendar in a non-Cocoar host.
|
|
231
|
+
|
|
232
|
+
### Calendar-specific
|
|
233
|
+
|
|
234
|
+
| Token | Light | Dark | Purpose |
|
|
235
|
+
|---|---|---|---|
|
|
236
|
+
| `--coar-color-accent` | → `--coar-color-accent-500`, then `#2563eb` | same | Today markers (day number, column header, agenda badge), default event fill in list surfaces, cross-zone hint dot. Never defined by the package: it follows the vue-ui accent ramp (so `--coar-accent` rebrands it) unless the host sets it explicitly. |
|
|
237
|
+
| `--coar-calendar-bg` | `#fff` | `#18181b` | Background of every cell, header, and band. |
|
|
238
|
+
| `--coar-calendar-bg-today` | `rgba(37, 99, 235, 0.04)` | `#2563eb24` | Today highlight on month cells + day-column tint. |
|
|
239
|
+
| `--coar-calendar-bg-weekend` | `#f6f7f9` | `#212125` | Weekend tint (Sat/Sun) on month cells + day-columns. |
|
|
240
|
+
| `--coar-calendar-bg-other-month` | `#fafafb` | `#131316` | Leading / trailing days outside the active month. |
|
|
241
|
+
| `--coar-calendar-border` | `#d1d5db` | `#3f3f46` | Cell borders, header underlines, axis dividers. |
|
|
242
|
+
| `--coar-calendar-grid-line` | `#e3e5e9` | `#2c2c30` | The slot-line gradient inside time-grid columns. |
|
|
243
|
+
| `--coar-calendar-agenda-divider` | → `--coar-calendar-border`, then `#f3f4f6` | `#27272a` | Row separator between agenda entries. |
|
|
244
|
+
| `--coar-calendar-event-default-bg` | → `--coar-color-accent-soft`, then `#93c5fd` | `#1e3a8a` | Fill of events without a `meta.color`. |
|
|
245
|
+
| `--coar-calendar-point-edge-height` | `3px` | — | Start-edge bar on point events (timed, no `end`) in Day / Week. |
|
|
246
|
+
| `--coar-calendar-point-body-opacity` | `0.38` | — | Body fill opacity of point events (title stays fully opaque). |
|
|
247
|
+
| `--coar-calendar-scroll-inset-bottom` | `0px` | — | Extra scroll room at the bottom of every scrolling surface (Day / Week grids, Month, List, Agenda, Timeline, Year), so the last rows can clear host chrome that overlays the bottom edge. See [Host chrome over the bottom edge](#host-chrome-over-the-bottom-edge). |
|
|
248
|
+
| `--coar-time-grid-axis-width` | `80px` | — | Width of the hour-axis on the left of Day / Week. |
|
|
249
|
+
| `--coar-time-grid-header-height` | _auto_ | — | Sticky day-of-week header min-height. |
|
|
250
|
+
|
|
251
|
+
Light values are per-usage `var()` fallbacks — the tokens are undefined in light mode, so a single override anywhere wins. Dark values ship with the package stylesheet (see below).
|
|
252
|
+
|
|
253
|
+
### Host chrome over the bottom edge
|
|
254
|
+
|
|
255
|
+
On phones something almost always sits over the bottom of the viewport — a tab bar, a floating action button, the home-indicator safe area. Padding *around* the calendar doesn't help: the scroll container is inside the component, and outer padding shrinks the surface instead of letting the content scroll past the overlay. `--coar-calendar-scroll-inset-bottom` adds that room *inside* every scrolling surface (the same content inset the iOS calendar applies), and keyboard / focus scrolling honours it through `scroll-padding-bottom`.
|
|
256
|
+
|
|
257
|
+
```css
|
|
258
|
+
/* A 56 px bottom bar plus the device's safe area. */
|
|
259
|
+
.my-app .coar-calendar {
|
|
260
|
+
--coar-calendar-scroll-inset-bottom: calc(56px + env(safe-area-inset-bottom));
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The token is read at runtime, so it can change with the host's layout (a bar that hides while scrolling, a sheet that opens). Standalone sub-views read it the same way.
|
|
265
|
+
### Inherited from the design system
|
|
266
|
+
|
|
267
|
+
Used as direct `var()` references. Override at the design-system level rather than per-calendar-instance unless you need a calendar-only variant.
|
|
268
|
+
|
|
269
|
+
| Group | Tokens |
|
|
270
|
+
|---|---|
|
|
271
|
+
| Palette | `--coar-color-accent`, `--coar-color-accent-soft`, `--coar-color-danger`, `--coar-background-accent-primary`, `--coar-background-neutral-primary`, `--coar-background-neutral-tertiary`, `--coar-surface-subtle` |
|
|
272
|
+
| Text | `--coar-text-base`, `--coar-text-subtle` (calendar-local names — not defined by `@cocoar/vue-ui`; dark values ship with this package), `--coar-text-neutral-primary` |
|
|
273
|
+
| Type | `--coar-font-size-base`, `--coar-font-size-sm`, `--coar-font-size-xs`, `--coar-body-base-family` |
|
|
274
|
+
| Shape | `--coar-radius-md`, `--coar-radius-xs`, `--coar-border-neutral-tertiary` |
|
|
275
|
+
|
|
276
|
+
### Dark mode
|
|
277
|
+
|
|
278
|
+
Dark values for every calendar token ship with the package stylesheet — no consumer CSS needed. They activate on either trigger:
|
|
279
|
+
|
|
280
|
+
- `.dark-mode` class on `<html>` or any ancestor (the Cocoar convention, same as `@cocoar/vue-ui`)
|
|
281
|
+
- `[data-theme="dark"]` attribute
|
|
282
|
+
|
|
283
|
+
The values mirror the SwiftUI port (`Cocoar.Calendar.iOS`, `CalendarTheme.dark`), so web and iOS render identically dark. Accent and danger stay scheme-invariant, and event colors provided via `meta.color` are **not** remapped in either mode — dark-safe event palettes are the consumer's responsibility.
|
|
284
|
+
|
|
285
|
+
To customize, redefine any token under the same trigger after the package styles:
|
|
286
|
+
|
|
287
|
+
```css
|
|
288
|
+
.dark-mode,
|
|
289
|
+
[data-theme='dark'] {
|
|
290
|
+
--coar-calendar-bg: #101014;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
> **Tip: RTL**
|
|
295
|
+
>
|
|
296
|
+
> Layout-mirroring for `direction: rtl` is **not yet** wired (multi-day bars, resize handles, sticky-header positioning all assume LTR). If you need RTL support, open an issue — the math is mostly localised to the bar / handle inset calc()s, but it deserves a deliberate pass with proper test coverage rather than a one-shot patch.
|
|
297
|
+
|
|
298
|
+
## Performance notes
|
|
299
|
+
|
|
300
|
+
- **Variable-size virtualization.** The agenda surface uses a Fenwick-tree-backed measurement cache so every day-row keeps its natural height (header ~37 px, event ~42 px) without breaking anchor-restoration when the user scrolls.
|
|
301
|
+
- **No recycling pool.** Vue's keyed v-for diff turned out faster than a stable pool for typical slot content. Heavy custom renderers (charts, video) might still benefit; the surface accepts a custom recycling pool if needed.
|
|
302
|
+
- **Cluster-aware lane sizing.** Events with no transitive overlap render at full width even when busy parts of the same day have 3-deep stacks. Matches Google / Outlook behaviour.
|
|
303
|
+
- **LoAF, not rAF.** The CI perf gate measures Long Animation Frame entries; rAF FPS is unreliable under wheel-scroll on Chrome (input dispatch defers callbacks 1-2 vsync ticks without producing visual jank).
|
|
304
|
+
|
|
305
|
+
## Where to next
|
|
306
|
+
|
|
307
|
+
- **[`<CoarCalendar>` (composer)](./coar-calendar.md)** — the top-level shell + the full builder API reference.
|
|
308
|
+
- **[Year View](./year-view.md)** — responsive twelve-month overview and drill-in.
|
|
309
|
+
- **[Month Views](./month-view.md)** — continuous Compact / Stacked / Details months and responsive Month List.
|
|
310
|
+
- **[Day View](./day-view.md)** — one-day or width-aware multi-day time-grid surface.
|
|
311
|
+
- **[Week View](./week-view.md)** — 7-day time-grid + all-day band.
|
|
312
|
+
- **[Agenda View](./agenda-view.md)** — virtualized chronological list grouped by day.
|