@burdenoff/microfe-conversations 2026.527.2 → 2026.528.2
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/ConversationsRoot.js +1 -1
- package/dist/conversations/ConversationsRoot.js.map +1 -1
- package/dist/conversations/ConversationsUserRoot.js +1 -1
- package/dist/conversations/ConversationsUserRoot.js.map +1 -1
- package/dist/conversations/ConversationsUserRoutes.js +6 -6
- package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
- package/dist/conversations/components/ConversationRow.js +176 -0
- package/dist/conversations/components/ConversationRow.js.map +1 -0
- package/dist/conversations/components/EmptyState.js +10 -21
- package/dist/conversations/components/EmptyState.js.map +1 -1
- package/dist/conversations/components/InboxLayout.js +189 -0
- package/dist/conversations/components/InboxLayout.js.map +1 -0
- package/dist/conversations/components/NewConversationDialog.js +285 -0
- package/dist/conversations/components/NewConversationDialog.js.map +1 -0
- package/dist/conversations/components/PageLayout.js +16 -34
- package/dist/conversations/components/PageLayout.js.map +1 -1
- package/dist/conversations/components/PageSection.js +10 -19
- package/dist/conversations/components/PageSection.js.map +1 -1
- package/dist/conversations/hooks/useConversationQueries.js +48 -34
- package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
- package/dist/conversations/pages/ConversationListPage.js +187 -226
- package/dist/conversations/pages/ConversationListPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsLandingPage.js +109 -99
- package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +201 -182
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/dist/conversations/pages/NewConversationPage.js +19 -280
- package/dist/conversations/pages/NewConversationPage.js.map +1 -1
- package/dist/generated/wspace-operations.js +72 -56
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +2 -2
- package/dist/conversations/components/ConversationCard.js +0 -119
- package/dist/conversations/components/ConversationCard.js.map +0 -1
|
@@ -0,0 +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 { Button } 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 */}\n <div className=\"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 <div className=\"flex items-center justify-between gap-2\">\n <h1 className=\"text-base font-semibold text-text-primary\">\n {tr(\"conversations.title\", \"Conversations\")}\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 <div className=\"flex max-w-sm flex-col items-center gap-3 text-center\">\n <div className=\"flex size-16 items-center justify-center rounded-full bg-bg-sunken\">\n <MessageSquarePlus\n size={28}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n <Button type=\"button\" onClick={onNew} className=\"mt-2\">\n <MessageSquarePlus size={14} />\n {newLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useTr as e } from "../utils/i18n.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { useCreateConversation as n } from "../hooks/useConversationQueries.js";
|
|
4
|
+
import { nativeImpact as r, nativeNotify as i } from "../../utils/nativeBridge.js";
|
|
5
|
+
import { useCallback as a, useMemo as o, useState as s } from "react";
|
|
6
|
+
import { Globe as c, Hash as l, Lock as u, Megaphone as d, MessageCircle as ee, Shield as f, Users as p, X as m } from "lucide-react";
|
|
7
|
+
import { useEventBus as h } from "@burdenoff/fe-libs/shared/events";
|
|
8
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
9
|
+
import { Dialog as v, DialogContent as y, DialogDescription as b, DialogFooter as x, DialogHeader as S, DialogTitle as C } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
//#region src/conversations/components/NewConversationDialog.tsx
|
|
11
|
+
function te(e) {
|
|
12
|
+
if ("graphQLErrors" in e) {
|
|
13
|
+
let t = e.graphQLErrors;
|
|
14
|
+
if (t && t.length > 0 && t[0]?.message) return t[0].message;
|
|
15
|
+
}
|
|
16
|
+
return e.message;
|
|
17
|
+
}
|
|
18
|
+
function w({ open: w, onOpenChange: T, onCreated: E }) {
|
|
19
|
+
let { createConversation: D, loading: O, error: k } = n(), A = h(), j = e(), M = o(() => [
|
|
20
|
+
{
|
|
21
|
+
type: "DIRECT",
|
|
22
|
+
label: j("conversations.directMessage", "Direct Message"),
|
|
23
|
+
description: j("conversations.directMessageDescription", "One-on-one private conversation"),
|
|
24
|
+
icon: /* @__PURE__ */ g(ee, { size: 20 })
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "GROUP",
|
|
28
|
+
label: j("conversations.groupChat", "Group Chat"),
|
|
29
|
+
description: j("conversations.groupChatDescription", "Invite multiple people to a group conversation"),
|
|
30
|
+
icon: /* @__PURE__ */ g(p, { size: 20 })
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "CHANNEL",
|
|
34
|
+
label: j("conversations.channel", "Channel"),
|
|
35
|
+
description: j("conversations.channelDescription", "A topic-based channel that anyone can join"),
|
|
36
|
+
icon: /* @__PURE__ */ g(l, { size: 20 })
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "ANNOUNCEMENT",
|
|
40
|
+
label: j("conversations.announcement", "Announcement"),
|
|
41
|
+
description: j("conversations.announcementDescription", "Broadcast messages to a large audience"),
|
|
42
|
+
icon: /* @__PURE__ */ g(d, { size: 20 })
|
|
43
|
+
}
|
|
44
|
+
], [j]), N = o(() => [
|
|
45
|
+
{
|
|
46
|
+
value: "PUBLIC",
|
|
47
|
+
label: j("conversations.public", "Public"),
|
|
48
|
+
description: j("conversations.publicDescription", "Anyone in the workspace can discover and join"),
|
|
49
|
+
icon: /* @__PURE__ */ g(c, { size: 16 })
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: "PRIVATE",
|
|
53
|
+
label: j("conversations.private", "Private"),
|
|
54
|
+
description: j("conversations.privateDescription", "Only invited members can access"),
|
|
55
|
+
icon: /* @__PURE__ */ g(u, { size: 16 })
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: "PROTECTED",
|
|
59
|
+
label: j("conversations.protected", "Protected"),
|
|
60
|
+
description: j("conversations.protectedDescription", "Discoverable but requires approval to join"),
|
|
61
|
+
icon: /* @__PURE__ */ g(f, { size: 16 })
|
|
62
|
+
}
|
|
63
|
+
], [j]), [P, F] = s("GROUP"), [I, L] = s(""), [R, z] = s(""), [B, V] = s("PRIVATE"), [H, U] = s(""), [W, G] = s([]), K = P === "DIRECT", q = !K, J = a(() => {
|
|
64
|
+
F("GROUP"), L(""), z(""), V("PRIVATE"), U(""), G([]);
|
|
65
|
+
}, []), Y = a(() => {
|
|
66
|
+
let e = H.trim();
|
|
67
|
+
!e || W.includes(e) || (G(K ? [e] : (t) => [...t, e]), U(""));
|
|
68
|
+
}, [
|
|
69
|
+
H,
|
|
70
|
+
W,
|
|
71
|
+
K
|
|
72
|
+
]), X = a((e) => {
|
|
73
|
+
G((t) => t.filter((t) => t !== e));
|
|
74
|
+
}, []), Z = P === "DIRECT" || P === "GROUP", Q = a((e) => {
|
|
75
|
+
e || J(), T(e);
|
|
76
|
+
}, [T, J]), ne = a(async () => {
|
|
77
|
+
if (!(Z && W.length === 0) && !(q && !I.trim())) {
|
|
78
|
+
r("light");
|
|
79
|
+
try {
|
|
80
|
+
let e = await D({
|
|
81
|
+
type: P,
|
|
82
|
+
title: q ? I.trim() : void 0,
|
|
83
|
+
description: R.trim() || void 0,
|
|
84
|
+
participantIds: W,
|
|
85
|
+
visibility: B
|
|
86
|
+
});
|
|
87
|
+
if (!e) {
|
|
88
|
+
i("error");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
i("success"), A.emit("conversations.conversation.created", {
|
|
92
|
+
conversationId: e.id,
|
|
93
|
+
conversationType: P,
|
|
94
|
+
source: "new-conversation-dialog"
|
|
95
|
+
}), J(), T(!1), E?.(e.id);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw i("error"), e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [
|
|
101
|
+
A,
|
|
102
|
+
Z,
|
|
103
|
+
W,
|
|
104
|
+
q,
|
|
105
|
+
I,
|
|
106
|
+
D,
|
|
107
|
+
P,
|
|
108
|
+
R,
|
|
109
|
+
B,
|
|
110
|
+
J,
|
|
111
|
+
T,
|
|
112
|
+
E
|
|
113
|
+
]), $ = (!Z || W.length > 0) && (!q || I.trim().length > 0);
|
|
114
|
+
return /* @__PURE__ */ g(v, {
|
|
115
|
+
open: w,
|
|
116
|
+
onOpenChange: Q,
|
|
117
|
+
children: /* @__PURE__ */ _(y, {
|
|
118
|
+
className: "sm:max-w-[640px] max-h-[90vh] overflow-y-auto",
|
|
119
|
+
"data-tour": "new-conversation-dialog",
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ _(S, { children: [/* @__PURE__ */ g(C, { children: j("conversations.newConversation", "New Conversation") }), /* @__PURE__ */ g(b, { children: j("conversations.newConversationSubtitle", "Start a new conversation with your team") })] }),
|
|
122
|
+
/* @__PURE__ */ _("div", {
|
|
123
|
+
className: "space-y-6",
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ _("section", {
|
|
126
|
+
"data-tour": "conversation-type-selector",
|
|
127
|
+
className: "space-y-3",
|
|
128
|
+
children: [/* @__PURE__ */ g("h3", {
|
|
129
|
+
className: "text-sm font-semibold text-foreground",
|
|
130
|
+
children: j("conversations.conversationType", "Conversation Type")
|
|
131
|
+
}), /* @__PURE__ */ g("div", {
|
|
132
|
+
className: "grid grid-cols-1 sm:grid-cols-2 gap-3",
|
|
133
|
+
children: M.map((e) => /* @__PURE__ */ _("button", {
|
|
134
|
+
type: "button",
|
|
135
|
+
onClick: () => {
|
|
136
|
+
F(e.type), e.type === "DIRECT" && G((e) => e.slice(0, 1));
|
|
137
|
+
},
|
|
138
|
+
className: t("flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150", P === e.type ? "border-primary bg-primary/5 ring-1 ring-primary/20" : "border-border hover:border-muted-foreground/30 hover:bg-muted/30"),
|
|
139
|
+
children: [/* @__PURE__ */ g("div", {
|
|
140
|
+
className: t("p-2 rounded-lg", P === e.type ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"),
|
|
141
|
+
children: e.icon
|
|
142
|
+
}), /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
|
|
143
|
+
className: "text-sm font-medium text-foreground",
|
|
144
|
+
children: e.label
|
|
145
|
+
}), /* @__PURE__ */ g("p", {
|
|
146
|
+
className: "text-xs text-muted-foreground mt-0.5",
|
|
147
|
+
children: e.description
|
|
148
|
+
})] })]
|
|
149
|
+
}, e.type))
|
|
150
|
+
})]
|
|
151
|
+
}),
|
|
152
|
+
!K && /* @__PURE__ */ _("section", {
|
|
153
|
+
"data-tour": "conversation-visibility-selector",
|
|
154
|
+
className: "space-y-3",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ g("h3", {
|
|
157
|
+
className: "text-sm font-semibold text-foreground",
|
|
158
|
+
children: j("conversations.visibility", "Visibility")
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ g("div", {
|
|
161
|
+
className: "flex gap-2 flex-wrap",
|
|
162
|
+
children: N.map((e) => /* @__PURE__ */ _("button", {
|
|
163
|
+
type: "button",
|
|
164
|
+
onClick: () => V(e.value),
|
|
165
|
+
className: t("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all", B === e.value ? "border-primary bg-primary/5 text-primary font-medium" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted/30"),
|
|
166
|
+
children: [e.icon, e.label]
|
|
167
|
+
}, e.value))
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ g("p", {
|
|
170
|
+
className: "text-xs text-muted-foreground",
|
|
171
|
+
children: N.find((e) => e.value === B)?.description
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
}),
|
|
175
|
+
q && /* @__PURE__ */ _("section", {
|
|
176
|
+
"data-tour": "conversation-create-details",
|
|
177
|
+
className: "space-y-3",
|
|
178
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ _("label", {
|
|
179
|
+
htmlFor: "conv-title",
|
|
180
|
+
className: "text-sm font-semibold text-foreground",
|
|
181
|
+
children: [
|
|
182
|
+
j("conversations.name", "Name"),
|
|
183
|
+
" ",
|
|
184
|
+
/* @__PURE__ */ g("span", {
|
|
185
|
+
className: "text-destructive",
|
|
186
|
+
children: "*"
|
|
187
|
+
})
|
|
188
|
+
]
|
|
189
|
+
}), /* @__PURE__ */ g("input", {
|
|
190
|
+
id: "conv-title",
|
|
191
|
+
type: "text",
|
|
192
|
+
value: I,
|
|
193
|
+
onChange: (e) => L(e.target.value),
|
|
194
|
+
placeholder: P === "CHANNEL" ? j("conversations.channelNamePlaceholder", "e.g., engineering-general") : j("conversations.conversationNamePlaceholder", "e.g., Product Launch Q1"),
|
|
195
|
+
maxLength: 200,
|
|
196
|
+
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
197
|
+
})] }), /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
198
|
+
htmlFor: "conv-desc",
|
|
199
|
+
className: "text-sm font-semibold text-foreground",
|
|
200
|
+
children: j("conversations.description", "Description")
|
|
201
|
+
}), /* @__PURE__ */ g("textarea", {
|
|
202
|
+
id: "conv-desc",
|
|
203
|
+
value: R,
|
|
204
|
+
onChange: (e) => z(e.target.value),
|
|
205
|
+
placeholder: j("conversations.descriptionPlaceholder", "What is this conversation about?"),
|
|
206
|
+
rows: 2,
|
|
207
|
+
maxLength: 1e3,
|
|
208
|
+
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none"
|
|
209
|
+
})] })]
|
|
210
|
+
}),
|
|
211
|
+
/* @__PURE__ */ _("section", {
|
|
212
|
+
"data-tour": "conversation-participants",
|
|
213
|
+
className: "space-y-3",
|
|
214
|
+
children: [
|
|
215
|
+
/* @__PURE__ */ _("h3", {
|
|
216
|
+
className: "text-sm font-semibold text-foreground",
|
|
217
|
+
children: [K ? j("conversations.participantId", "Participant ID") : j("conversations.participantIds", "Participant IDs"), !Z && /* @__PURE__ */ _("span", {
|
|
218
|
+
className: "ml-1.5 text-xs font-normal text-muted-foreground",
|
|
219
|
+
children: [
|
|
220
|
+
"(",
|
|
221
|
+
j("conversations.optional", "optional"),
|
|
222
|
+
")"
|
|
223
|
+
]
|
|
224
|
+
})]
|
|
225
|
+
}),
|
|
226
|
+
W.length > 0 && /* @__PURE__ */ g("div", {
|
|
227
|
+
className: "flex flex-wrap gap-2",
|
|
228
|
+
children: W.map((e) => /* @__PURE__ */ _("span", {
|
|
229
|
+
className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium",
|
|
230
|
+
children: [e, /* @__PURE__ */ g("button", {
|
|
231
|
+
type: "button",
|
|
232
|
+
"aria-label": j("conversations.removeParticipant", "Remove participant"),
|
|
233
|
+
onClick: () => X(e),
|
|
234
|
+
className: "p-0.5 rounded-full hover:bg-primary/20",
|
|
235
|
+
children: /* @__PURE__ */ g(m, { size: 12 })
|
|
236
|
+
})]
|
|
237
|
+
}, e))
|
|
238
|
+
}),
|
|
239
|
+
/* @__PURE__ */ _("div", {
|
|
240
|
+
className: "flex items-center gap-2",
|
|
241
|
+
children: [/* @__PURE__ */ g("input", {
|
|
242
|
+
type: "text",
|
|
243
|
+
"aria-label": j("conversations.participantInputLabel", "Add participant"),
|
|
244
|
+
value: H,
|
|
245
|
+
onChange: (e) => U(e.target.value),
|
|
246
|
+
onKeyDown: (e) => {
|
|
247
|
+
e.key === "Enter" && (e.preventDefault(), Y());
|
|
248
|
+
},
|
|
249
|
+
placeholder: j("conversations.participantInputPlaceholder", "Enter user ID and press Enter"),
|
|
250
|
+
className: "w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
251
|
+
}), /* @__PURE__ */ g("button", {
|
|
252
|
+
type: "button",
|
|
253
|
+
onClick: Y,
|
|
254
|
+
className: "px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted/50",
|
|
255
|
+
children: j("conversations.add", "Add")
|
|
256
|
+
})]
|
|
257
|
+
})
|
|
258
|
+
]
|
|
259
|
+
}),
|
|
260
|
+
k && /* @__PURE__ */ g("div", {
|
|
261
|
+
className: "text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2",
|
|
262
|
+
children: te(k)
|
|
263
|
+
})
|
|
264
|
+
]
|
|
265
|
+
}),
|
|
266
|
+
/* @__PURE__ */ _(x, { children: [/* @__PURE__ */ g("button", {
|
|
267
|
+
type: "button",
|
|
268
|
+
onClick: () => Q(!1),
|
|
269
|
+
className: "px-4 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors",
|
|
270
|
+
children: j("conversations.cancel", "Cancel")
|
|
271
|
+
}), /* @__PURE__ */ g("button", {
|
|
272
|
+
type: "button",
|
|
273
|
+
onClick: () => void ne(),
|
|
274
|
+
disabled: !$ || O,
|
|
275
|
+
className: t("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !O ? "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm" : "bg-muted text-muted-foreground cursor-not-allowed"),
|
|
276
|
+
children: O ? j("conversations.creating", "Creating...") : j("conversations.createConversation", "Create Conversation")
|
|
277
|
+
})] })
|
|
278
|
+
]
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { w as NewConversationDialog };
|
|
284
|
+
|
|
285
|
+
//# sourceMappingURL=NewConversationDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NewConversationDialog.js","names":[],"sources":["../../../src/conversations/components/NewConversationDialog.tsx"],"sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@burdenoff/fe-libs/ui\";\nimport {\n Globe,\n Hash,\n Lock,\n Megaphone,\n MessageCircle,\n Shield,\n Users,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport { useCreateConversation } from \"../hooks/useConversationQueries\";\nimport type { ConversationType, ConversationVisibility } from \"../types\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\n/** Extract the first meaningful GraphQL error message from an ApolloError. */\nfunction getErrorMessage(error: Error): string {\n if (\"graphQLErrors\" in error) {\n const gqlErrors = (error as { graphQLErrors?: Array<{ message?: string }> })\n .graphQLErrors;\n if (gqlErrors && gqlErrors.length > 0 && gqlErrors[0]?.message) {\n return gqlErrors[0].message;\n }\n }\n return error.message;\n}\n\ninterface ConversationTypeOption {\n type: ConversationType;\n label: string;\n description: string;\n icon: React.ReactNode;\n}\n\nexport interface NewConversationDialogProps {\n /** Whether the dialog is open. */\n open: boolean;\n /** Open-state change handler. Fires with `false` on close (overlay click,\n * Escape, X button, Cancel). */\n onOpenChange: (open: boolean) => void;\n /**\n * Called after a conversation is successfully created. Receives the new\n * conversation id so the parent can navigate to the detail route.\n * The dialog also closes itself (via `onOpenChange(false)`) before this\n * fires, so the parent does not need to close the dialog manually.\n */\n onCreated?: (conversationId: string) => void;\n}\n\n/**\n * NewConversationDialog — modal form for creating a new conversation.\n *\n * Renders inside the canonical `<Dialog>` primitive from\n * `@burdenoff/fe-libs/ui` so it inherits theme tokens, focus management,\n * and overlay behaviour from the shell.\n *\n * Form state is owned locally; on successful submit the dialog clears\n * state, closes itself, and invokes `onCreated` with the new\n * conversation id.\n */\nexport function NewConversationDialog({\n open,\n onOpenChange,\n onCreated,\n}: NewConversationDialogProps) {\n const { createConversation, loading, error } = useCreateConversation();\n const bus = useEventBus();\n const tr = useTr();\n\n const CONVERSATION_TYPES: ConversationTypeOption[] = useMemo(\n () => [\n {\n type: \"DIRECT\" as ConversationType,\n label: tr(\"conversations.directMessage\", \"Direct Message\"),\n description: tr(\n \"conversations.directMessageDescription\",\n \"One-on-one private conversation\",\n ),\n icon: <MessageCircle size={20} />,\n },\n {\n type: \"GROUP\" as ConversationType,\n label: tr(\"conversations.groupChat\", \"Group Chat\"),\n description: tr(\n \"conversations.groupChatDescription\",\n \"Invite multiple people to a group conversation\",\n ),\n icon: <Users size={20} />,\n },\n {\n type: \"CHANNEL\" as ConversationType,\n label: tr(\"conversations.channel\", \"Channel\"),\n description: tr(\n \"conversations.channelDescription\",\n \"A topic-based channel that anyone can join\",\n ),\n icon: <Hash size={20} />,\n },\n {\n type: \"ANNOUNCEMENT\" as ConversationType,\n label: tr(\"conversations.announcement\", \"Announcement\"),\n description: tr(\n \"conversations.announcementDescription\",\n \"Broadcast messages to a large audience\",\n ),\n icon: <Megaphone size={20} />,\n },\n ],\n [tr],\n );\n\n const VISIBILITY_OPTIONS: Array<{\n value: ConversationVisibility;\n label: string;\n description: string;\n icon: React.ReactNode;\n }> = useMemo(\n () => [\n {\n value: \"PUBLIC\" as ConversationVisibility,\n label: tr(\"conversations.public\", \"Public\"),\n description: tr(\n \"conversations.publicDescription\",\n \"Anyone in the workspace can discover and join\",\n ),\n icon: <Globe size={16} />,\n },\n {\n value: \"PRIVATE\" as ConversationVisibility,\n label: tr(\"conversations.private\", \"Private\"),\n description: tr(\n \"conversations.privateDescription\",\n \"Only invited members can access\",\n ),\n icon: <Lock size={16} />,\n },\n {\n value: \"PROTECTED\" as ConversationVisibility,\n label: tr(\"conversations.protected\", \"Protected\"),\n description: tr(\n \"conversations.protectedDescription\",\n \"Discoverable but requires approval to join\",\n ),\n icon: <Shield size={16} />,\n },\n ],\n [tr],\n );\n\n const [selectedType, setSelectedType] = useState<ConversationType>(\"GROUP\");\n const [title, setTitle] = useState(\"\");\n const [description, setDescription] = useState(\"\");\n const [visibility, setVisibility] =\n useState<ConversationVisibility>(\"PRIVATE\");\n const [participantInput, setParticipantInput] = useState(\"\");\n const [participantIds, setParticipantIds] = useState<string[]>([]);\n\n const isDirectMessage = selectedType === \"DIRECT\";\n const needsTitle = !isDirectMessage;\n\n const resetForm = useCallback(() => {\n setSelectedType(\"GROUP\");\n setTitle(\"\");\n setDescription(\"\");\n setVisibility(\"PRIVATE\");\n setParticipantInput(\"\");\n setParticipantIds([]);\n }, []);\n\n const addParticipant = useCallback(() => {\n const value = participantInput.trim();\n if (!value || participantIds.includes(value)) return;\n if (isDirectMessage) {\n setParticipantIds([value]);\n } else {\n setParticipantIds((prev) => [...prev, value]);\n }\n setParticipantInput(\"\");\n }, [participantInput, participantIds, isDirectMessage]);\n\n const removeParticipant = useCallback((participantId: string) => {\n setParticipantIds((prev) => prev.filter((id) => id !== participantId));\n }, []);\n\n const requiresParticipants =\n selectedType === \"DIRECT\" || selectedType === \"GROUP\";\n\n const handleOpenChange = useCallback(\n (nextOpen: boolean) => {\n if (!nextOpen) {\n resetForm();\n }\n onOpenChange(nextOpen);\n },\n [onOpenChange, resetForm],\n );\n\n const handleCreate = useCallback(async () => {\n if (requiresParticipants && participantIds.length === 0) return;\n if (needsTitle && !title.trim()) return;\n\n nativeImpact(\"light\");\n try {\n const conversation = await createConversation({\n type: selectedType,\n title: needsTitle ? title.trim() : undefined,\n description: description.trim() || undefined,\n participantIds,\n visibility,\n });\n\n if (!conversation) {\n nativeNotify(\"error\");\n return;\n }\n nativeNotify(\"success\");\n bus.emit(\"conversations.conversation.created\", {\n conversationId: conversation.id,\n conversationType: selectedType,\n source: \"new-conversation-dialog\",\n });\n resetForm();\n onOpenChange(false);\n onCreated?.(conversation.id);\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n }, [\n bus,\n requiresParticipants,\n participantIds,\n needsTitle,\n title,\n createConversation,\n selectedType,\n description,\n visibility,\n resetForm,\n onOpenChange,\n onCreated,\n ]);\n\n const canCreate =\n (!requiresParticipants || participantIds.length > 0) &&\n (!needsTitle || title.trim().length > 0);\n\n return (\n <Dialog open={open} onOpenChange={handleOpenChange}>\n <DialogContent\n className=\"sm:max-w-[640px] max-h-[90vh] overflow-y-auto\"\n data-tour=\"new-conversation-dialog\"\n >\n <DialogHeader>\n <DialogTitle>\n {tr(\"conversations.newConversation\", \"New Conversation\")}\n </DialogTitle>\n <DialogDescription>\n {tr(\n \"conversations.newConversationSubtitle\",\n \"Start a new conversation with your team\",\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n <section data-tour=\"conversation-type-selector\" className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.conversationType\", \"Conversation Type\")}\n </h3>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {CONVERSATION_TYPES.map((option) => (\n <button\n key={option.type}\n type=\"button\"\n onClick={() => {\n setSelectedType(option.type);\n if (option.type === \"DIRECT\") {\n setParticipantIds((prev) => prev.slice(0, 1));\n }\n }}\n className={cn(\n \"flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150\",\n selectedType === option.type\n ? \"border-primary bg-primary/5 ring-1 ring-primary/20\"\n : \"border-border hover:border-muted-foreground/30 hover:bg-muted/30\",\n )}\n >\n <div\n className={cn(\n \"p-2 rounded-lg\",\n selectedType === option.type\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {option.icon}\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {option.label}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {option.description}\n </p>\n </div>\n </button>\n ))}\n </div>\n </section>\n\n {!isDirectMessage && (\n <section\n data-tour=\"conversation-visibility-selector\"\n className=\"space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.visibility\", \"Visibility\")}\n </h3>\n <div className=\"flex gap-2 flex-wrap\">\n {VISIBILITY_OPTIONS.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => setVisibility(option.value)}\n className={cn(\n \"flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all\",\n visibility === option.value\n ? \"border-primary bg-primary/5 text-primary font-medium\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/30\",\n )}\n >\n {option.icon}\n {option.label}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {\n VISIBILITY_OPTIONS.find(\n (option) => option.value === visibility,\n )?.description\n }\n </p>\n </section>\n )}\n\n {needsTitle && (\n <section\n data-tour=\"conversation-create-details\"\n className=\"space-y-3\"\n >\n <div>\n <label\n htmlFor=\"conv-title\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.name\", \"Name\")}{\" \"}\n <span className=\"text-destructive\">*</span>\n </label>\n <input\n id=\"conv-title\"\n type=\"text\"\n value={title}\n onChange={(event) => setTitle(event.target.value)}\n placeholder={\n selectedType === \"CHANNEL\"\n ? tr(\n \"conversations.channelNamePlaceholder\",\n \"e.g., engineering-general\",\n )\n : tr(\n \"conversations.conversationNamePlaceholder\",\n \"e.g., Product Launch Q1\",\n )\n }\n maxLength={200}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n </div>\n <div>\n <label\n htmlFor=\"conv-desc\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.description\", \"Description\")}\n </label>\n <textarea\n id=\"conv-desc\"\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n placeholder={tr(\n \"conversations.descriptionPlaceholder\",\n \"What is this conversation about?\",\n )}\n rows={2}\n maxLength={1000}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none\"\n />\n </div>\n </section>\n )}\n\n <section data-tour=\"conversation-participants\" className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {isDirectMessage\n ? tr(\"conversations.participantId\", \"Participant ID\")\n : tr(\"conversations.participantIds\", \"Participant IDs\")}\n {!requiresParticipants && (\n <span className=\"ml-1.5 text-xs font-normal text-muted-foreground\">\n ({tr(\"conversations.optional\", \"optional\")})\n </span>\n )}\n </h3>\n\n {participantIds.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {participantIds.map((participantId) => (\n <span\n key={participantId}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium\"\n >\n {participantId}\n <button\n type=\"button\"\n aria-label={tr(\n \"conversations.removeParticipant\",\n \"Remove participant\",\n )}\n onClick={() => removeParticipant(participantId)}\n className=\"p-0.5 rounded-full hover:bg-primary/20\"\n >\n <X size={12} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n aria-label={tr(\n \"conversations.participantInputLabel\",\n \"Add participant\",\n )}\n value={participantInput}\n onChange={(event) => setParticipantInput(event.target.value)}\n onKeyDown={(event) => {\n if (event.key !== \"Enter\") return;\n event.preventDefault();\n addParticipant();\n }}\n placeholder={tr(\n \"conversations.participantInputPlaceholder\",\n \"Enter user ID and press Enter\",\n )}\n className=\"w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n <button\n type=\"button\"\n onClick={addParticipant}\n className=\"px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.add\", \"Add\")}\n </button>\n </div>\n </section>\n\n {error && (\n <div className=\"text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2\">\n {getErrorMessage(error)}\n </div>\n )}\n </div>\n\n <DialogFooter>\n <button\n type=\"button\"\n onClick={() => handleOpenChange(false)}\n className=\"px-4 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n {tr(\"conversations.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleCreate()}\n disabled={!canCreate || loading}\n className={cn(\n \"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all\",\n canCreate && !loading\n ? \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground cursor-not-allowed\",\n )}\n >\n {loading\n ? tr(\"conversations.creating\", \"Creating...\")\n : tr(\"conversations.createConversation\", \"Create Conversation\")}\n </button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,8 +2,9 @@ import { cn as e } from "../utils/cn.js";
|
|
|
2
2
|
import "./PageSection.js";
|
|
3
3
|
import "./EmptyState.js";
|
|
4
4
|
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
5
|
+
import { PageHeader as r } from "@burdenoff/fe-libs/chrome";
|
|
5
6
|
//#region src/conversations/components/PageLayout.tsx
|
|
6
|
-
var
|
|
7
|
+
var i = {
|
|
7
8
|
sm: "max-w-screen-sm",
|
|
8
9
|
md: "max-w-screen-md",
|
|
9
10
|
lg: "max-w-screen-lg",
|
|
@@ -12,47 +13,28 @@ var r = {
|
|
|
12
13
|
"3xl": "max-w-[1600px]",
|
|
13
14
|
full: "max-w-full"
|
|
14
15
|
};
|
|
15
|
-
function
|
|
16
|
+
function a({ children: a, title: o, subtitle: s, icon: c, headerActions: l, headerContent: u, maxWidth: d = "xl", showHeader: f = !0, className: p, contentClassName: m, dataTour: h }) {
|
|
16
17
|
return /* @__PURE__ */ t("div", {
|
|
17
|
-
"data-tour":
|
|
18
|
-
className: e("min-h-full w-full", "px-4 sm:px-6 lg:px-8 xl:px-12", "py-6 sm:py-8 lg:py-10", "animate-in fade-in-0 slide-in-from-bottom-4 duration-500",
|
|
18
|
+
"data-tour": h,
|
|
19
|
+
className: e("min-h-full w-full", "px-4 sm:px-6 lg:px-8 xl:px-12", "py-6 sm:py-8 lg:py-10", "animate-in fade-in-0 slide-in-from-bottom-4 duration-500", p),
|
|
19
20
|
children: /* @__PURE__ */ n("div", {
|
|
20
|
-
className: e("mx-auto w-full",
|
|
21
|
-
children: [
|
|
22
|
-
className: "mb-6 sm:mb-8",
|
|
23
|
-
children: [/* @__PURE__ */
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
children: [s && /* @__PURE__ */ t("div", {
|
|
30
|
-
className: "flex-shrink-0 p-2.5 rounded-xl bg-primary/10 text-primary",
|
|
31
|
-
children: s
|
|
32
|
-
}), a && /* @__PURE__ */ t("h1", {
|
|
33
|
-
className: "text-2xl sm:text-3xl font-bold tracking-tight text-foreground",
|
|
34
|
-
children: a
|
|
35
|
-
})]
|
|
36
|
-
}), o && /* @__PURE__ */ t("p", {
|
|
37
|
-
className: "text-sm sm:text-base text-muted-foreground max-w-2xl",
|
|
38
|
-
children: o
|
|
39
|
-
})]
|
|
40
|
-
}), c && /* @__PURE__ */ t("div", {
|
|
41
|
-
className: "flex items-center gap-2 flex-shrink-0",
|
|
42
|
-
children: c
|
|
43
|
-
})]
|
|
44
|
-
}), l && /* @__PURE__ */ t("div", {
|
|
45
|
-
className: "mt-4",
|
|
46
|
-
children: l
|
|
47
|
-
})]
|
|
21
|
+
className: e("mx-auto w-full", i[d], m),
|
|
22
|
+
children: [f && (o || l) && /* @__PURE__ */ n("div", {
|
|
23
|
+
className: "mb-6 sm:mb-8 space-y-4",
|
|
24
|
+
children: [/* @__PURE__ */ t(r, {
|
|
25
|
+
title: o ?? "",
|
|
26
|
+
description: s,
|
|
27
|
+
icon: c,
|
|
28
|
+
actions: l
|
|
29
|
+
}), u && /* @__PURE__ */ t("div", { children: u })]
|
|
48
30
|
}), /* @__PURE__ */ t("div", {
|
|
49
31
|
className: "space-y-6",
|
|
50
|
-
children:
|
|
32
|
+
children: a
|
|
51
33
|
})]
|
|
52
34
|
})
|
|
53
35
|
});
|
|
54
36
|
}
|
|
55
37
|
//#endregion
|
|
56
|
-
export {
|
|
38
|
+
export { a as PageLayout };
|
|
57
39
|
|
|
58
40
|
//# sourceMappingURL=PageLayout.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/conversations/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\ninterface PageLayoutProps {\n children: ReactNode;\n /** Page title */\n title?: string;\n /** Page subtitle/description */\n subtitle?: string;\n /** Icon component to display next to title */\n icon?: ReactNode;\n /** Actions to display in header (buttons, etc.) */\n headerActions?: ReactNode;\n /** Additional header content below title */\n headerContent?: ReactNode;\n /** Maximum width of content: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' */\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\";\n /** Whether to show the header section */\n showHeader?: boolean;\n /** Additional CSS classes for the container */\n className?: string;\n /** Additional CSS classes for the content area */\n contentClassName?: string;\n /** Stable selector for guided tours and smoke tests */\n dataTour?: string;\n}\n\nconst maxWidthClasses = {\n sm: \"max-w-screen-sm\", // 640px\n md: \"max-w-screen-md\", // 768px\n lg: \"max-w-screen-lg\", // 1024px\n xl: \"max-w-screen-xl\", // 1280px\n \"2xl\": \"max-w-screen-2xl\", // 1536px\n \"3xl\": \"max-w-[1600px]\",\n full: \"max-w-full\",\n};\n\n/**\n * PageLayout
|
|
1
|
+
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/conversations/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { PageHeader as CanonicalPageHeader } from \"@burdenoff/fe-libs/chrome\";\nimport { cn } from \"../utils/cn\";\n\ninterface PageLayoutProps {\n children: ReactNode;\n /** Page title */\n title?: string;\n /** Page subtitle/description */\n subtitle?: string;\n /** Icon component to display next to title */\n icon?: ReactNode;\n /** Actions to display in header (buttons, etc.) */\n headerActions?: ReactNode;\n /** Additional header content below title */\n headerContent?: ReactNode;\n /** Maximum width of content: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' */\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\";\n /** Whether to show the header section */\n showHeader?: boolean;\n /** Additional CSS classes for the container */\n className?: string;\n /** Additional CSS classes for the content area */\n contentClassName?: string;\n /** Stable selector for guided tours and smoke tests */\n dataTour?: string;\n}\n\nconst maxWidthClasses = {\n sm: \"max-w-screen-sm\", // 640px\n md: \"max-w-screen-md\", // 768px\n lg: \"max-w-screen-lg\", // 1024px\n xl: \"max-w-screen-xl\", // 1280px\n \"2xl\": \"max-w-screen-2xl\", // 1536px\n \"3xl\": \"max-w-[1600px]\",\n full: \"max-w-full\",\n};\n\n/**\n * PageLayout — local wrapper that delegates header rendering to the\n * canonical `<PageHeader>` from `@burdenoff/fe-libs/chrome` while\n * preserving the legacy `headerContent` slot and the max-width container\n * chrome that microfe-conversations pages rely on.\n */\nexport function PageLayout({\n children,\n title,\n subtitle,\n icon,\n headerActions,\n headerContent,\n maxWidth = \"xl\",\n showHeader = true,\n className,\n contentClassName,\n dataTour,\n}: PageLayoutProps) {\n return (\n <div\n data-tour={dataTour}\n className={cn(\n \"min-h-full w-full\",\n \"px-4 sm:px-6 lg:px-8 xl:px-12\",\n \"py-6 sm:py-8 lg:py-10\",\n \"animate-in fade-in-0 slide-in-from-bottom-4 duration-500\",\n className,\n )}\n >\n <div\n className={cn(\n \"mx-auto w-full\",\n maxWidthClasses[maxWidth],\n contentClassName,\n )}\n >\n {showHeader && (title || headerActions) && (\n <div className=\"mb-6 sm:mb-8 space-y-4\">\n <CanonicalPageHeader\n title={title ?? \"\"}\n description={subtitle}\n icon={icon}\n actions={headerActions}\n />\n {headerContent && <div>{headerContent}</div>}\n </div>\n )}\n\n <div className=\"space-y-6\">{children}</div>\n </div>\n </div>\n );\n}\n\nexport { PageSection } from \"./PageSection\";\nexport { EmptyState } from \"./EmptyState\";\n"],"mappings":";;;;;;AA4BA,IAAM,IAAkB;CACtB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,OAAO;CACP,MAAM;CACP;AAQD,SAAgB,EAAW,EACzB,aACA,UACA,aACA,SACA,kBACA,kBACA,cAAW,MACX,gBAAa,IACb,cACA,qBACA,eACkB;AAClB,QACE,kBAAC,OAAD;EACE,aAAW;EACX,WAAW,EACT,qBACA,iCACA,yBACA,4DACA,EACD;YAED,kBAAC,OAAD;GACE,WAAW,EACT,kBACA,EAAgB,IAChB,EACD;aALH,CAOG,MAAe,KAAS,MACvB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,OAAO,KAAS;KAChB,aAAa;KACP;KACN,SAAS;KACT,CAAA,EACD,KAAiB,kBAAC,OAAD,EAAA,UAAM,GAAoB,CAAA,CACxC;OAGR,kBAAC,OAAD;IAAK,WAAU;IAAa;IAAe,CAAA,CACvC;;EACF,CAAA"}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as e } from "react/jsx-runtime";
|
|
2
|
+
import { FormSection as t } from "@burdenoff/fe-libs/form";
|
|
3
3
|
//#region src/conversations/components/PageSection.tsx
|
|
4
|
-
function
|
|
5
|
-
return /* @__PURE__ */
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
children: i
|
|
12
|
-
}), a && /* @__PURE__ */ t("p", {
|
|
13
|
-
className: "text-sm text-muted-foreground",
|
|
14
|
-
children: a
|
|
15
|
-
})] }), o && /* @__PURE__ */ t("div", {
|
|
16
|
-
className: "flex items-center gap-2",
|
|
17
|
-
children: o
|
|
18
|
-
})]
|
|
19
|
-
}), r]
|
|
4
|
+
function n({ children: n, title: r, description: i, actions: a, className: o }) {
|
|
5
|
+
return /* @__PURE__ */ e(t, {
|
|
6
|
+
title: r ?? "",
|
|
7
|
+
description: i,
|
|
8
|
+
actions: a,
|
|
9
|
+
className: o,
|
|
10
|
+
children: n
|
|
20
11
|
});
|
|
21
12
|
}
|
|
22
13
|
//#endregion
|
|
23
|
-
export {
|
|
14
|
+
export { n as PageSection };
|
|
24
15
|
|
|
25
16
|
//# sourceMappingURL=PageSection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageSection.js","names":[],"sources":["../../../src/conversations/components/PageSection.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport {
|
|
1
|
+
{"version":3,"file":"PageSection.js","names":[],"sources":["../../../src/conversations/components/PageSection.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { FormSection } from \"@burdenoff/fe-libs/form\";\n\n/**\n * PageSection — local wrapper that delegates to the canonical\n * `<FormSection>` from `@burdenoff/fe-libs/form`. Keeps the legacy\n * `title`/`description`/`actions`/`className` shape so existing call\n * sites stay green.\n */\nexport function PageSection({\n children,\n title,\n description,\n actions,\n className,\n}: {\n children: ReactNode;\n title?: string;\n description?: string;\n actions?: ReactNode;\n className?: string;\n}) {\n return (\n <FormSection\n title={title ?? \"\"}\n description={description}\n actions={actions}\n className={className}\n >\n {children}\n </FormSection>\n );\n}\n"],"mappings":";;;AASA,SAAgB,EAAY,EAC1B,aACA,UACA,gBACA,YACA,gBAOC;AACD,QACE,kBAAC,GAAD;EACE,OAAO,KAAS;EACH;EACJ;EACE;EAEV;EACW,CAAA"}
|