@burdenoff/microfe-conversations 2026.625.2 → 2026.625.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/components/InboxLayout.js +85 -78
- package/dist/conversations/components/InboxLayout.js.map +1 -1
- package/dist/conversations/pages/AdminConversationsLandingPage.js +107 -61
- package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js +100 -69
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +311 -239
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +64 -56
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,118 +3,125 @@ import { useTr as t } from "../utils/i18n.js";
|
|
|
3
3
|
import { cn as n } from "../utils/cn.js";
|
|
4
4
|
import { useConversationsStore as r } from "../store/conversationsStore.js";
|
|
5
5
|
import { useCallback as i, useMemo as a } from "react";
|
|
6
|
-
import { ArrowLeft as o, MessageSquarePlus as s,
|
|
7
|
-
import { useLocation as
|
|
8
|
-
import { jsx as
|
|
9
|
-
import { AuroraBackground as
|
|
10
|
-
import { Tabs as
|
|
6
|
+
import { ArrowLeft as o, MessageSquarePlus as s, MessagesSquare as c, Search as l, X as u } from "lucide-react";
|
|
7
|
+
import { useLocation as d, useNavigate as f, useParams as p } from "react-router-dom";
|
|
8
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
9
|
+
import { AuroraBackground as g, Button as _, GradientText as v, IllustratedEmptyState as y, PagePurpose as b } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
import { Tabs as x } from "@burdenoff/fe-libs/chrome";
|
|
11
11
|
//#region src/conversations/components/InboxLayout.tsx
|
|
12
|
-
function
|
|
13
|
-
let
|
|
12
|
+
function S({ activeTab: y, onTabChange: S, archivedEnabled: w = !0, searchQuery: T, onSearchChange: E, filterChips: D, listSlot: O, detailSlot: k, emptyDetailSlot: A }) {
|
|
13
|
+
let j = f(), M = t(), { basePath: N } = e(), { setActiveConversationId: P } = r(), F = p(), I = d(), L = !!F.conversationId && !!k, R = a(() => {
|
|
14
14
|
let e = [
|
|
15
15
|
{
|
|
16
16
|
id: "all",
|
|
17
|
-
label:
|
|
17
|
+
label: M("conversations.tabs.all", "All")
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
id: "direct",
|
|
21
|
-
label:
|
|
21
|
+
label: M("conversations.tabs.direct", "Direct")
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
id: "groups",
|
|
25
|
-
label:
|
|
25
|
+
label: M("conversations.tabs.groups", "Groups")
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
id: "channels",
|
|
29
|
-
label:
|
|
29
|
+
label: M("conversations.tabs.channels", "Channels")
|
|
30
30
|
}
|
|
31
31
|
];
|
|
32
|
-
return
|
|
32
|
+
return w && e.push({
|
|
33
33
|
id: "archived",
|
|
34
|
-
label:
|
|
34
|
+
label: M("conversations.tabs.archived", "Archived")
|
|
35
35
|
}), e;
|
|
36
|
-
}, [
|
|
37
|
-
let e =
|
|
38
|
-
t.set("new", "true"),
|
|
36
|
+
}, [w, M]), z = i(() => {
|
|
37
|
+
let e = N || "/conversations", t = new URLSearchParams(I.search);
|
|
38
|
+
t.set("new", "true"), j(`${e}?${t.toString()}`);
|
|
39
39
|
}, [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
]),
|
|
44
|
-
|
|
40
|
+
N,
|
|
41
|
+
I.search,
|
|
42
|
+
j
|
|
43
|
+
]), B = i(() => {
|
|
44
|
+
P(null), j(`${N || "/conversations"}${I.search}`);
|
|
45
45
|
}, [
|
|
46
|
+
N,
|
|
47
|
+
I.search,
|
|
46
48
|
j,
|
|
47
|
-
P
|
|
48
|
-
k,
|
|
49
|
-
M
|
|
49
|
+
P
|
|
50
50
|
]);
|
|
51
|
-
return /* @__PURE__ */
|
|
51
|
+
return /* @__PURE__ */ h("div", {
|
|
52
52
|
"data-tour": "conversations-inbox",
|
|
53
53
|
className: "flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas",
|
|
54
|
-
children: [/* @__PURE__ */
|
|
55
|
-
"aria-label":
|
|
56
|
-
className: n("flex flex-col min-h-0 border-r border-border-default bg-bg-surface", "w-full md:w-[320px] md:flex-shrink-0",
|
|
57
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ h("aside", {
|
|
55
|
+
"aria-label": M("conversations.listPaneLabel", "Conversations list"),
|
|
56
|
+
className: n("flex flex-col min-h-0 border-r border-border-default bg-bg-surface", "w-full md:w-[320px] md:flex-shrink-0", L ? "hidden md:flex" : "flex"),
|
|
57
|
+
children: [/* @__PURE__ */ h("div", {
|
|
58
58
|
className: "relative isolate overflow-hidden sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3",
|
|
59
59
|
children: [
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
/* @__PURE__ */
|
|
60
|
+
/* @__PURE__ */ m(g, { intensity: "subtle" }),
|
|
61
|
+
/* @__PURE__ */ h("div", {
|
|
62
62
|
className: "flex items-center justify-between gap-2",
|
|
63
|
-
children: [/* @__PURE__ */
|
|
63
|
+
children: [/* @__PURE__ */ m("h1", {
|
|
64
64
|
className: "text-base font-semibold text-text-primary",
|
|
65
|
-
children: /* @__PURE__ */
|
|
66
|
-
}), /* @__PURE__ */
|
|
65
|
+
children: /* @__PURE__ */ m(v, { children: M("conversations.title", "Conversations") })
|
|
66
|
+
}), /* @__PURE__ */ h(_, {
|
|
67
67
|
"data-tour": "conversation-new-button",
|
|
68
68
|
type: "button",
|
|
69
69
|
size: "sm",
|
|
70
|
-
onClick:
|
|
71
|
-
"aria-label":
|
|
72
|
-
children: [/* @__PURE__ */
|
|
70
|
+
onClick: z,
|
|
71
|
+
"aria-label": M("conversations.createNewConversation", "Create a new conversation"),
|
|
72
|
+
children: [/* @__PURE__ */ m(s, { size: 14 }), /* @__PURE__ */ m("span", {
|
|
73
73
|
className: "hidden sm:inline",
|
|
74
|
-
children:
|
|
74
|
+
children: M("conversations.newConversation", "New")
|
|
75
75
|
})]
|
|
76
76
|
})]
|
|
77
77
|
}),
|
|
78
|
-
/* @__PURE__ */ m(
|
|
78
|
+
/* @__PURE__ */ m(b, {
|
|
79
|
+
icon: /* @__PURE__ */ m(c, {
|
|
80
|
+
size: 16,
|
|
81
|
+
"aria-hidden": "true"
|
|
82
|
+
}),
|
|
83
|
+
children: M("conversations.purpose", "Your messaging hub: direct messages, group chats, and topic channels with the people in this workspace. Search, filter, or start a new conversation to keep work discussions in one place.")
|
|
84
|
+
}),
|
|
85
|
+
/* @__PURE__ */ h("div", {
|
|
79
86
|
className: "relative",
|
|
80
87
|
children: [
|
|
81
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ m(l, {
|
|
82
89
|
size: 14,
|
|
83
90
|
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary",
|
|
84
91
|
"aria-hidden": "true"
|
|
85
92
|
}),
|
|
86
|
-
/* @__PURE__ */
|
|
93
|
+
/* @__PURE__ */ m("input", {
|
|
87
94
|
"data-tour": "conversation-search",
|
|
88
95
|
type: "search",
|
|
89
|
-
value:
|
|
90
|
-
onChange: (e) =>
|
|
91
|
-
placeholder:
|
|
92
|
-
"aria-label":
|
|
96
|
+
value: T,
|
|
97
|
+
onChange: (e) => E(e.target.value),
|
|
98
|
+
placeholder: M("conversations.search", "Search conversations..."),
|
|
99
|
+
"aria-label": M("conversations.searchAria", "Search conversations"),
|
|
93
100
|
className: n("w-full rounded-md border border-border-default bg-bg-elevated", "py-1.5 pl-8 pr-8 text-sm text-text-primary", "placeholder:text-text-secondary", "focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40")
|
|
94
101
|
}),
|
|
95
|
-
|
|
102
|
+
T.length > 0 && /* @__PURE__ */ m("button", {
|
|
96
103
|
type: "button",
|
|
97
|
-
onClick: () =>
|
|
98
|
-
"aria-label":
|
|
104
|
+
onClick: () => E(""),
|
|
105
|
+
"aria-label": M("conversations.clearSearch", "Clear conversation search"),
|
|
99
106
|
className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary",
|
|
100
|
-
children: /* @__PURE__ */
|
|
107
|
+
children: /* @__PURE__ */ m(u, { size: 12 })
|
|
101
108
|
})
|
|
102
109
|
]
|
|
103
110
|
}),
|
|
104
|
-
/* @__PURE__ */
|
|
111
|
+
/* @__PURE__ */ m("div", {
|
|
105
112
|
"data-tour": "conversations-tabs",
|
|
106
|
-
children: /* @__PURE__ */
|
|
107
|
-
items:
|
|
108
|
-
activeTabId:
|
|
109
|
-
onTabChange: (e) =>
|
|
113
|
+
children: /* @__PURE__ */ m(x, {
|
|
114
|
+
items: R,
|
|
115
|
+
activeTabId: y,
|
|
116
|
+
onTabChange: (e) => S(e),
|
|
110
117
|
variant: "underline",
|
|
111
118
|
size: "sm"
|
|
112
119
|
})
|
|
113
120
|
}),
|
|
114
|
-
|
|
121
|
+
D && D.length > 0 && /* @__PURE__ */ m("div", {
|
|
115
122
|
"data-tour": "conversations-filter-chips",
|
|
116
123
|
className: "flex flex-wrap gap-1.5",
|
|
117
|
-
children:
|
|
124
|
+
children: D.map((e) => /* @__PURE__ */ h("button", {
|
|
118
125
|
type: "button",
|
|
119
126
|
onClick: e.onToggle,
|
|
120
127
|
"aria-pressed": e.active,
|
|
@@ -123,51 +130,51 @@ function b({ activeTab: v, onTabChange: b, archivedEnabled: S = !0, searchQuery:
|
|
|
123
130
|
}, e.id))
|
|
124
131
|
})
|
|
125
132
|
]
|
|
126
|
-
}), /* @__PURE__ */
|
|
133
|
+
}), /* @__PURE__ */ m("div", {
|
|
127
134
|
"data-tour": "conversations-list-scroll",
|
|
128
135
|
className: "flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
|
|
129
|
-
children:
|
|
136
|
+
children: O
|
|
130
137
|
})]
|
|
131
|
-
}), /* @__PURE__ */
|
|
132
|
-
"aria-label":
|
|
133
|
-
className: n("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface",
|
|
134
|
-
children: [
|
|
138
|
+
}), /* @__PURE__ */ h("section", {
|
|
139
|
+
"aria-label": M("conversations.detailPaneLabel", "Conversation detail"),
|
|
140
|
+
className: n("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface", L ? "flex" : "hidden md:flex"),
|
|
141
|
+
children: [L && /* @__PURE__ */ m("div", {
|
|
135
142
|
className: "md:hidden border-b border-border-subtle px-3 py-2",
|
|
136
|
-
children: /* @__PURE__ */
|
|
143
|
+
children: /* @__PURE__ */ h(_, {
|
|
137
144
|
variant: "ghost",
|
|
138
145
|
size: "sm",
|
|
139
|
-
onClick:
|
|
140
|
-
"aria-label":
|
|
141
|
-
children: [/* @__PURE__ */
|
|
146
|
+
onClick: B,
|
|
147
|
+
"aria-label": M("conversations.backToInbox", "Back to conversations list"),
|
|
148
|
+
children: [/* @__PURE__ */ m(o, { size: 14 }), M("conversations.backToInbox", "Back to inbox")]
|
|
142
149
|
})
|
|
143
|
-
}), /* @__PURE__ */
|
|
150
|
+
}), /* @__PURE__ */ m("div", {
|
|
144
151
|
className: "flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden",
|
|
145
|
-
children:
|
|
146
|
-
onNew:
|
|
147
|
-
title:
|
|
148
|
-
description:
|
|
149
|
-
newLabel:
|
|
152
|
+
children: k ?? A ?? /* @__PURE__ */ m(C, {
|
|
153
|
+
onNew: z,
|
|
154
|
+
title: M("conversations.detailEmpty.title", "Select a conversation"),
|
|
155
|
+
description: M("conversations.detailEmpty.description", "Pick a conversation from the list — or start a new one."),
|
|
156
|
+
newLabel: M("conversations.detailEmpty.newCta", "Start a new conversation")
|
|
150
157
|
})
|
|
151
158
|
})]
|
|
152
159
|
})]
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
|
-
function
|
|
156
|
-
return /* @__PURE__ */
|
|
162
|
+
function C({ title: e, description: t, newLabel: n, onNew: r }) {
|
|
163
|
+
return /* @__PURE__ */ m("div", {
|
|
157
164
|
className: "flex h-full items-center justify-center p-8",
|
|
158
|
-
children: /* @__PURE__ */
|
|
165
|
+
children: /* @__PURE__ */ m(y, {
|
|
159
166
|
illustration: "empty-vibes",
|
|
160
167
|
title: e,
|
|
161
168
|
description: t,
|
|
162
|
-
action: /* @__PURE__ */
|
|
169
|
+
action: /* @__PURE__ */ h(_, {
|
|
163
170
|
type: "button",
|
|
164
171
|
onClick: r,
|
|
165
|
-
children: [/* @__PURE__ */
|
|
172
|
+
children: [/* @__PURE__ */ m(s, { size: 14 }), n]
|
|
166
173
|
})
|
|
167
174
|
})
|
|
168
175
|
});
|
|
169
176
|
}
|
|
170
177
|
//#endregion
|
|
171
|
-
export {
|
|
178
|
+
export { S as InboxLayout };
|
|
172
179
|
|
|
173
180
|
//# sourceMappingURL=InboxLayout.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InboxLayout.js","names":[],"sources":["../../../src/conversations/components/InboxLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCallback, useMemo } from \"react\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport {\n Button,\n AuroraBackground,\n GradientText,\n IllustratedEmptyState,\n} from \"@burdenoff/fe-libs/ui\";\nimport { ArrowLeft, MessageSquarePlus, Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Identifier for each inbox tab. Drives the conversation list filter and is\n * mirrored to / read from the `?tab=` URL search param.\n */\nexport type InboxTabId = \"all\" | \"direct\" | \"groups\" | \"channels\" | \"archived\";\n\nexport const INBOX_TAB_IDS: InboxTabId[] = [\n \"all\",\n \"direct\",\n \"groups\",\n \"channels\",\n \"archived\",\n];\n\nexport interface FilterChipDescriptor {\n id: \"unread\" | \"pinned\";\n label: string;\n active: boolean;\n onToggle: () => void;\n icon?: ReactNode;\n}\n\nexport interface InboxLayoutProps {\n /**\n * Currently active inbox tab. Drives which list view is rendered in the\n * left pane.\n */\n activeTab: InboxTabId;\n /** Tab change handler. Used by the host page to sync URL state. */\n onTabChange: (id: InboxTabId) => void;\n /**\n * Whether the Archived tab is exposed. Gated behind the `conversations.archived-view`\n * feature flag — when false the tab is hidden from the bar entirely.\n */\n archivedEnabled?: boolean;\n /** Search input value (controlled). */\n searchQuery: string;\n /** Search input change handler. */\n onSearchChange: (value: string) => void;\n /**\n * Quick toggleable filter chips rendered below the tabs. Pass an empty\n * array (or omit) to hide the chip row entirely — useful on the Archived\n * tab where pin/unread filters don't make sense.\n */\n filterChips?: FilterChipDescriptor[];\n /** Scrollable list content rendered in the left pane below the chrome. */\n listSlot: ReactNode;\n /**\n * Right-pane content. When a conversation is selected the parent should\n * pass the conversation detail subtree here. When nothing is selected the\n * empty-state slot below is used instead.\n */\n detailSlot?: ReactNode;\n /**\n * Empty-state rendered in the right pane when `detailSlot` is null /\n * undefined. Defaults to a generic \"Select a conversation\" prompt.\n */\n emptyDetailSlot?: ReactNode;\n}\n\n/**\n * Two-pane inbox shell for the conversations microfrontend.\n *\n * Left pane: fixed 320px (collapses to full-width below `md` when no\n * conversation is selected, the way Slack / Linear / Discord behave on\n * mobile). Holds:\n * - sticky header (title + New CTA)\n * - search input\n * - tabs (All / Direct / Groups / Channels / Archived)\n * - filter chips (Unread / Pinned)\n * - scrollable conversation list\n *\n * Right pane: detail subtree OR empty state.\n *\n * The component is presentation-only — URL <-> tab sync, query\n * construction, and conversation selection all live in the host page.\n */\nexport function InboxLayout({\n activeTab,\n onTabChange,\n archivedEnabled = true,\n searchQuery,\n onSearchChange,\n filterChips,\n listSlot,\n detailSlot,\n emptyDetailSlot,\n}: InboxLayoutProps) {\n const navigate = useNavigate();\n const tr = useTr();\n const { basePath } = useConversations();\n const { setActiveConversationId } = useConversationsStore();\n const params = useParams<{ conversationId?: string }>();\n const location = useLocation();\n // A conversation is \"open\" whenever the URL targets a detail route\n // (/:conversationId). We can't rely solely on a passed detailSlot —\n // routing decides whether to mount the detail page.\n const conversationOpen =\n Boolean(params.conversationId) && Boolean(detailSlot);\n\n const tabs: TabItem[] = useMemo(() => {\n const items: TabItem[] = [\n { id: \"all\", label: tr(\"conversations.tabs.all\", \"All\") },\n { id: \"direct\", label: tr(\"conversations.tabs.direct\", \"Direct\") },\n { id: \"groups\", label: tr(\"conversations.tabs.groups\", \"Groups\") },\n { id: \"channels\", label: tr(\"conversations.tabs.channels\", \"Channels\") },\n ];\n if (archivedEnabled) {\n items.push({\n id: \"archived\",\n label: tr(\"conversations.tabs.archived\", \"Archived\"),\n });\n }\n return items;\n }, [archivedEnabled, tr]);\n\n const handleNewConversation = useCallback(() => {\n // Open the create-conversation dialog in-place via `?new=true` on the\n // inbox landing page (parent route). This replaces the legacy\n // `/conversations/new` full-page navigation — the user stays inside\n // the inbox and the dialog is mounted there.\n const inboxRoot = basePath || \"/conversations\";\n const currentSearch = new URLSearchParams(location.search);\n currentSearch.set(\"new\", \"true\");\n navigate(`${inboxRoot}?${currentSearch.toString()}`);\n }, [basePath, location.search, navigate]);\n\n const handleBackToList = useCallback(() => {\n // Clear the open conversation id so the empty state shows and routing\n // returns to the inbox shell. We keep the current tab via location.search.\n setActiveConversationId(null);\n navigate(`${basePath || \"/conversations\"}${location.search}`);\n }, [basePath, location.search, navigate, setActiveConversationId]);\n\n return (\n <div\n data-tour=\"conversations-inbox\"\n className=\"flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas\"\n >\n {/* Left pane: list. On md+ always visible as a fixed sidebar. Below md,\n hidden when a conversation is open (full-screen detail mode). */}\n <aside\n aria-label={tr(\"conversations.listPaneLabel\", \"Conversations list\")}\n className={cn(\n \"flex flex-col min-h-0 border-r border-border-default bg-bg-surface\",\n \"w-full md:w-[320px] md:flex-shrink-0\",\n conversationOpen ? \"hidden md:flex\" : \"flex\",\n )}\n >\n {/* Sticky header: title + new CTA. Hosts the MFE's single aurora hero\n backdrop (decorative, -z-10 — never moves content). */}\n <div className=\"relative isolate overflow-hidden sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"flex items-center justify-between gap-2\">\n <h1 className=\"text-base font-semibold text-text-primary\">\n <GradientText>\n {tr(\"conversations.title\", \"Conversations\")}\n </GradientText>\n </h1>\n <Button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n size=\"sm\"\n onClick={handleNewConversation}\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n >\n <MessageSquarePlus size={14} />\n <span className=\"hidden sm:inline\">\n {tr(\"conversations.newConversation\", \"New\")}\n </span>\n </Button>\n </div>\n\n {/* Search */}\n <div className=\"relative\">\n <Search\n size={14}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n data-tour=\"conversation-search\"\n type=\"search\"\n value={searchQuery}\n onChange={(event) => onSearchChange(event.target.value)}\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n aria-label={tr(\n \"conversations.searchAria\",\n \"Search conversations\",\n )}\n className={cn(\n \"w-full rounded-md border border-border-default bg-bg-elevated\",\n \"py-1.5 pl-8 pr-8 text-sm text-text-primary\",\n \"placeholder:text-text-secondary\",\n \"focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40\",\n )}\n />\n {searchQuery.length > 0 && (\n <button\n type=\"button\"\n onClick={() => onSearchChange(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n )}\n </div>\n\n {/* Tabs */}\n <div data-tour=\"conversations-tabs\">\n <Tabs\n items={tabs}\n activeTabId={activeTab}\n onTabChange={(id: string) => onTabChange(id as InboxTabId)}\n variant=\"underline\"\n size=\"sm\"\n />\n </div>\n\n {/* Filter chips */}\n {filterChips && filterChips.length > 0 && (\n <div\n data-tour=\"conversations-filter-chips\"\n className=\"flex flex-wrap gap-1.5\"\n >\n {filterChips.map((chip) => (\n <button\n key={chip.id}\n type=\"button\"\n onClick={chip.onToggle}\n aria-pressed={chip.active}\n className={cn(\n \"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors\",\n chip.active\n ? \"border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n )}\n >\n {chip.icon}\n {chip.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Scrollable list */}\n <div\n data-tour=\"conversations-list-scroll\"\n className=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden\"\n >\n {listSlot}\n </div>\n </aside>\n\n {/* Right pane: detail OR empty state */}\n <section\n aria-label={tr(\"conversations.detailPaneLabel\", \"Conversation detail\")}\n className={cn(\n \"flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface\",\n conversationOpen ? \"flex\" : \"hidden md:flex\",\n )}\n >\n {conversationOpen && (\n // Mobile-only back button. On md+ the list and detail render\n // side-by-side so the back arrow would be useless.\n <div className=\"md:hidden border-b border-border-subtle px-3 py-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleBackToList}\n aria-label={tr(\n \"conversations.backToInbox\",\n \"Back to conversations list\",\n )}\n >\n <ArrowLeft size={14} />\n {tr(\"conversations.backToInbox\", \"Back to inbox\")}\n </Button>\n </div>\n )}\n <div className=\"flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden\">\n {detailSlot ?? emptyDetailSlot ?? (\n <InboxEmptyDetail\n onNew={handleNewConversation}\n title={tr(\n \"conversations.detailEmpty.title\",\n \"Select a conversation\",\n )}\n description={tr(\n \"conversations.detailEmpty.description\",\n \"Pick a conversation from the list — or start a new one.\",\n )}\n newLabel={tr(\n \"conversations.detailEmpty.newCta\",\n \"Start a new conversation\",\n )}\n />\n )}\n </div>\n </section>\n </div>\n );\n}\n\ninterface InboxEmptyDetailProps {\n title: string;\n description: string;\n newLabel: string;\n onNew: () => void;\n}\n\n/**\n * Default empty-state rendered in the detail pane when no conversation is\n * currently selected. Lifted out so the host page (or tests) can override\n * the copy via `emptyDetailSlot`.\n */\nfunction InboxEmptyDetail({\n title,\n description,\n newLabel,\n onNew,\n}: InboxEmptyDetailProps) {\n return (\n <div className=\"flex h-full items-center justify-center p-8\">\n <IllustratedEmptyState\n illustration=\"empty-vibes\"\n title={title}\n description={description}\n action={\n <Button type=\"button\" onClick={onNew}>\n <MessageSquarePlus size={14} />\n {newLabel}\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InboxLayout.js","names":[],"sources":["../../../src/conversations/components/InboxLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCallback, useMemo } from \"react\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport {\n Button,\n AuroraBackground,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport {\n ArrowLeft,\n MessageSquarePlus,\n MessagesSquare,\n Search,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Identifier for each inbox tab. Drives the conversation list filter and is\n * mirrored to / read from the `?tab=` URL search param.\n */\nexport type InboxTabId = \"all\" | \"direct\" | \"groups\" | \"channels\" | \"archived\";\n\nexport const INBOX_TAB_IDS: InboxTabId[] = [\n \"all\",\n \"direct\",\n \"groups\",\n \"channels\",\n \"archived\",\n];\n\nexport interface FilterChipDescriptor {\n id: \"unread\" | \"pinned\";\n label: string;\n active: boolean;\n onToggle: () => void;\n icon?: ReactNode;\n}\n\nexport interface InboxLayoutProps {\n /**\n * Currently active inbox tab. Drives which list view is rendered in the\n * left pane.\n */\n activeTab: InboxTabId;\n /** Tab change handler. Used by the host page to sync URL state. */\n onTabChange: (id: InboxTabId) => void;\n /**\n * Whether the Archived tab is exposed. Gated behind the `conversations.archived-view`\n * feature flag — when false the tab is hidden from the bar entirely.\n */\n archivedEnabled?: boolean;\n /** Search input value (controlled). */\n searchQuery: string;\n /** Search input change handler. */\n onSearchChange: (value: string) => void;\n /**\n * Quick toggleable filter chips rendered below the tabs. Pass an empty\n * array (or omit) to hide the chip row entirely — useful on the Archived\n * tab where pin/unread filters don't make sense.\n */\n filterChips?: FilterChipDescriptor[];\n /** Scrollable list content rendered in the left pane below the chrome. */\n listSlot: ReactNode;\n /**\n * Right-pane content. When a conversation is selected the parent should\n * pass the conversation detail subtree here. When nothing is selected the\n * empty-state slot below is used instead.\n */\n detailSlot?: ReactNode;\n /**\n * Empty-state rendered in the right pane when `detailSlot` is null /\n * undefined. Defaults to a generic \"Select a conversation\" prompt.\n */\n emptyDetailSlot?: ReactNode;\n}\n\n/**\n * Two-pane inbox shell for the conversations microfrontend.\n *\n * Left pane: fixed 320px (collapses to full-width below `md` when no\n * conversation is selected, the way Slack / Linear / Discord behave on\n * mobile). Holds:\n * - sticky header (title + New CTA)\n * - search input\n * - tabs (All / Direct / Groups / Channels / Archived)\n * - filter chips (Unread / Pinned)\n * - scrollable conversation list\n *\n * Right pane: detail subtree OR empty state.\n *\n * The component is presentation-only — URL <-> tab sync, query\n * construction, and conversation selection all live in the host page.\n */\nexport function InboxLayout({\n activeTab,\n onTabChange,\n archivedEnabled = true,\n searchQuery,\n onSearchChange,\n filterChips,\n listSlot,\n detailSlot,\n emptyDetailSlot,\n}: InboxLayoutProps) {\n const navigate = useNavigate();\n const tr = useTr();\n const { basePath } = useConversations();\n const { setActiveConversationId } = useConversationsStore();\n const params = useParams<{ conversationId?: string }>();\n const location = useLocation();\n // A conversation is \"open\" whenever the URL targets a detail route\n // (/:conversationId). We can't rely solely on a passed detailSlot —\n // routing decides whether to mount the detail page.\n const conversationOpen =\n Boolean(params.conversationId) && Boolean(detailSlot);\n\n const tabs: TabItem[] = useMemo(() => {\n const items: TabItem[] = [\n { id: \"all\", label: tr(\"conversations.tabs.all\", \"All\") },\n { id: \"direct\", label: tr(\"conversations.tabs.direct\", \"Direct\") },\n { id: \"groups\", label: tr(\"conversations.tabs.groups\", \"Groups\") },\n { id: \"channels\", label: tr(\"conversations.tabs.channels\", \"Channels\") },\n ];\n if (archivedEnabled) {\n items.push({\n id: \"archived\",\n label: tr(\"conversations.tabs.archived\", \"Archived\"),\n });\n }\n return items;\n }, [archivedEnabled, tr]);\n\n const handleNewConversation = useCallback(() => {\n // Open the create-conversation dialog in-place via `?new=true` on the\n // inbox landing page (parent route). This replaces the legacy\n // `/conversations/new` full-page navigation — the user stays inside\n // the inbox and the dialog is mounted there.\n const inboxRoot = basePath || \"/conversations\";\n const currentSearch = new URLSearchParams(location.search);\n currentSearch.set(\"new\", \"true\");\n navigate(`${inboxRoot}?${currentSearch.toString()}`);\n }, [basePath, location.search, navigate]);\n\n const handleBackToList = useCallback(() => {\n // Clear the open conversation id so the empty state shows and routing\n // returns to the inbox shell. We keep the current tab via location.search.\n setActiveConversationId(null);\n navigate(`${basePath || \"/conversations\"}${location.search}`);\n }, [basePath, location.search, navigate, setActiveConversationId]);\n\n return (\n <div\n data-tour=\"conversations-inbox\"\n className=\"flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas\"\n >\n {/* Left pane: list. On md+ always visible as a fixed sidebar. Below md,\n hidden when a conversation is open (full-screen detail mode). */}\n <aside\n aria-label={tr(\"conversations.listPaneLabel\", \"Conversations list\")}\n className={cn(\n \"flex flex-col min-h-0 border-r border-border-default bg-bg-surface\",\n \"w-full md:w-[320px] md:flex-shrink-0\",\n conversationOpen ? \"hidden md:flex\" : \"flex\",\n )}\n >\n {/* Sticky header: title + new CTA. Hosts the MFE's single aurora hero\n backdrop (decorative, -z-10 — never moves content). */}\n <div className=\"relative isolate overflow-hidden sticky top-0 z-10 flex flex-col gap-3 border-b border-border-subtle bg-bg-surface px-4 pt-4 pb-3\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"flex items-center justify-between gap-2\">\n <h1 className=\"text-base font-semibold text-text-primary\">\n <GradientText>\n {tr(\"conversations.title\", \"Conversations\")}\n </GradientText>\n </h1>\n <Button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n size=\"sm\"\n onClick={handleNewConversation}\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n >\n <MessageSquarePlus size={14} />\n <span className=\"hidden sm:inline\">\n {tr(\"conversations.newConversation\", \"New\")}\n </span>\n </Button>\n </div>\n\n {/* Page purpose — explains, in plain language, what the inbox is\n for so a first-time user understands the surface at a glance. */}\n <PagePurpose icon={<MessagesSquare size={16} aria-hidden=\"true\" />}>\n {tr(\n \"conversations.purpose\",\n \"Your messaging hub: direct messages, group chats, and topic channels with the people in this workspace. Search, filter, or start a new conversation to keep work discussions in one place.\",\n )}\n </PagePurpose>\n\n {/* Search */}\n <div className=\"relative\">\n <Search\n size={14}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n data-tour=\"conversation-search\"\n type=\"search\"\n value={searchQuery}\n onChange={(event) => onSearchChange(event.target.value)}\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n aria-label={tr(\n \"conversations.searchAria\",\n \"Search conversations\",\n )}\n className={cn(\n \"w-full rounded-md border border-border-default bg-bg-elevated\",\n \"py-1.5 pl-8 pr-8 text-sm text-text-primary\",\n \"placeholder:text-text-secondary\",\n \"focus:border-action-primary-border focus:outline-none focus:ring-2 focus:ring-action-primary-border/40\",\n )}\n />\n {searchQuery.length > 0 && (\n <button\n type=\"button\"\n onClick={() => onSearchChange(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n )}\n </div>\n\n {/* Tabs */}\n <div data-tour=\"conversations-tabs\">\n <Tabs\n items={tabs}\n activeTabId={activeTab}\n onTabChange={(id: string) => onTabChange(id as InboxTabId)}\n variant=\"underline\"\n size=\"sm\"\n />\n </div>\n\n {/* Filter chips */}\n {filterChips && filterChips.length > 0 && (\n <div\n data-tour=\"conversations-filter-chips\"\n className=\"flex flex-wrap gap-1.5\"\n >\n {filterChips.map((chip) => (\n <button\n key={chip.id}\n type=\"button\"\n onClick={chip.onToggle}\n aria-pressed={chip.active}\n className={cn(\n \"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors\",\n chip.active\n ? \"border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n )}\n >\n {chip.icon}\n {chip.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Scrollable list */}\n <div\n data-tour=\"conversations-list-scroll\"\n className=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden\"\n >\n {listSlot}\n </div>\n </aside>\n\n {/* Right pane: detail OR empty state */}\n <section\n aria-label={tr(\"conversations.detailPaneLabel\", \"Conversation detail\")}\n className={cn(\n \"flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface\",\n conversationOpen ? \"flex\" : \"hidden md:flex\",\n )}\n >\n {conversationOpen && (\n // Mobile-only back button. On md+ the list and detail render\n // side-by-side so the back arrow would be useless.\n <div className=\"md:hidden border-b border-border-subtle px-3 py-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleBackToList}\n aria-label={tr(\n \"conversations.backToInbox\",\n \"Back to conversations list\",\n )}\n >\n <ArrowLeft size={14} />\n {tr(\"conversations.backToInbox\", \"Back to inbox\")}\n </Button>\n </div>\n )}\n <div className=\"flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden\">\n {detailSlot ?? emptyDetailSlot ?? (\n <InboxEmptyDetail\n onNew={handleNewConversation}\n title={tr(\n \"conversations.detailEmpty.title\",\n \"Select a conversation\",\n )}\n description={tr(\n \"conversations.detailEmpty.description\",\n \"Pick a conversation from the list — or start a new one.\",\n )}\n newLabel={tr(\n \"conversations.detailEmpty.newCta\",\n \"Start a new conversation\",\n )}\n />\n )}\n </div>\n </section>\n </div>\n );\n}\n\ninterface InboxEmptyDetailProps {\n title: string;\n description: string;\n newLabel: string;\n onNew: () => void;\n}\n\n/**\n * Default empty-state rendered in the detail pane when no conversation is\n * currently selected. Lifted out so the host page (or tests) can override\n * the copy via `emptyDetailSlot`.\n */\nfunction InboxEmptyDetail({\n title,\n description,\n newLabel,\n onNew,\n}: InboxEmptyDetailProps) {\n return (\n <div className=\"flex h-full items-center justify-center p-8\">\n <IllustratedEmptyState\n illustration=\"empty-vibes\"\n title={title}\n description={description}\n action={\n <Button type=\"button\" onClick={onNew}>\n <MessageSquarePlus size={14} />\n {newLabel}\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useTr as e } from "../utils/i18n.js";
|
|
2
2
|
import { useMemo as t } from "react";
|
|
3
3
|
import { Activity as n, ChevronRight as r, ListChecks as i, MessageCircle as a, Package as o, ShieldCheck as s } from "lucide-react";
|
|
4
|
-
import { Link as c } from "react-router-dom";
|
|
5
|
-
import { jsx as
|
|
6
|
-
import { EmphasisPanel as
|
|
4
|
+
import { Link as c, useNavigate as l } from "react-router-dom";
|
|
5
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
6
|
+
import { EmphasisPanel as f, GlassCard as p, GradientText as m, NextSteps as h, PagePurpose as g } from "@burdenoff/fe-libs/ui";
|
|
7
7
|
//#region src/conversations/pages/AdminConversationsLandingPage.tsx
|
|
8
|
-
var
|
|
8
|
+
var _ = [
|
|
9
9
|
{
|
|
10
10
|
id: "moderation",
|
|
11
11
|
labelKey: "conversations.admin.moderation",
|
|
@@ -13,7 +13,7 @@ var m = [
|
|
|
13
13
|
descriptionKey: "conversations.admin.moderationDescription",
|
|
14
14
|
descriptionFallback: "Reported and flagged messages, moderation actions, and reviewer assignments.",
|
|
15
15
|
path: "moderation",
|
|
16
|
-
icon: /* @__PURE__ */
|
|
16
|
+
icon: /* @__PURE__ */ u(s, {
|
|
17
17
|
className: "size-5",
|
|
18
18
|
"aria-hidden": "true"
|
|
19
19
|
}),
|
|
@@ -26,7 +26,7 @@ var m = [
|
|
|
26
26
|
descriptionKey: "conversations.admin.bulkOperationsDescription",
|
|
27
27
|
descriptionFallback: "Bulk archive / unarchive, participant edits, and cross-conversation actions.",
|
|
28
28
|
path: "bulk",
|
|
29
|
-
icon: /* @__PURE__ */
|
|
29
|
+
icon: /* @__PURE__ */ u(i, {
|
|
30
30
|
className: "size-5",
|
|
31
31
|
"aria-hidden": "true"
|
|
32
32
|
})
|
|
@@ -38,7 +38,7 @@ var m = [
|
|
|
38
38
|
descriptionKey: "conversations.admin.auditLogDescription",
|
|
39
39
|
descriptionFallback: "Activity stream for admin / moderator actions on workspace conversations.",
|
|
40
40
|
path: "audit",
|
|
41
|
-
icon: /* @__PURE__ */
|
|
41
|
+
icon: /* @__PURE__ */ u(n, {
|
|
42
42
|
className: "size-5",
|
|
43
43
|
"aria-hidden": "true"
|
|
44
44
|
})
|
|
@@ -50,78 +50,124 @@ var m = [
|
|
|
50
50
|
descriptionKey: "conversations.admin.appInstallationsDescription",
|
|
51
51
|
descriptionFallback: "Install, configure, and remove conversation apps and integrations.",
|
|
52
52
|
path: "apps",
|
|
53
|
-
icon: /* @__PURE__ */
|
|
53
|
+
icon: /* @__PURE__ */ u(o, {
|
|
54
54
|
className: "size-5",
|
|
55
55
|
"aria-hidden": "true"
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
];
|
|
59
|
-
function
|
|
60
|
-
let
|
|
59
|
+
function v() {
|
|
60
|
+
let o = e(), v = l(), y = t(() => _.map((e) => ({
|
|
61
61
|
...e,
|
|
62
|
-
label:
|
|
63
|
-
description:
|
|
64
|
-
})), [
|
|
65
|
-
|
|
62
|
+
label: o(e.labelKey, e.labelFallback),
|
|
63
|
+
description: o(e.descriptionKey, e.descriptionFallback)
|
|
64
|
+
})), [o]), b = t(() => [
|
|
65
|
+
{
|
|
66
|
+
id: "review-moderation",
|
|
67
|
+
label: o("conversations.admin.nextSteps.moderation.label", "Clear the moderation queue"),
|
|
68
|
+
description: o("conversations.admin.nextSteps.moderation.description", "Review reported messages and apply decisions."),
|
|
69
|
+
icon: /* @__PURE__ */ u(s, {
|
|
70
|
+
className: "size-4",
|
|
71
|
+
"aria-hidden": "true"
|
|
72
|
+
}),
|
|
73
|
+
onClick: () => v("moderation")
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "bulk-cleanup",
|
|
77
|
+
label: o("conversations.admin.nextSteps.bulk.label", "Tidy up conversations in bulk"),
|
|
78
|
+
description: o("conversations.admin.nextSteps.bulk.description", "Archive or remove stale conversations across the workspace."),
|
|
79
|
+
icon: /* @__PURE__ */ u(i, {
|
|
80
|
+
className: "size-4",
|
|
81
|
+
"aria-hidden": "true"
|
|
82
|
+
}),
|
|
83
|
+
onClick: () => v("bulk")
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "check-audit",
|
|
87
|
+
label: o("conversations.admin.nextSteps.audit.label", "Check recent admin activity"),
|
|
88
|
+
description: o("conversations.admin.nextSteps.audit.description", "See what moderators and admins have changed lately."),
|
|
89
|
+
icon: /* @__PURE__ */ u(n, {
|
|
90
|
+
className: "size-4",
|
|
91
|
+
"aria-hidden": "true"
|
|
92
|
+
}),
|
|
93
|
+
onClick: () => v("audit")
|
|
94
|
+
}
|
|
95
|
+
], [v, o]);
|
|
96
|
+
return /* @__PURE__ */ u("div", {
|
|
66
97
|
className: "flex flex-col h-full",
|
|
67
|
-
children: /* @__PURE__ */
|
|
98
|
+
children: /* @__PURE__ */ u("div", {
|
|
68
99
|
className: "flex-1 min-h-0 overflow-auto p-4 sm:p-6",
|
|
69
|
-
children: /* @__PURE__ */
|
|
100
|
+
children: /* @__PURE__ */ d("div", {
|
|
70
101
|
className: "mx-auto w-full max-w-3xl space-y-6",
|
|
71
|
-
children: [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
children: /* @__PURE__ */ l(a, {
|
|
76
|
-
className: "size-5",
|
|
102
|
+
children: [
|
|
103
|
+
/* @__PURE__ */ u(g, {
|
|
104
|
+
icon: /* @__PURE__ */ u(a, {
|
|
105
|
+
size: 16,
|
|
77
106
|
"aria-hidden": "true"
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
}),
|
|
108
|
+
children: o("conversations.admin.purpose", "Govern every conversation in this workspace from one place — moderate reported messages, run bulk archive or cleanup operations, audit moderator actions, and manage the apps that plug into conversations.")
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ u(f, { children: /* @__PURE__ */ d("div", {
|
|
111
|
+
className: "flex items-start gap-3",
|
|
112
|
+
children: [/* @__PURE__ */ u("div", {
|
|
113
|
+
className: "p-2 rounded-lg bg-gloss-card text-text-secondary border border-border-seam",
|
|
114
|
+
children: /* @__PURE__ */ u(a, {
|
|
115
|
+
className: "size-5",
|
|
116
|
+
"aria-hidden": "true"
|
|
117
|
+
})
|
|
118
|
+
}), /* @__PURE__ */ d("div", {
|
|
119
|
+
className: "min-w-0",
|
|
120
|
+
children: [/* @__PURE__ */ u("h1", {
|
|
121
|
+
className: "text-lg font-semibold text-text-primary",
|
|
122
|
+
children: /* @__PURE__ */ u(m, { children: o("conversations.admin.landingTitle", "Conversations admin") })
|
|
123
|
+
}), /* @__PURE__ */ u("p", {
|
|
124
|
+
className: "mt-1 text-sm text-text-secondary",
|
|
125
|
+
children: o("conversations.admin.landingDescription", "Governance tools for workspace conversations. Select a section below to continue.")
|
|
126
|
+
})]
|
|
87
127
|
})]
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
children: [/* @__PURE__ */ l("div", {
|
|
99
|
-
className: "p-2 rounded-md bg-bg-sunken text-text-secondary group-hover:text-text-primary",
|
|
100
|
-
children: e.icon
|
|
101
|
-
}), /* @__PURE__ */ u("div", {
|
|
102
|
-
className: "flex-1 min-w-0",
|
|
128
|
+
}) }),
|
|
129
|
+
/* @__PURE__ */ u("div", {
|
|
130
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
131
|
+
children: y.map((e) => /* @__PURE__ */ u(c, {
|
|
132
|
+
to: e.path,
|
|
133
|
+
className: "group rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
134
|
+
children: /* @__PURE__ */ d(p, {
|
|
135
|
+
treatment: "glass",
|
|
136
|
+
glow: e.featured,
|
|
137
|
+
className: "flex items-start gap-3 border-border-seam p-4 shadow-[var(--shadow-pop)]",
|
|
103
138
|
children: [/* @__PURE__ */ u("div", {
|
|
104
|
-
className: "
|
|
105
|
-
children:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
className: "
|
|
110
|
-
|
|
139
|
+
className: "p-2 rounded-md bg-bg-sunken text-text-secondary group-hover:text-text-primary",
|
|
140
|
+
children: e.icon
|
|
141
|
+
}), /* @__PURE__ */ d("div", {
|
|
142
|
+
className: "flex-1 min-w-0",
|
|
143
|
+
children: [/* @__PURE__ */ d("div", {
|
|
144
|
+
className: "flex items-center justify-between gap-2",
|
|
145
|
+
children: [/* @__PURE__ */ u("span", {
|
|
146
|
+
className: "text-sm font-medium text-text-primary",
|
|
147
|
+
children: e.label
|
|
148
|
+
}), /* @__PURE__ */ u(r, {
|
|
149
|
+
className: "size-4 text-text-secondary",
|
|
150
|
+
"aria-hidden": "true"
|
|
151
|
+
})]
|
|
152
|
+
}), /* @__PURE__ */ u("p", {
|
|
153
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
154
|
+
children: e.description
|
|
111
155
|
})]
|
|
112
|
-
}), /* @__PURE__ */ l("p", {
|
|
113
|
-
className: "mt-1 text-xs text-text-secondary",
|
|
114
|
-
children: e.description
|
|
115
156
|
})]
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
},
|
|
119
|
-
|
|
157
|
+
})
|
|
158
|
+
}, e.id))
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ u(h, {
|
|
161
|
+
storageKey: "conversations-admin-landing",
|
|
162
|
+
title: o("conversations.admin.nextSteps.title", "Recommended next steps"),
|
|
163
|
+
steps: b
|
|
164
|
+
})
|
|
165
|
+
]
|
|
120
166
|
})
|
|
121
167
|
})
|
|
122
168
|
});
|
|
123
169
|
}
|
|
124
170
|
//#endregion
|
|
125
|
-
export {
|
|
171
|
+
export { v as AdminConversationsLandingPage };
|
|
126
172
|
|
|
127
173
|
//# sourceMappingURL=AdminConversationsLandingPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/AdminConversationsLandingPage.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useMemo } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport {\n MessageCircle,\n ShieldCheck,\n ListChecks,\n Activity,\n Package,\n ChevronRight,\n} from \"lucide-react\";\nimport { GlassCard, EmphasisPanel, GradientText } from \"@burdenoff/fe-libs/ui\";\nimport { useTr } from \"../utils/i18n\";\n\ninterface AdminSectionDefinition {\n id: string;\n /** i18n key used for the card label */\n labelKey: string;\n /** English fallback rendered when the i18n key is not resolved */\n labelFallback: string;\n /** i18n key used for the card description */\n descriptionKey: string;\n /** English fallback for the description */\n descriptionFallback: string;\n /** Route path relative to the admin basePath */\n path: string;\n icon: ReactNode;\n /** The single featured section — gets the glowing glass treatment. */\n featured?: boolean;\n}\n\n// Module-scoped section catalogue. The icon JSX is stable (purely presentational,\n// no context or dynamic props) so constructing once at parse time avoids rebuilding\n// four lucide nodes on every render. If icons ever need to respond to context\n// (theme, RBAC), move the icon field into the useMemo mapper below.\nconst SECTION_DEFINITIONS: AdminSectionDefinition[] = [\n {\n id: \"moderation\",\n labelKey: \"conversations.admin.moderation\",\n labelFallback: \"Moderation\",\n descriptionKey: \"conversations.admin.moderationDescription\",\n descriptionFallback:\n \"Reported and flagged messages, moderation actions, and reviewer assignments.\",\n path: \"moderation\",\n icon: <ShieldCheck className=\"size-5\" aria-hidden=\"true\" />,\n featured: true,\n },\n {\n id: \"bulk\",\n labelKey: \"conversations.admin.bulkOperations\",\n labelFallback: \"Bulk Operations\",\n descriptionKey: \"conversations.admin.bulkOperationsDescription\",\n descriptionFallback:\n \"Bulk archive / unarchive, participant edits, and cross-conversation actions.\",\n path: \"bulk\",\n icon: <ListChecks className=\"size-5\" aria-hidden=\"true\" />,\n },\n {\n id: \"audit\",\n labelKey: \"conversations.admin.auditLog\",\n labelFallback: \"Audit Log\",\n descriptionKey: \"conversations.admin.auditLogDescription\",\n descriptionFallback:\n \"Activity stream for admin / moderator actions on workspace conversations.\",\n path: \"audit\",\n icon: <Activity className=\"size-5\" aria-hidden=\"true\" />,\n },\n {\n id: \"apps\",\n labelKey: \"conversations.admin.appInstallations\",\n labelFallback: \"App Installations\",\n descriptionKey: \"conversations.admin.appInstallationsDescription\",\n descriptionFallback:\n \"Install, configure, and remove conversation apps and integrations.\",\n path: \"apps\",\n icon: <Package className=\"size-5\" aria-hidden=\"true\" />,\n },\n];\n\n/**\n * Admin-facing landing page for the Conversations microfrontend.\n *\n * Lightweight overview + section directory. Each card links to its canonical\n * sub-route (`/moderation`, `/bulk`, `/audit`, `/apps`) for the API-backed\n * admin surfaces.\n */\nexport function AdminConversationsLandingPage() {\n const tr = useTr();\n\n const sections = useMemo(\n () =>\n SECTION_DEFINITIONS.map((section) => ({\n ...section,\n label: tr(section.labelKey, section.labelFallback),\n description: tr(section.descriptionKey, section.descriptionFallback),\n })),\n [tr],\n );\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex-1 min-h-0 overflow-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-3xl space-y-6\">\n {/* Hero / summary zone — the single emphasis panel on this page. */}\n <EmphasisPanel>\n <div className=\"flex items-start gap-3\">\n <div className=\"p-2 rounded-lg bg-gloss-card text-text-secondary border border-border-seam\">\n <MessageCircle className=\"size-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0\">\n <h1 className=\"text-lg font-semibold text-text-primary\">\n <GradientText>\n {tr(\n \"conversations.admin.landingTitle\",\n \"Conversations admin\",\n )}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"conversations.admin.landingDescription\",\n \"Governance tools for workspace conversations. Select a section below to continue.\",\n )}\n </p>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {sections.map((section) => (\n <Link\n key={section.id}\n to={section.path}\n className=\"group rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <GlassCard\n treatment=\"glass\"\n glow={section.featured}\n className=\"flex items-start gap-3 border-border-seam p-4 shadow-[var(--shadow-pop)]\"\n >\n <div className=\"p-2 rounded-md bg-bg-sunken text-text-secondary group-hover:text-text-primary\">\n {section.icon}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">\n {section.label}\n </span>\n <ChevronRight\n className=\"size-4 text-text-secondary\"\n aria-hidden=\"true\"\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {section.description}\n </p>\n </div>\n </GlassCard>\n </Link>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAmCA,IAAM,IAAgD;CACpD;EACE,IAAI;EACJ,UAAU;EACV,eAAe;EACf,gBAAgB;EAChB,qBACE;EACF,MAAM;EACN,MAAM,kBAAC,GAAD;GAAa,WAAU;GAAS,eAAY;GAAS,CAAA;EAC3D,UAAU;EACX;CACD;EACE,IAAI;EACJ,UAAU;EACV,eAAe;EACf,gBAAgB;EAChB,qBACE;EACF,MAAM;EACN,MAAM,kBAAC,GAAD;GAAY,WAAU;GAAS,eAAY;GAAS,CAAA;EAC3D;CACD;EACE,IAAI;EACJ,UAAU;EACV,eAAe;EACf,gBAAgB;EAChB,qBACE;EACF,MAAM;EACN,MAAM,kBAAC,GAAD;GAAU,WAAU;GAAS,eAAY;GAAS,CAAA;EACzD;CACD;EACE,IAAI;EACJ,UAAU;EACV,eAAe;EACf,gBAAgB;EAChB,qBACE;EACF,MAAM;EACN,MAAM,kBAAC,GAAD;GAAS,WAAU;GAAS,eAAY;GAAS,CAAA;EACxD;CACF;AASD,SAAgB,IAAgC;CAC9C,IAAM,IAAK,GAAO,EAEZ,IAAW,QAEb,EAAoB,KAAK,OAAa;EACpC,GAAG;EACH,OAAO,EAAG,EAAQ,UAAU,EAAQ,cAAc;EAClD,aAAa,EAAG,EAAQ,gBAAgB,EAAQ,oBAAoB;EACrE,EAAE,EACL,CAAC,EAAG,CACL;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAe,WAAU;OAAS,eAAY;OAAS,CAAA;MACnD,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBACZ,kBAAC,GAAD,EAAA,UACG,EACC,oCACA,sBACD,EACY,CAAA;OACZ,CAAA,EACL,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,0CACA,oFACD;OACC,CAAA,CACA;QACF;QACQ,CAAA,EAEhB,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAS,KAAK,MACb,kBAAC,GAAD;MAEE,IAAI,EAAQ;MACZ,WAAU;gBAEV,kBAAC,GAAD;OACE,WAAU;OACV,MAAM,EAAQ;OACd,WAAU;iBAHZ,CAKE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAQ;QACL,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAQ;UACJ,CAAA,EACP,kBAAC,GAAD;UACE,WAAU;UACV,eAAY;UACZ,CAAA,CACE;YACN,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAQ;SACP,CAAA,CACA;UACI;;MACP,EA3BA,EAAQ,GA2BR,CACP;KACE,CAAA,CACF;;GACF,CAAA;EACF,CAAA"}
|
|
1
|
+
{"version":3,"file":"AdminConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/AdminConversationsLandingPage.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useMemo } from \"react\";\nimport { Link, useNavigate } from \"react-router-dom\";\nimport {\n MessageCircle,\n ShieldCheck,\n ListChecks,\n Activity,\n Package,\n ChevronRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n GradientText,\n NextSteps,\n PagePurpose,\n type NextStep,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useTr } from \"../utils/i18n\";\n\ninterface AdminSectionDefinition {\n id: string;\n /** i18n key used for the card label */\n labelKey: string;\n /** English fallback rendered when the i18n key is not resolved */\n labelFallback: string;\n /** i18n key used for the card description */\n descriptionKey: string;\n /** English fallback for the description */\n descriptionFallback: string;\n /** Route path relative to the admin basePath */\n path: string;\n icon: ReactNode;\n /** The single featured section — gets the glowing glass treatment. */\n featured?: boolean;\n}\n\n// Module-scoped section catalogue. The icon JSX is stable (purely presentational,\n// no context or dynamic props) so constructing once at parse time avoids rebuilding\n// four lucide nodes on every render. If icons ever need to respond to context\n// (theme, RBAC), move the icon field into the useMemo mapper below.\nconst SECTION_DEFINITIONS: AdminSectionDefinition[] = [\n {\n id: \"moderation\",\n labelKey: \"conversations.admin.moderation\",\n labelFallback: \"Moderation\",\n descriptionKey: \"conversations.admin.moderationDescription\",\n descriptionFallback:\n \"Reported and flagged messages, moderation actions, and reviewer assignments.\",\n path: \"moderation\",\n icon: <ShieldCheck className=\"size-5\" aria-hidden=\"true\" />,\n featured: true,\n },\n {\n id: \"bulk\",\n labelKey: \"conversations.admin.bulkOperations\",\n labelFallback: \"Bulk Operations\",\n descriptionKey: \"conversations.admin.bulkOperationsDescription\",\n descriptionFallback:\n \"Bulk archive / unarchive, participant edits, and cross-conversation actions.\",\n path: \"bulk\",\n icon: <ListChecks className=\"size-5\" aria-hidden=\"true\" />,\n },\n {\n id: \"audit\",\n labelKey: \"conversations.admin.auditLog\",\n labelFallback: \"Audit Log\",\n descriptionKey: \"conversations.admin.auditLogDescription\",\n descriptionFallback:\n \"Activity stream for admin / moderator actions on workspace conversations.\",\n path: \"audit\",\n icon: <Activity className=\"size-5\" aria-hidden=\"true\" />,\n },\n {\n id: \"apps\",\n labelKey: \"conversations.admin.appInstallations\",\n labelFallback: \"App Installations\",\n descriptionKey: \"conversations.admin.appInstallationsDescription\",\n descriptionFallback:\n \"Install, configure, and remove conversation apps and integrations.\",\n path: \"apps\",\n icon: <Package className=\"size-5\" aria-hidden=\"true\" />,\n },\n];\n\n/**\n * Admin-facing landing page for the Conversations microfrontend.\n *\n * Lightweight overview + section directory. Each card links to its canonical\n * sub-route (`/moderation`, `/bulk`, `/audit`, `/apps`) for the API-backed\n * admin surfaces.\n */\nexport function AdminConversationsLandingPage() {\n const tr = useTr();\n const navigate = useNavigate();\n\n const sections = useMemo(\n () =>\n SECTION_DEFINITIONS.map((section) => ({\n ...section,\n label: tr(section.labelKey, section.labelFallback),\n description: tr(section.descriptionKey, section.descriptionFallback),\n })),\n [tr],\n );\n\n // Recommended starting points for an admin landing here for the first time.\n // Each routes to a real admin sub-surface.\n const nextSteps = useMemo<NextStep[]>(\n () => [\n {\n id: \"review-moderation\",\n label: tr(\n \"conversations.admin.nextSteps.moderation.label\",\n \"Clear the moderation queue\",\n ),\n description: tr(\n \"conversations.admin.nextSteps.moderation.description\",\n \"Review reported messages and apply decisions.\",\n ),\n icon: <ShieldCheck className=\"size-4\" aria-hidden=\"true\" />,\n onClick: () => navigate(\"moderation\"),\n },\n {\n id: \"bulk-cleanup\",\n label: tr(\n \"conversations.admin.nextSteps.bulk.label\",\n \"Tidy up conversations in bulk\",\n ),\n description: tr(\n \"conversations.admin.nextSteps.bulk.description\",\n \"Archive or remove stale conversations across the workspace.\",\n ),\n icon: <ListChecks className=\"size-4\" aria-hidden=\"true\" />,\n onClick: () => navigate(\"bulk\"),\n },\n {\n id: \"check-audit\",\n label: tr(\n \"conversations.admin.nextSteps.audit.label\",\n \"Check recent admin activity\",\n ),\n description: tr(\n \"conversations.admin.nextSteps.audit.description\",\n \"See what moderators and admins have changed lately.\",\n ),\n icon: <Activity className=\"size-4\" aria-hidden=\"true\" />,\n onClick: () => navigate(\"audit\"),\n },\n ],\n [navigate, tr],\n );\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex-1 min-h-0 overflow-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-3xl space-y-6\">\n <PagePurpose icon={<MessageCircle size={16} aria-hidden=\"true\" />}>\n {tr(\n \"conversations.admin.purpose\",\n \"Govern every conversation in this workspace from one place — moderate reported messages, run bulk archive or cleanup operations, audit moderator actions, and manage the apps that plug into conversations.\",\n )}\n </PagePurpose>\n\n {/* Hero / summary zone — the single emphasis panel on this page. */}\n <EmphasisPanel>\n <div className=\"flex items-start gap-3\">\n <div className=\"p-2 rounded-lg bg-gloss-card text-text-secondary border border-border-seam\">\n <MessageCircle className=\"size-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0\">\n <h1 className=\"text-lg font-semibold text-text-primary\">\n <GradientText>\n {tr(\n \"conversations.admin.landingTitle\",\n \"Conversations admin\",\n )}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"conversations.admin.landingDescription\",\n \"Governance tools for workspace conversations. Select a section below to continue.\",\n )}\n </p>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {sections.map((section) => (\n <Link\n key={section.id}\n to={section.path}\n className=\"group rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <GlassCard\n treatment=\"glass\"\n glow={section.featured}\n className=\"flex items-start gap-3 border-border-seam p-4 shadow-[var(--shadow-pop)]\"\n >\n <div className=\"p-2 rounded-md bg-bg-sunken text-text-secondary group-hover:text-text-primary\">\n {section.icon}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">\n {section.label}\n </span>\n <ChevronRight\n className=\"size-4 text-text-secondary\"\n aria-hidden=\"true\"\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {section.description}\n </p>\n </div>\n </GlassCard>\n </Link>\n ))}\n </div>\n\n <NextSteps\n storageKey=\"conversations-admin-landing\"\n title={tr(\n \"conversations.admin.nextSteps.title\",\n \"Recommended next steps\",\n )}\n steps={nextSteps}\n />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|