@abraca/nuxt 2.11.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 (74) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +7 -0
  3. package/dist/runtime/components/ADocPickerModal.d.vue.ts +31 -0
  4. package/dist/runtime/components/ADocPickerModal.vue +191 -0
  5. package/dist/runtime/components/ADocPickerModal.vue.d.ts +31 -0
  6. package/dist/runtime/components/ADocumentTree.vue +65 -0
  7. package/dist/runtime/components/AEditor.d.vue.ts +17 -10
  8. package/dist/runtime/components/AEditor.vue +232 -164
  9. package/dist/runtime/components/AEditor.vue.d.ts +17 -10
  10. package/dist/runtime/components/ANodePanel.d.vue.ts +9 -1
  11. package/dist/runtime/components/ANodePanel.vue +547 -473
  12. package/dist/runtime/components/ANodePanel.vue.d.ts +9 -1
  13. package/dist/runtime/components/ATagsEditor.d.vue.ts +19 -0
  14. package/dist/runtime/components/ATagsEditor.vue +60 -0
  15. package/dist/runtime/components/ATagsEditor.vue.d.ts +19 -0
  16. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  17. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  18. package/dist/runtime/components/chat/AChatInput.d.vue.ts +11 -6
  19. package/dist/runtime/components/chat/AChatInput.vue +33 -2
  20. package/dist/runtime/components/chat/AChatInput.vue.d.ts +11 -6
  21. package/dist/runtime/components/chat/AChatList.d.vue.ts +12 -0
  22. package/dist/runtime/components/chat/AChatList.vue +76 -32
  23. package/dist/runtime/components/chat/AChatList.vue.d.ts +12 -0
  24. package/dist/runtime/components/chat/AChatMessages.d.vue.ts +4 -0
  25. package/dist/runtime/components/chat/AChatMessages.vue +57 -4
  26. package/dist/runtime/components/chat/AChatMessages.vue.d.ts +4 -0
  27. package/dist/runtime/components/chat/AChatPanel.d.vue.ts +6 -2
  28. package/dist/runtime/components/chat/AChatPanel.vue +17 -1
  29. package/dist/runtime/components/chat/AChatPanel.vue.d.ts +6 -2
  30. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -1
  31. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  32. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  33. package/dist/runtime/components/renderers/AChartRenderer.client.d.vue.ts +17 -0
  34. package/dist/runtime/components/renderers/AChartRenderer.client.vue +622 -0
  35. package/dist/runtime/components/renderers/AChartRenderer.client.vue.d.ts +17 -0
  36. package/dist/runtime/components/renderers/AGraphRenderer.vue +64 -15
  37. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  38. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  39. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  40. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  41. package/dist/runtime/components/renderers/sheets/ASheetsGrid.d.vue.ts +2 -2
  42. package/dist/runtime/components/renderers/sheets/ASheetsGrid.vue.d.ts +2 -2
  43. package/dist/runtime/components/settings/ASettingsAppearancePanel.d.vue.ts +3 -0
  44. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue +67 -0
  45. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue.d.ts +3 -0
  46. package/dist/runtime/components/settings/ASettingsGroup.d.vue.ts +24 -0
  47. package/dist/runtime/components/settings/ASettingsGroup.vue +31 -0
  48. package/dist/runtime/components/settings/ASettingsGroup.vue.d.ts +24 -0
  49. package/dist/runtime/components/settings/ASettingsModal.vue +84 -53
  50. package/dist/runtime/components/settings/ASettingsPlaceholder.d.vue.ts +20 -0
  51. package/dist/runtime/components/settings/ASettingsPlaceholder.vue +32 -0
  52. package/dist/runtime/components/settings/ASettingsPlaceholder.vue.d.ts +20 -0
  53. package/dist/runtime/components/settings/ASettingsRow.d.vue.ts +34 -0
  54. package/dist/runtime/components/settings/ASettingsRow.vue +34 -0
  55. package/dist/runtime/components/settings/ASettingsRow.vue.d.ts +34 -0
  56. package/dist/runtime/components/settings/sections.d.ts +37 -0
  57. package/dist/runtime/components/settings/sections.js +45 -0
  58. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
  59. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
  60. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  61. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  62. package/dist/runtime/composables/useChat.d.ts +22 -1
  63. package/dist/runtime/composables/useChat.js +79 -8
  64. package/dist/runtime/composables/useNodeContextMenu.d.ts +4 -0
  65. package/dist/runtime/composables/useNodeContextMenu.js +18 -0
  66. package/dist/runtime/composables/useSettingsModal.d.ts +1 -1
  67. package/dist/runtime/locale.d.ts +8 -0
  68. package/dist/runtime/locale.js +9 -1
  69. package/dist/runtime/utils/chatContent.d.ts +20 -2
  70. package/dist/runtime/utils/chatContent.js +20 -1
  71. package/dist/runtime/utils/docTypes.js +43 -0
  72. package/dist/runtime/utils/titleSync.d.ts +130 -0
  73. package/dist/runtime/utils/titleSync.js +53 -0
  74. package/package.json +11 -1
