@abraca/nuxt 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/module.d.mts +46 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +95 -2
  4. package/dist/runtime/assets/editor.css +1 -0
  5. package/dist/runtime/components/ACommandPalette.vue +4 -1
  6. package/dist/runtime/components/ADocRenderer.d.vue.ts +29 -0
  7. package/dist/runtime/components/ADocRenderer.vue +99 -0
  8. package/dist/runtime/components/ADocRenderer.vue.d.ts +29 -0
  9. package/dist/runtime/components/ADocTypeSelect.vue +4 -1
  10. package/dist/runtime/components/ADocumentTree.vue +78 -19
  11. package/dist/runtime/components/AEditor.d.vue.ts +9 -4
  12. package/dist/runtime/components/AEditor.vue +102 -7
  13. package/dist/runtime/components/AEditor.vue.d.ts +9 -4
  14. package/dist/runtime/components/AFloatingWindow.vue +1 -1
  15. package/dist/runtime/components/AIconPicker.vue +8 -2
  16. package/dist/runtime/components/ANodePanel.vue +100 -61
  17. package/dist/runtime/components/ANotifications.vue +35 -8
  18. package/dist/runtime/components/APermissionGuard.vue +3 -1
  19. package/dist/runtime/components/APresence.vue +14 -3
  20. package/dist/runtime/components/AProvider.vue +7 -1
  21. package/dist/runtime/components/AVoiceBar.vue +57 -15
  22. package/dist/runtime/components/AVoiceTile.vue +4 -1
  23. package/dist/runtime/components/AWindowLayer.vue +1 -1
  24. package/dist/runtime/components/aware/AArea.vue +1 -1
  25. package/dist/runtime/components/aware/AAvatar.vue +85 -16
  26. package/dist/runtime/components/aware/AButton.vue +5 -1
  27. package/dist/runtime/components/aware/ACursorLabel.vue +5 -1
  28. package/dist/runtime/components/aware/ADocBadge.vue +4 -1
  29. package/dist/runtime/components/aware/AFacepile.vue +13 -3
  30. package/dist/runtime/components/aware/AInput.vue +5 -1
  31. package/dist/runtime/components/aware/ATextarea.vue +5 -1
  32. package/dist/runtime/components/aware/AUserList.vue +8 -2
  33. package/dist/runtime/components/renderers/ACalendarRenderer.d.vue.ts +12 -1
  34. package/dist/runtime/components/renderers/ACalendarRenderer.vue +388 -114
  35. package/dist/runtime/components/renderers/ACalendarRenderer.vue.d.ts +12 -1
  36. package/dist/runtime/components/renderers/ACallRenderer.d.vue.ts +13 -0
  37. package/dist/runtime/components/renderers/ACallRenderer.vue +169 -0
  38. package/dist/runtime/components/renderers/ACallRenderer.vue.d.ts +13 -0
  39. package/dist/runtime/components/renderers/AChecklistRenderer.d.vue.ts +19 -0
  40. package/dist/runtime/components/renderers/AChecklistRenderer.vue +581 -0
  41. package/dist/runtime/components/renderers/AChecklistRenderer.vue.d.ts +19 -0
  42. package/dist/runtime/components/renderers/ADashboardRenderer.d.vue.ts +19 -0
  43. package/dist/runtime/components/renderers/ADashboardRenderer.vue +1372 -0
  44. package/dist/runtime/components/renderers/ADashboardRenderer.vue.d.ts +19 -0
  45. package/dist/runtime/components/renderers/AGalleryCoverImage.d.vue.ts +8 -0
  46. package/dist/runtime/components/renderers/AGalleryCoverImage.vue +60 -0
  47. package/dist/runtime/components/renderers/AGalleryCoverImage.vue.d.ts +8 -0
  48. package/dist/runtime/components/renderers/AGalleryRenderer.d.vue.ts +12 -1
  49. package/dist/runtime/components/renderers/AGalleryRenderer.vue +221 -55
  50. package/dist/runtime/components/renderers/AGalleryRenderer.vue.d.ts +12 -1
  51. package/dist/runtime/components/renderers/AGraphRenderer.d.vue.ts +19 -0
  52. package/dist/runtime/components/renderers/AGraphRenderer.vue +1027 -0
  53. package/dist/runtime/components/renderers/AGraphRenderer.vue.d.ts +19 -0
  54. package/dist/runtime/components/renderers/AKanbanRenderer.d.vue.ts +13 -1
  55. package/dist/runtime/components/renderers/AKanbanRenderer.vue +474 -140
  56. package/dist/runtime/components/renderers/AKanbanRenderer.vue.d.ts +13 -1
  57. package/dist/runtime/components/renderers/AMapRenderer.d.vue.ts +19 -0
  58. package/dist/runtime/components/renderers/AMapRenderer.vue +1622 -0
  59. package/dist/runtime/components/renderers/AMapRenderer.vue.d.ts +19 -0
  60. package/dist/runtime/components/renderers/AOutlineRenderer.d.vue.ts +12 -1
  61. package/dist/runtime/components/renderers/AOutlineRenderer.vue +294 -134
  62. package/dist/runtime/components/renderers/AOutlineRenderer.vue.d.ts +12 -1
  63. package/dist/runtime/components/renderers/ATableRenderer.d.vue.ts +12 -1
  64. package/dist/runtime/components/renderers/ATableRenderer.vue +437 -145
  65. package/dist/runtime/components/renderers/ATableRenderer.vue.d.ts +12 -1
  66. package/dist/runtime/components/renderers/ATimelineRenderer.d.vue.ts +19 -0
  67. package/dist/runtime/components/renderers/ATimelineRenderer.vue +446 -0
  68. package/dist/runtime/components/renderers/ATimelineRenderer.vue.d.ts +19 -0
  69. package/dist/runtime/composables/useAwareness.js +5 -0
  70. package/dist/runtime/composables/useBroadcastSync.d.ts +18 -0
  71. package/dist/runtime/composables/useBroadcastSync.js +26 -0
  72. package/dist/runtime/composables/useChat.js +4 -2
  73. package/dist/runtime/composables/useChatUsers.js +2 -1
  74. package/dist/runtime/composables/useCommandPalette.js +62 -3
  75. package/dist/runtime/composables/useConnectionStatus.js +7 -0
  76. package/dist/runtime/composables/useDevicePairing.d.ts +58 -0
  77. package/dist/runtime/composables/useDevicePairing.js +108 -0
  78. package/dist/runtime/composables/useDocExport.d.ts +5 -0
  79. package/dist/runtime/composables/useDocExport.js +2 -2
  80. package/dist/runtime/composables/useDocImport.js +4 -3
  81. package/dist/runtime/composables/useDocSeo.d.ts +20 -0
  82. package/dist/runtime/composables/useDocSeo.js +44 -0
  83. package/dist/runtime/composables/useDocSlugs.d.ts +7 -0
  84. package/dist/runtime/composables/useDocSlugs.js +20 -0
  85. package/dist/runtime/composables/useDocTree.d.ts +34 -0
  86. package/dist/runtime/composables/useDocTree.js +35 -0
  87. package/dist/runtime/composables/useEditorDragHandle.js +2 -1
  88. package/dist/runtime/composables/useEditorMentions.js +4 -2
  89. package/dist/runtime/composables/useEditorSuggestions.d.ts +1 -0
  90. package/dist/runtime/composables/useEditorSuggestions.js +9 -2
  91. package/dist/runtime/composables/useEditorToolbar.js +2 -1
  92. package/dist/runtime/composables/useFileIndex.js +2 -1
  93. package/dist/runtime/composables/useFileTransfer.d.ts +112 -0
  94. package/dist/runtime/composables/useFileTransfer.js +171 -0
  95. package/dist/runtime/composables/useFollowUser.js +2 -1
  96. package/dist/runtime/composables/useInvites.d.ts +56 -0
  97. package/dist/runtime/composables/useInvites.js +77 -0
  98. package/dist/runtime/composables/useNodePanel.d.ts +14 -0
  99. package/dist/runtime/composables/useNodePanel.js +52 -0
  100. package/dist/runtime/composables/useNotifications.js +4 -2
  101. package/dist/runtime/composables/usePasskeyAccounts.js +4 -2
  102. package/dist/runtime/composables/useSearchIndex.d.ts +1 -0
  103. package/dist/runtime/composables/useSearchIndex.js +13 -5
  104. package/dist/runtime/composables/useServerInfo.d.ts +31 -0
  105. package/dist/runtime/composables/useServerInfo.js +80 -0
  106. package/dist/runtime/composables/useSlugRoute.d.ts +6 -0
  107. package/dist/runtime/composables/useSlugRoute.js +19 -0
  108. package/dist/runtime/composables/useSpaces.d.ts +37 -0
  109. package/dist/runtime/composables/useSpaces.js +83 -0
  110. package/dist/runtime/composables/useTouchDrag.d.ts +34 -0
  111. package/dist/runtime/composables/useTouchDrag.js +191 -0
  112. package/dist/runtime/composables/useTrash.d.ts +1 -1
  113. package/dist/runtime/composables/useTrash.js +6 -3
  114. package/dist/runtime/composables/useWebRTC.d.ts +50 -0
  115. package/dist/runtime/composables/useWebRTC.js +177 -0
  116. package/dist/runtime/extensions/meta-field.d.ts +4 -1
  117. package/dist/runtime/extensions/steps.js +1 -1
  118. package/dist/runtime/extensions/views/AccordionItemView.vue +13 -3
  119. package/dist/runtime/extensions/views/AccordionView.vue +4 -1
  120. package/dist/runtime/extensions/views/BadgeView.vue +11 -2
  121. package/dist/runtime/extensions/views/CalloutView.vue +4 -1
  122. package/dist/runtime/extensions/views/CardGroupView.vue +4 -1
  123. package/dist/runtime/extensions/views/CardView.vue +17 -3
  124. package/dist/runtime/extensions/views/CodeGroupView.vue +4 -1
  125. package/dist/runtime/extensions/views/CollapsibleView.vue +8 -2
  126. package/dist/runtime/extensions/views/FileNodeView.vue +32 -8
  127. package/dist/runtime/extensions/views/KbdView.vue +8 -2
  128. package/dist/runtime/extensions/views/MetaFieldView.vue +208 -46
  129. package/dist/runtime/extensions/views/ProseIconView.vue +8 -2
  130. package/dist/runtime/extensions/views/TabsView.vue +17 -4
  131. package/dist/runtime/locale.d.ts +71 -0
  132. package/dist/runtime/locale.js +71 -0
  133. package/dist/runtime/plugin-abracadabra.client.js +29 -3
  134. package/dist/runtime/plugin-abracadabra.server.js +2 -0
  135. package/dist/runtime/server/api/_abracadabra/render/[docId].get.d.ts +1 -1
  136. package/dist/runtime/server/api/_abracadabra/render/[docId].get.js +29 -4
  137. package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.d.ts +2 -0
  138. package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.js +43 -0
  139. package/dist/runtime/server/api/_abracadabra/slugs.get.d.ts +2 -0
  140. package/dist/runtime/server/api/_abracadabra/slugs.get.js +7 -0
  141. package/dist/runtime/server/plugins/abracadabra-service.js +10 -5
  142. package/dist/runtime/server/runners/doc-tree-cache.js +4 -0
  143. package/dist/runtime/server/utils/slugMap.d.ts +32 -0
  144. package/dist/runtime/server/utils/slugMap.js +58 -0
  145. package/dist/runtime/types.d.ts +1 -0
  146. package/dist/runtime/utils/docTypes.d.ts +29 -1
  147. package/dist/runtime/utils/docTypes.js +129 -1
  148. package/dist/runtime/utils/markdownToYjs.js +2 -2
  149. package/dist/runtime/utils/sdkRef.d.ts +2 -0
  150. package/dist/runtime/utils/sdkRef.js +7 -0
  151. package/dist/runtime/utils/slugify.d.ts +40 -0
  152. package/dist/runtime/utils/slugify.js +36 -0
  153. package/dist/types.d.mts +6 -0
  154. package/package.json +32 -19
