@abraca/nuxt 2.10.0 → 2.13.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 (127) hide show
  1. package/dist/module.d.mts +14 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/assets/editor.css +1 -1
  5. package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
  6. package/dist/runtime/components/AConnectionBadge.vue +79 -0
  7. package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
  8. package/dist/runtime/components/ADocPickerModal.d.vue.ts +31 -0
  9. package/dist/runtime/components/ADocPickerModal.vue +191 -0
  10. package/dist/runtime/components/ADocPickerModal.vue.d.ts +31 -0
  11. package/dist/runtime/components/ADocumentTree.vue +65 -0
  12. package/dist/runtime/components/AEditor.d.vue.ts +19 -12
  13. package/dist/runtime/components/AEditor.vue +243 -165
  14. package/dist/runtime/components/AEditor.vue.d.ts +19 -12
  15. package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
  16. package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
  17. package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
  18. package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
  19. package/dist/runtime/components/AModalShell.vue +105 -0
  20. package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
  21. package/dist/runtime/components/ANodePanel.d.vue.ts +17 -7
  22. package/dist/runtime/components/ANodePanel.vue +550 -451
  23. package/dist/runtime/components/ANodePanel.vue.d.ts +17 -7
  24. package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
  25. package/dist/runtime/components/ANodePanelHeader.vue +17 -3
  26. package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
  27. package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
  28. package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
  29. package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
  30. package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
  31. package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
  32. package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
  33. package/dist/runtime/components/ATagsEditor.d.vue.ts +19 -0
  34. package/dist/runtime/components/ATagsEditor.vue +60 -0
  35. package/dist/runtime/components/ATagsEditor.vue.d.ts +19 -0
  36. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  37. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  38. package/dist/runtime/components/chat/AChatInput.d.vue.ts +11 -6
  39. package/dist/runtime/components/chat/AChatInput.vue +33 -2
  40. package/dist/runtime/components/chat/AChatInput.vue.d.ts +11 -6
  41. package/dist/runtime/components/chat/AChatList.d.vue.ts +12 -0
  42. package/dist/runtime/components/chat/AChatList.vue +76 -32
  43. package/dist/runtime/components/chat/AChatList.vue.d.ts +12 -0
  44. package/dist/runtime/components/chat/AChatMessages.d.vue.ts +4 -0
  45. package/dist/runtime/components/chat/AChatMessages.vue +57 -4
  46. package/dist/runtime/components/chat/AChatMessages.vue.d.ts +4 -0
  47. package/dist/runtime/components/chat/AChatPanel.d.vue.ts +6 -2
  48. package/dist/runtime/components/chat/AChatPanel.vue +17 -1
  49. package/dist/runtime/components/chat/AChatPanel.vue.d.ts +6 -2
  50. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -1
  51. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  52. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  53. package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
  54. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
  55. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
  56. package/dist/runtime/components/renderers/AChartRenderer.client.d.vue.ts +17 -0
  57. package/dist/runtime/components/renderers/AChartRenderer.client.vue +622 -0
  58. package/dist/runtime/components/renderers/AChartRenderer.client.vue.d.ts +17 -0
  59. package/dist/runtime/components/renderers/AGraphRenderer.vue +64 -15
  60. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  61. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  62. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  63. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  64. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  65. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  66. package/dist/runtime/components/renderers/sheets/ASheetsGrid.d.vue.ts +2 -2
  67. package/dist/runtime/components/renderers/sheets/ASheetsGrid.vue.d.ts +2 -2
  68. package/dist/runtime/components/settings/ASettingsAppearancePanel.d.vue.ts +3 -0
  69. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue +67 -0
  70. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue.d.ts +3 -0
  71. package/dist/runtime/components/settings/ASettingsGroup.d.vue.ts +24 -0
  72. package/dist/runtime/components/settings/ASettingsGroup.vue +31 -0
  73. package/dist/runtime/components/settings/ASettingsGroup.vue.d.ts +24 -0
  74. package/dist/runtime/components/settings/ASettingsModal.vue +84 -53
  75. package/dist/runtime/components/settings/ASettingsPlaceholder.d.vue.ts +20 -0
  76. package/dist/runtime/components/settings/ASettingsPlaceholder.vue +32 -0
  77. package/dist/runtime/components/settings/ASettingsPlaceholder.vue.d.ts +20 -0
  78. package/dist/runtime/components/settings/ASettingsRow.d.vue.ts +34 -0
  79. package/dist/runtime/components/settings/ASettingsRow.vue +34 -0
  80. package/dist/runtime/components/settings/ASettingsRow.vue.d.ts +34 -0
  81. package/dist/runtime/components/settings/sections.d.ts +37 -0
  82. package/dist/runtime/components/settings/sections.js +45 -0
  83. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
  84. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
  85. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
  86. package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
  87. package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
  88. package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
  89. package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
  90. package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
  91. package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
  92. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  93. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  94. package/dist/runtime/composables/useChat.d.ts +22 -1
  95. package/dist/runtime/composables/useChat.js +79 -8
  96. package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
  97. package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
  98. package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
  99. package/dist/runtime/composables/useDocSnapshots.js +5 -0
  100. package/dist/runtime/composables/useEditor.d.ts +1 -1
  101. package/dist/runtime/composables/useEditor.js +120 -0
  102. package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
  103. package/dist/runtime/composables/useEditorToolbar.js +78 -56
  104. package/dist/runtime/composables/useNodeContextMenu.d.ts +14 -0
  105. package/dist/runtime/composables/useNodeContextMenu.js +59 -1
  106. package/dist/runtime/composables/useSettingsModal.d.ts +1 -1
  107. package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
  108. package/dist/runtime/composables/useSwipeGesture.js +140 -0
  109. package/dist/runtime/extensions/document-header.js +16 -6
  110. package/dist/runtime/extensions/document-meta.js +344 -19
  111. package/dist/runtime/extensions/meta-field.js +42 -0
  112. package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
  113. package/dist/runtime/extensions/views/FieldView.vue +51 -19
  114. package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
  115. package/dist/runtime/locale.d.ts +8 -0
  116. package/dist/runtime/locale.js +9 -1
  117. package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
  118. package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
  119. package/dist/runtime/plugin-abracadabra.client.js +12 -2
  120. package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
  121. package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
  122. package/dist/runtime/utils/chatContent.d.ts +20 -2
  123. package/dist/runtime/utils/chatContent.js +20 -1
  124. package/dist/runtime/utils/docTypes.js +43 -0
  125. package/dist/runtime/utils/titleSync.d.ts +130 -0
  126. package/dist/runtime/utils/titleSync.js +53 -0
  127. package/package.json +11 -4