@@ -16,12 +16,20 @@ type __VLS_Props = {
16
16
  * slots are unaffected.
17
17
  */
18
18
  tabs?: Array<'editor' | 'properties' | 'chat' | 'settings' | 'serverSettings'>;
19
+ /**
20
+ * Build the URL the "open as full page" (expand) button navigates to.
21
+ * Defaults to a slug-aware path derived from the configured `docBasePath`
22
+ * (`useDocSlugs().getDocUrl`), which resolves to the doc's slug when it has
23
+ * one and falls back to the raw id. Override to target a custom route shape.
24
+ */
25
+ docHref?: (id: string) => string;
19
26
  };
20
27
  declare var __VLS_11: {
21
28
  nodeId: string | null;
22
- nodeLabel: string | undefined;
29
+ nodeLabel: any;
23
30
  editor: any;
24
31
  meta: DocPageMeta;
32
+ activeTab: string;
25
33
  docTypeDef: import("../utils/docTypes.js").DocTypeDefinition;
26
34
  };
27
35
  type __VLS_Slots = {} & {
@@ -0,0 +1,19 @@
1
+ type __VLS_Props = {
2
+ open: boolean;
3
+ /** Current tags for the document. */
4
+ tags?: string[];
5
+ /** Optional modal heading. */
6
+ title?: string;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:open": (v: boolean) => any;
10
+ save: (tags: string[]) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:open"?: ((v: boolean) => any) | undefined;
13
+ onSave?: ((tags: string[]) => any) | undefined;
14
+ }>, {
15
+ tags: string[];
16
+ title: string;
17
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
@@ -0,0 +1,60 @@
1
+ <script setup>
2
+ import { ref, watch } from "vue";
3
+ import AModalShell from "./AModalShell.vue";
4
+ const props = defineProps({
5
+ open: { type: Boolean, required: true },
6
+ tags: { type: Array, required: false, default: () => [] },
7
+ title: { type: String, required: false, default: "Edit tags" }
8
+ });
9
+ const emit = defineEmits(["update:open", "save"]);
10
+ const draft = ref([]);
11
+ watch(() => props.open, (isOpen) => {
12
+ if (isOpen) draft.value = [...props.tags ?? []];
13
+ }, { immediate: true });
14
+ function save() {
15
+ const cleaned = Array.from(
16
+ new Set(draft.value.map((t) => t.trim()).filter(Boolean))
17
+ );
18
+ emit("save", cleaned);
19
+ emit("update:open", false);
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <AModalShell
25
+ :open="open"
26
+ :title="title"
27
+ max-width="sm:max-w-md"
28
+ @update:open="emit('update:open', $event)"
29
+ >
30
+ <UFormField
31
+ label="Tags"
32
+ hint="Press Enter to add"
33
+ >
34
+ <UInputTags
35
+ v-model="draft"
36
+ placeholder="Add a tag…"
37
+ size="md"
38
+ class="w-full"
39
+ :ui="{ root: 'w-full' }"
40
+ />
41
+ </UFormField>
42
+
43
+ <template #footer>
44
+ <div class="flex items-center justify-end gap-2 w-full">
45
+ <UButton
46
+ label="Cancel"
47
+ color="neutral"
48
+ variant="ghost"
49
+ @click="emit('update:open', false)"
50
+ />
51
+ <UButton
52
+ label="Save"
53
+ color="primary"
54
+ icon="i-lucide-check"
55
+ @click="save"
56
+ />
57
+ </div>
58
+ </template>
59
+ </AModalShell>
60
+ </template>
@@ -0,0 +1,19 @@
1
+ type __VLS_Props = {
2
+ open: boolean;
3
+ /** Current tags for the document. */
4
+ tags?: string[];
5
+ /** Optional modal heading. */
6
+ title?: string;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:open": (v: boolean) => any;
10
+ save: (tags: string[]) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:open"?: ((v: boolean) => any) | undefined;
13
+ onSave?: ((tags: string[]) => any) | undefined;
14
+ }>, {
15
+ tags: string[];
16
+ title: string;
17
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  awareness: boolean;
13
13
  tag: "video" | "audio";
14
14
  live: boolean;
15
- total: boolean;
16
15
  controls: boolean;
16
+ total: boolean;
17
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
18
  declare const _default: typeof __VLS_export;
19
19
  export default _default;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  awareness: boolean;
13
13
  tag: "video" | "audio";
14
14
  live: boolean;
15
- total: boolean;
16
15
  controls: boolean;
16
+ total: boolean;
17
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
18
  declare const _default: typeof __VLS_export;
19
19
  export default _default;
@@ -1,14 +1,14 @@
1
- /**
2
- * Chat message input with @mention autocomplete and document drop support.
3
- *
4
- * Ported from cou-sh/app/components/chat/ChatInput.vue — decoupled from
5
- * useChat/useChatUsers/usePermissions. App provides handlers via emits.
6
- */
7
1
  export interface ChatMentionUser {
8
2
  id: string;
9
3
  name: string;
10
4
  isAgent?: boolean;
11
5
  }
6
+ /** Minimal shape of the message being replied to (for the draft chip). */
7
+ export interface ReplyTarget {
8
+ id: string;
9
+ senderName?: string;
10
+ content?: string;
11
+ }
12
12
  type __VLS_Props = {
13
13
  /** Whether to auto-focus the input on mount */
14
14
  autofocus?: boolean;
@@ -17,6 +17,8 @@ type __VLS_Props = {
17
17
  canSend?: boolean;
18
18
  /** Online users for @mention autocomplete */
19
19
  mentionUsers?: ChatMentionUser[];
20
+ /** Message being replied to — shows a draft chip above the composer. */
21
+ replyTo?: ReplyTarget | null;
20
22
  };
21
23
  declare function focus(): void;
22
24
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
@@ -28,6 +30,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
28
30
  docId: string;
29
31
  label: string;
30
32
  }) => any;
33
+ "cancel-reply": () => any;
31
34
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
32
35
  onSend?: ((content: string) => any) | undefined;
33
36
  onTyping?: (() => any) | undefined;
@@ -35,7 +38,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
35
38
  docId: string;
36
39
  label: string;
37
40
  }) => any) | undefined;