@@ -0,0 +1,446 @@
1
+ <script setup>
2
+ import { ref, computed, watch, onBeforeUnmount } from "vue";
3
+ import { useRendererBase } from "../../composables/useRendererBase";
4
+ import { useTouchDrag } from "../../composables/useTouchDrag";
5
+ import { useNodePanel } from "../../composables/useNodePanel";
6
+ import { DEFAULT_LOCALE } from "../../locale";
7
+ const props = defineProps({
8
+ docId: { type: String, required: true },
9
+ childProvider: { type: null, required: true },
10
+ docLabel: { type: String, required: true },
11
+ pageTypes: { type: Array, required: false },
12
+ labels: { type: Object, required: false },
13
+ editable: { type: Boolean, required: false, default: true }
14
+ });
15
+ const config = useRuntimeConfig();
16
+ const locale = computed(() => ({
17
+ ...DEFAULT_LOCALE.renderers.timeline,
18
+ ...config.public?.abracadabra?.locale?.renderers?.timeline ?? {},
19
+ ...props.labels ?? {}
20
+ }));
21
+ const { tree, childDoc, childProviderRef, states, setLocalState, connectedUsers } = useRendererBase(props);
22
+ const {
23
+ openNodeId,
24
+ openNodeLabel,
25
+ openNodeProvider,
26
+ openNode,
27
+ closePanel
28
+ } = useNodePanel(childProviderRef);
29
+ const myClientId = computed(() => props.childProvider?.awareness?.clientID ?? 0);
30
+ const zoomLevel = ref("month");
31
+ const ZOOM_CONFIG = {
32
+ week: { totalDays: 14, pixelsPerDay: 40, stepDays: 1 },
33
+ month: { totalDays: 60, pixelsPerDay: 14, stepDays: 7 },
34
+ quarter: { totalDays: 180, pixelsPerDay: 4, stepDays: 30 }
35
+ };
36
+ const cfg = computed(() => ZOOM_CONFIG[zoomLevel.value]);
37
+ const epics = computed(() => tree.childrenOf(null));
38
+ function addEpic() {
39
+ if (!props.editable) return;
40
+ const id = tree.createChild(null, locale.value.untitled);
41
+ const now = /* @__PURE__ */ new Date();
42
+ const end = new Date(now);
43
+ end.setDate(end.getDate() + 14);
44
+ tree.updateMeta(id, {
45
+ dateStart: now.toISOString().slice(0, 10),
46
+ dateEnd: end.toISOString().slice(0, 10),
47
+ taskProgress: 0,
48
+ color: "#6366f1"
49
+ });
50
+ }
51
+ function addTask(epicId) {
52
+ if (!props.editable) return;
53
+ const id = tree.createChild(epicId, locale.value.untitled);
54
+ const now = /* @__PURE__ */ new Date();
55
+ const end = new Date(now);
56
+ end.setDate(end.getDate() + 7);
57
+ tree.updateMeta(id, {
58
+ dateStart: now.toISOString().slice(0, 10),
59
+ dateEnd: end.toISOString().slice(0, 10),
60
+ taskProgress: 0,
61
+ color: "#818cf8"
62
+ });
63
+ }
64
+ const startOfAxis = computed(() => {
65
+ const d = /* @__PURE__ */ new Date();
66
+ d.setDate(1);
67
+ d.setMonth(d.getMonth() - 1);
68
+ return d;
69
+ });
70
+ const axisWidth = computed(() => cfg.value.totalDays * cfg.value.pixelsPerDay);
71
+ function dateToOffset(dateStr) {
72
+ const d = new Date(dateStr);
73
+ const diff = Math.floor(
74
+ (d.getTime() - startOfAxis.value.getTime()) / (1e3 * 60 * 60 * 24)
75
+ );
76
+ return diff * cfg.value.pixelsPerDay;
77
+ }
78
+ function spanWidth(start, end) {
79
+ const s = new Date(start);
80
+ const e = new Date(end);
81
+ const days = Math.max(
82
+ 1,
83
+ Math.ceil((e.getTime() - s.getTime()) / (1e3 * 60 * 60 * 24))
84
+ );
85
+ return days * cfg.value.pixelsPerDay;
86
+ }
87
+ const axisLabels = computed(() => {
88
+ const labels = [];
89
+ for (let i = 0; i < cfg.value.totalDays; i += cfg.value.stepDays) {
90
+ const d = new Date(startOfAxis.value);
91
+ d.setDate(d.getDate() + i);
92
+ const fmt = zoomLevel.value === "week" ? { weekday: "short", day: "numeric" } : { month: "short", day: "numeric" };
93
+ labels.push({
94
+ label: d.toLocaleDateString("en-US", fmt),
95
+ offset: i * cfg.value.pixelsPerDay
96
+ });
97
+ }
98
+ return labels;
99
+ });
100
+ const todayOffset = computed(() => dateToOffset((/* @__PURE__ */ new Date()).toISOString().slice(0, 10)));
101
+ function orderBetween(list, targetIdx) {
102
+ const prev = list[targetIdx - 1];
103
+ const next = list[targetIdx];
104
+ if (!prev && !next) return Date.now();
105
+ if (!prev) return next.order - 1e3;
106
+ if (!next) return prev.order + 1e3;
107
+ return (prev.order + next.order) / 2;
108
+ }
109
+ const {
110
+ dragId: epicDragId,
111
+ dragOverId: epicDragOver,
112
+ handlePointerDown: handleEpicPointerDown
113
+ } = useTouchDrag({
114
+ onDrop: (srcId, targetId) => {
115
+ if (!props.editable) return;
116
+ const targetIdx = epics.value.findIndex((ep) => ep.id === targetId);
117
+ const newOrder = orderBetween(epics.value, targetIdx);
118
+ tree.moveEntry(srcId, null, newOrder);
119
+ }
120
+ });
121
+ function taskFocusers(taskId) {
122
+ return states.value.filter(
123
+ (s) => s.clientId !== myClientId.value && s["timeline:focused"] === taskId
124
+ );
125
+ }
126
+ function focusTask(id) {
127
+ setLocalState({ "timeline:focused": id });
128
+ }
129
+ function clearFocus() {
130
+ setLocalState({ "timeline:focused": null });
131
+ }
132
+ const barDragId = ref(null);
133
+ const barDragStart = ref({ clientX: 0, origStart: "", origEnd: "" });
134
+ const barDidMove = ref(false);
135
+ const barHoverEpicId = ref(null);
136
+ let lastDaysShifted = 0;
137
+ function onBarPointerDown(e, id) {
138
+ if (!props.editable) return;
139
+ if (e.button !== 0) return;
140
+ e.stopPropagation();
141
+ const entry = tree.entries.value.find((en) => en.id === id);
142
+ const meta = entry?.meta;
143
+ if (!meta?.dateStart || !meta?.dateEnd) return;
144
+ barDragId.value = id;
145
+ barDidMove.value = false;
146
+ barHoverEpicId.value = null;
147
+ lastDaysShifted = 0;
148
+ barDragStart.value = {
149
+ clientX: e.clientX,
150
+ origStart: meta.dateStart,
151
+ origEnd: meta.dateEnd
152
+ };
153
+ e.currentTarget.setPointerCapture(e.pointerId);
154
+ }
155
+ function onBarPointerMove(e, id) {
156
+ if (barDragId.value !== id) return;
157
+ const entry = tree.entries.value.find((en) => en.id === id);
158
+ if (!entry?.meta?.dateStart) return;
159
+ const dx = e.clientX - barDragStart.value.clientX;
160
+ if (Math.abs(dx) > 4) barDidMove.value = true;
161
+ const els = document.elementsFromPoint(e.clientX, e.clientY);
162
+ const epicEl = els.find((el) => el.dataset?.epicId);
163
+ barHoverEpicId.value = epicEl?.dataset.epicId ?? null;
164
+ const daysShifted = Math.round(dx / cfg.value.pixelsPerDay);
165
+ if (daysShifted === lastDaysShifted) return;
166
+ lastDaysShifted = daysShifted;
167
+ const MS = 24 * 60 * 60 * 1e3;
168
+ const s0 = new Date(barDragStart.value.origStart).getTime();
169
+ const e0 = new Date(barDragStart.value.origEnd).getTime();
170
+ tree.updateMeta(id, {
171
+ dateStart: new Date(s0 + daysShifted * MS).toISOString().slice(0, 10),
172
+ dateEnd: new Date(e0 + daysShifted * MS).toISOString().slice(0, 10)
173
+ });
174
+ }
175
+ function onBarPointerUp(id, epicId) {
176
+ if (barDragId.value !== id) return;
177
+ barDragId.value = null;
178
+ if (epicId && barHoverEpicId.value && barHoverEpicId.value !== epicId) {
179
+ tree.moveEntry(id, barHoverEpicId.value, Date.now());
180
+ }
181
+ barHoverEpicId.value = null;
182
+ }
183
+ function onBarClick(id, label) {
184
+ if (barDidMove.value) return;
185
+ openNode(id, label);
186
+ }
187
+ watch(childDoc, (doc) => {
188
+ if (!doc) return;
189
+ const legacyMap = doc.getMap("task-dates");
190
+ if (legacyMap.size === 0) return;
191
+ legacyMap.forEach((val, id) => {
192
+ if (!tree.treeMap.get(id)?.meta?.dateStart) {
193
+ tree.updateMeta(id, {
194
+ dateStart: val.start,
195
+ dateEnd: val.end,
196
+ taskProgress: val.progress ?? 0,
197
+ color: val.color
198
+ });
199
+ }
200
+ });
201
+ doc.transact(() => {
202
+ for (const key of [...legacyMap.keys()]) legacyMap.delete(key);
203
+ });
204
+ }, { immediate: true });
205
+ const containerRef = ref(null);
206
+ function onPointerMove(e) {
207
+ const rect = containerRef.value?.getBoundingClientRect();
208
+ if (!rect) return;
209
+ setLocalState({
210
+ pos: {
211
+ x: (e.clientX - rect.left) / rect.width * 100,
212
+ y: (e.clientY - rect.top) / rect.height * 100
213
+ }
214
+ });
215
+ }
216
+ function clearCursor() {
217
+ setLocalState({ pos: null });
218
+ }
219
+ onBeforeUnmount(() => {
220
+ setLocalState({ "pos": null, "timeline:focused": null });
221
+ });
222
+ defineExpose({ connectedUsers });
223
+ </script>
224
+
225
+ <template>
226
+ <div
227
+ ref="containerRef"
228
+ class="flex-1 min-h-0 flex flex-col relative"
229
+ @pointermove="onPointerMove"
230
+ @pointerleave="clearCursor"
231
+ >
232
+ <!-- Toolbar -->
233
+ <div class="flex items-center justify-between px-4 py-2 border-b border-(--ui-border) shrink-0">
234
+ <div class="flex gap-1">
235
+ <UButton
236
+ v-for="z in ['week', 'month', 'quarter']"
237
+ :key="z"
238
+ :label="locale[z]"
239
+ size="xs"
240
+ :variant="zoomLevel === z ? 'solid' : 'ghost'"
241
+ color="neutral"
242
+ @click="zoomLevel = z"
243
+ />
244
+ </div>
245
+ <UButton
246
+ v-if="editable"
247
+ icon="i-lucide-plus"
248
+ size="xs"
249
+ variant="ghost"
250
+ color="neutral"
251
+ :label="locale.addEpic"
252
+ @click="addEpic"
253
+ />
254
+ </div>
255
+
256
+ <!-- Content -->
257
+ <div class="flex-1 overflow-auto">
258
+ <!-- Empty state -->
259
+ <div
260
+ v-if="epics.length === 0"
261
+ class="flex flex-col items-center justify-center h-full gap-3 text-center"
262
+ >
263
+ <UIcon
264
+ name="i-lucide-gantt-chart"
265
+ class="size-10 text-(--ui-text-dimmed)"
266
+ />
267
+ <p class="text-sm text-(--ui-text-muted)">
268
+ {{ locale.empty }}
269
+ </p>
270
+ <UButton
271
+ v-if="editable"
272
+ icon="i-lucide-plus"
273
+ :label="locale.addEpic"
274
+ size="sm"
275
+ @click="addEpic"
276
+ />
277
+ </div>
278
+
279
+ <div
280
+ v-else
281
+ class="flex h-full"
282
+ >
283
+ <!-- Label panel -->
284
+ <div class="w-48 flex-shrink-0 border-r border-(--ui-border) sticky left-0 bg-(--ui-bg) z-10">
285
+ <div class="h-8 border-b border-(--ui-border)" />
286
+ <TransitionGroup
287
+ name="tepic"
288
+ tag="div"
289
+ >
290
+ <div
291
+ v-for="epic in epics"
292
+ :key="epic.id"
293
+ >
294
+ <button
295
+ :data-drag-id="epic.id"
296
+ class="h-9 w-full flex items-center px-3 text-sm font-medium border-b border-(--ui-border) cursor-grab hover:bg-(--ui-bg-elevated) text-left transition-colors touch-none"
297
+ :class="{
298
+ 'border-t-2 border-(--ui-primary)': epicDragOver === epic.id,
299
+ 'opacity-40': epicDragId === epic.id
300
+ }"
301
+ @pointerdown="editable && handleEpicPointerDown($event, epic.id)"
302
+ @click="openNode(epic.id, epic.label)"
303
+ >
304
+ <UIcon
305
+ name="i-lucide-chevron-right"
306
+ class="size-3 mr-1.5 text-(--ui-text-muted) shrink-0"
307
+ />
308
+ <span class="truncate">{{ epic.label }}</span>
309
+ </button>
310
+
311
+ <button
312
+ v-for="task in tree.childrenOf(epic.id)"
313
+ :key="task.id"
314
+ class="h-8 w-full flex items-center px-6 text-xs border-b border-(--ui-border) hover:bg-(--ui-bg-elevated) text-left"
315
+ @click="openNode(task.id, task.label)"
316
+ >
317
+ <span class="truncate text-(--ui-text-dimmed)">
318
+ {{ task.label }}
319
+ </span>
320
+ </button>
321
+
322
+ <button
323
+ v-if="editable"
324
+ class="h-7 w-full flex items-center px-6 text-xs text-(--ui-text-dimmed) hover:text-(--ui-text) border-b border-(--ui-border) text-left"
325
+ @click="addTask(epic.id)"
326
+ >
327
+ + {{ locale.addTask.toLowerCase() }}
328
+ </button>
329
+ </div>
330
+ </TransitionGroup>
331
+ </div>
332
+
333
+ <!-- Timeline panel -->
334
+ <div class="flex-1 overflow-x-auto">
335
+ <div :style="{ width: `${axisWidth}px`, minWidth: '100%', position: 'relative' }">
336
+ <!-- Axis labels -->
337
+ <div class="h-8 relative border-b border-(--ui-border) sticky top-0 bg-(--ui-bg) z-10">
338
+ <div
339
+ v-for="lbl in axisLabels"
340
+ :key="lbl.offset"
341
+ class="absolute top-1.5 text-[10px] text-(--ui-text-muted) whitespace-nowrap px-0.5"
342
+ :style="{ left: `${lbl.offset}px` }"
343
+ >
344
+ {{ lbl.label }}
345
+ </div>
346
+ <!-- Today line -->
347
+ <div
348
+ class="absolute top-0 bottom-0 w-px bg-(--ui-primary) opacity-60"
349
+ :style="{ left: `${todayOffset}px` }"
350
+ />
351
+ </div>
352
+
353
+ <!-- Bars -->
354
+ <TransitionGroup
355
+ name="tepic"
356
+ tag="div"
357
+ >
358
+ <div
359
+ v-for="epic in epics"
360
+ :key="epic.id"
361
+ :data-epic-id="epic.id"
362
+ :class="barHoverEpicId === epic.id && barDragId ? 'bg-(--ui-primary)/5' : ''"
363
+ >
364
+ <!-- Epic bar row -->
365
+ <div class="h-9 relative border-b border-(--ui-border)">
366
+ <div
367
+ v-if="epic.meta?.dateStart && epic.meta?.dateEnd"
368
+ class="absolute top-2 h-5 rounded text-white text-[10px] flex items-center px-2 font-medium select-none transition-opacity"
369
+ :class="barDragId === epic.id ? 'opacity-60 cursor-grabbing' : 'cursor-grab hover:opacity-80'"
370
+ :style="{
371
+ left: `${dateToOffset(epic.meta.dateStart)}px`,
372
+ width: `${spanWidth(epic.meta.dateStart, epic.meta.dateEnd)}px`,
373
+ background: epic.meta.color ?? '#6366f1',
374
+ maxWidth: '100%',
375
+ ...taskFocusers(epic.id).length ? { outline: `2px solid ${taskFocusers(epic.id)[0].user?.color}`, outlineOffset: '1px' } : {}
376
+ }"
377
+ @pointerdown="onBarPointerDown($event, epic.id)"
378
+ @pointermove="onBarPointerMove($event, epic.id)"
379
+ @pointerup="onBarPointerUp(epic.id)"
380
+ @pointerenter="focusTask(epic.id)"
381
+ @pointerleave="clearFocus"
382
+ @click.stop="onBarClick(epic.id, epic.label)"
383
+ >
384
+ <span class="truncate">{{ epic.label }}</span>
385
+ </div>
386
+ </div>
387
+
388
+ <!-- Task bar rows -->
389
+ <div
390
+ v-for="task in tree.childrenOf(epic.id)"
391
+ :key="task.id"
392
+ class="h-8 relative border-b border-(--ui-border)"
393
+ >
394
+ <div
395
+ v-if="task.meta?.dateStart && task.meta?.dateEnd"
396
+ class="absolute top-1.5 h-5 rounded text-white text-[10px] flex items-center px-2 select-none opacity-85 transition-opacity"
397
+ :class="barDragId === task.id ? 'opacity-60 cursor-grabbing' : 'cursor-grab hover:opacity-80'"
398
+ :style="{
399
+ left: `${dateToOffset(task.meta.dateStart)}px`,
400
+ width: `${spanWidth(task.meta.dateStart, task.meta.dateEnd)}px`,
401
+ background: task.meta.color ?? '#818cf8',
402
+ maxWidth: '100%',
403
+ ...taskFocusers(task.id).length ? { outline: `2px solid ${taskFocusers(task.id)[0].user?.color}`, outlineOffset: '1px' } : {}
404
+ }"
405
+ @pointerdown="onBarPointerDown($event, task.id)"
406
+ @pointermove="onBarPointerMove($event, task.id)"
407
+ @pointerup="onBarPointerUp(task.id, epic.id)"
408
+ @pointerenter="focusTask(task.id)"
409
+ @pointerleave="clearFocus"
410
+ @click.stop="onBarClick(task.id, task.label)"
411
+ >
412
+ <span class="truncate">{{ task.label }}</span>
413
+ </div>
414
+ </div>
415
+
416
+ <!-- Add task spacer -->
417
+ <div
418
+ v-if="editable"
419
+ class="h-7 border-b border-(--ui-border)"
420
+ />
421
+ </div>
422
+ </TransitionGroup>
423
+
424
+ <!-- Today vertical line across bars -->
425
+ <div
426
+ class="absolute top-8 bottom-0 w-px bg-(--ui-primary) opacity-20 pointer-events-none"
427
+ :style="{ left: `${todayOffset}px` }"
428
+ />
429
+ </div>
430
+ </div>
431
+ </div>
432
+ </div>
433
+
434
+ <!-- Node panel -->
435
+ <ANodePanel
436
+ :node-id="openNodeId"
437
+ :node-label="openNodeLabel"
438
+ :child-provider="openNodeProvider"
439
+ @close="closePanel"
440
+ />
441
+ </div>
442
+ </template>
443
+
444
+ <style scoped>
445
+ .tepic-move{transition:transform .25s ease}.tepic-enter-active{transition:opacity .18s ease,transform .18s ease}.tepic-enter-from{opacity:0;transform:translateY(-6px) scale(.97)}.tepic-leave-active{transition:opacity .15s ease}.tepic-leave-to{opacity:0}
446
+ </style>
@@ -0,0 +1,19 @@
1
+ import { type RendererBaseProps } from '../../composables/useRendererBase.js';
2
+ import type { AbracadabraLocale } from '../../locale.js';
3
+ type __VLS_Props = RendererBaseProps & {
4
+ labels?: Partial<AbracadabraLocale['renderers']['timeline']>;
5
+ editable?: boolean;
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
8
+ connectedUsers: import("vue").ComputedRef<{
9
+ clientId: number;
10
+ name: string;
11
+ color: string;
12
+ avatar: string | undefined;
13
+ publicKey: any;
14
+ }[]>;
15
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
16
+ editable: boolean;
17
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
@@ -1,4 +1,5 @@
1
1
  import { shallowRef, computed, watch } from "vue";
2
+ import { tryOnScopeDispose } from "@vueuse/core";
2
3
  export function useAwareness() {
3
4
  const { provider } = useAbracadabra();
4
5
  const states = shallowRef(/* @__PURE__ */ new Map());
@@ -22,6 +23,10 @@ export function useAwareness() {
22
23
  _unsub = () => awareness.off("change", handler);
23
24
  }
24
25
  watch(provider, () => _subscribe(), { immediate: true });
26
+ tryOnScopeDispose(() => {
27
+ _unsub?.();
28
+ _unsub = null;
29
+ });
25
30
  const localState = computed(() => {
26
31
  const awareness = provider.value?.awareness;
27
32
  if (!awareness) return {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * useBroadcastSync
3
+ *
4
+ * Wraps BroadcastChannelSync from @abraca/dabra for cross-tab Y.js sync.
5
+ * Initialized by the main plugin after root provider syncs.
6
+ * No user action needed — automatically syncs across browser tabs on the same origin.
7
+ *
8
+ * Usage:
9
+ * const { isActive } = useBroadcastSync()
10
+ */
11
+ import type * as Y from 'yjs';
12
+ import type { Awareness } from 'y-protocols/awareness';
13
+ export declare function _initBroadcastSync(BroadcastChannelSync: any, doc: Y.Doc, docId: string, awareness?: Awareness | null): void;
14
+ export declare function _destroyBroadcastSync(): void;
15
+ export declare function useBroadcastSync(): {
16
+ /** Whether cross-tab BroadcastChannel sync is currently active. */
17
+ isActive: import("vue").Ref<boolean, boolean>;
18
+ };
@@ -0,0 +1,26 @@
1
+ import { ref } from "vue";
2
+ const isActive = ref(false);
3
+ let _sync = null;
4
+ export function _initBroadcastSync(BroadcastChannelSync, doc, docId, awareness) {
5
+ _destroyBroadcastSync();
6
+ try {
7
+ _sync = BroadcastChannelSync.forDoc(doc, docId, awareness ?? void 0);
8
+ _sync.connect();
9
+ isActive.value = true;
10
+ } catch (e) {
11
+ console.error("[abracadabra] BroadcastChannelSync init failed:", e);
12
+ }
13
+ }
14
+ export function _destroyBroadcastSync() {
15
+ if (_sync) {
16
+ _sync.destroy();
17
+ _sync = null;
18
+ }
19
+ isActive.value = false;
20
+ }
21
+ export function useBroadcastSync() {
22
+ return {
23
+ /** Whether cross-tab BroadcastChannel sync is currently active. */
24
+ isActive
25
+ };
26
+ }
@@ -32,7 +32,8 @@ function resolveChannelLabel(channelId, senderName, senderId) {
32
32
  try {
33
33
  const entry = doc.value?.getMap("doc-tree")?.get(docId);
34
34
  if (entry?.label) return `${entry.label} Chat`;
35
- } catch {
35
+ } catch (e) {
36
+ if (import.meta.dev) console.warn("[abracadabra] chat: failed to resolve channel label:", e);
36
37
  }
37
38
  return "Group Chat";
38
39
  }
@@ -134,7 +135,8 @@ export function _handleStatelessChat(payload) {
134
135
  break;
135
136
  }
136
137
  }
137
- } catch {
138
+ } catch (e) {
139
+ if (import.meta.dev) console.warn("[abracadabra] chat: failed to handle stateless message:", e);
138
140
  }
139
141
  }
140
142
  function sendMessage(channelId, content) {
@@ -19,7 +19,8 @@ export function useChatUsers() {
19
19
  });
20
20
  }
21
21
  }