@@ -87,6 +87,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
87
87
  "delete-snapshot": (version: number) => any;
88
88
  "restore-snapshot": (version: number) => any;
89
89
  "fork-snapshot": (version: number) => any;
90
+ "preview-snapshot": (version: number) => any;
90
91
  "load-more-snapshots": () => any;
91
92
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
92
93
  "onUpdate-meta"?: ((patch: Partial<DocPageMeta>) => any) | undefined;
@@ -105,6 +106,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
105
106
  "onDelete-snapshot"?: ((version: number) => any) | undefined;
106
107
  "onRestore-snapshot"?: ((version: number) => any) | undefined;
107
108
  "onFork-snapshot"?: ((version: number) => any) | undefined;
109
+ "onPreview-snapshot"?: ((version: number) => any) | undefined;
108
110
  "onLoad-more-snapshots"?: (() => any) | undefined;
109
111
  }>, {
110
112
  snapshots: SnapshotMeta[];
@@ -28,7 +28,7 @@ const props = defineProps({
28
28
  pendingSnapshotVersion: { type: [Number, null], required: false, default: null },
29
29
  creatingSnapshot: { type: Boolean, required: false, default: false }
30
30
  });
31
- const emit = defineEmits(["grant-permission", "change-role", "revoke-permission", "create-invite", "sync-doc", "open-encryption", "user-context-menu", "update-meta", "create-snapshot", "delete-snapshot", "restore-snapshot", "fork-snapshot", "load-more-snapshots"]);
31
+ const emit = defineEmits(["grant-permission", "change-role", "revoke-permission", "create-invite", "sync-doc", "open-encryption", "user-context-menu", "update-meta", "create-snapshot", "delete-snapshot", "restore-snapshot", "fork-snapshot", "preview-snapshot", "load-more-snapshots"]);
32
32
  const grantUserId = ref("");
33
33
  const grantRole = ref("editor");
34
34
  const showManualKeyInput = ref(false);
@@ -114,6 +114,13 @@ function runSnapConfirm() {
114
114
  else emit("restore-snapshot", c.version);
115
115
  }
116
116
  function snapshotMenu(version) {
117
+ const view = [
118
+ {
119
+ label: "View / compare",
120
+ icon: "i-lucide-eye",
121
+ onSelect: () => emit("preview-snapshot", version)
122
+ }
123
+ ];
117
124
  const manage = [
118
125
  {
119
126
  label: "Restore",
@@ -134,7 +141,7 @@ function snapshotMenu(version) {
134
141
  onSelect: () => askSnapDelete(version)
135
142
  }
136
143
  ];
137
- return props.isOwner ? [manage, destructive] : [manage];
144
+ return props.isOwner ? [view, manage, destructive] : [view, manage];
138
145
  }
139
146
  function triggerColor(trigger) {
140
147
  switch (trigger) {
@@ -728,7 +735,11 @@ function patchMeta(key, value) {
728
735
  :key="snap.version"
729
736
  class="flex items-center gap-2.5 p-3 rounded-lg bg-(--ui-bg-elevated)"
730
737
  >
731
- <div class="flex-1 min-w-0">
738
+ <button
739
+ type="button"
740
+ class="flex-1 min-w-0 text-left cursor-pointer"
741
+ @click="emit('preview-snapshot', snap.version)"
742
+ >
732
743
  <div class="flex items-center gap-2">
733
744
  <span class="text-sm font-medium text-(--ui-text-highlighted) tabular-nums">
734
745
  v{{ snap.version }}
@@ -749,7 +760,7 @@ function patchMeta(key, value) {
749
760
  <p class="text-xs text-(--ui-text-dimmed) mt-0.5">
750
761
  {{ formatSnapTime(snap.created_at) }} · {{ formatSnapSize(snap.size_bytes) }}
751
762
  </p>
752
- </div>
763
+ </button>
753
764
  <UDropdownMenu
754
765
  :items="snapshotMenu(snap.version)"
755
766
  :content="{ align: 'end' }"
@@ -87,6 +87,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
87
87
  "delete-snapshot": (version: number) => any;
88
88
  "restore-snapshot": (version: number) => any;
89
89
  "fork-snapshot": (version: number) => any;
90
+ "preview-snapshot": (version: number) => any;
90
91
  "load-more-snapshots": () => any;
91
92
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
92
93
  "onUpdate-meta"?: ((patch: Partial<DocPageMeta>) => any) | undefined;
@@ -105,6 +106,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
105
106
  "onDelete-snapshot"?: ((version: number) => any) | undefined;
106
107
  "onRestore-snapshot"?: ((version: number) => any) | undefined;
107
108
  "onFork-snapshot"?: ((version: number) => any) | undefined;
109
+ "onPreview-snapshot"?: ((version: number) => any) | undefined;
108
110
  "onLoad-more-snapshots"?: (() => any) | undefined;
109
111
  }>, {
110
112
  snapshots: SnapshotMeta[];
@@ -37,8 +37,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
37
37
  name: string;
38
38
  online: boolean | null;
39
39
  currentDocId: string | null;
40
- avatarStyle: string;
41
40
  isSelf: boolean;
41
+ avatarStyle: string;
42
42
  isFollowing: boolean;
43
43
  showFollow: boolean;
44
44
  currentDocLabel: string | null;
@@ -37,8 +37,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
37
37
  name: string;
38
38
  online: boolean | null;
39
39
  currentDocId: string | null;
40
- avatarStyle: string;
41
40
  isSelf: boolean;
41
+ avatarStyle: string;
42
42
  isFollowing: boolean;
43
43
  showFollow: boolean;
44
44
  currentDocLabel: string | null;
@@ -16,6 +16,8 @@ export interface ChatMessage {
16
16
  senderName: string;
17
17
  content: string;
18
18
  createdAt: number;
19
+ /** Id of the message this one replies to, if any (threaded reply). */
20
+ replyTo?: string;
19
21
  }
20
22
  export interface ChatChannel {
21
23
  id: string;
@@ -25,6 +27,12 @@ export interface ChatChannel {
25
27
  unreadCount: number;
26
28
  }
27
29
  export declare function buildDmChannelId(a: string, b: string): string;
30
+ declare function setMuted(id: string, on: boolean): void;
31
+ declare function setPinned(id: string, on: boolean): void;
32
+ declare function setHidden(id: string, on: boolean): void;
33
+ declare function isMuted(id: string): boolean;
34
+ declare function isPinned(id: string): boolean;
35
+ declare function isHidden(id: string): boolean;
28
36
  declare let _publicKeyB64: import("vue").Ref<string, string>;
29
37
  declare let _userName: import("vue").Ref<string, string>;
30
38
  export declare function _initChat(publicKeyB64Ref: typeof _publicKeyB64, userNameRef: typeof _userName): void;
@@ -53,6 +61,7 @@ declare function sendTyping(channelId: string): void;
53
61
  declare function setActiveChannel(channelId: string | null): void;
54
62
  export declare function useChat(): {
55
63
  channels: import("vue").Ref<Record<string, ChatChannel>, Record<string, ChatChannel>>;
64
+ channelList: import("vue").ComputedRef<ChatChannel[]>;
56
65
  activeChannel: import("vue").Ref<string | null, string | null>;
57
66
  messagesByChannel: import("vue").Ref<Record<string, ChatMessage[]>, Record<string, ChatMessage[]>>;
58
67
  typingByChannel: import("vue").Ref<Record<string, Map<string, string>>, Record<string, Map<string, string>>>;
@@ -62,13 +71,25 @@ export declare function useChat(): {
62
71
  sendTyping: typeof sendTyping;
63
72
  setActiveChannel: typeof setActiveChannel;
64
73
  buildDmChannelId: typeof buildDmChannelId;
74
+ mutedChannels: import("vue").Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>;
75
+ pinnedChannels: import("vue").Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>;
76
+ hiddenChannels: import("vue").Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>;
77
+ setMuted: typeof setMuted;
78
+ setPinned: typeof setPinned;
79
+ setHidden: typeof setHidden;
80
+ isMuted: typeof isMuted;
81
+ isPinned: typeof isPinned;
82
+ isHidden: typeof isHidden;
65
83
  };
66
84
  export declare function useChatChannel(channelId: string): {
67
85
  messages: import("vue").ComputedRef<ChatMessage[]>;
68
86
  channel: import("vue").ComputedRef<ChatChannel | undefined>;
69
87
  typingUsers: import("vue").ComputedRef<string[]>;
70
88
  unreadCount: import("vue").ComputedRef<number>;
71
- send: (content: string) => void;
89
+ send: (content: string, opts?: {
90
+ mentions?: string[];
91
+ replyTo?: string;
92
+ }) => void;
72
93
  loadHistory: (opts?: {
73
94
  before?: number;
74
95
  limit?: number;
@@ -33,6 +33,58 @@ const channels = ref({});
33
33
  const typingByChannel = ref({});
34
34
  const activeChannel = ref(null);
35
35
  const typingTimers = /* @__PURE__ */ new Map();
36
+ function _loadIdSet(key) {
37
+ if (typeof localStorage === "undefined") return /* @__PURE__ */ new Set();
38
+ try {
39
+ const raw = localStorage.getItem(key);
40
+ return raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
41
+ } catch {
42
+ return /* @__PURE__ */ new Set();
43
+ }
44
+ }
45
+ function _saveIdSet(key, set) {
46
+ if (typeof localStorage === "undefined") return;
47
+ try {
48
+ localStorage.setItem(key, JSON.stringify([...set]));
49
+ } catch {
50
+ }
51
+ }
52
+ const mutedChannels = ref(_loadIdSet("abracadabra_chat_muted"));
53
+ const pinnedChannels = ref(_loadIdSet("abracadabra_chat_pinned"));
54
+ const hiddenChannels = ref(_loadIdSet("abracadabra_chat_hidden"));
55
+ function _setPref(ref_, key, id, on) {
56
+ const next = new Set(ref_.value);
57
+ if (on) next.add(id);
58
+ else next.delete(id);
59
+ ref_.value = next;
60
+ _saveIdSet(key, next);
61
+ }
62
+ function setMuted(id, on) {
63
+ _setPref(mutedChannels, "abracadabra_chat_muted", id, on);
64
+ }
65
+ function setPinned(id, on) {
66
+ _setPref(pinnedChannels, "abracadabra_chat_pinned", id, on);
67
+ }
68
+ function setHidden(id, on) {
69
+ _setPref(hiddenChannels, "abracadabra_chat_hidden", id, on);
70
+ }
71
+ function isMuted(id) {
72
+ return mutedChannels.value.has(id);
73
+ }
74
+ function isPinned(id) {
75
+ return pinnedChannels.value.has(id);
76
+ }
77
+ function isHidden(id) {
78
+ return hiddenChannels.value.has(id);
79
+ }
80
+ const channelList = computed(
81
+ () => Object.values(channels.value).filter((c) => !hiddenChannels.value.has(c.id)).sort((a, b) => {
82
+ const pa = pinnedChannels.value.has(a.id) ? 1 : 0;
83
+ const pb = pinnedChannels.value.has(b.id) ? 1 : 0;
84
+ if (pa !== pb) return pb - pa;
85
+ return (b.lastMessage?.createdAt ?? 0) - (a.lastMessage?.createdAt ?? 0);
86
+ })
87
+ );
36
88
  let _publicKeyB64 = ref("");
37
89
  let _userName = ref("");
38
90
  export function _initChat(publicKeyB64Ref, userNameRef) {
@@ -112,12 +164,13 @@ function addMessage(msg) {
112
164
  if (chan) {
113
165
  const isActive = activeChannel.value === msg.channel;
114
166
  const isOwn = msg.senderId === _publicKeyB64.value;
167
+ const suppress = isActive || isOwn || mutedChannels.value.has(msg.channel);
115
168
  channels.value = {
116
169
  ...channels.value,
117
170
  [msg.channel]: {
118
171
  ...chan,
119
172
  lastMessage: msg,
120
- unreadCount: isActive || isOwn ? chan.unreadCount : chan.unreadCount + 1
173
+ unreadCount: suppress ? chan.unreadCount : chan.unreadCount + 1
121
174
  }
122
175
  };
123
176
  }
@@ -148,7 +201,8 @@ export function _handleStatelessChat(payload) {
148
201
  // as "chat is broken / messages aren't syncing".
149
202
  senderName: rec.sender_name ?? resolveSenderName(senderId),
150
203
  content: rec.content ?? "",
151
- createdAt: tsMs
204
+ createdAt: tsMs,
205
+ ...rec.reply_to ? { replyTo: rec.reply_to } : {}
152
206
  };
153
207
  ensureChannel(msg.channel, resolveChannelLabel(msg.channel, msg.senderName, msg.senderId));
154
208
  addMessage(msg);
@@ -234,7 +288,8 @@ function sendMessage(channelId, content, opts) {
234
288
  senderId: _publicKeyB64.value,
235
289
  senderName: userName?.value ?? _userName.value,
236
290
  content: trimmed,
237
- createdAt: Date.now()
291
+ createdAt: Date.now(),
292
+ ...opts?.replyTo ? { replyTo: opts.replyTo } : {}
238
293
  });
239
294
  provider.value.sendStateless(JSON.stringify({
240
295
  type: "messages:send",
@@ -284,7 +339,9 @@ async function _attachPeriodProvider(channel, rootProv, periodId) {
284
339
  senderId: f.senderId,
285
340
  senderName: "",
286
341
  content,
287
- createdAt: f.createdAt
342
+ createdAt: f.createdAt,
343
+ // Best-effort: the SDK's folded record may carry reply linkage.
344
+ ...f.reply_to || f.replyTo ? { replyTo: f.reply_to ?? f.replyTo } : {}
288
345
  });
289
346
  }
290
347
  };
@@ -360,11 +417,15 @@ function setActiveChannel(channelId) {
360
417
  }
361
418
  }
362
419
  const totalUnreadCount = computed(
363
- () => Object.values(channels.value).reduce((sum, ch) => sum + ch.unreadCount, 0)
420
+ () => Object.values(channels.value).reduce(
421
+ (sum, ch) => sum + (mutedChannels.value.has(ch.id) ? 0 : ch.unreadCount),
422
+ 0
423
+ )
364
424
  );
365
425
  export function useChat() {
366
426
  return {
367
427
  channels,
428
+ channelList,
368
429
  activeChannel,
369
430
  messagesByChannel,
370
431
  typingByChannel,
@@ -373,7 +434,17 @@ export function useChat() {
373
434
  fetchHistory,
374
435
  sendTyping,
375
436
  setActiveChannel,
376
- buildDmChannelId
437
+ buildDmChannelId,
438
+ // Channel preferences
439
+ mutedChannels,
440
+ pinnedChannels,
441
+ hiddenChannels,
442
+ setMuted,
443
+ setPinned,
444
+ setHidden,
445
+ isMuted,
446
+ isPinned,
447
+ isHidden
377
448
  };
378
449
  }
379
450
  export function useChatChannel(channelId) {
@@ -392,8 +463,8 @@ export function useChatChannel(channelId) {
392
463
  stop();
393
464
  });
394
465
  }
395
- function send(content) {
396
- sendMessage(key, content);
466
+ function send(content, opts) {
467
+ sendMessage(key, content, opts);
397
468
  }
398
469
  function loadHistory(opts) {
399
470
  fetchHistory(key, opts);
@@ -6,7 +6,7 @@
6
6
  * Caps depth at `maxDepth` (default 8) to handle malformed cycles
7
7
  * defensively. The last item is the current doc and has no `to`.
8
8
  */
9
- import { type ComputedRef, type MaybeRef } from 'vue';
9
+ import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
10
10
  export interface DocBreadcrumbItem {
11
11
  id: string;
12
12
  label: string;
@@ -14,8 +14,23 @@ export interface DocBreadcrumbItem {
14
14
  /** Navigation target — undefined for the current doc */
15
15
  to?: string;
16
16
  }
17
- export declare function useDocBreadcrumb(docId: MaybeRef<string | null | undefined>, options?: {
17
+ /**
18
+ * The trail split for single-line rendering: when the full trail is longer
19
+ * than `maxVisible`, the middle collapses into `hidden` (shown behind a "…"
20
+ * overflow menu), keeping the first crumb (`head`) and the last
21
+ * `maxVisible - 2` crumbs (`tail`). `overflowed` is false when no collapse is
22
+ * needed — then `head` holds the whole trail and `hidden`/`tail` are empty.
23
+ */
24
+ export interface DocBreadcrumbCollapsed {
25
+ head: DocBreadcrumbItem[];
26
+ hidden: DocBreadcrumbItem[];
27
+ tail: DocBreadcrumbItem[];
28
+ overflowed: boolean;
29
+ }
30
+ export declare function useDocBreadcrumb(docId: MaybeRefOrGetter<string | null | undefined>, options?: {
18
31
  maxDepth?: number;
32
+ maxVisible?: number;
19
33
  }): {
20
34
  items: ComputedRef<DocBreadcrumbItem[]>;
35
+ collapsed: ComputedRef<DocBreadcrumbCollapsed>;
21
36
  };
@@ -1,14 +1,15 @@
1
- import { computed, unref } from "vue";
1
+ import { computed, toValue } from "vue";
2
2
  import { resolveDocType } from "../utils/docTypes.js";
3
3
  import { useRuntimeConfig } from "#imports";
4
4
  import { useDocTree } from "./useDocTree.js";
5
5
  export function useDocBreadcrumb(docId, options = {}) {
6
6
  const maxDepth = options.maxDepth ?? 8;
7
+ const maxVisible = options.maxVisible ?? 0;
7
8
  const config = useRuntimeConfig();
8
9
  const docBasePath = config.public?.abracadabra?.docBasePath ?? "/doc";
9
10
  const tree = useDocTree();
10
11
  const items = computed(() => {
11
- const startId = unref(docId);
12
+ const startId = toValue(docId);
12
13
  if (!startId) return [];
13
14
  const trail = [];
14
15
  let id = startId;
@@ -29,5 +30,18 @@ export function useDocBreadcrumb(docId, options = {}) {
29
30
  }
30
31
  return trail;
31
32
  });
32
- return { items };
33
+ const collapsed = computed(() => {
34
+ const all = items.value;
35
+ if (maxVisible <= 0 || all.length <= maxVisible) {
36
+ return { head: all, hidden: [], tail: [], overflowed: false };
37
+ }
38
+ const tailCount = Math.max(1, maxVisible - 2);
39
+ return {
40
+ head: all.slice(0, 1),
41
+ hidden: all.slice(1, all.length - tailCount),
42
+ tail: all.slice(all.length - tailCount),
43
+ overflowed: true
44
+ };
45
+ });
46
+ return { items, collapsed };
33
47
  }
@@ -19,7 +19,7 @@
19
19
  * onMounted(() => snaps.fetchList())
20
20
  */
21
21
  import { type ComputedRef, type InjectionKey, type Ref } from 'vue';
22
- import type { SnapshotMeta } from '@abraca/dabra';
22
+ import type { SnapshotData, SnapshotMeta } from '@abraca/dabra';
23
23
  import type { AbracadabraLocale } from '../locale.js';
24
24
  export type DocSnapshotsCtx = ReturnType<typeof useDocSnapshots>;
25
25
  export declare const DOC_SNAPSHOTS_KEY: InjectionKey<DocSnapshotsCtx>;
@@ -72,6 +72,7 @@ export declare function useDocSnapshots(docId: Ref<string> | ComputedRef<string>
72
72
  pending: Ref<Record<number, "delete" | "restore" | "fork" | undefined>, Record<number, "delete" | "restore" | "fork" | undefined>>;
73
73
  fetchList: () => Promise<void>;
74
74
  loadMore: () => Promise<void>;
75
+ getSnapshot: (version: number) => Promise<SnapshotData | null>;
75
76
  create: (label?: string) => Promise<void>;
76
77
  remove: (version: number) => Promise<void>;
77
78
  restore: (version: number) => Promise<void>;
@@ -120,6 +120,10 @@ export function useDocSnapshots(docId, overrides) {
120
120
  loading.value = false;
121
121
  }
122
122
  }
123
+ async function getSnapshot(version) {
124
+ if (!client.value || !docId.value) return null;
125
+ return client.value.getSnapshot(docId.value, version, { include: "files" });
126
+ }
123
127
  async function create(label) {
124
128
  if (!client.value || !docId.value) return;
125
129
  creating.value = true;
@@ -226,6 +230,7 @@ export function useDocSnapshots(docId, overrides) {
226
230
  // Actions
227
231
  fetchList,
228
232
  loadMore,
233
+ getSnapshot,
229
234
  create,
230
235
  remove,
231
236
  restore,
@@ -15,7 +15,7 @@
15
15
  * Ported from cou-sh/app/composables/useEditorCollaboration.ts.
16
16
  */
17
17
  import { type Ref, type ShallowRef } from 'vue';
18
- import type { Extensions } from '@tiptap/core';
18
+ import { type Extensions } from '@tiptap/core';
19
19
  import type { CollaborationUser } from '../types.js';
20
20
  export { type CollaborationUser } from '../types.js';
21
21
  export interface UseEditorOptions {
@@ -1,4 +1,5 @@
1
1
  import { ref, shallowRef, watch, onUnmounted } from "vue";
2
+ import { Extension } from "@tiptap/core";
2
3
  import { XmlElement } from "yjs";
3
4
  import { useNuxtApp } from "#imports";
4
5
  export function useEditor(options) {
@@ -7,10 +8,21 @@ export function useEditor(options) {
7
8
  const connectedUsers = ref([]);
8
9
  const extensions = shallowRef([]);
9
10
  const ready = ref(false);
11
+ const activeColors = ref(/* @__PURE__ */ new Set());
12
+ const colorTimeouts = /* @__PURE__ */ new Map();
13
+ const CARET_ACTIVITY_TIMEOUT = 3e3;
10
14
  let cleanup = null;
15
+ let cursorBroadcastTimer = null;
11
16
  watch(childProvider, async (prov) => {
12
17
  cleanup?.();
13
18
  cleanup = null;
19
+ if (cursorBroadcastTimer) {
20
+ clearTimeout(cursorBroadcastTimer);
21
+ cursorBroadcastTimer = null;
22
+ }
23
+ colorTimeouts.forEach((t) => clearTimeout(t));
24
+ colorTimeouts.clear();
25
+ activeColors.value = /* @__PURE__ */ new Set();
14
26
  if (!prov) {
15
27
  ready.value = false;
16
28
  extensions.value = [];
@@ -34,6 +46,23 @@ export function useEditor(options) {
34
46
  CollaborationCaret.configure({
35
47
  provider: prov,
36
48
  user,
49
+ // Custom caret element: a thin coloured bar with a floating name label.
50
+ // Visibility is activity-gated — the `.is-active` class (added here when
51
+ // the author is in activeColors, and toggled live by the watcher below)
52
+ // is what makes the caret visible (editor.css defaults it to opacity:0).
53
+ render: (u) => {
54
+ const cursor = document.createElement("span");
55
+ cursor.classList.add("collaboration-carets__caret");
56
+ cursor.setAttribute("style", `border-color: ${u.color}`);
57
+ cursor.dataset.userColor = u.color;
58
+ const label = document.createElement("div");
59
+ label.classList.add("collaboration-carets__label");
60
+ label.setAttribute("style", `background-color: ${u.color}`);
61
+ label.insertBefore(document.createTextNode(u.name), null);
62
+ cursor.insertBefore(label, null);
63
+ if (activeColors.value.has(u.color)) cursor.classList.add("is-active");
64
+ return cursor;
65
+ },
37
66
  selectionRender: (u) => {
38
67
  const color = u.color || "#999";
39
68
  const selectionColor = color.startsWith("hsl") ? color.replace("hsl", "hsla").replace(")", ", 0.4)") : `${color}66`;
@@ -43,6 +72,20 @@ export function useEditor(options) {
43
72
  };
44
73
  }
45
74
  }),
75
+ // Broadcast the local cursor position to child awareness (throttled,
76
+ // 150ms trailing) so presence/follow consumers can track the caret
77
+ // without coupling to the CollaborationCaret extension internals.
78
+ Extension.create({
79
+ name: "cursor-position-broadcast",
80
+ onSelectionUpdate() {
81
+ const { from, to } = this.editor.state.selection;
82
+ if (cursorBroadcastTimer) clearTimeout(cursorBroadcastTimer);
83
+ cursorBroadcastTimer = setTimeout(() => {
84
+ cursorBroadcastTimer = null;
85
+ prov.awareness?.setLocalStateField("cursorPos", { from, to });
86
+ }, 150);
87
+ }
88
+ }),
46
89
  ...extraExtensions
47
90
  ];
48
91
  const ydoc = prov.document;
@@ -92,6 +135,13 @@ export function useEditor(options) {
92
135
  dedupeDocumentMeta();
93
136
  ensureDocumentMeta();
94
137
  }
138
+ const onProviderSynced = () => {
139
+ dedupeHeaders();
140
+ dedupeDocumentMeta();
141
+ ensureDocumentMeta();
142
+ };
143
+ prov.on?.("synced", onProviderSynced);
144
+ if (prov.isSynced) Promise.resolve().then(onProviderSynced);
95
145
  ready.value = true;
96
146
  const updateUsers = () => {
97
147
  const states = prov.awareness?.getStates() ?? /* @__PURE__ */ new Map();
@@ -104,14 +154,84 @@ export function useEditor(options) {
104
154
  });
105
155
  connectedUsers.value = users;
106
156
  };
157
+ const markActive = (color) => {
158
+ activeColors.value.add(color);
159
+ activeColors.value = new Set(activeColors.value);
160
+ if (colorTimeouts.has(color)) clearTimeout(colorTimeouts.get(color));
161
+ colorTimeouts.set(color, setTimeout(() => {
162
+ activeColors.value.delete(color);
163
+ activeColors.value = new Set(activeColors.value);
164
+ colorTimeouts.delete(color);
165
+ }, CARET_ACTIVITY_TIMEOUT));
166
+ };
167
+ const onDocUpdate = (_update, _origin, _doc, transaction) => {
168
+ const beforeState = transaction.beforeState;
169
+ const afterState = transaction.afterState;
170
+ if (!beforeState || !afterState) return;
171
+ const localClientId = ydoc.clientID;
172
+ const states = prov.awareness?.getStates() ?? /* @__PURE__ */ new Map();
173
+ for (const [clientId, clock] of afterState) {
174
+ if (clientId === localClientId) continue;
175
+ const beforeClock = beforeState.get(clientId) ?? 0;
176
+ if (clock > beforeClock) {
177
+ const state = states.get(clientId);
178
+ const color = state?.user?.color;
179
+ if (color) markActive(color);
180
+ }
181
+ }
182
+ };
183
+ ydoc?.on?.("update", onDocUpdate);
107
184
  prov.awareness?.on("change", updateUsers);
108
185
  updateUsers();
186
+ const fadeOutTimeouts = /* @__PURE__ */ new Map();
187
+ let caretSyncTimer = null;
188
+ const stopActiveWatch = watch(activeColors, (colors) => {
189
+ if (caretSyncTimer) clearTimeout(caretSyncTimer);
190
+ caretSyncTimer = setTimeout(() => {
191
+ caretSyncTimer = null;
192
+ const carets = document.querySelectorAll(".collaboration-carets__caret[data-user-color]");
193
+ carets.forEach((el) => {
194
+ const color = el.dataset.userColor;
195
+ if (!color) return;
196
+ if (colors.has(color)) {
197
+ if (fadeOutTimeouts.has(color)) {
198
+ clearTimeout(fadeOutTimeouts.get(color));
199
+ fadeOutTimeouts.delete(color);
200
+ }
201
+ el.classList.remove("is-hidden");
202
+ requestAnimationFrame(() => el.classList.add("is-active"));
203
+ } else {
204
+ el.classList.remove("is-active");
205
+ if (!fadeOutTimeouts.has(color)) {
206
+ fadeOutTimeouts.set(color, setTimeout(() => {
207
+ el.classList.add("is-hidden");
208
+ fadeOutTimeouts.delete(color);
209
+ }, 300));
210
+ }
211
+ }
212
+ });
213
+ }, 100);
214
+ });
109
215
  cleanup = () => {
216
+ ydoc?.off?.("update", onDocUpdate);
217
+ prov.off?.("synced", onProviderSynced);
110
218
  prov.awareness?.off("change", updateUsers);
219
+ stopActiveWatch();
220
+ if (caretSyncTimer) {
221
+ clearTimeout(caretSyncTimer);
222
+ caretSyncTimer = null;
223
+ }
224
+ fadeOutTimeouts.forEach((t) => clearTimeout(t));
225
+ fadeOutTimeouts.clear();
111
226
  if (fragment) {
112
227
  fragment.unobserve(dedupeHeaders);
113
228
  fragment.unobserve(dedupeDocumentMeta);
114
229
  }
230
+ if (cursorBroadcastTimer) {
231
+ clearTimeout(cursorBroadcastTimer);
232
+ cursorBroadcastTimer = null;
233
+ }
234
+ prov.awareness?.setLocalStateField("cursorPos", null);
115
235
  };
116
236
  }, { immediate: true });
117
237
  onUnmounted(() => cleanup?.());
@@ -1,12 +1,18 @@
1
1
  /**
2
2
  * useEditorToolbar
3
3
  *
4
- * Returns pre-built toolbar item groups for UEditorToolbar,
5
- * merging the plugin registry's contributions with standard formatting items.
4
+ * Returns pre-built toolbar item groups for UEditorToolbar, merging the plugin
5
+ * registry's contributions with standard formatting items.
6
+ *
7
+ * Three building blocks, all individually usable:
8
+ * - `items` full toolbar incl. undo/redo — for a docked/main toolbar.
9
+ * - `bubbleItems` selection bubble — same formatting groups WITHOUT undo/redo
10
+ * (undo/redo belongs in the panel header, not a text bubble).
11
+ * - `getTableToolbarItems(editor)` table-context actions (row/column/table ops).
6
12
  *
7
13
  * Usage:
8
- * const { items } = useEditorToolbar()
9
- * <UEditorToolbar :editor="editor" :items="items" />
14
+ * const { bubbleItems } = useEditorToolbar({ editor, docId })
15
+ * <UEditorToolbar :editor="editor" :items="bubbleItems" layout="bubble" />
10
16
  */
11
17
  import type { Editor } from '@tiptap/vue-3';
12
18
  export interface UseEditorToolbarOptions {
@@ -19,4 +25,6 @@ export interface UseEditorToolbarOptions {
19
25
  }
20
26
  export declare function useEditorToolbar(options?: UseEditorToolbarOptions): {
21
27
  items: import("vue").ComputedRef<any[][]>;
28
+ bubbleItems: import("vue").ComputedRef<any[][]>;
29
+ getTableToolbarItems: (editor: Editor) => any[][];
22
30
  };