41
+ "onCancel-reply"?: (() => any) | undefined;
38
42
  }>, {
43
+ replyTo: ReplyTarget | null;
39
44
  placeholder: string;
40
45
  autofocus: boolean;
41
46
  canSend: boolean;
@@ -1,12 +1,17 @@
1
1
  <script setup>
2
2
  import { ref, computed, nextTick, onMounted } from "vue";
3
+ import { previewMessageContent } from "../../utils/chatContent";
3
4
  const props = defineProps({
4
5
  autofocus: { type: Boolean, required: false, default: false },
5
6
  placeholder: { type: String, required: false, default: "Type a message\u2026" },
6
7
  canSend: { type: Boolean, required: false, default: true },
7
- mentionUsers: { type: Array, required: false, default: () => [] }
8
+ mentionUsers: { type: Array, required: false, default: () => [] },
9
+ replyTo: { type: [Object, null], required: false, default: null }
8
10
  });
9
- const emit = defineEmits(["send", "typing", "doc-drop"]);
11
+ const emit = defineEmits(["send", "typing", "doc-drop", "cancel-reply"]);
12
+ const replyPreview = computed(
13
+ () => props.replyTo?.content ? previewMessageContent(props.replyTo.content) : ""
14
+ );
10
15
  const messageInput = ref("");
11
16
  const inputRef = ref(null);
12
17
  const isDraggingDoc = ref(false);
@@ -192,6 +197,32 @@ defineExpose({ focus });
192
197
  </div>
193
198
  </Transition>
194
199
 
200
+ <!-- Reply draft chip -->
201
+ <div
202
+ v-if="replyTo"
203
+ class="flex items-center gap-2 mb-2 px-2.5 py-1.5 rounded-lg bg-(--ui-bg-elevated) border-l-2 border-(--ui-primary) text-xs"
204
+ >
205
+ <UIcon
206
+ name="i-lucide-reply"
207
+ class="size-3.5 shrink-0 text-(--ui-text-muted)"
208
+ />
209
+ <div class="min-w-0 flex-1">
210
+ <span class="text-(--ui-text-muted)">Replying to </span>
211
+ <span class="font-medium text-(--ui-text-highlighted)">{{ replyTo.senderName || "message" }}</span>
212
+ <span
213
+ v-if="replyPreview"
214
+ class="text-(--ui-text-dimmed)"
215
+ > · {{ replyPreview }}</span>
216
+ </div>
217
+ <UButton
218
+ icon="i-lucide-x"
219
+ size="xs"
220
+ color="neutral"
221
+ variant="ghost"
222
+ @click="emit('cancel-reply')"
223
+ />
224
+ </div>
225
+
195
226
  <div class="flex items-end gap-2">
196
227
  <textarea
197
228
  ref="inputRef"
@@ -1,14 +1,14 @@
1
- /**
2
- * Chat message input with @mention autocomplete and document drop support.
3
- *
4
- * Ported from cou-sh/app/components/chat/ChatInput.vue — decoupled from
5
- * useChat/useChatUsers/usePermissions. App provides handlers via emits.
6
- */
7
1
  export interface ChatMentionUser {
8
2
  id: string;
9
3
  name: string;
10
4
  isAgent?: boolean;
11
5
  }
6
+ /** Minimal shape of the message being replied to (for the draft chip). */
7
+ export interface ReplyTarget {
8
+ id: string;
9
+ senderName?: string;
10
+ content?: string;
11
+ }
12
12
  type __VLS_Props = {
13
13
  /** Whether to auto-focus the input on mount */
14
14
  autofocus?: boolean;
@@ -17,6 +17,8 @@ type __VLS_Props = {
17
17
  canSend?: boolean;
18
18
  /** Online users for @mention autocomplete */
19
19
  mentionUsers?: ChatMentionUser[];
20
+ /** Message being replied to — shows a draft chip above the composer. */
21
+ replyTo?: ReplyTarget | null;
20
22
  };
21
23
  declare function focus(): void;
22
24
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
@@ -28,6 +30,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
28
30
  docId: string;
29
31
  label: string;
30
32
  }) => any;