22
- } catch {
22
+ } catch (e) {
23
+ if (import.meta.dev) console.warn("[abracadabra] chat-users: failed to read awareness states:", e);
23
24
  }
24
25
  return result;
25
26
  });
@@ -5,6 +5,7 @@ const searchTerm = ref("");
5
5
  export function useCommandPalette() {
6
6
  const { doc, isReady, provider } = useAbracadabra();
7
7
  const registry = usePluginRegistry();
8
+ const { getDocUrl } = useDocSlugs();
8
9
  const recentDocIds = useLocalStorage("abra_recent_docs", []);
9
10
  function open(term = "") {
10
11
  searchTerm.value = term;
@@ -43,7 +44,7 @@ export function useCommandPalette() {
43
44
  suffix: entry.type,
44
45
  onClick: () => {
45
46
  trackRecentDoc(id);
46
- navigateTo(`${useRuntimeConfig().public.abracadabra?.docBasePath ?? "/doc"}/${id}`);
47
+ navigateTo(getDocUrl(id));
47
48
  close();
48
49
  }
49
50
  });
@@ -52,7 +53,64 @@ export function useCommandPalette() {
52
53
  if (sorted.length > 0) {
53
54
  result.push({ id: "docs", label: q ? "Documents" : "Recent", items: sorted });
54
55
  }
55
- } catch {
56
+ } catch (e) {
57
+ if (import.meta.dev) console.warn("[abracadabra] command palette: failed to scan document tree:", e);
58
+ }
59
+ }
60
+ {
61
+ const actionItems = [
62
+ {
63
+ id: "action:new-doc",
64
+ label: "New Document",
65
+ icon: "i-lucide-file-plus",
66
+ shortcut: ["meta", "N"],
67
+ onClick: () => {
68
+ if (!doc.value || !isReady.value) return;
69
+ const id = crypto.randomUUID();
70
+ doc.value.getMap("doc-tree").set(id, { label: "Untitled", parentId: null, order: Date.now(), type: "doc" });
71
+ navigateTo(getDocUrl(id));
72
+ close();
73
+ }
74
+ },
75
+ {
76
+ id: "action:home",
77
+ label: "Go to Home",
78
+ icon: "i-lucide-home",
79
+ onClick: () => {
80
+ navigateTo("/app");
81
+ close();
82
+ }
83
+ },
84
+ {
85
+ id: "action:chat",
86
+ label: "Go to Chat",
87
+ icon: "i-lucide-message-circle",
88
+ onClick: () => {
89
+ navigateTo("/chat");
90
+ close();
91
+ }
92
+ },
93
+ {
94
+ id: "action:inbox",
95
+ label: "Go to Inbox",
96
+ icon: "i-lucide-inbox",
97
+ onClick: () => {
98
+ navigateTo("/inbox");
99
+ close();
100
+ }
101
+ },
102
+ {
103
+ id: "action:settings",
104
+ label: "Go to Settings",
105
+ icon: "i-lucide-settings",
106
+ onClick: () => {
107
+ navigateTo("/settings");
108
+ close();
109
+ }
110
+ }
111
+ ].filter((item) => !q || item.label.toLowerCase().includes(q));
112
+ if (actionItems.length > 0) {
113
+ result.push({ id: "actions", label: "Actions", items: actionItems });
56
114
  }
57
115
  }
