@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,212 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/virtual-list.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Virtual List
|
|
4
|
+
|
|
5
|
+
`useVirtualList` is a framework-agnostic composable that returns the slice of rows currently inside the viewport (plus an overscan buffer). Use it to render very large lists without putting thousands of DOM nodes in the tree. It's the primitive behind [`CoarListbox`'s virtual mode](./listbox.md#virtual-scrolling), but is independently exported and usable in any Vue component that scrolls.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { useVirtualList } from '@cocoar/vue-ui';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Standalone example
|
|
12
|
+
|
|
13
|
+
The demo below is a plain `<div>` scroller — **no listbox involved**. 50,000 synthetic log lines are described by data, but only ~20 rows are ever in the DOM:
|
|
14
|
+
|
|
15
|
+
**Demo — `virtual-list/demos/StandaloneVirtualLog.vue`**
|
|
16
|
+
|
|
17
|
+
```vue
|
|
18
|
+
<template>
|
|
19
|
+
<div>
|
|
20
|
+
<div class="toolbar">
|
|
21
|
+
<button type="button" @click="scrollToMiddle">Scroll to 25,000</button>
|
|
22
|
+
<button type="button" @click="scrollToEnd">Scroll to end</button>
|
|
23
|
+
<span class="muted">Rendered: {{ virtualRows.length }} / {{ logs.length.toLocaleString() }} rows</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div ref="scrollRef" class="log">
|
|
26
|
+
<!-- Spacer provides the full virtual height so the browser's native scrollbar is accurate. -->
|
|
27
|
+
<div class="spacer" :style="{ height: totalSize + 'px' }">
|
|
28
|
+
<div
|
|
29
|
+
v-for="row in virtualRows"
|
|
30
|
+
:key="row.index"
|
|
31
|
+
class="line"
|
|
32
|
+
:class="`line--${logs[row.index].level}`"
|
|
33
|
+
:style="{ transform: `translateY(${row.start}px)`, height: row.size + 'px' }"
|
|
34
|
+
>
|
|
35
|
+
<span class="idx">{{ String(row.index + 1).padStart(5, '0') }}</span>
|
|
36
|
+
<span class="lvl">{{ logs[row.index].level.toUpperCase() }}</span>
|
|
37
|
+
<span class="msg">{{ logs[row.index].message }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script setup lang="ts">
|
|
45
|
+
import { useTemplateRef } from 'vue';
|
|
46
|
+
import { useVirtualList } from '@cocoar/vue-ui';
|
|
47
|
+
|
|
48
|
+
interface LogLine { level: 'info' | 'warn' | 'error'; message: string }
|
|
49
|
+
|
|
50
|
+
const LEVELS: LogLine['level'][] = ['info', 'info', 'info', 'info', 'warn', 'error'];
|
|
51
|
+
const MSG_POOL = [
|
|
52
|
+
'Served GET /api/principals in 14ms',
|
|
53
|
+
'Cache miss on token-introspect; re-fetching',
|
|
54
|
+
'Scheduled cleanup tick — no candidates',
|
|
55
|
+
'POST /teams/42/members (batch 12)',
|
|
56
|
+
'Upstream 502 — retry 2/3 queued',
|
|
57
|
+
'Rate limit 4/sec threshold reached',
|
|
58
|
+
'Invalidated cache segment users:42',
|
|
59
|
+
'WS heartbeat latency 83ms',
|
|
60
|
+
'Released connection back to pool (id=17)',
|
|
61
|
+
'User alice.mueller logged in from 10.0.3.22',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const logs: LogLine[] = Array.from({ length: 50_000 }, (_, i) => ({
|
|
65
|
+
level: LEVELS[i % LEVELS.length],
|
|
66
|
+
message: `${MSG_POOL[i % MSG_POOL.length]} — seq ${i}`,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const scrollRef = useTemplateRef<HTMLElement>('scrollRef');
|
|
70
|
+
|
|
71
|
+
const { virtualRows, totalSize, scrollToIndex } = useVirtualList({
|
|
72
|
+
count: logs.length,
|
|
73
|
+
itemSize: 22,
|
|
74
|
+
overscan: 8,
|
|
75
|
+
scrollElement: scrollRef,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function scrollToMiddle() { scrollToIndex(25000, 'start'); }
|
|
79
|
+
function scrollToEnd() { scrollToIndex(logs.length - 1, 'end'); }
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
.toolbar {
|
|
84
|
+
display: flex; align-items: center; gap: 10px;
|
|
85
|
+
margin-bottom: 8px; font-size: 12px;
|
|
86
|
+
}
|
|
87
|
+
.toolbar button {
|
|
88
|
+
border: 1px solid #d1d5db; background: white;
|
|
89
|
+
padding: 4px 10px; border-radius: 3px; cursor: pointer;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
}
|
|
92
|
+
.toolbar button:hover { background: #f3f4f6; }
|
|
93
|
+
.muted { color: #64748b; margin-left: auto; }
|
|
94
|
+
.log {
|
|
95
|
+
height: 300px;
|
|
96
|
+
overflow-y: auto;
|
|
97
|
+
border: 1px solid #d1d5db;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
background: #0f172a;
|
|
100
|
+
color: #cbd5e1;
|
|
101
|
+
font-family: 'Cascadia Code', Consolas, Monaco, monospace;
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
line-height: 22px;
|
|
104
|
+
}
|
|
105
|
+
.spacer { position: relative; }
|
|
106
|
+
.line {
|
|
107
|
+
position: absolute;
|
|
108
|
+
left: 0; right: 0;
|
|
109
|
+
display: flex; gap: 10px; padding: 0 10px;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.idx { color: #475569; flex-shrink: 0; }
|
|
114
|
+
.lvl { flex-shrink: 0; font-weight: 600; width: 48px; }
|
|
115
|
+
.line--info .lvl { color: #60a5fa; }
|
|
116
|
+
.line--warn .lvl { color: #fbbf24; }
|
|
117
|
+
.line--error .lvl { color: #f87171; }
|
|
118
|
+
.msg { overflow: hidden; text-overflow: ellipsis; }
|
|
119
|
+
</style>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## When to reach for it
|
|
123
|
+
|
|
124
|
+
- Pre-loaded catalogs of a few thousand+ items that would otherwise jank the browser
|
|
125
|
+
- Chat history, timelines, command logs
|
|
126
|
+
- Large custom tables you build yourself (not for `CoarDataGrid`, which handles this internally)
|
|
127
|
+
- Any component with a scroll container where DOM-node count is the bottleneck
|
|
128
|
+
|
|
129
|
+
If your data is backend-paginated and can be filtered server-side, virtual scrolling is rarely necessary — the DOM stays small on its own.
|
|
130
|
+
|
|
131
|
+
## Usage
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { useTemplateRef } from 'vue';
|
|
135
|
+
import { useVirtualList } from '@cocoar/vue-ui';
|
|
136
|
+
|
|
137
|
+
const scrollRef = useTemplateRef<HTMLElement>('scrollRef');
|
|
138
|
+
|
|
139
|
+
const { virtualRows, totalSize, scrollToIndex } = useVirtualList({
|
|
140
|
+
count: () => items.value.length,
|
|
141
|
+
itemSize: 32,
|
|
142
|
+
overscan: 5,
|
|
143
|
+
scrollElement: scrollRef,
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Template skeleton — a spacer establishes the full scroll height and each visible row is absolutely positioned at its offset:
|
|
148
|
+
|
|
149
|
+
```vue
|
|
150
|
+
<div ref="scrollRef" style="overflow: auto; height: 400px;">
|
|
151
|
+
<div :style="{ height: totalSize + 'px', position: 'relative' }">
|
|
152
|
+
<div
|
|
153
|
+
v-for="row in virtualRows"
|
|
154
|
+
:key="row.index"
|
|
155
|
+
:style="{
|
|
156
|
+
position: 'absolute', left: 0, right: 0,
|
|
157
|
+
transform: `translateY(${row.start}px)`,
|
|
158
|
+
height: row.size + 'px',
|
|
159
|
+
}"
|
|
160
|
+
>
|
|
161
|
+
{{ items[row.index] }}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Variable item heights
|
|
168
|
+
|
|
169
|
+
`itemSize` accepts either a fixed number or a per-index callback. Groups, headings, or mixed content get different heights while the cumulative-offset math stays O(log n) per scroll event:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
useVirtualList({
|
|
173
|
+
count: () => entries.value.length,
|
|
174
|
+
itemSize: (index) => entries.value[index].isHeading ? 28 : 44,
|
|
175
|
+
scrollElement: scrollRef,
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## API
|
|
180
|
+
|
|
181
|
+
### `UseVirtualListOptions`
|
|
182
|
+
|
|
183
|
+
| Option | Type | Default | Description |
|
|
184
|
+
|---|---|---|---|
|
|
185
|
+
| `count` | `MaybeRefOrGetter<number>` | — | Total item count. Reactive — changes trigger a recomputation of the offset table. |
|
|
186
|
+
| `itemSize` | `MaybeRefOrGetter<number \| (index) => number>` | — | Fixed pixel height, or a per-index function for variable heights. |
|
|
187
|
+
| `overscan` | `MaybeRefOrGetter<number>` | `5` | Extra rows rendered above/below the viewport as a scroll buffer. |
|
|
188
|
+
| `scrollElement` | `Ref<HTMLElement \| null>` | — | The scrolling container. Attach via `useTemplateRef`. |
|
|
189
|
+
|
|
190
|
+
### Return value
|
|
191
|
+
|
|
192
|
+
| Field | Type | Description |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `virtualRows` | `ComputedRef<VirtualRow[]>` | The rows currently in the viewport + overscan window. |
|
|
195
|
+
| `totalSize` | `ComputedRef<number>` | Sum of all item heights — bind this to your spacer's `height`. |
|
|
196
|
+
| `scrollToIndex` | `(index, align?) => void` | Programmatically scroll an index into view. `align`: `'auto'` (default), `'start'`, `'center'`, `'end'`. |
|
|
197
|
+
| `offsetFor` | `(index) => number` | Debug/test helper — returns the cumulative pixel offset for an index. |
|
|
198
|
+
|
|
199
|
+
### `VirtualRow`
|
|
200
|
+
|
|
201
|
+
| Field | Type | Description |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `index` | `number` | Position in the underlying list (0-based). |
|
|
204
|
+
| `start` | `number` | Pixel offset from the top of the spacer. Use `translateY(start)`. |
|
|
205
|
+
| `size` | `number` | Row height in pixels. |
|
|
206
|
+
|
|
207
|
+
## Behavior notes
|
|
208
|
+
|
|
209
|
+
- **Fixed viewport:** the composable tracks the container's `clientHeight` on every scroll event and — when available — via a `ResizeObserver`. In environments without `ResizeObserver`, it falls back to the `window.resize` event.
|
|
210
|
+
- **SSR-safe:** the `ResizeObserver` and DOM access are all guarded; the composable returns empty rows until the scroll element is mounted.
|
|
211
|
+
- **Binary-search offset table:** `count` and `itemSize` are reactive. Changing them rebuilds the cumulative offset array (`O(n)` once); scroll events then binary-search (`O(log n)`) for the visible window.
|
|
212
|
+
- **Dynamic measurement** (rendering items and measuring their actual heights) is *not* supported — pick an `itemSize` or function that matches your row heights. For the common "some rows are 32px, some are 48px" case, per-index function is enough.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/wizard.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Wizard (Preview)
|
|
4
|
+
|
|
5
|
+
A multi-step flow shell — built to live **inside a modal** (it renders no modal of its own). Three things make it a wizard rather than a plain stepper:
|
|
6
|
+
|
|
7
|
+
- **Animated body resize.** Only the active step is mounted, and the body smoothly animates its height between steps — so the surrounding modal grows / shrinks to fit each page.
|
|
8
|
+
- **A scrollable indicator that follows you.** The step indicator can be far wider (or taller) than the modal; it scrolls and auto-centers the active step on every move.
|
|
9
|
+
- **Edge-placeable indicator.** `indicatorPosition` puts the progress strip on any of the four edges.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { CoarWizard, type CoarWizardStep } from '@cocoar/vue-ui';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> **Warning: Preview**
|
|
16
|
+
>
|
|
17
|
+
> `CoarWizard` is new and on the `0.0.x` line. The API (props, slots, events) may still change before it stabilises — pin a version if you depend on it.
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
Pass an ordered `steps` array — each step's `id` is the name of its content slot, and only the active step is rendered. Bind the active step with `v-model:step`. Try the **Top / Right / Bottom / Left** toggle, and watch the modal resize as you move between the short and tall steps. Step 1 gates **Next** via `canAdvance` until a name is entered.
|
|
22
|
+
|
|
23
|
+
**Demo — `wizard/demos/WizardBasic.vue`**
|
|
24
|
+
|
|
25
|
+
```vue
|
|
26
|
+
<template>
|
|
27
|
+
<div class="wiz-demo">
|
|
28
|
+
<div class="wiz-demo__controls">
|
|
29
|
+
<span class="wiz-demo__label">Indicator:</span>
|
|
30
|
+
<CoarSegmentedControl v-model="pos" :options="positions" size="xs" />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- A bounded frame standing in for a routed modal. The wizard resizes it
|
|
34
|
+
per step (animated); the indicator scrolls inside its own width. -->
|
|
35
|
+
<div class="wiz-modal">
|
|
36
|
+
<div class="wiz-modal__header">Create OAuth API</div>
|
|
37
|
+
<div class="wiz-modal__body">
|
|
38
|
+
<CoarWizard
|
|
39
|
+
:steps="steps"
|
|
40
|
+
v-model:step="step"
|
|
41
|
+
:indicator-position="pos"
|
|
42
|
+
finish-label="Create"
|
|
43
|
+
@finish="created = true"
|
|
44
|
+
>
|
|
45
|
+
<template #basics>
|
|
46
|
+
<div class="wiz-page">
|
|
47
|
+
<p class="wiz-page__hint">Name your API client. Next is blocked until a name is entered.</p>
|
|
48
|
+
<CoarTextInput v-model="form.name" placeholder="My API client" />
|
|
49
|
+
<CoarTextInput v-model="form.desc" placeholder="Description (optional)" />
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<template #type>
|
|
54
|
+
<div class="wiz-page">
|
|
55
|
+
<p class="wiz-page__hint">What kind of client is this?</p>
|
|
56
|
+
<label v-for="t in types" :key="t.id" class="wiz-radio">
|
|
57
|
+
<input type="radio" :value="t.id" v-model="form.type" />
|
|
58
|
+
<span><strong>{{ t.label }}</strong><br /><small>{{ t.desc }}</small></span>
|
|
59
|
+
</label>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<template #redirects>
|
|
64
|
+
<div class="wiz-page">
|
|
65
|
+
<p class="wiz-page__hint">Allowed redirect URIs (this step is intentionally tall).</p>
|
|
66
|
+
<CoarTextInput
|
|
67
|
+
v-for="(_, i) in form.redirects"
|
|
68
|
+
:key="i"
|
|
69
|
+
v-model="form.redirects[i]"
|
|
70
|
+
placeholder="https://app.example.com/callback"
|
|
71
|
+
/>
|
|
72
|
+
<button class="wiz-add" type="button" @click="form.redirects.push('')">+ Add URI</button>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<template #scopes>
|
|
77
|
+
<div class="wiz-page">
|
|
78
|
+
<p class="wiz-page__hint">Pick the scopes this client may request.</p>
|
|
79
|
+
<label v-for="s in scopeList" :key="s" class="wiz-check">
|
|
80
|
+
<input type="checkbox" :value="s" v-model="form.scopes" /> {{ s }}
|
|
81
|
+
</label>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<template #review>
|
|
86
|
+
<div class="wiz-page">
|
|
87
|
+
<p class="wiz-page__hint">Review — a short step again, so the modal shrinks back.</p>
|
|
88
|
+
<ul class="wiz-review">
|
|
89
|
+
<li><b>Name:</b> {{ form.name || '—' }}</li>
|
|
90
|
+
<li><b>Type:</b> {{ form.type }}</li>
|
|
91
|
+
<li><b>Redirects:</b> {{ form.redirects.filter(Boolean).length }}</li>
|
|
92
|
+
<li><b>Scopes:</b> {{ form.scopes.length }}</li>
|
|
93
|
+
</ul>
|
|
94
|
+
<p v-if="created" class="wiz-done">✓ Created!</p>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
</CoarWizard>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<script setup lang="ts">
|
|
104
|
+
import { computed, reactive, ref } from 'vue';
|
|
105
|
+
import {
|
|
106
|
+
CoarWizard,
|
|
107
|
+
CoarSegmentedControl,
|
|
108
|
+
CoarTextInput,
|
|
109
|
+
type CoarWizardStep,
|
|
110
|
+
type WizardIndicatorPosition,
|
|
111
|
+
type CoarSegmentedControlOption,
|
|
112
|
+
} from '@cocoar/vue-ui';
|
|
113
|
+
|
|
114
|
+
const pos = ref<WizardIndicatorPosition>('top');
|
|
115
|
+
const positions: CoarSegmentedControlOption<WizardIndicatorPosition>[] = [
|
|
116
|
+
{ value: 'top', label: 'Top' },
|
|
117
|
+
{ value: 'right', label: 'Right' },
|
|
118
|
+
{ value: 'bottom', label: 'Bottom' },
|
|
119
|
+
{ value: 'left', label: 'Left' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const step = ref('basics');
|
|
123
|
+
const created = ref(false);
|
|
124
|
+
|
|
125
|
+
const form = reactive({
|
|
126
|
+
name: '',
|
|
127
|
+
desc: '',
|
|
128
|
+
type: 'spa',
|
|
129
|
+
redirects: ['', '', ''],
|
|
130
|
+
scopes: [] as string[],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const types = [
|
|
134
|
+
{ id: 'spa', label: 'Single-page app', desc: 'Public client, PKCE' },
|
|
135
|
+
{ id: 'web', label: 'Web server', desc: 'Confidential client with a secret' },
|
|
136
|
+
{ id: 'machine', label: 'Machine-to-machine', desc: 'Client credentials grant' },
|
|
137
|
+
];
|
|
138
|
+
const scopeList = ['openid', 'profile', 'email', 'offline_access', 'api:read', 'api:write', 'admin'];
|
|
139
|
+
|
|
140
|
+
// `canAdvance` on the first step gates Next until a name is typed.
|
|
141
|
+
const steps = computed<CoarWizardStep[]>(() => [
|
|
142
|
+
{ id: 'basics', label: 'Basics', description: 'Name', canAdvance: form.name.trim().length > 0 },
|
|
143
|
+
{ id: 'type', label: 'Client type' },
|
|
144
|
+
{ id: 'redirects', label: 'Redirect URIs', optional: true },
|
|
145
|
+
{ id: 'scopes', label: 'Scopes' },
|
|
146
|
+
{ id: 'review', label: 'Review' },
|
|
147
|
+
]);
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<style scoped>
|
|
151
|
+
.wiz-demo { display: flex; flex-direction: column; gap: 12px; }
|
|
152
|
+
.wiz-demo__controls { display: flex; align-items: center; gap: 8px; }
|
|
153
|
+
.wiz-demo__label { font-size: 13px; color: var(--coar-text-neutral-secondary); }
|
|
154
|
+
|
|
155
|
+
/* Modal stand-in: fixed width, content-driven height (so the resize shows). */
|
|
156
|
+
.wiz-modal {
|
|
157
|
+
width: 460px;
|
|
158
|
+
max-width: 100%;
|
|
159
|
+
border: 1px solid var(--coar-border-neutral);
|
|
160
|
+
border-radius: var(--coar-radius-xl);
|
|
161
|
+
background: var(--coar-background-neutral-primary);
|
|
162
|
+
box-shadow: var(--coar-shadow-m, 0 8px 24px rgba(0, 0, 0, 0.12));
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
}
|
|
165
|
+
.wiz-modal__header {
|
|
166
|
+
padding: 14px 18px;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
border-bottom: 1px solid var(--coar-border-neutral);
|
|
169
|
+
}
|
|
170
|
+
.wiz-modal__body { padding: 18px; }
|
|
171
|
+
|
|
172
|
+
.wiz-page { display: flex; flex-direction: column; gap: 10px; }
|
|
173
|
+
.wiz-page__hint { margin: 0; font-size: 13px; color: var(--coar-text-neutral-secondary); }
|
|
174
|
+
|
|
175
|
+
.wiz-radio, .wiz-check {
|
|
176
|
+
display: flex; gap: 8px; align-items: flex-start;
|
|
177
|
+
font-size: 14px; cursor: pointer;
|
|
178
|
+
}
|
|
179
|
+
.wiz-check { align-items: center; }
|
|
180
|
+
.wiz-add {
|
|
181
|
+
align-self: flex-start; border: none; background: transparent; cursor: pointer;
|
|
182
|
+
color: var(--coar-text-accent-primary); font: inherit; padding: 2px 0;
|
|
183
|
+
}
|
|
184
|
+
.wiz-review { margin: 0; padding-left: 1.1em; font-size: 14px; line-height: 1.7; }
|
|
185
|
+
.wiz-done { margin: 4px 0 0; color: var(--coar-text-semantic-success-bold, #16a34a); font-weight: 600; }
|
|
186
|
+
</style>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```vue
|
|
190
|
+
<template>
|
|
191
|
+
<CoarWizard :steps="steps" v-model:step="step" @finish="create">
|
|
192
|
+
<template #basics>…</template>
|
|
193
|
+
<template #scopes>…</template>
|
|
194
|
+
<template #review>…</template>
|
|
195
|
+
</CoarWizard>
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<script setup lang="ts">
|
|
199
|
+
import { ref } from 'vue';
|
|
200
|
+
import { CoarWizard, type CoarWizardStep } from '@cocoar/vue-ui';
|
|
201
|
+
|
|
202
|
+
const step = ref('basics');
|
|
203
|
+
const steps: CoarWizardStep[] = [
|
|
204
|
+
{ id: 'basics', label: 'Basics', canAdvance: true },
|
|
205
|
+
{ id: 'scopes', label: 'Scopes' },
|
|
206
|
+
{ id: 'review', label: 'Review' },
|
|
207
|
+
];
|
|
208
|
+
</script>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> **Tip: Designed for modals**
|
|
212
|
+
>
|
|
213
|
+
> The wizard renders no modal — drop it into your modal / routed-modal body. Give that modal a **content-driven height** (and a `max-height` for very tall steps) so the per-step resize animation is visible.
|
|
214
|
+
|
|
215
|
+
## Props
|
|
216
|
+
|
|
217
|
+
| Prop | Type | Default | Description |
|
|
218
|
+
|---|---|---|---|
|
|
219
|
+
| `steps` | `CoarWizardStep[]` | — | Ordered steps. Each `id` names that step's content slot. |
|
|
220
|
+
| `step` (`v-model:step`) | `string` | _first step_ | The active step id. Uncontrolled (defaults to the first step) when not bound. |
|
|
221
|
+
| `indicatorPosition` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'top'` | Which edge the indicator sits on. `top`/`bottom` scroll horizontally; `left`/`right` vertically. |
|
|
222
|
+
| `freeNavigation` | `boolean` | `false` | Allow jumping to any non-disabled step from the indicator (default: only completed steps are clickable). |
|
|
223
|
+
| `hideFooter` | `boolean` | `false` | Hide the built-in Back / Next / Finish footer. |
|
|
224
|
+
| `disableAnimation` | `boolean` | `false` | Skip the height + content transition (also auto-skipped under `prefers-reduced-motion`). |
|
|
225
|
+
| `backLabel` / `nextLabel` / `finishLabel` | `string` | `Back` / `Next` / `Finish` | Built-in button labels. |
|
|
226
|
+
|
|
227
|
+
### `CoarWizardStep`
|
|
228
|
+
|
|
229
|
+
| Field | Type | Description |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `id` | `string` | Stable id + content slot name. |
|
|
232
|
+
| `label` | `string` | Indicator label. |
|
|
233
|
+
| `description` | `string?` | Optional second line under the label. |
|
|
234
|
+
| `optional` | `boolean?` | Marks the step optional in the indicator. |
|
|
235
|
+
| `canAdvance` | `boolean?` | When `false`, the built-in Next is disabled on this step (per-step gate). Defaults to allowed. |
|
|
236
|
+
| `disabled` | `boolean?` | Prevents navigating to this step from the indicator. |
|
|
237
|
+
|
|
238
|
+
## Slots
|
|
239
|
+
|
|
240
|
+
| Slot | Props | Description |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `<step id>` | `{ step, index }` | Content for that step. Only the active step's slot is rendered. |
|
|
243
|
+
| `footer` | `{ next, back, goTo, isFirst, isLast, canAdvance, activeStep, activeIndex }` | Replace the built-in footer with your own controls. |
|
|
244
|
+
|
|
245
|
+
## Events
|
|
246
|
+
|
|
247
|
+
| Event | Payload | Description |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `update:step` | `string` | Active step id changed. |
|
|
250
|
+
| `step-change` | `(id, index)` | Active step changed. |
|
|
251
|
+
| `finish` | — | Next pressed on the last step. |
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/zoned-date-time-picker.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Zoned DateTime Picker
|
|
4
|
+
|
|
5
|
+
The full-featured datetime picker for timezone-aware values. It captures a date, time, and IANA timezone as a single `Temporal.ZonedDateTime`, making it easy to derive UTC instants for storage while preserving the user's original intent.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { CoarZonedDateTimePicker } from '@cocoar/vue-ui';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> **Info**
|
|
12
|
+
>
|
|
13
|
+
> **Store intent, derive math.** Persist the user's chosen local time and timezone. You can always recalculate the UTC instant later -- and if DST rules change in the future, the recalculation will still be correct.
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
The picker defaults to the user's system timezone. Select a date, adjust the time, and optionally change the timezone. The bound value carries all three pieces of information.
|
|
18
|
+
|
|
19
|
+
**Demo — `zoned-date-time-picker/demos/BasicZonedDateTimePicker.vue`**
|
|
20
|
+
|
|
21
|
+
```vue
|
|
22
|
+
<template>
|
|
23
|
+
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 360px;">
|
|
24
|
+
<CoarFormField label="Meeting Time">
|
|
25
|
+
<CoarZonedDateTimePicker
|
|
26
|
+
v-model="value"
|
|
27
|
+
placeholder="DD.MM.YYYY HH:mm"
|
|
28
|
+
/>
|
|
29
|
+
</CoarFormField>
|
|
30
|
+
<span style="font-size: 13px; color: #64748b;">
|
|
31
|
+
ZonedDateTime: {{ value?.toString() ?? 'none' }}
|
|
32
|
+
</span>
|
|
33
|
+
<span v-if="value" style="font-size: 13px; color: #64748b;">
|
|
34
|
+
UTC Instant: {{ value.toInstant().toString() }}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { ref } from 'vue';
|
|
41
|
+
import { CoarZonedDateTimePicker, CoarFormField } from '@cocoar/vue-ui';
|
|
42
|
+
import type { Temporal } from '@js-temporal/polyfill';
|
|
43
|
+
|
|
44
|
+
const value = ref<Temporal.ZonedDateTime | null>(null);
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Working with UTC
|
|
49
|
+
|
|
50
|
+
Converting to a UTC instant for API calls or database storage is a one-liner:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Get UTC instant from ZonedDateTime
|
|
54
|
+
const utcInstant = value.value?.toInstant().toString();
|
|
55
|
+
// → "2024-03-15T14:30:00Z"
|
|
56
|
+
|
|
57
|
+
// Get ISO string with offset
|
|
58
|
+
const isoString = value.value?.toString();
|
|
59
|
+
// → "2024-03-15T15:30:00+01:00[Europe/Berlin]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## States
|
|
63
|
+
|
|
64
|
+
All standard form states are supported: `required`, `error`, `disabled`, and `readonly`.
|
|
65
|
+
|
|
66
|
+
**Demo — `zoned-date-time-picker/demos/ZonedDateTimePickerStates.vue`**
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<template>
|
|
70
|
+
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 360px;">
|
|
71
|
+
<CoarFormField label="Required" required>
|
|
72
|
+
<CoarZonedDateTimePicker placeholder="DD.MM.YYYY HH:mm" :required="true" />
|
|
73
|
+
</CoarFormField>
|
|
74
|
+
<CoarFormField label="With Error" error="Invalid date/time">
|
|
75
|
+
<CoarZonedDateTimePicker placeholder="DD.MM.YYYY HH:mm" />
|
|
76
|
+
</CoarFormField>
|
|
77
|
+
<CoarFormField label="Disabled">
|
|
78
|
+
<CoarZonedDateTimePicker placeholder="DD.MM.YYYY HH:mm" :disabled="true" />
|
|
79
|
+
</CoarFormField>
|
|
80
|
+
<CoarFormField label="Readonly">
|
|
81
|
+
<CoarZonedDateTimePicker placeholder="DD.MM.YYYY HH:mm" :readonly="true" />
|
|
82
|
+
</CoarFormField>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<script setup lang="ts">
|
|
87
|
+
import { CoarZonedDateTimePicker, CoarFormField } from '@cocoar/vue-ui';
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Sizes
|
|
92
|
+
|
|
93
|
+
Four sizes that stay visually aligned with every other Cocoar input component.
|
|
94
|
+
|
|
95
|
+
**Demo — `zoned-date-time-picker/demos/ZonedDateTimePickerSizes.vue`**
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<template>
|
|
99
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
|
100
|
+
<CoarFormField label="Extra Small">
|
|
101
|
+
<CoarZonedDateTimePicker size="xs" placeholder="DD.MM.YYYY HH:mm" />
|
|
102
|
+
</CoarFormField>
|
|
103
|
+
<CoarFormField label="Small">
|
|
104
|
+
<CoarZonedDateTimePicker size="s" placeholder="DD.MM.YYYY HH:mm" />
|
|
105
|
+
</CoarFormField>
|
|
106
|
+
<CoarFormField label="Medium">
|
|
107
|
+
<CoarZonedDateTimePicker size="m" placeholder="DD.MM.YYYY HH:mm" />
|
|
108
|
+
</CoarFormField>
|
|
109
|
+
<CoarFormField label="Large">
|
|
110
|
+
<CoarZonedDateTimePicker size="l" placeholder="DD.MM.YYYY HH:mm" />
|
|
111
|
+
</CoarFormField>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<script setup lang="ts">
|
|
116
|
+
import { CoarZonedDateTimePicker, CoarFormField } from '@cocoar/vue-ui';
|
|
117
|
+
</script>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Accessibility
|
|
121
|
+
|
|
122
|
+
### Keyboard Navigation
|
|
123
|
+
|
|
124
|
+
| Key | Action |
|
|
125
|
+
|-----|--------|
|
|
126
|
+
| `Tab` | Move focus between date, time, and timezone inputs |
|
|
127
|
+
| `Enter` | Open calendar / confirm selection |
|
|
128
|
+
| `Escape` | Close calendar dropdown |
|
|
129
|
+
| `Arrow Keys` | Navigate within the calendar |
|
|
130
|
+
|
|
131
|
+
### Screen Reader Support
|
|
132
|
+
|
|
133
|
+
- Label text announces on focus
|
|
134
|
+
- Date, time, and timezone portions are independently accessible
|
|
135
|
+
- Selected timezone is announced
|
|
136
|
+
- Required and error states announced
|
|
137
|
+
|
|
138
|
+
## i18n Keys
|
|
139
|
+
|
|
140
|
+
These keys can be translated via [`@cocoar/vue-localization`](../foundations/localization/translations.md).
|
|
141
|
+
|
|
142
|
+
| Key | Default (English) | Used as |
|
|
143
|
+
|-----|-------------------|---------|
|
|
144
|
+
| `coar.ui.zonedDateTimePicker.dialog` | `'Date, time and timezone picker'` | Overlay dialog `aria-label` |
|
|
145
|
+
| `coar.ui.zonedDateTimePicker.clearValue` | `'Clear value'` | Clear button `aria-label` |
|
|
146
|
+
| `coar.ui.zonedDateTimePicker.openPicker` | `'Open date and time picker'` | Calendar button `aria-label` |
|
|
147
|
+
| `coar.ui.zonedDateTimePicker.timezoneIndicator` | `'Timezone: {tz}'` | Timezone indicator `aria-label` |
|
|
148
|
+
| `coar.ui.zonedDateTimePicker.clickToToggle` | `'Click to toggle.'` | Timezone indicator `aria-label` suffix |
|
|
149
|
+
| `coar.ui.zonedDateTimePicker.searchTimezone` | `'Search timezone...'` | Timezone search placeholder |
|
|
150
|
+
| `coar.ui.zonedDateTimePicker.closeTimezoneSearch` | `'Close timezone search'` | Close search button `aria-label` |
|
|
151
|
+
| `coar.ui.zonedDateTimePicker.displayTimezone` | `'Display Timezone'` | Display timezone section label |
|
|
152
|
+
| `coar.ui.zonedDateTimePicker.eventTimezone` | `'Event timezone'` | Footer placeholder text |
|
|
153
|
+
| `coar.ui.zonedDateTimePicker.cancelTimezoneEdit` | `'Cancel timezone edit'` | Cancel button `aria-label` |
|
|
154
|
+
| `coar.ui.zonedDateTimePicker.changeEventTimezone` | `'Change event timezone'` | Settings button `aria-label` |
|
|
155
|
+
| `coar.ui.datePicker.jumpToToday` | `'Jump to today\'s month'` | Scroll-to-today button `aria-label` |
|
|
156
|
+
| `coar.ui.datePicker.previousYear` | `'Previous year'` | Previous year button `aria-label` |
|
|
157
|
+
| `coar.ui.datePicker.nextYear` | `'Next year'` | Next year button `aria-label` |
|
|
158
|
+
| `coar.ui.datePicker.months` | `'Months'` | Month grid `aria-label` |
|
|
159
|
+
| `coar.ui.timePicker.*` | *(see DateTime Picker)* | Time picker spinbutton labels |
|
|
160
|
+
|
|
161
|
+
## API
|
|
162
|
+
|
|
163
|
+
### Props
|
|
164
|
+
|
|
165
|
+
| Prop | Type | Default | Description |
|
|
166
|
+
|------|------|---------|-------------|
|
|
167
|
+
| `v-model` | `Temporal.ZonedDateTime \| null` | `null` | Selected zoned datetime |
|
|
168
|
+
| `timezone` | `string` | user's timezone | IANA timezone ID (e.g. `'Europe/Berlin'`) |
|
|
169
|
+
| `placeholder` | `string` | `''` | Placeholder text |
|
|
170
|
+
| `size` | `'xs' \| 's' \| 'm' \| 'l'` | `'m'` | Input size |
|
|
171
|
+
| `disabled` | `boolean` | `false` | Disable the picker |
|
|
172
|
+
| `readonly` | `boolean` | `false` | Make read-only |
|
|
173
|
+
| `required` | `boolean` | `false` | Mark as required |
|
|
174
|
+
| `error` | `boolean` | `false` | Error state (red border + `aria-invalid`). Auto-injected from a wrapping [`CoarFormField`](./form-field.md). |
|
|
175
|
+
| `id` | `string` | `''` | Explicit input id (else taken from `CoarFormField`, else auto). |
|
|
176
|
+
|
|
177
|
+
> **Label, hint, error message and the status icon live on the wrapping [`CoarFormField`](./form-field.md)** — not on the picker. Wrap the picker in a `CoarFormField` for a label, the required asterisk, validation messages and the inline status indicator; the picker auto-adopts the field's id, error state and `aria-describedby` via injection.
|