33
+ "cancel-reply": () => any;
31
34
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
32
35
  onSend?: ((content: string) => any) | undefined;
33
36
  onTyping?: (() => any) | undefined;
@@ -35,7 +38,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
35
38
  docId: string;
36
39
  label: string;
37
40
  }) => any) | undefined;
41
+ "onCancel-reply"?: (() => any) | undefined;
38
42
  }>, {
43
+ replyTo: ReplyTarget | null;
39
44
  placeholder: string;
40
45
  autofocus: boolean;
41
46
  canSend: boolean;
@@ -32,28 +32,40 @@ type __VLS_Props = {
32
32
  channels?: ChatChannelItem[];
33
33
  notifications?: NotificationItem[];
34
34
  notificationUnreadCount?: number;
35
+ /** Ids of muted channels (shown dimmed; right-click to toggle). */
36
+ mutedIds?: string[];
37
+ /** Ids of pinned channels (shown with a pin; right-click to toggle). */
38
+ pinnedIds?: string[];
35
39
  };
36
40
  type __VLS_ModelProps = {
37
41
  'selectedChannelId'?: string | null;
38
42
  };
39
43
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
40
44
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
45
+ hide: (channelId: string) => any;
41
46
  "select-channel": (channelId: string) => any;
42
47
  "select-notification": (notification: NotificationItem) => any;
43
48
  "mark-all-read": () => any;
44
49
  "load-more": () => any;
50
+ "toggle-mute": (channelId: string) => any;
51
+ "toggle-pin": (channelId: string) => any;
45
52
  "update:selectedChannelId": (value: string | null) => any;
46
53
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
54
+ onHide?: ((channelId: string) => any) | undefined;
47
55
  "onSelect-channel"?: ((channelId: string) => any) | undefined;
48
56
  "onSelect-notification"?: ((notification: NotificationItem) => any) | undefined;
49
57
  "onMark-all-read"?: (() => any) | undefined;
50
58
  "onLoad-more"?: (() => any) | undefined;
59
+ "onToggle-mute"?: ((channelId: string) => any) | undefined;
60
+ "onToggle-pin"?: ((channelId: string) => any) | undefined;
51
61
  "onUpdate:selectedChannelId"?: ((value: string | null) => any) | undefined;
52
62
  }>, {
53
63
  notifications: NotificationItem[];
54
64
  mode: "chat" | "notifications";
55
65
  channels: ChatChannelItem[];
56
66
  notificationUnreadCount: number;
67
+ mutedIds: string[];
68
+ pinnedIds: string[];
57
69
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
70
  declare const _default: typeof __VLS_export;
59
71
  export default _default;
@@ -1,15 +1,44 @@
1
1
  <script setup>
2
- const _props = defineProps({
2
+ const props = defineProps({
3
3
  mode: { type: String, required: false, default: "chat" },
4
4
  channels: { type: Array, required: false, default: () => [] },
5
5
  notifications: { type: Array, required: false, default: () => [] },
6
- notificationUnreadCount: { type: Number, required: false, default: 0 }
6
+ notificationUnreadCount: { type: Number, required: false, default: 0 },
7
+ mutedIds: { type: Array, required: false, default: () => [] },
8
+ pinnedIds: { type: Array, required: false, default: () => [] }
7
9
  });
8
- const emit = defineEmits(["select-channel", "select-notification", "mark-all-read", "load-more"]);
10
+ const emit = defineEmits(["select-channel", "select-notification", "mark-all-read", "load-more", "toggle-mute", "toggle-pin", "hide"]);
9
11
  const selectedChannelId = defineModel("selectedChannelId", { type: [String, null], ...{ default: null } });
10
12
  function channelIcon(ch) {
11
13
  return ch.type === "dm" ? "i-lucide-user" : "i-lucide-users";
12
14
  }
15
+ function isMutedItem(id) {
16
+ return props.mutedIds.includes(id);
17
+ }
18
+ function isPinnedItem(id) {
19
+ return props.pinnedIds.includes(id);
20
+ }
21
+ function channelMenuItems(ch) {
22
+ return [[
23
+ {
24
+ label: isPinnedItem(ch.id) ? "Unpin" : "Pin",
25
+ icon: isPinnedItem(ch.id) ? "i-lucide-pin-off" : "i-lucide-pin",
26
+ onSelect: () => emit("toggle-pin", ch.id)
27
+ },
28
+ {
29
+ label: isMutedItem(ch.id) ? "Unmute" : "Mute",
30
+ icon: isMutedItem(ch.id) ? "i-lucide-bell" : "i-lucide-bell-off",
31
+ onSelect: () => emit("toggle-mute", ch.id)
32
+ }
33
+ ], [
34
+ {
35
+ label: "Hide",
36
+ icon: "i-lucide-eye-off",
37
+ color: "error",
38
+ onSelect: () => emit("hide", ch.id)
39
+ }
40
+ ]];
41
+ }
13
42
  function formatTimestamp(ts) {
14
43
  if (!ts) return "";
15
44
  const d = new Date(ts);
@@ -142,43 +171,58 @@ function onClickNotification(n) {
142
171
  </p>
143
172
  </div>
144
173
 
145
- <div
174
+ <UContextMenu
146
175
  v-for="ch in channels"
147
176
  :key="ch.id"
148
- class="p-4 sm:px-6 text-sm cursor-pointer border-l-2 transition-colors"
149
- :class="[
177
+ :items="channelMenuItems(ch)"
178
+ >
179
+ <div
180
+ class="p-4 sm:px-6 text-sm cursor-pointer border-l-2 transition-colors"
181
+ :class="[
150
182
  ch.unreadCount > 0 ? 'text-(--ui-text-highlighted)' : 'text-(--ui-text-muted)',
183
+ isMutedItem(ch.id) ? 'opacity-60' : '',
151
184
  selectedChannelId === ch.id ? 'border-(--ui-color-primary-500) bg-(--ui-color-primary-500)/10' : 'border-transparent hover:border-(--ui-color-primary-500) hover:bg-(--ui-color-primary-500)/5'
152
185
  ]"
153
- @click="selectedChannelId = ch.id;
186
+ @click="selectedChannelId = ch.id;
154
187
  emit('select-channel', ch.id)"
155
- >
156
- <div
157
- class="flex items-center justify-between"
158
- :class="[ch.unreadCount > 0 && 'font-semibold']"
159
188
  >
160
- <div class="flex items-center gap-2.5">
161
- <UIcon
162
- :name="channelIcon(ch)"
163
- class="size-4 text-(--ui-text-dimmed) shrink-0"
164
- />
165
- <span class="truncate">{{ ch.label }}</span>
166
- <span
167
- v-if="ch.unreadCount > 0"
168
- class="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-(--ui-color-error-500) text-white text-[11px] font-bold leading-none"
169
- >{{ ch.unreadCount }}</span>
189
+ <div
190
+ class="flex items-center justify-between"
191
+ :class="[ch.unreadCount > 0 && 'font-semibold']"
192
+ >
193
+ <div class="flex items-center gap-2.5 min-w-0">
194
+ <UIcon
195
+ :name="channelIcon(ch)"
196
+ class="size-4 text-(--ui-text-dimmed) shrink-0"
197
+ />
198
+ <span class="truncate">{{ ch.label }}</span>
199
+ <UIcon
200
+ v-if="isPinnedItem(ch.id)"
201
+ name="i-lucide-pin"
202
+ class="size-3 text-(--ui-text-dimmed) shrink-0"
203
+ />
204
+ <UIcon
205
+ v-if="isMutedItem(ch.id)"
206
+ name="i-lucide-bell-off"
207
+ class="size-3 text-(--ui-text-dimmed) shrink-0"
208
+ />
209
+ <span
210
+ v-if="ch.unreadCount > 0 && !isMutedItem(ch.id)"
211
+ class="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-(--ui-color-error-500) text-white text-[11px] font-bold leading-none"
212
+ >{{ ch.unreadCount }}</span>
213
+ </div>
214
+ <span class="text-xs text-(--ui-text-dimmed) shrink-0">
215
+ {{ formatTimestamp(ch.lastMessage?.createdAt) }}
216
+ </span>
217
+ </div>
218
+ <div
219
+ v-if="ch.lastMessage"
220
+ class="flex items-center gap-1 mt-0.5 pl-6.5 min-w-0"
221
+ >
222
+ <span class="text-xs text-(--ui-text-dimmed) font-medium shrink-0">{{ ch.lastMessage.senderName }}:</span>
223
+ <span class="text-xs text-(--ui-text-dimmed) truncate flex-1">{{ ch.lastMessage.content }}</span>
170
224
  </div>
171
- <span class="text-xs text-(--ui-text-dimmed) shrink-0">
172
- {{ formatTimestamp(ch.lastMessage?.createdAt) }}
173
- </span>
174
- </div>
175
- <div
176
- v-if="ch.lastMessage"
177
- class="flex items-center gap-1 mt-0.5 pl-6.5 min-w-0"
178
- >
179
- <span class="text-xs text-(--ui-text-dimmed) font-medium shrink-0">{{ ch.lastMessage.senderName }}:</span>
180
- <span class="text-xs text-(--ui-text-dimmed) truncate flex-1">{{ ch.lastMessage.content }}</span>
181
225
  </div>
182
- </div>
226
+ </UContextMenu>
183
227
  </div>
184
228
  </template>
@@ -32,28 +32,40 @@ type __VLS_Props = {
32
32
  channels?: ChatChannelItem[];
33
33
  notifications?: NotificationItem[];
34
34
  notificationUnreadCount?: number;
35
+ /** Ids of muted channels (shown dimmed; right-click to toggle). */
36
+ mutedIds?: string[];
37
+ /** Ids of pinned channels (shown with a pin; right-click to toggle). */
38
+ pinnedIds?: string[];
35
39
  };
36
40
  type __VLS_ModelProps = {
37
41
  'selectedChannelId'?: string | null;
38
42
  };
39
43
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
40
44
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
45
+ hide: (channelId: string) => any;
41
46
  "select-channel": (channelId: string) => any;
42
47
  "select-notification": (notification: NotificationItem) => any;
43
48
  "mark-all-read": () => any;
44
49
  "load-more": () => any;
50
+ "toggle-mute": (channelId: string) => any;
51
+ "toggle-pin": (channelId: string) => any;
45
52
  "update:selectedChannelId": (value: string | null) => any;
46
53
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
54
+ onHide?: ((channelId: string) => any) | undefined;
47
55
  "onSelect-channel"?: ((channelId: string) => any) | undefined;
48
56
  "onSelect-notification"?: ((notification: NotificationItem) => any) | undefined;
49
57
  "onMark-all-read"?: (() => any) | undefined;
50
58
  "onLoad-more"?: (() => any) | undefined;
59
+ "onToggle-mute"?: ((channelId: string) => any) | undefined;
60
+ "onToggle-pin"?: ((channelId: string) => any) | undefined;
51
61
  "onUpdate:selectedChannelId"?: ((value: string | null) => any) | undefined;
52
62
  }>, {
53
63
  notifications: NotificationItem[];
54
64
  mode: "chat" | "notifications";
55
65
  channels: ChatChannelItem[];
56
66
  notificationUnreadCount: number;
67
+ mutedIds: string[];
68
+ pinnedIds: string[];
57
69
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
70
  declare const _default: typeof __VLS_export;
59
71
  export default _default;
@@ -6,6 +6,8 @@ export interface ChatMessage {
6
6
  createdAt: number;
7
7
  /** 'sending' | 'delivered' | 'read' */
8
8
  readState?: string;
9
+ /** Id of the message this one replies to (threaded reply). */
10
+ replyTo?: string;
9
11
  }
10
12
  type __VLS_Props = {
11
13
  messages: ChatMessage[];
@@ -16,9 +18,11 @@ type __VLS_Props = {
16
18
  emptySubtext?: string;
17
19
  };
18
20
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
21
+ reply: (message: ChatMessage) => any;
19
22
  "open-doc": (docId: string) => any;
20
23
  "jump-to-quote": (docId: string, from: number, to: number) => any;
21
24
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onReply?: ((message: ChatMessage) => any) | undefined;
22
26
  "onOpen-doc"?: ((docId: string) => any) | undefined;
23
27
  "onJump-to-quote"?: ((docId: string, from: number, to: number) => any) | undefined;
24
28
  }>, {
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
- import { parseMessageContent, renderMessageSegments } from "../../utils/chatContent";
3
+ import { hasRichText, parseMessageContent, previewMessageContent, renderMessageSegments } from "../../utils/chatContent";
4
4
  const props = defineProps({
5
5
  messages: { type: Array, required: true },
6
6
  currentUserId: { type: String, required: false, default: "" },
@@ -8,7 +8,15 @@ const props = defineProps({
8
8
  emptyText: { type: String, required: false, default: "No messages yet" },
9
9
  emptySubtext: { type: String, required: false, default: "Be the first to send a message" }
10
10
  });
11
- const emit = defineEmits(["open-doc", "jump-to-quote"]);
11
+ const emit = defineEmits(["open-doc", "jump-to-quote", "reply"]);
12
+ const messageById = computed(() => {
13
+ const map = /* @__PURE__ */ new Map();
14
+ for (const m of props.messages) map.set(m.id, m);
15
+ return map;
16
+ });
17
+ function repliedTo(msg) {
18
+ return msg.replyTo ? messageById.value.get(msg.replyTo) : void 0;
19
+ }
12
20
  const groupedMessages = computed(() => {
13
21
  return props.messages.map((msg, i) => {
14
22
  const prev = props.messages[i - 1];
@@ -27,7 +35,7 @@ function isSelf(senderId) {
27
35
  return senderId === props.currentUserId;
28
36
  }
29
37
  function hasRichContent(text) {
30
- return /@\w+/.test(text) || /https?:\/\//.test(text);
38
+ return hasRichText(text);
31
39
  }
32
40
  </script>
33
41
 
@@ -70,12 +78,44 @@ function hasRichContent(text) {
70
78
  </div>
71
79
 
72
80
  <div
73
- class="max-w-[75%] rounded-xl px-3 py-2"
81
+ class="group relative max-w-[75%] rounded-xl px-3 py-2"
74
82
  :class="[
75
83
  isSelf(msg.senderId) ? 'bg-(--ui-color-primary-100) dark:bg-(--ui-color-primary-900)/30' : 'bg-(--ui-bg-elevated)',
76
84
  msg.isFirstInGroup ? '' : isSelf(msg.senderId) ? 'rounded-tr-sm' : 'rounded-tl-sm'
77
85
  ]"
78
86
  >
87
+ <!-- Reply affordance — appears on hover -->
88
+ <UButton
89
+ icon="i-lucide-reply"
90
+ size="xs"
91
+ color="neutral"
92
+ variant="ghost"
93
+ class="absolute -top-2 opacity-0 group-hover:opacity-100 transition-opacity bg-(--ui-bg-elevated) ring-1 ring-(--ui-border)"
94
+ :class="isSelf(msg.senderId) ? '-left-2' : '-right-2'"
95
+ :aria-label="`Reply to ${msg.senderName}`"
96
+ @click="emit('reply', msg)"
97
+ />
98
+
99
+ <!-- Quoted preview of the message being replied to -->
100
+ <button
101
+ v-if="msg.replyTo"
102
+ type="button"
103
+ class="flex items-center gap-1.5 w-full mb-1 px-2 py-1 rounded-md bg-(--ui-bg)/50 border-l-2 border-(--ui-primary) text-left"
104
+ @click="repliedTo(msg) && emit('reply', repliedTo(msg))"
105
+ >
106
+ <UIcon
107
+ name="i-lucide-reply"
108
+ class="size-3 shrink-0 text-(--ui-text-dimmed)"
109
+ />
110
+ <span class="text-[11px] text-(--ui-text-muted) truncate">
111
+ <template v-if="repliedTo(msg)">
112
+ <span class="font-medium">{{ repliedTo(msg).senderName || "message" }}</span>
113
+ · {{ previewMessageContent(repliedTo(msg).content) }}
114
+ </template>
115
+ <template v-else>Replied to an earlier message</template>
116
+ </span>
117
+ </button>
118
+
79
119
  <!-- Name + time: first in group -->
80
120
  <div
81
121
  v-if="msg.isFirstInGroup"
@@ -123,6 +163,19 @@ function hasRichContent(text) {
123
163
  v-else-if="seg.type === 'mention'"
124
164
  class="text-(--ui-color-primary-500) font-medium"
125
165
  >@{{ seg.name }}</span>
166
+ <strong
167
+ v-else-if="seg.type === 'bold'"
168
+ class="font-semibold"
169
+ >{{ seg.value }}</strong>
170
+ <em v-else-if="seg.type === 'italic'">{{ seg.value }}</em>
171
+ <span
172
+ v-else-if="seg.type === 'strike'"
173
+ class="line-through"
174
+ >{{ seg.value }}</span>
175
+ <code
176
+ v-else-if="seg.type === 'code'"
177
+ class="px-1 py-0.5 rounded bg-(--ui-bg-accented) font-mono text-[0.85em]"
178
+ >{{ seg.value }}</code>
126
179
  <template v-else>
127
180
  {{ seg.value }}
128
181
  </template>
@@ -6,6 +6,8 @@ export interface ChatMessage {
6
6
  createdAt: number;
7
7
  /** 'sending' | 'delivered' | 'read' */
8
8
  readState?: string;
9
+ /** Id of the message this one replies to (threaded reply). */
10
+ replyTo?: string;
9
11
  }
10
12
  type __VLS_Props = {
11
13
  messages: ChatMessage[];
@@ -16,9 +18,11 @@ type __VLS_Props = {
16
18
  emptySubtext?: string;
17
19
  };
18
20
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
21
+ reply: (message: ChatMessage) => any;
19
22
  "open-doc": (docId: string) => any;
20
23
  "jump-to-quote": (docId: string, from: number, to: number) => any;
21
24
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onReply?: ((message: ChatMessage) => any) | undefined;
22
26
  "onOpen-doc"?: ((docId: string) => any) | undefined;
23
27
  "onJump-to-quote"?: ((docId: string, from: number, to: number) => any) | undefined;
24
28
  }>, {