@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,400 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/document-viewer/annotations.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Annotations
|
|
4
|
+
|
|
5
|
+
`CoarDocumentViewer` ships with a built-in annotation layer that supports four types — **marker** highlights, comment **notes**, free-hand **ink**, and **freetext** boxes. Annotations are **controlled**: the consumer owns the data, the viewer emits events, the consumer applies changes.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
CoarDocumentViewer,
|
|
10
|
+
type CoarPdfAnnotation,
|
|
11
|
+
type CoarPdfAnnotationMode,
|
|
12
|
+
type CoarPdfAnnotationCreatePayload,
|
|
13
|
+
type CoarPdfAnnotationUpdatePayload,
|
|
14
|
+
} from '@cocoar/vue-document-viewer';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Live demo
|
|
18
|
+
|
|
19
|
+
**Demo — `document-viewer/demos/AnnotationsDemo.vue`**
|
|
20
|
+
|
|
21
|
+
```vue
|
|
22
|
+
<template>
|
|
23
|
+
<ClientOnly>
|
|
24
|
+
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
25
|
+
<div style="display: flex; gap: 8px; flex-wrap: wrap; font-size: 13px;">
|
|
26
|
+
<span style="color: var(--vp-c-text-2);">Mode: <strong>{{ annotationMode }}</strong></span>
|
|
27
|
+
<span style="color: var(--vp-c-text-2);">Annotations: <strong>{{ annotations.length }}</strong></span>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
:disabled="annotations.length === 0"
|
|
31
|
+
style="font-size: 12px; padding: 2px 8px; border-radius: 4px; cursor: pointer;"
|
|
32
|
+
@click="annotations = []"
|
|
33
|
+
>Clear all</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div style="height: 520px; border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden;">
|
|
36
|
+
<component
|
|
37
|
+
:is="Viewer"
|
|
38
|
+
v-if="Viewer && source"
|
|
39
|
+
:source="source"
|
|
40
|
+
v-model:annotation-mode="annotationMode"
|
|
41
|
+
:annotations="annotations"
|
|
42
|
+
:show-annotations-panel="true"
|
|
43
|
+
@annotation:create="onCreate"
|
|
44
|
+
@annotation:update="onUpdate"
|
|
45
|
+
@annotation:delete="onDelete"
|
|
46
|
+
/>
|
|
47
|
+
<div v-else class="dv-loading">Loading viewer…</div>
|
|
48
|
+
</div>
|
|
49
|
+
<p style="font-size: 12px; color: var(--vp-c-text-2); margin: 0;">
|
|
50
|
+
Pick a drawing mode (marker / note / draw / text) in the toolbar, then
|
|
51
|
+
interact with the image. The consumer owns the annotations array; the
|
|
52
|
+
viewer emits create/update/delete and the parent assigns IDs.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</ClientOnly>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { computed, onMounted, ref, shallowRef, type Component } from 'vue';
|
|
60
|
+
import { SINGLE_IMAGE_URL } from './_shared';
|
|
61
|
+
|
|
62
|
+
// Loose runtime types — full types are imported at code-block level on the
|
|
63
|
+
// annotations.md page itself. SSR has to skip the package entirely (it pulls
|
|
64
|
+
// browser-only DOM APIs at module eval time).
|
|
65
|
+
type Anno = { id: string; type: string; pageIndex: number; color: string; createdAt: string; comment?: string } & Record<string, unknown>;
|
|
66
|
+
type CreatePayload = Omit<Anno, 'id' | 'createdAt'>;
|
|
67
|
+
type UpdatePayload = { id: string; patch: Partial<Anno> };
|
|
68
|
+
|
|
69
|
+
const Viewer = shallowRef<Component | null>(null);
|
|
70
|
+
const imageSource = shallowRef<((opts: { url: string }) => unknown) | null>(null);
|
|
71
|
+
|
|
72
|
+
const source = computed(() => imageSource.value?.({ url: SINGLE_IMAGE_URL }) ?? null);
|
|
73
|
+
const annotationMode = ref<'view' | 'select' | 'eraser' | 'marker' | 'comment' | 'ink' | 'freetext'>('view');
|
|
74
|
+
const annotations = ref<Anno[]>([]);
|
|
75
|
+
|
|
76
|
+
let nextId = 1;
|
|
77
|
+
function makeId() {
|
|
78
|
+
return `demo-${Date.now()}-${nextId++}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onCreate(payload: CreatePayload) {
|
|
82
|
+
annotations.value = [
|
|
83
|
+
...annotations.value,
|
|
84
|
+
{ ...payload, id: makeId(), createdAt: new Date().toISOString() } as Anno,
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onUpdate(payload: UpdatePayload) {
|
|
89
|
+
annotations.value = annotations.value.map((a) =>
|
|
90
|
+
a.id === payload.id ? ({ ...a, ...payload.patch } as Anno) : a,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function onDelete(id: string) {
|
|
95
|
+
annotations.value = annotations.value.filter((a) => a.id !== id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onMounted(async () => {
|
|
99
|
+
const mod = await import('@cocoar/vue-document-viewer');
|
|
100
|
+
await import('@cocoar/vue-document-viewer/styles');
|
|
101
|
+
imageSource.value = mod.imageSource;
|
|
102
|
+
Viewer.value = mod.CoarDocumentViewer;
|
|
103
|
+
});
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
.dv-loading {
|
|
108
|
+
height: 100%;
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
color: var(--coar-text-neutral-tertiary, #6b7280);
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Pick a mode in the toolbar (marker / note / draw / text), then interact with the image. The consumer assigns `id` + `createdAt` on creation, merges patches on update, and removes on delete. Try **switch to Select**, then drag an existing annotation, or click the 3-dot menu in the right panel.
|
|
119
|
+
|
|
120
|
+
## Annotation types
|
|
121
|
+
|
|
122
|
+
Four discriminated-union types, all sharing a `BaseAnnotation` shape:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
interface BaseAnnotation {
|
|
126
|
+
id: string;
|
|
127
|
+
type: 'marker' | 'comment' | 'ink' | 'freetext';
|
|
128
|
+
pageIndex: number; // 0-based
|
|
129
|
+
color: string; // CSS color
|
|
130
|
+
createdAt: string; // ISO timestamp
|
|
131
|
+
createdBy?: string; // Consumer-provided display string
|
|
132
|
+
comment?: string; // Every type may carry a side comment
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Marker (highlighter)
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
interface CoarPdfMarkerAnnotation extends BaseAnnotation {
|
|
140
|
+
type: 'marker';
|
|
141
|
+
strokes: CoarPdfPoint[][]; // SVG-style polyline list
|
|
142
|
+
width: number; // Stroke width in CSS px at zoom=1
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Drawn like a felt-tip marker, rendered with `mix-blend-mode: multiply` so the underlying text reads through.
|
|
147
|
+
|
|
148
|
+
### Comment pin
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface CoarPdfCommentAnnotation extends BaseAnnotation {
|
|
152
|
+
type: 'comment';
|
|
153
|
+
anchor: CoarPdfPoint; // Pin location in normalized page coords
|
|
154
|
+
comment: string; // REQUIRED for comment annotations
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Drops a pin at the click point and opens a popover for the comment body. The comment text is mandatory — empty pins are never created.
|
|
159
|
+
|
|
160
|
+
### Ink (freehand)
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
interface CoarPdfInkAnnotation extends BaseAnnotation {
|
|
164
|
+
type: 'ink';
|
|
165
|
+
strokes: CoarPdfPoint[][];
|
|
166
|
+
width: number;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Same wire shape as marker but rendered as opaque strokes (no blend mode, thinner default width).
|
|
171
|
+
|
|
172
|
+
### Freetext
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface CoarPdfFreetextAnnotation extends BaseAnnotation {
|
|
176
|
+
type: 'freetext';
|
|
177
|
+
rect: CoarPdfRect; // Box geometry in normalized page coords
|
|
178
|
+
text: string; // The visible label
|
|
179
|
+
fontSize: number; // CSS px at zoom=1
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
User clicks to drop a text box; the textarea writes to `text` (not `comment`). Optional `comment` still works as a side note.
|
|
184
|
+
|
|
185
|
+
## Coordinate system
|
|
186
|
+
|
|
187
|
+
All coordinates are **page-relative and normalized to `[0..1]`**:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
interface CoarPdfPoint { x: number; y: number; }
|
|
191
|
+
interface CoarPdfRect { x: number; y: number; w: number; h: number; }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This means the same annotation renders correctly at any zoom level and rotation — the viewer multiplies by the current viewport at render time. Storage stays portable: an annotation drawn at 100 % zoom plays back identically at 250 %, on a different screen, or after a rotation.
|
|
195
|
+
|
|
196
|
+
## Lifecycle
|
|
197
|
+
|
|
198
|
+
The viewer never mutates the `annotations` array directly. Instead it emits three events that the consumer applies:
|
|
199
|
+
|
|
200
|
+
### Create
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { v4 as uuid } from 'uuid';
|
|
204
|
+
|
|
205
|
+
function onCreate(payload: CoarPdfAnnotationCreatePayload) {
|
|
206
|
+
annotations.value = [
|
|
207
|
+
...annotations.value,
|
|
208
|
+
{
|
|
209
|
+
...payload,
|
|
210
|
+
id: uuid(),
|
|
211
|
+
createdAt: new Date().toISOString(),
|
|
212
|
+
createdBy: currentUser.displayName,
|
|
213
|
+
} as CoarPdfAnnotation,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`CoarPdfAnnotationCreatePayload` is `CoarPdfAnnotation` with the consumer-owned fields stripped (`'id' | 'createdAt' | 'createdBy'`). The viewer fills in everything else — type, color, geometry, stroke data — based on the active mode and pointer input.
|
|
219
|
+
|
|
220
|
+
### Update
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
function onUpdate({ id, patch }: CoarPdfAnnotationUpdatePayload) {
|
|
224
|
+
annotations.value = annotations.value.map((a) =>
|
|
225
|
+
a.id === id ? ({ ...a, ...patch } as CoarPdfAnnotation) : a,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Fired when the user edits an annotation through its popover (color, comment, freetext body) or drags it to a new position (anchor / rect / strokes). `patch` is a partial of the relevant annotation shape — apply with a plain spread.
|
|
231
|
+
|
|
232
|
+
### Delete
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
function onDelete(id: string) {
|
|
236
|
+
annotations.value = annotations.value.filter((a) => a.id !== id);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Fired when the user picks "Delete" from the panel's 3-dot menu, or when the eraser tool removes the last stroke from a marker / ink annotation.
|
|
241
|
+
|
|
242
|
+
## Modes
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
type CoarPdfAnnotationMode =
|
|
246
|
+
| 'view' // Read-only — existing annotations clickable, no new ones
|
|
247
|
+
| 'select' // Existing annotations clickable AND draggable
|
|
248
|
+
| 'eraser' // Click a stroke on marker/ink to remove it
|
|
249
|
+
| 'marker' // Drawing
|
|
250
|
+
| 'comment' // Drawing — drops a pin
|
|
251
|
+
| 'ink' // Drawing
|
|
252
|
+
| 'freetext'; // Drawing — drops a text box
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Mode is two-way bound — the toolbar's mode buttons update it, but the consumer can also drive it from outside (e.g. a keyboard shortcut, a parent's "Start commenting" CTA):
|
|
256
|
+
|
|
257
|
+
```vue
|
|
258
|
+
<CoarDocumentViewer
|
|
259
|
+
:source="src"
|
|
260
|
+
v-model:annotation-mode="mode"
|
|
261
|
+
:annotations="annotations"
|
|
262
|
+
/>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// Keyboard shortcut from anywhere
|
|
267
|
+
useEventListener(window, 'keydown', (e) => {
|
|
268
|
+
if (e.key === 'm' && (e.ctrlKey || e.metaKey)) mode.value = 'marker';
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Color palette
|
|
273
|
+
|
|
274
|
+
The mode picker shows a color selector with seven defaults:
|
|
275
|
+
|
|
276
|
+
| Color | Hex |
|
|
277
|
+
|---|---|
|
|
278
|
+
| Yellow | `#fde68a` |
|
|
279
|
+
| Pink | `#fca5a5` |
|
|
280
|
+
| Green | `#86efac` |
|
|
281
|
+
| Blue | `#93c5fd` |
|
|
282
|
+
| Purple | `#c4b5fd` |
|
|
283
|
+
| Saturated yellow | `#facc15` |
|
|
284
|
+
| Hot pink | `#ec4899` |
|
|
285
|
+
|
|
286
|
+
Pass `:annotation-colors="[...]"` to override the entire palette. Each color is a CSS string — anything `rgb()`, `hsl()`, `#xxx`, `oklch()` etc. that the browser accepts works.
|
|
287
|
+
|
|
288
|
+
## Panel + filters
|
|
289
|
+
|
|
290
|
+
`:show-annotations-panel="true"` opens the right rail with:
|
|
291
|
+
|
|
292
|
+
- **Info section** at the top (source metadata — see [Overview](./index.md)).
|
|
293
|
+
- **Annotation list** — every annotation, grouped by page or chronologically. Click an entry to select + scroll-to. 3-dot menu per entry: edit comment, change color, delete.
|
|
294
|
+
- **Filter chips** — toggle each type on/off (marker / note / ink / freetext).
|
|
295
|
+
- **Search input** — substring search over comments and freetext bodies.
|
|
296
|
+
- **Sort toggle** — By page / Chronological.
|
|
297
|
+
|
|
298
|
+
The list is read-only — every interaction goes through the same `annotation:update` / `annotation:delete` event pipeline as the in-page surface, so the consumer-owned state stays canonical.
|
|
299
|
+
|
|
300
|
+
## Worked example — collaborative review
|
|
301
|
+
|
|
302
|
+
```vue
|
|
303
|
+
<script setup lang="ts">
|
|
304
|
+
import { ref, computed } from 'vue';
|
|
305
|
+
import {
|
|
306
|
+
CoarDocumentViewer,
|
|
307
|
+
type CoarPdfAnnotation,
|
|
308
|
+
type CoarPdfAnnotationMode,
|
|
309
|
+
type CoarPdfAnnotationCreatePayload,
|
|
310
|
+
type CoarPdfAnnotationUpdatePayload,
|
|
311
|
+
} from '@cocoar/vue-document-viewer';
|
|
312
|
+
import { pdfSource } from '@cocoar/vue-document-viewer/pdf';
|
|
313
|
+
import { v4 as uuid } from 'uuid';
|
|
314
|
+
|
|
315
|
+
const props = defineProps<{
|
|
316
|
+
documentId: string;
|
|
317
|
+
currentUser: { id: string; displayName: string };
|
|
318
|
+
}>();
|
|
319
|
+
|
|
320
|
+
const annotations = ref<CoarPdfAnnotation[]>([]);
|
|
321
|
+
const mode = ref<CoarPdfAnnotationMode>('view');
|
|
322
|
+
const source = computed(() =>
|
|
323
|
+
pdfSource({ url: `/api/files/${props.documentId}/source.pdf`, withCredentials: true }),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Initial load
|
|
327
|
+
onMounted(async () => {
|
|
328
|
+
annotations.value = await api.listAnnotations(props.documentId);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
async function onCreate(payload: CoarPdfAnnotationCreatePayload) {
|
|
332
|
+
const next: CoarPdfAnnotation = {
|
|
333
|
+
...payload,
|
|
334
|
+
id: uuid(),
|
|
335
|
+
createdAt: new Date().toISOString(),
|
|
336
|
+
createdBy: props.currentUser.displayName,
|
|
337
|
+
} as CoarPdfAnnotation;
|
|
338
|
+
// Optimistic insert; rollback on failure
|
|
339
|
+
annotations.value = [...annotations.value, next];
|
|
340
|
+
try { await api.createAnnotation(props.documentId, next); }
|
|
341
|
+
catch (err) { annotations.value = annotations.value.filter(a => a.id !== next.id); throw err; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function onUpdate({ id, patch }: CoarPdfAnnotationUpdatePayload) {
|
|
345
|
+
const before = annotations.value;
|
|
346
|
+
annotations.value = annotations.value.map(a => a.id === id ? { ...a, ...patch } as CoarPdfAnnotation : a);
|
|
347
|
+
try { await api.updateAnnotation(props.documentId, id, patch); }
|
|
348
|
+
catch (err) { annotations.value = before; throw err; }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function onDelete(id: string) {
|
|
352
|
+
const before = annotations.value;
|
|
353
|
+
annotations.value = annotations.value.filter(a => a.id !== id);
|
|
354
|
+
try { await api.deleteAnnotation(props.documentId, id); }
|
|
355
|
+
catch (err) { annotations.value = before; throw err; }
|
|
356
|
+
}
|
|
357
|
+
</script>
|
|
358
|
+
|
|
359
|
+
<template>
|
|
360
|
+
<CoarDocumentViewer
|
|
361
|
+
:source="source"
|
|
362
|
+
v-model:annotation-mode="mode"
|
|
363
|
+
:annotations="annotations"
|
|
364
|
+
:show-annotations-panel="true"
|
|
365
|
+
storage-key="`doc-${documentId}`"
|
|
366
|
+
@annotation:create="onCreate"
|
|
367
|
+
@annotation:update="onUpdate"
|
|
368
|
+
@annotation:delete="onDelete"
|
|
369
|
+
/>
|
|
370
|
+
</template>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The controlled pattern makes optimistic updates + rollback trivial: snapshot `annotations.value`, apply locally, and revert on API failure.
|
|
374
|
+
|
|
375
|
+
## Persistence shape
|
|
376
|
+
|
|
377
|
+
Annotations are plain JSON — no `Date` objects, no class instances, no internal references. Store as-is:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// PostgreSQL JSONB column
|
|
381
|
+
CREATE TABLE annotations (
|
|
382
|
+
id UUID PRIMARY KEY,
|
|
383
|
+
document_id UUID NOT NULL REFERENCES documents,
|
|
384
|
+
data JSONB NOT NULL -- one CoarPdfAnnotation
|
|
385
|
+
);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
// MongoDB / Firestore — embed directly
|
|
390
|
+
{ documentId: "...", annotations: CoarPdfAnnotation[] }
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Because coordinates are normalized, annotations migrate across documents only when the page geometry matches; they're fully portable across rendering environments (different screens, zoom levels, rotation).
|
|
394
|
+
|
|
395
|
+
## Tips
|
|
396
|
+
|
|
397
|
+
- **Always wrap `annotations` mutations in a fresh array** (`[...]`, `.map`, `.filter`) so Vue's reactivity sees the change.
|
|
398
|
+
- **Generate the id on create**, not before — the create event fires only after the user finishes the drawing gesture (releases the mouse / taps "Save"). Pre-creating ids causes orphan entries when the gesture is cancelled.
|
|
399
|
+
- **Touch interactions work the same as mouse** — drawing modes accept both. The viewer normalizes pointer events internally.
|
|
400
|
+
- **Eraser is destructive** — clicking a stroke on a marker/ink annotation deletes that stroke; deleting the last stroke fires `annotation:delete` for the whole annotation. There's no per-stroke undo built in; consumers wanting one should keep a history snapshot ring around their `annotations` array.
|