@burdenoff/microfe-conversations 2026.830.1 → 2026.924.1

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.
@@ -1,8 +1,8 @@
1
- import { cn as e } from "../utils/cn.js";
2
- import { memo as t, useState as n } from "react";
3
- import { Check as r, CheckCheck as i, FileText as a, Image as o, MoreHorizontal as s, Pencil as c, Pin as l, Reply as u, SmilePlus as d, Trash2 as f } from "lucide-react";
4
- import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
5
- import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
1
+ import { useTr as e } from "../utils/i18n.js";
2
+ import { cn as t } from "../utils/cn.js";
3
+ import { memo as n, useState as r } from "react";
4
+ import { Check as i, CheckCheck as a, FileText as o, Image as s, MoreHorizontal as c, Pencil as l, Pin as u, Reply as d, SmilePlus as f, Trash2 as p } from "lucide-react";
5
+ import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
6
6
  //#region src/conversations/components/MessageBubble.tsx
7
7
  function _(e) {
8
8
  return new Date(e).toLocaleTimeString(void 0, {
@@ -12,20 +12,20 @@ function _(e) {
12
12
  }
13
13
  function v({ status: e, failedLabel: t }) {
14
14
  switch (e) {
15
- case "SENDING": return /* @__PURE__ */ m("span", { className: "size-3 rounded-full border border-muted-foreground animate-pulse" });
16
- case "SENT": return /* @__PURE__ */ m(r, {
15
+ case "SENDING": return /* @__PURE__ */ h("span", { className: "size-3 rounded-full border border-muted-foreground animate-pulse" });
16
+ case "SENT": return /* @__PURE__ */ h(i, {
17
17
  size: 12,
18
18
  className: "text-text-secondary"
19
19
  });
20
- case "DELIVERED": return /* @__PURE__ */ m(i, {
20
+ case "DELIVERED": return /* @__PURE__ */ h(a, {
21
21
  size: 12,
22
22
  className: "text-text-secondary"
23
23
  });
24
- case "READ": return /* @__PURE__ */ m(i, {
24
+ case "READ": return /* @__PURE__ */ h(a, {
25
25
  size: 12,
26
26
  className: "text-primary"
27
27
  });
28
- case "FAILED": return /* @__PURE__ */ m("span", {
28
+ case "FAILED": return /* @__PURE__ */ h("span", {
29
29
  className: "text-[10px] text-status-error-text font-medium",
30
30
  children: t
31
31
  });
@@ -35,161 +35,164 @@ function v({ status: e, failedLabel: t }) {
35
35
  function y(e) {
36
36
  return e.startsWith("image/");
37
37
  }
38
- var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showSender: b = !0, onReply: C, onReact: w, onEdit: T, onDelete: E, onTogglePin: D }) {
39
- let [O, k] = n(!1), [A, j] = n(!1), [M, N] = n(!1), { t: P } = g(), F = (e, t) => {
40
- let n = P(e);
41
- return n === e ? t : n;
42
- }, { sender: I, content: L, type: R, status: z, reactions: B, attachments: V, isEdited: H, createdAt: U, isPinned: W } = t, G = R === "SYSTEM", K = M || O || A;
43
- return G ? /* @__PURE__ */ m("div", {
38
+ var b = n(function({ message: n, isOwnMessage: i = !1, showAvatar: a = !0, showSender: b = !0, onReply: C, onReact: w, onEdit: T, onDelete: E, onTogglePin: D }) {
39
+ let [O, k] = r(!1), [A, j] = r(!1), [M, N] = r(!1), P = e(), { sender: F, content: I, type: L, status: R, reactions: z, attachments: B, isEdited: V, createdAt: H, isPinned: U } = n, W = L === "SYSTEM", G = M || O || A;
40
+ return W ? /* @__PURE__ */ h("div", {
44
41
  className: "flex justify-center my-2 px-4",
45
- children: /* @__PURE__ */ m("span", {
42
+ children: /* @__PURE__ */ h("span", {
46
43
  className: "text-xs text-text-secondary bg-bg-sunken/50 px-3 py-1 rounded-full",
47
- children: L
44
+ children: I
48
45
  })
49
- }) : /* @__PURE__ */ h("div", {
46
+ }) : /* @__PURE__ */ g("div", {
50
47
  onMouseEnter: () => N(!0),
51
48
  onMouseLeave: () => N(!1),
52
- className: e("group flex gap-2 px-4 py-1", r ? "flex-row-reverse" : "flex-row"),
49
+ className: t("group flex gap-2 px-4 py-1", i ? "flex-row-reverse" : "flex-row"),
53
50
  children: [
54
- i ? /* @__PURE__ */ m("div", {
55
- className: e("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", r ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
56
- children: I.avatarUrl ? /* @__PURE__ */ m("img", {
57
- src: I.avatarUrl,
58
- alt: I.name,
51
+ a ? /* @__PURE__ */ h("div", {
52
+ className: t("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", i ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
53
+ children: F.avatarUrl ? /* @__PURE__ */ h("img", {
54
+ src: F.avatarUrl,
55
+ alt: F.name,
59
56
  className: "w-full h-full rounded-full object-cover"
60
- }) : I.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
61
- }) : /* @__PURE__ */ m("div", { className: "w-8 flex-shrink-0" }),
62
- /* @__PURE__ */ h("div", {
63
- className: e("flex flex-col max-w-[85%] sm:max-w-[70%]", r ? "items-end" : "items-start"),
57
+ }) : F.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
58
+ }) : /* @__PURE__ */ h("div", { className: "w-8 flex-shrink-0" }),
59
+ /* @__PURE__ */ g("div", {
60
+ className: t("flex flex-col max-w-[85%] sm:max-w-[70%]", i ? "items-end" : "items-start"),
64
61
  children: [
65
- b && !r && /* @__PURE__ */ h("div", {
62
+ b && !i && /* @__PURE__ */ g("div", {
66
63
  className: "flex items-center gap-1.5 mb-0.5",
67
- children: [/* @__PURE__ */ m("span", {
64
+ children: [/* @__PURE__ */ h("span", {
68
65
  className: "text-xs font-semibold text-text-primary",
69
- children: I.name
70
- }), W && /* @__PURE__ */ m(l, {
66
+ children: F.name
67
+ }), U && /* @__PURE__ */ h(u, {
71
68
  size: 10,
72
69
  className: "text-status-warning-text"
73
70
  })]
74
71
  }),
75
- /* @__PURE__ */ h("div", {
76
- className: e("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", r ? "bg-action-primary-bg text-action-primary-text rounded-tr-md" : "bg-bg-sunken/70 text-text-primary rounded-tl-md"),
77
- children: [/* @__PURE__ */ m("p", {
72
+ /* @__PURE__ */ g("div", {
73
+ className: t("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", i ? "bg-action-primary-bg text-action-primary-text rounded-tr-md" : "bg-bg-sunken/70 text-text-primary rounded-tl-md"),
74
+ children: [/* @__PURE__ */ h("p", {
78
75
  className: "whitespace-pre-wrap break-words",
79
- children: L
80
- }), V.length > 0 && /* @__PURE__ */ m("div", {
76
+ children: I
77
+ }), B.length > 0 && /* @__PURE__ */ h("div", {
81
78
  className: "mt-2 grid gap-2",
82
- children: V.map((t) => {
83
- let n = e("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", r ? "bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15" : "bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70"), i = /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m("div", {
79
+ children: B.map((e) => {
80
+ let n = t("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", i ? "bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15" : "bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70"), r = /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h("div", {
84
81
  className: "flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/40",
85
- children: t.thumbnail && y(t.mimeType) ? /* @__PURE__ */ m("img", {
86
- src: t.thumbnail,
87
- alt: t.fileName,
82
+ children: e.thumbnail && y(e.mimeType) ? /* @__PURE__ */ h("img", {
83
+ src: e.thumbnail,
84
+ alt: e.fileName,
88
85
  className: "h-full w-full object-contain"
89
- }) : y(t.mimeType) ? /* @__PURE__ */ m(o, { size: 16 }) : /* @__PURE__ */ m(a, { size: 16 })
90
- }), /* @__PURE__ */ h("div", {
86
+ }) : y(e.mimeType) ? /* @__PURE__ */ h(s, { size: 16 }) : /* @__PURE__ */ h(o, { size: 16 })
87
+ }), /* @__PURE__ */ g("div", {
91
88
  className: "min-w-0 flex-1",
92
- children: [/* @__PURE__ */ m("div", {
89
+ children: [/* @__PURE__ */ h("div", {
93
90
  className: "truncate font-medium",
94
- children: t.fileName
95
- }), /* @__PURE__ */ h("div", {
91
+ children: e.fileName
92
+ }), /* @__PURE__ */ g("div", {
96
93
  className: "truncate text-[10px] opacity-70",
97
94
  children: [
98
- S(t.fileSize),
99
- " - ",
100
- t.mimeType
95
+ S(e.fileSize, {
96
+ B: P("conversations.fileSizeUnits.B", "B"),
97
+ KB: P("conversations.fileSizeUnits.KB", "KB"),
98
+ MB: P("conversations.fileSizeUnits.MB", "MB"),
99
+ GB: P("conversations.fileSizeUnits.GB", "GB")
100
+ }),
101
+ " ",
102
+ "- ",
103
+ e.mimeType
101
104
  ]
102
105
  })]
103
106
  })] });
104
- return t.downloadUrl ? /* @__PURE__ */ m("a", {
105
- href: t.downloadUrl,
107
+ return e.downloadUrl ? /* @__PURE__ */ h("a", {
108
+ href: e.downloadUrl,
106
109
  target: "_blank",
107
110
  rel: "noreferrer",
108
111
  className: n,
109
- title: t.fileName,
110
- children: i
111
- }, t.id) : /* @__PURE__ */ m("div", {
112
+ title: e.fileName,
113
+ children: r
114
+ }, e.id) : /* @__PURE__ */ h("div", {
112
115
  className: n,
113
- title: t.fileName,
114
- children: i
115
- }, t.id);
116
+ title: e.fileName,
117
+ children: r
118
+ }, e.id);
116
119
  })
117
120
  })]
118
121
  }),
119
- B.length > 0 && /* @__PURE__ */ m("div", {
122
+ z.length > 0 && /* @__PURE__ */ h("div", {
120
123
  className: "flex flex-wrap gap-1 mt-1",
121
- children: B.map((n) => /* @__PURE__ */ h("button", {
124
+ children: z.map((e) => /* @__PURE__ */ g("button", {
122
125
  type: "button",
123
- onClick: () => w?.(t.id, n.emoji),
124
- className: e("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", n.currentUserReacted ? "bg-action-primary-bg/10 border-primary/30 text-primary" : "bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken"),
125
- children: [/* @__PURE__ */ m("span", { children: n.emoji }), /* @__PURE__ */ m("span", {
126
+ onClick: () => w?.(n.id, e.emoji),
127
+ className: t("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", e.currentUserReacted ? "bg-action-primary-bg/10 border-primary/30 text-primary" : "bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken"),
128
+ children: [/* @__PURE__ */ h("span", { children: e.emoji }), /* @__PURE__ */ h("span", {
126
129
  className: "font-medium",
127
- children: n.count
130
+ children: e.count
128
131
  })]
129
- }, n.emoji))
132
+ }, e.emoji))
130
133
  }),
131
- /* @__PURE__ */ h("div", {
134
+ /* @__PURE__ */ g("div", {
132
135
  className: "flex items-center gap-1.5 mt-0.5 px-1",
133
136
  children: [
134
- /* @__PURE__ */ m("span", {
137
+ /* @__PURE__ */ h("span", {
135
138
  className: "text-[10px] text-text-secondary",
136
- children: _(U)
139
+ children: _(H)
137
140
  }),
138
- H && /* @__PURE__ */ m("span", {
141
+ V && /* @__PURE__ */ h("span", {
139
142
  className: "text-[10px] text-text-secondary italic",
140
- children: F("conversations.edited", "edited")
143
+ children: P("conversations.edited", "edited")
141
144
  }),
142
- r && /* @__PURE__ */ m(v, {
143
- status: z,
144
- failedLabel: F("conversations.failed", "Failed")
145
+ i && /* @__PURE__ */ h(v, {
146
+ status: R,
147
+ failedLabel: P("conversations.failed", "Failed")
145
148
  })
146
149
  ]
147
150
  })
148
151
  ]
149
152
  }),
150
- /* @__PURE__ */ h("div", {
153
+ /* @__PURE__ */ g("div", {
151
154
  "data-tour": "conversation-message-actions",
152
155
  role: "toolbar",
153
- "aria-label": F("conversations.messageActions", "Message actions"),
156
+ "aria-label": P("conversations.messageActions", "Message actions"),
154
157
  tabIndex: 0,
155
158
  onFocus: () => N(!0),
156
159
  onBlur: (e) => {
157
160
  e.currentTarget.contains(e.relatedTarget) || N(!1);
158
161
  },
159
- className: e("relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", K && "sm:opacity-100"),
162
+ className: t("relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", G && "sm:opacity-100"),
160
163
  children: [
161
- t.permissions.canReact && /* @__PURE__ */ m(x, {
162
- icon: /* @__PURE__ */ m(d, { size: 14 }),
163
- label: F("conversations.react", "React"),
164
+ n.permissions.canReact && /* @__PURE__ */ h(x, {
165
+ icon: /* @__PURE__ */ h(f, { size: 14 }),
166
+ label: P("conversations.react", "React"),
164
167
  pressed: O,
165
168
  onClick: () => {
166
169
  k((e) => !e), j(!1);
167
170
  }
168
171
  }),
169
- t.permissions.canReply && /* @__PURE__ */ m(x, {
170
- icon: /* @__PURE__ */ m(u, { size: 14 }),
171
- label: F("conversations.reply", "Reply"),
172
- onClick: () => C?.(t)
172
+ n.permissions.canReply && /* @__PURE__ */ h(x, {
173
+ icon: /* @__PURE__ */ h(d, { size: 14 }),
174
+ label: P("conversations.reply", "Reply"),
175
+ onClick: () => C?.(n)
173
176
  }),
174
- t.permissions.canEdit && /* @__PURE__ */ m(x, {
175
- icon: /* @__PURE__ */ m(c, { size: 14 }),
176
- label: F("conversations.edit", "Edit"),
177
- onClick: () => T?.(t)
177
+ n.permissions.canEdit && /* @__PURE__ */ h(x, {
178
+ icon: /* @__PURE__ */ h(l, { size: 14 }),
179
+ label: P("conversations.edit", "Edit"),
180
+ onClick: () => T?.(n)
178
181
  }),
179
- t.permissions.canDelete && /* @__PURE__ */ m(x, {
180
- icon: /* @__PURE__ */ m(f, { size: 14 }),
181
- label: F("conversations.delete", "Delete"),
182
- onClick: () => E?.(t.id)
182
+ n.permissions.canDelete && /* @__PURE__ */ h(x, {
183
+ icon: /* @__PURE__ */ h(p, { size: 14 }),
184
+ label: P("conversations.delete", "Delete"),
185
+ onClick: () => E?.(n.id)
183
186
  }),
184
- /* @__PURE__ */ m(x, {
185
- icon: /* @__PURE__ */ m(s, { size: 14 }),
186
- label: F("conversations.more", "More"),
187
+ /* @__PURE__ */ h(x, {
188
+ icon: /* @__PURE__ */ h(c, { size: 14 }),
189
+ label: P("conversations.more", "More"),
187
190
  pressed: A,
188
191
  onClick: () => {
189
192
  j((e) => !e), k(!1);
190
193
  }
191
194
  }),
192
- O && /* @__PURE__ */ m("div", {
195
+ O && /* @__PURE__ */ h("div", {
193
196
  className: "absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg",
194
197
  children: [
195
198
  "👍",
@@ -197,25 +200,25 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
197
200
  "🎉",
198
201
  "👀",
199
202
  "✅"
200
- ].map((e) => /* @__PURE__ */ m("button", {
203
+ ].map((e) => /* @__PURE__ */ h("button", {
201
204
  type: "button",
202
205
  onClick: () => {
203
- w?.(t.id, e), k(!1);
206
+ w?.(n.id, e), k(!1);
204
207
  },
205
208
  className: "rounded-md px-2 py-1 text-sm hover:bg-bg-sunken/80",
206
- "aria-label": `${F("conversations.reactWith", "React with")} ${e}`,
209
+ "aria-label": `${P("conversations.reactWith", "React with")} ${e}`,
207
210
  children: e
208
211
  }, e))
209
212
  }),
210
- A && t.permissions.canPin && D && /* @__PURE__ */ m("div", {
213
+ A && n.permissions.canPin && D && /* @__PURE__ */ h("div", {
211
214
  className: "absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg",
212
- children: /* @__PURE__ */ h("button", {
215
+ children: /* @__PURE__ */ g("button", {
213
216
  type: "button",
214
217
  onClick: () => {
215
- D(t.id, t.isPinned), j(!1);
218
+ D(n.id, n.isPinned), j(!1);
216
219
  },
217
220
  className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken/80",
218
- children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? F("conversations.unpinMessage", "Unpin message") : F("conversations.pinMessage", "Pin message")]
221
+ children: [/* @__PURE__ */ h(u, { size: 14 }), n.isPinned ? P("conversations.unpinMessage", "Unpin message") : P("conversations.pinMessage", "Pin message")]
219
222
  })
220
223
  })
221
224
  ]
@@ -224,7 +227,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
224
227
  });
225
228
  });
226
229
  function x({ icon: e, label: t, onClick: n, pressed: r }) {
227
- return /* @__PURE__ */ m("button", {
230
+ return /* @__PURE__ */ h("button", {
228
231
  type: "button",
229
232
  onClick: n,
230
233
  "aria-label": t,
@@ -234,15 +237,15 @@ function x({ icon: e, label: t, onClick: n, pressed: r }) {
234
237
  children: e
235
238
  });
236
239
  }
237
- function S(e) {
238
- if (e === 0) return "0 B";
239
- let t = 1024, n = [
240
- "B",
241
- "KB",
242
- "MB",
243
- "GB"
244
- ], r = Math.floor(Math.log(e) / Math.log(t));
245
- return `${parseFloat((e / t ** +r).toFixed(1))} ${n[r]}`;
240
+ function S(e, t) {
241
+ if (e === 0) return `0 ${t.B}`;
242
+ let n = 1024, r = [
243
+ t.B,
244
+ t.KB,
245
+ t.MB,
246
+ t.GB
247
+ ], i = Math.floor(Math.log(e) / Math.log(n));
248
+ return `${parseFloat((e / n ** +i).toFixed(1))} ${r[i]}`;
246
249
  }
247
250
  //#endregion
248
251
  export { b as MessageBubble };
@@ -1 +1 @@
1
- {"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"size-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-text-secondary\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-text-secondary\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-status-error-text font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const [showActions, setShowActions] = useState(false);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n const actionsVisible = showActions || showReactionPicker || showMoreMenu;\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-text-secondary bg-bg-sunken/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-text-primary\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-action-primary-bg text-action-primary-text rounded-tr-md\"\n : \"bg-bg-sunken/70 text-text-primary rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15\"\n : \"bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-action-primary-bg/10 border-primary/30 text-primary\"\n : \"bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-text-secondary\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-text-secondary italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n <div\n data-tour=\"conversation-message-actions\"\n role=\"toolbar\"\n aria-label={tr(\"conversations.messageActions\", \"Message actions\")}\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"sm:opacity-100\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-bg-sunken/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"flex min-h-9 min-w-9 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-bg-sunken/80 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"size-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-text-secondary\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-text-secondary\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-status-error-text font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const [showActions, setShowActions] = useState(false);\n const tr = useTr();\n\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n const actionsVisible = showActions || showReactionPicker || showMoreMenu;\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-text-secondary bg-bg-sunken/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-text-primary\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-action-primary-bg text-action-primary-text rounded-tr-md\"\n : \"bg-bg-sunken/70 text-text-primary rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15\"\n : \"bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize, {\n B: tr(\"conversations.fileSizeUnits.B\", \"B\"),\n KB: tr(\"conversations.fileSizeUnits.KB\", \"KB\"),\n MB: tr(\"conversations.fileSizeUnits.MB\", \"MB\"),\n GB: tr(\"conversations.fileSizeUnits.GB\", \"GB\"),\n })}{\" \"}\n - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-action-primary-bg/10 border-primary/30 text-primary\"\n : \"bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-text-secondary\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-text-secondary italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n <div\n data-tour=\"conversation-message-actions\"\n role=\"toolbar\"\n aria-label={tr(\"conversations.messageActions\", \"Message actions\")}\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"sm:opacity-100\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-bg-sunken/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"flex min-h-9 min-w-9 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-bg-sunken/80 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(\n bytes: number,\n labels: { B: string; KB: string; MB: string; GB: string },\n): string {\n if (bytes === 0) return `0 ${labels.B}`;\n const k = 1024;\n const sizes = [labels.B, labels.KB, labels.MB, labels.GB];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
@@ -1,23 +1,20 @@
1
- import { AccessDenied as e } from "@burdenoff/fe-libs/shared/components";
2
- import { Loader2 as t } from "lucide-react";
3
- import { useAuth as n, usePermissions as r } from "@burdenoff/fe-libs/shared/providers/shell";
4
- import { Fragment as i, jsx as a } from "react/jsx-runtime";
5
- import { useI18n as o } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
1
+ import { useTr as e } from "../utils/i18n.js";
2
+ import { AccessDenied as t } from "@burdenoff/fe-libs/shared/components";
3
+ import { Loader2 as n } from "lucide-react";
4
+ import { useAuth as r, usePermissions as i } from "@burdenoff/fe-libs/shared/providers/shell";
5
+ import { Fragment as a, jsx as o } from "react/jsx-runtime";
6
6
  //#region src/conversations/components/PermissionGuard.tsx
7
7
  var s = ({ permission: s, children: c, allowUnavailablePermissions: l = !1 }) => {
8
- let { isAuthenticated: u, tokens: d } = n(), { error: f, hasPermission: p, isLoading: m, permissions: h, query: g, scopeId: _ } = r(), { t: v } = o(), y = (e, t) => {
9
- let n = v(e);
10
- return n === e ? t : n;
11
- };
12
- if (m) return /* @__PURE__ */ a("div", {
8
+ let { isAuthenticated: u, tokens: d } = r(), { error: f, hasPermission: p, isLoading: m, permissions: h, query: g, scopeId: _ } = i(), v = e();
9
+ if (m) return /* @__PURE__ */ o("div", {
13
10
  className: "flex min-h-screen items-center justify-center bg-bg-canvas",
14
- children: /* @__PURE__ */ a(t, { className: "size-8 animate-spin text-primary" })
11
+ children: /* @__PURE__ */ o(n, { className: "size-8 animate-spin text-primary" })
15
12
  });
16
- let b = !h && !f && u && !!d?.accessToken && !!_ && g.status === "pending" && g.fetchStatus === "idle";
17
- return !p(s) && !(l && b) ? /* @__PURE__ */ a(e, {
18
- title: y("conversations.accessDenied", "Access Denied"),
19
- description: y("conversations.noPermissionView", "You don't have permission to view this page.")
20
- }) : /* @__PURE__ */ a(i, { children: c });
13
+ let y = !h && !f && u && !!d?.accessToken && !!_ && g.status === "pending" && g.fetchStatus === "idle";
14
+ return !p(s) && !(l && y) ? /* @__PURE__ */ o(t, {
15
+ title: v("conversations.accessDenied", "Access Denied"),
16
+ description: v("conversations.noPermissionView", "You don't have permission to view this page.")
17
+ }) : /* @__PURE__ */ o(a, { children: c });
21
18
  };
22
19
  //#endregion
23
20
  export { s as PermissionGuard };
@@ -1 +1 @@
1
- {"version":3,"file":"PermissionGuard.js","names":[],"sources":["../../../src/conversations/components/PermissionGuard.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport {\n useAuth,\n usePermissions,\n} from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { AccessDenied } from \"@burdenoff/fe-libs/shared/components\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport type { ConversationsPermission } from \"../constants/permissions\";\n\ninterface PermissionGuardProps {\n permission: ConversationsPermission;\n children: ReactNode;\n allowUnavailablePermissions?: boolean;\n}\n\n/**\n * Wraps page content with a permission check.\n * Shows a loading spinner while permissions resolve, then either\n * renders children (if permitted) or an AccessDenied screen.\n *\n * `allowUnavailablePermissions` is for user-facing routes whose backend\n * resolvers remain authoritative but whose host shell may not have hydrated\n * local RBAC context yet. It only permits rendering when permission data is\n * unavailable; explicit loaded denials still render AccessDenied.\n */\nexport const PermissionGuard: FC<PermissionGuardProps> = ({\n permission,\n children,\n allowUnavailablePermissions = false,\n}) => {\n const { isAuthenticated, tokens } = useAuth();\n const { error, hasPermission, isLoading, permissions, query, scopeId } =\n usePermissions();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n if (isLoading) {\n return (\n <div className=\"flex min-h-screen items-center justify-center bg-bg-canvas\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n }\n\n const permissionHydrationPending =\n !permissions &&\n !error &&\n isAuthenticated &&\n !!tokens?.accessToken &&\n !!scopeId &&\n query.status === \"pending\" &&\n query.fetchStatus === \"idle\";\n\n if (\n !hasPermission(permission) &&\n !(allowUnavailablePermissions && permissionHydrationPending)\n ) {\n return (\n <AccessDenied\n title={tr(\"conversations.accessDenied\", \"Access Denied\")}\n description={tr(\n \"conversations.noPermissionView\",\n \"You don't have permission to view this page.\",\n )}\n />\n );\n }\n\n return <>{children}</>;\n};\n"],"mappings":";;;;;;AA0BA,IAAa,KAA6C,EACxD,eACA,aACA,iCAA8B,SAC1B;CACJ,IAAM,EAAE,oBAAiB,cAAW,GAAS,EACvC,EAAE,UAAO,kBAAe,cAAW,gBAAa,UAAO,eAC3D,GAAgB,EACZ,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;;AAGpC,KAAI,EACF,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAS,WAAU,oCAAqC,CAAA;EACpD,CAAA;CAIV,IAAM,IACJ,CAAC,KACD,CAAC,KACD,KACA,CAAC,CAAC,GAAQ,eACV,CAAC,CAAC,KACF,EAAM,WAAW,aACjB,EAAM,gBAAgB;AAiBxB,QAdE,CAAC,EAAc,EAAW,IAC1B,EAAE,KAA+B,KAG/B,kBAAC,GAAD;EACE,OAAO,EAAG,8BAA8B,gBAAgB;EACxD,aAAa,EACX,kCACA,+CACD;EACD,CAAA,GAIC,kBAAA,GAAA,EAAG,aAAY,CAAA"}
1
+ {"version":3,"file":"PermissionGuard.js","names":[],"sources":["../../../src/conversations/components/PermissionGuard.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport {\n useAuth,\n usePermissions,\n} from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { AccessDenied } from \"@burdenoff/fe-libs/shared/components\";\nimport { useTr } from \"../utils/i18n\";\nimport type { ConversationsPermission } from \"../constants/permissions\";\n\ninterface PermissionGuardProps {\n permission: ConversationsPermission;\n children: ReactNode;\n allowUnavailablePermissions?: boolean;\n}\n\n/**\n * Wraps page content with a permission check.\n * Shows a loading spinner while permissions resolve, then either\n * renders children (if permitted) or an AccessDenied screen.\n *\n * `allowUnavailablePermissions` is for user-facing routes whose backend\n * resolvers remain authoritative but whose host shell may not have hydrated\n * local RBAC context yet. It only permits rendering when permission data is\n * unavailable; explicit loaded denials still render AccessDenied.\n */\nexport const PermissionGuard: FC<PermissionGuardProps> = ({\n permission,\n children,\n allowUnavailablePermissions = false,\n}) => {\n const { isAuthenticated, tokens } = useAuth();\n const { error, hasPermission, isLoading, permissions, query, scopeId } =\n usePermissions();\n const tr = useTr();\n\n if (isLoading) {\n return (\n <div className=\"flex min-h-screen items-center justify-center bg-bg-canvas\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n }\n\n const permissionHydrationPending =\n !permissions &&\n !error &&\n isAuthenticated &&\n !!tokens?.accessToken &&\n !!scopeId &&\n query.status === \"pending\" &&\n query.fetchStatus === \"idle\";\n\n if (\n !hasPermission(permission) &&\n !(allowUnavailablePermissions && permissionHydrationPending)\n ) {\n return (\n <AccessDenied\n title={tr(\"conversations.accessDenied\", \"Access Denied\")}\n description={tr(\n \"conversations.noPermissionView\",\n \"You don't have permission to view this page.\",\n )}\n />\n );\n }\n\n return <>{children}</>;\n};\n"],"mappings":";;;;;;AA0BA,IAAa,KAA6C,EACxD,eACA,aACA,iCAA8B,SAC1B;CACJ,IAAM,EAAE,oBAAiB,cAAW,GAAS,EACvC,EAAE,UAAO,kBAAe,cAAW,gBAAa,UAAO,eAC3D,GAAgB,EACZ,IAAK,GAAO;AAElB,KAAI,EACF,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAS,WAAU,oCAAqC,CAAA;EACpD,CAAA;CAIV,IAAM,IACJ,CAAC,KACD,CAAC,KACD,KACA,CAAC,CAAC,GAAQ,eACV,CAAC,CAAC,KACF,EAAM,WAAW,aACjB,EAAM,gBAAgB;AAiBxB,QAdE,CAAC,EAAc,EAAW,IAC1B,EAAE,KAA+B,KAG/B,kBAAC,GAAD;EACE,OAAO,EAAG,8BAA8B,gBAAgB;EACxD,aAAa,EACX,kCACA,+CACD;EACD,CAAA,GAIC,kBAAA,GAAA,EAAG,aAAY,CAAA"}
@@ -3,3 +3,4 @@ import "./ConversationsRoot.js";
3
3
  import "./ConversationsUserRoot.js";
4
4
  import "./ConversationsAdminRoot.js";
5
5
  import "./store/conversationsStore.js";
6
+ import "../translations/en.js";