58
116
  try {
@@ -75,7 +133,8 @@ export function useCommandPalette() {
75
133
  result.push({ id: group.id ?? "plugin", label: group.label, items: filtered });
76
134
  }
77
135
  }
78
- } catch {
136
+ } catch (e) {
137
+ if (import.meta.dev) console.warn("[abracadabra] command palette: failed to load plugin commands:", e);
79
138
  }
80
139
  groups.value = result;
81
140
  }
@@ -1,4 +1,5 @@
1
1
  import { computed, ref, toValue, watch } from "vue";
2
+ import { tryOnScopeDispose } from "@vueuse/core";
2
3
  import { useAbraLocale } from "./useAbraLocale.js";
3
4
  import { useAbracadabra } from "./useAbracadabra.js";
4
5
  export function useConnectionStatus(status, synced) {
@@ -40,6 +41,12 @@ export function useConnectionStatus(status, synced) {
40
41
  stableStatus.value = next;
41
42
  }
42
43
  }, { immediate: true });
44
+ tryOnScopeDispose(() => {
45
+ if (offlineTimer) {
46
+ clearTimeout(offlineTimer);
47
+ offlineTimer = null;
48
+ }
49
+ });
43
50
  const label = computed(() => {
44
51
  const s = stableStatus.value;
45
52
  if (s === "connected") return locale.connected;