@burdenoff/microfe-conversations 2026.528.1 → 2026.529.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,7 +38,7 @@ var d = (d) => {
38
38
  {
39
39
  id: "conversations-new",
40
40
  label: g("conversations.newConversation", "New conversation"),
41
- path: t(m, "new"),
41
+ path: `${m}?new=true`,
42
42
  icon: /* @__PURE__ */ u(c, { size: 16 }),
43
43
  order: 30
44
44
  },
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsRoot.js","names":[],"sources":["../../src/conversations/ConversationsRoot.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect } from \"react\";\nimport { ErrorBoundary } from \"@burdenoff/fe-libs/shared/components\";\nimport { FeatureFlagProvider } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { MessageCircle, Plus, Settings } from \"lucide-react\";\nimport { ConversationsProvider } from \"./providers/ConversationsProvider\";\nimport type { NavItem, ConversationsRootProps } from \"./types\";\nimport { joinPath } from \"./utils/navigation\";\nimport { useTr } from \"./utils/i18n\";\nimport { ConversationsUserRoutes } from \"./ConversationsUserRoutes\";\n\n/**\n * Root component for the Conversations microfrontend.\n *\n * This is the main entry point that host applications import and render.\n * It sets up the necessary providers and renders the internal routing.\n * Automatically registers navigation items with the shell if registration functions are provided.\n *\n * @example\n * ```tsx\n * import { ConversationsRoot, type ConversationsRootProps } from '@burdenoff/micro-fe/conversations';\n *\n * function App() {\n * const props: ConversationsRootProps = {\n * basePath: '/conversations',\n * navigate: (path) => router.push(path),\n * currentUser: { id: '1', email: 'user@example.com', firstName: 'John', lastName: 'Doe' },\n * workspaceId: 'ws-123',\n * apiGatewayUrl: 'https://api.example.com',\n * authToken: 'token',\n * defaultView: 'inbox',\n * onConversationSelect: (conversation) => console.log('Conversation selected:', conversation),\n * registerNavItems: (items) => { return () => {}; },\n * };\n *\n * return <ConversationsRoot {...props} />;\n * }\n * ```\n */\nexport const ConversationsRoot: FC<ConversationsRootProps> = (props) => {\n const {\n registerNavItems,\n registerFooterItems,\n basePath = \"/\",\n workspaceId,\n } = props;\n const tr = useTr();\n\n useEffect(() => {\n if (!registerNavItems) return;\n\n const archivedPath = `${joinPath(basePath, \"list\")}?filter=archived`;\n\n const conversationsNavItem: NavItem = {\n id: \"conversations\",\n label: tr(\"conversations.title\", \"Conversations\"),\n path: basePath,\n icon: <MessageCircle size={20} />,\n order: 10,\n group: \"conversations\",\n children: [\n {\n id: \"conversations-inbox\",\n label: tr(\"conversations.inbox\", \"Inbox\"),\n path: basePath,\n icon: <MessageCircle size={16} />,\n order: 10,\n },\n {\n id: \"conversations-list\",\n label: tr(\"conversations.allConversations\", \"All conversations\"),\n path: joinPath(basePath, \"list\"),\n icon: <MessageCircle size={16} />,\n order: 20,\n },\n {\n id: \"conversations-new\",\n label: tr(\"conversations.newConversation\", \"New conversation\"),\n path: joinPath(basePath, \"new\"),\n icon: <Plus size={16} />,\n order: 30,\n },\n {\n id: \"conversations-archived\",\n label: tr(\"conversations.archived\", \"Archived\"),\n path: archivedPath,\n icon: <MessageCircle size={16} />,\n order: 40,\n },\n {\n id: \"conversations-settings\",\n label: tr(\"conversations.settings\", \"Settings\"),\n path: joinPath(basePath, \"settings\"),\n icon: <Settings size={16} />,\n order: 50,\n },\n ],\n };\n\n const cleanup = registerNavItems(conversationsNavItem);\n return cleanup;\n }, [registerNavItems, basePath, tr]);\n\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: \"conversations-status\",\n label: tr(\"conversations.conversationsReady\", \"Conversations Ready\"),\n type: \"status\" as const,\n order: 10,\n section: \"left\" as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems, tr]);\n\n return (\n <ErrorBoundary\n fallback={\n <div className=\"p-6 text-center text-text-secondary\" role=\"alert\">\n {tr(\n \"conversations.moduleLoadError\",\n \"Something went wrong loading this module. Please refresh the page.\",\n )}\n </div>\n }\n onError={(error, errorInfo) => {\n // SRE forwarding — see ConversationsUserRoot for the migration target.\n console.error(\n \"[microfe-conversations:legacy-root] error boundary caught\",\n {\n message: error.message,\n stack: error.stack,\n componentStack: errorInfo.componentStack,\n },\n );\n }}\n >\n <FeatureFlagProvider\n workspaceId={workspaceId ?? null}\n gateway=\"global\"\n defaultEnabled={true}\n >\n <ConversationsProvider {...props}>\n <ConversationsUserRoutes />\n </ConversationsProvider>\n </FeatureFlagProvider>\n </ErrorBoundary>\n );\n};\n"],"mappings":";;;;;;;;;;AAuCA,IAAa,KAAiD,MAAU;CACtE,IAAM,EACJ,qBACA,wBACA,cAAW,KACX,mBACE,GACE,IAAK,GAAO;AA0ElB,QAxEA,QAAgB;AACd,MAAI,CAAC,EAAkB;EAEvB,IAAM,IAAe,GAAG,EAAS,GAAU,OAAO,CAAC;AAiDnD,SADgB,EA9CsB;GACpC,IAAI;GACJ,OAAO,EAAG,uBAAuB,gBAAgB;GACjD,MAAM;GACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;GACjC,OAAO;GACP,OAAO;GACP,UAAU;IACR;KACE,IAAI;KACJ,OAAO,EAAG,uBAAuB,QAAQ;KACzC,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,kCAAkC,oBAAoB;KAChE,MAAM,EAAS,GAAU,OAAO;KAChC,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,iCAAiC,mBAAmB;KAC9D,MAAM,EAAS,GAAU,MAAM;KAC/B,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;KACxB,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM,EAAS,GAAU,WAAW;KACpC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;KAC5B,OAAO;KACR;IACF;GACF,CAEqD;IAErD;EAAC;EAAkB;EAAU;EAAG,CAAC,EAEpC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO,EAAG,oCAAoC,sBAAsB;GACpE,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,GAAqB,EAAG,CAAC,EAG3B,kBAAC,GAAD;EACE,UACE,kBAAC,OAAD;GAAK,WAAU;GAAsC,MAAK;aACvD,EACC,iCACA,qEACD;GACG,CAAA;EAER,UAAU,GAAO,MAAc;AAE7B,WAAQ,MACN,6DACA;IACE,SAAS,EAAM;IACf,OAAO,EAAM;IACb,gBAAgB,EAAU;IAC3B,CACF;;YAGH,kBAAC,GAAD;GACE,aAAa,KAAe;GAC5B,SAAQ;GACR,gBAAgB;aAEhB,kBAAC,GAAD;IAAuB,GAAI;cACzB,kBAAC,GAAD,EAA2B,CAAA;IACL,CAAA;GACJ,CAAA;EACR,CAAA"}
1
+ {"version":3,"file":"ConversationsRoot.js","names":[],"sources":["../../src/conversations/ConversationsRoot.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect } from \"react\";\nimport { ErrorBoundary } from \"@burdenoff/fe-libs/shared/components\";\nimport { FeatureFlagProvider } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { MessageCircle, Plus, Settings } from \"lucide-react\";\nimport { ConversationsProvider } from \"./providers/ConversationsProvider\";\nimport type { NavItem, ConversationsRootProps } from \"./types\";\nimport { joinPath } from \"./utils/navigation\";\nimport { useTr } from \"./utils/i18n\";\nimport { ConversationsUserRoutes } from \"./ConversationsUserRoutes\";\n\n/**\n * Root component for the Conversations microfrontend.\n *\n * This is the main entry point that host applications import and render.\n * It sets up the necessary providers and renders the internal routing.\n * Automatically registers navigation items with the shell if registration functions are provided.\n *\n * @example\n * ```tsx\n * import { ConversationsRoot, type ConversationsRootProps } from '@burdenoff/micro-fe/conversations';\n *\n * function App() {\n * const props: ConversationsRootProps = {\n * basePath: '/conversations',\n * navigate: (path) => router.push(path),\n * currentUser: { id: '1', email: 'user@example.com', firstName: 'John', lastName: 'Doe' },\n * workspaceId: 'ws-123',\n * apiGatewayUrl: 'https://api.example.com',\n * authToken: 'token',\n * defaultView: 'inbox',\n * onConversationSelect: (conversation) => console.log('Conversation selected:', conversation),\n * registerNavItems: (items) => { return () => {}; },\n * };\n *\n * return <ConversationsRoot {...props} />;\n * }\n * ```\n */\nexport const ConversationsRoot: FC<ConversationsRootProps> = (props) => {\n const {\n registerNavItems,\n registerFooterItems,\n basePath = \"/\",\n workspaceId,\n } = props;\n const tr = useTr();\n\n useEffect(() => {\n if (!registerNavItems) return;\n\n const archivedPath = `${joinPath(basePath, \"list\")}?filter=archived`;\n\n const conversationsNavItem: NavItem = {\n id: \"conversations\",\n label: tr(\"conversations.title\", \"Conversations\"),\n path: basePath,\n icon: <MessageCircle size={20} />,\n order: 10,\n group: \"conversations\",\n children: [\n {\n id: \"conversations-inbox\",\n label: tr(\"conversations.inbox\", \"Inbox\"),\n path: basePath,\n icon: <MessageCircle size={16} />,\n order: 10,\n },\n {\n id: \"conversations-list\",\n label: tr(\"conversations.allConversations\", \"All conversations\"),\n path: joinPath(basePath, \"list\"),\n icon: <MessageCircle size={16} />,\n order: 20,\n },\n {\n id: \"conversations-new\",\n label: tr(\"conversations.newConversation\", \"New conversation\"),\n // Canonical UX is the in-place modal opened via `?new=true` on\n // the inbox landing page — no full-page nav, the user stays in\n // the conversations surface.\n path: `${basePath}?new=true`,\n icon: <Plus size={16} />,\n order: 30,\n },\n {\n id: \"conversations-archived\",\n label: tr(\"conversations.archived\", \"Archived\"),\n path: archivedPath,\n icon: <MessageCircle size={16} />,\n order: 40,\n },\n {\n id: \"conversations-settings\",\n label: tr(\"conversations.settings\", \"Settings\"),\n path: joinPath(basePath, \"settings\"),\n icon: <Settings size={16} />,\n order: 50,\n },\n ],\n };\n\n const cleanup = registerNavItems(conversationsNavItem);\n return cleanup;\n }, [registerNavItems, basePath, tr]);\n\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: \"conversations-status\",\n label: tr(\"conversations.conversationsReady\", \"Conversations Ready\"),\n type: \"status\" as const,\n order: 10,\n section: \"left\" as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems, tr]);\n\n return (\n <ErrorBoundary\n fallback={\n <div className=\"p-6 text-center text-text-secondary\" role=\"alert\">\n {tr(\n \"conversations.moduleLoadError\",\n \"Something went wrong loading this module. Please refresh the page.\",\n )}\n </div>\n }\n onError={(error, errorInfo) => {\n // SRE forwarding — see ConversationsUserRoot for the migration target.\n console.error(\n \"[microfe-conversations:legacy-root] error boundary caught\",\n {\n message: error.message,\n stack: error.stack,\n componentStack: errorInfo.componentStack,\n },\n );\n }}\n >\n <FeatureFlagProvider\n workspaceId={workspaceId ?? null}\n gateway=\"global\"\n defaultEnabled={true}\n >\n <ConversationsProvider {...props}>\n <ConversationsUserRoutes />\n </ConversationsProvider>\n </FeatureFlagProvider>\n </ErrorBoundary>\n );\n};\n"],"mappings":";;;;;;;;;;AAuCA,IAAa,KAAiD,MAAU;CACtE,IAAM,EACJ,qBACA,wBACA,cAAW,KACX,mBACE,GACE,IAAK,GAAO;AA6ElB,QA3EA,QAAgB;AACd,MAAI,CAAC,EAAkB;EAEvB,IAAM,IAAe,GAAG,EAAS,GAAU,OAAO,CAAC;AAoDnD,SADgB,EAjDsB;GACpC,IAAI;GACJ,OAAO,EAAG,uBAAuB,gBAAgB;GACjD,MAAM;GACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;GACjC,OAAO;GACP,OAAO;GACP,UAAU;IACR;KACE,IAAI;KACJ,OAAO,EAAG,uBAAuB,QAAQ;KACzC,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,kCAAkC,oBAAoB;KAChE,MAAM,EAAS,GAAU,OAAO;KAChC,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,iCAAiC,mBAAmB;KAI9D,MAAM,GAAG,EAAS;KAClB,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;KACxB,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM,EAAS,GAAU,WAAW;KACpC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;KAC5B,OAAO;KACR;IACF;GACF,CAEqD;IAErD;EAAC;EAAkB;EAAU;EAAG,CAAC,EAEpC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO,EAAG,oCAAoC,sBAAsB;GACpE,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,GAAqB,EAAG,CAAC,EAG3B,kBAAC,GAAD;EACE,UACE,kBAAC,OAAD;GAAK,WAAU;GAAsC,MAAK;aACvD,EACC,iCACA,qEACD;GACG,CAAA;EAER,UAAU,GAAO,MAAc;AAE7B,WAAQ,MACN,6DACA;IACE,SAAS,EAAM;IACf,OAAO,EAAM;IACb,gBAAgB,EAAU;IAC3B,CACF;;YAGH,kBAAC,GAAD;GACE,aAAa,KAAe;GAC5B,SAAQ;GACR,gBAAgB;aAEhB,kBAAC,GAAD;IAAuB,GAAI;cACzB,kBAAC,GAAD,EAA2B,CAAA;IACL,CAAA;GACJ,CAAA;EACR,CAAA"}
@@ -38,7 +38,7 @@ var d = (d) => {
38
38
  {
39
39
  id: "conversations-new",
40
40
  label: g("conversations.newConversation", "New conversation"),
41
- path: t(m, "new"),
41
+ path: `${m}?new=true`,
42
42
  icon: /* @__PURE__ */ u(c, { size: 16 }),
43
43
  order: 30
44
44
  },
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsUserRoot.js","names":[],"sources":["../../src/conversations/ConversationsUserRoot.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect } from \"react\";\nimport { ErrorBoundary } from \"@burdenoff/fe-libs/shared/components\";\nimport { FeatureFlagProvider } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { MessageCircle, Plus, Settings } from \"lucide-react\";\nimport { ConversationsProvider } from \"./providers/ConversationsProvider\";\nimport type { NavItem, ConversationsRootProps } from \"./types\";\nimport { joinPath } from \"./utils/navigation\";\nimport { useTr } from \"./utils/i18n\";\nimport { ConversationsUserRoutes } from \"./ConversationsUserRoutes\";\n\n/**\n * User-facing Root component for the Conversations microfrontend.\n *\n * Mounted by user app-shells (vibecontrols-app, bigconsole-app,\n * workspaces-app, …). Registers the user nav tree (Inbox, All\n * conversations, New conversation, Archived, Settings), sets the\n * provider variant to `\"user\"` internally (host-passed `variant`\n * values are ignored), and renders `ConversationsUserRoutes`.\n *\n * Admin governance surfaces live in `ConversationsAdminRoot` and\n * must not be reached through this Root.\n */\nexport const ConversationsUserRoot: FC<ConversationsRootProps> = (props) => {\n const {\n registerNavItems,\n registerFooterItems,\n basePath = \"/\",\n workspaceId,\n } = props;\n const tr = useTr();\n\n useEffect(() => {\n if (!registerNavItems) return;\n\n const archivedPath = `${joinPath(basePath, \"list\")}?filter=archived`;\n\n const conversationsNavItem: NavItem = {\n id: \"conversations\",\n label: tr(\"conversations.title\", \"Conversations\"),\n path: basePath,\n icon: <MessageCircle size={20} />,\n order: 10,\n group: \"conversations\",\n children: [\n {\n id: \"conversations-inbox\",\n label: tr(\"conversations.inbox\", \"Inbox\"),\n path: basePath,\n icon: <MessageCircle size={16} />,\n order: 10,\n },\n {\n id: \"conversations-list\",\n label: tr(\"conversations.allConversations\", \"All conversations\"),\n path: joinPath(basePath, \"list\"),\n icon: <MessageCircle size={16} />,\n order: 20,\n },\n {\n id: \"conversations-new\",\n label: tr(\"conversations.newConversation\", \"New conversation\"),\n path: joinPath(basePath, \"new\"),\n icon: <Plus size={16} />,\n order: 30,\n },\n {\n id: \"conversations-archived\",\n label: tr(\"conversations.archived\", \"Archived\"),\n path: archivedPath,\n icon: <MessageCircle size={16} />,\n order: 40,\n },\n {\n id: \"conversations-settings\",\n label: tr(\"conversations.settings\", \"Settings\"),\n path: joinPath(basePath, \"settings\"),\n icon: <Settings size={16} />,\n order: 50,\n },\n ],\n };\n\n const cleanup = registerNavItems(conversationsNavItem);\n return cleanup;\n }, [registerNavItems, basePath, tr]);\n\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: \"conversations-status\",\n label: tr(\"conversations.conversationsReady\", \"Conversations Ready\"),\n type: \"status\" as const,\n order: 10,\n section: \"left\" as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems, tr]);\n\n return (\n <ErrorBoundary\n fallback={\n <div className=\"p-6 text-center text-text-secondary\" role=\"alert\">\n {tr(\n \"conversations.moduleLoadError\",\n \"Something went wrong loading this module. Please refresh the page.\",\n )}\n </div>\n }\n onError={(error, errorInfo) => {\n // SRE forwarding: log to the host's structured logger.\n // Per spec 47 the MFE does NOT call Rybbit / OTel directly — once the\n // shell event bus exposes a `conversations.error` event name in the\n // BurdenoffEventName union, swap the console.error here to\n // `bus.emit('conversations.error', { ... })`.\n // Tracker: ~/products/dev/stability/vibecontrols-conversations-stability.md Phase 3.7.\n console.error(\"[microfe-conversations] error boundary caught\", {\n message: error.message,\n stack: error.stack,\n componentStack: errorInfo.componentStack,\n });\n }}\n >\n <FeatureFlagProvider\n workspaceId={workspaceId ?? null}\n gateway=\"global\"\n defaultEnabled={true}\n >\n <ConversationsProvider {...props} variant=\"user\">\n <ConversationsUserRoutes />\n </ConversationsProvider>\n </FeatureFlagProvider>\n </ErrorBoundary>\n );\n};\n"],"mappings":";;;;;;;;;;AAuBA,IAAa,KAAqD,MAAU;CAC1E,IAAM,EACJ,qBACA,wBACA,cAAW,KACX,mBACE,GACE,IAAK,GAAO;AA0ElB,QAxEA,QAAgB;AACd,MAAI,CAAC,EAAkB;EAEvB,IAAM,IAAe,GAAG,EAAS,GAAU,OAAO,CAAC;AAiDnD,SADgB,EA9CsB;GACpC,IAAI;GACJ,OAAO,EAAG,uBAAuB,gBAAgB;GACjD,MAAM;GACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;GACjC,OAAO;GACP,OAAO;GACP,UAAU;IACR;KACE,IAAI;KACJ,OAAO,EAAG,uBAAuB,QAAQ;KACzC,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,kCAAkC,oBAAoB;KAChE,MAAM,EAAS,GAAU,OAAO;KAChC,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,iCAAiC,mBAAmB;KAC9D,MAAM,EAAS,GAAU,MAAM;KAC/B,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;KACxB,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM,EAAS,GAAU,WAAW;KACpC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;KAC5B,OAAO;KACR;IACF;GACF,CAEqD;IAErD;EAAC;EAAkB;EAAU;EAAG,CAAC,EAEpC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO,EAAG,oCAAoC,sBAAsB;GACpE,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,GAAqB,EAAG,CAAC,EAG3B,kBAAC,GAAD;EACE,UACE,kBAAC,OAAD;GAAK,WAAU;GAAsC,MAAK;aACvD,EACC,iCACA,qEACD;GACG,CAAA;EAER,UAAU,GAAO,MAAc;AAO7B,WAAQ,MAAM,iDAAiD;IAC7D,SAAS,EAAM;IACf,OAAO,EAAM;IACb,gBAAgB,EAAU;IAC3B,CAAC;;YAGJ,kBAAC,GAAD;GACE,aAAa,KAAe;GAC5B,SAAQ;GACR,gBAAgB;aAEhB,kBAAC,GAAD;IAAuB,GAAI;IAAO,SAAQ;cACxC,kBAAC,GAAD,EAA2B,CAAA;IACL,CAAA;GACJ,CAAA;EACR,CAAA"}
1
+ {"version":3,"file":"ConversationsUserRoot.js","names":[],"sources":["../../src/conversations/ConversationsUserRoot.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect } from \"react\";\nimport { ErrorBoundary } from \"@burdenoff/fe-libs/shared/components\";\nimport { FeatureFlagProvider } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { MessageCircle, Plus, Settings } from \"lucide-react\";\nimport { ConversationsProvider } from \"./providers/ConversationsProvider\";\nimport type { NavItem, ConversationsRootProps } from \"./types\";\nimport { joinPath } from \"./utils/navigation\";\nimport { useTr } from \"./utils/i18n\";\nimport { ConversationsUserRoutes } from \"./ConversationsUserRoutes\";\n\n/**\n * User-facing Root component for the Conversations microfrontend.\n *\n * Mounted by user app-shells (vibecontrols-app, bigconsole-app,\n * workspaces-app, …). Registers the user nav tree (Inbox, All\n * conversations, New conversation, Archived, Settings), sets the\n * provider variant to `\"user\"` internally (host-passed `variant`\n * values are ignored), and renders `ConversationsUserRoutes`.\n *\n * Admin governance surfaces live in `ConversationsAdminRoot` and\n * must not be reached through this Root.\n */\nexport const ConversationsUserRoot: FC<ConversationsRootProps> = (props) => {\n const {\n registerNavItems,\n registerFooterItems,\n basePath = \"/\",\n workspaceId,\n } = props;\n const tr = useTr();\n\n useEffect(() => {\n if (!registerNavItems) return;\n\n const archivedPath = `${joinPath(basePath, \"list\")}?filter=archived`;\n\n const conversationsNavItem: NavItem = {\n id: \"conversations\",\n label: tr(\"conversations.title\", \"Conversations\"),\n path: basePath,\n icon: <MessageCircle size={20} />,\n order: 10,\n group: \"conversations\",\n children: [\n {\n id: \"conversations-inbox\",\n label: tr(\"conversations.inbox\", \"Inbox\"),\n path: basePath,\n icon: <MessageCircle size={16} />,\n order: 10,\n },\n {\n id: \"conversations-list\",\n label: tr(\"conversations.allConversations\", \"All conversations\"),\n path: joinPath(basePath, \"list\"),\n icon: <MessageCircle size={16} />,\n order: 20,\n },\n {\n id: \"conversations-new\",\n label: tr(\"conversations.newConversation\", \"New conversation\"),\n // Canonical UX is the in-place modal opened via `?new=true` on\n // the inbox landing page — no full-page nav, the user stays in\n // the conversations surface.\n path: `${basePath}?new=true`,\n icon: <Plus size={16} />,\n order: 30,\n },\n {\n id: \"conversations-archived\",\n label: tr(\"conversations.archived\", \"Archived\"),\n path: archivedPath,\n icon: <MessageCircle size={16} />,\n order: 40,\n },\n {\n id: \"conversations-settings\",\n label: tr(\"conversations.settings\", \"Settings\"),\n path: joinPath(basePath, \"settings\"),\n icon: <Settings size={16} />,\n order: 50,\n },\n ],\n };\n\n const cleanup = registerNavItems(conversationsNavItem);\n return cleanup;\n }, [registerNavItems, basePath, tr]);\n\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: \"conversations-status\",\n label: tr(\"conversations.conversationsReady\", \"Conversations Ready\"),\n type: \"status\" as const,\n order: 10,\n section: \"left\" as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems, tr]);\n\n return (\n <ErrorBoundary\n fallback={\n <div className=\"p-6 text-center text-text-secondary\" role=\"alert\">\n {tr(\n \"conversations.moduleLoadError\",\n \"Something went wrong loading this module. Please refresh the page.\",\n )}\n </div>\n }\n onError={(error, errorInfo) => {\n // SRE forwarding: log to the host's structured logger.\n // Per spec 47 the MFE does NOT call Rybbit / OTel directly — once the\n // shell event bus exposes a `conversations.error` event name in the\n // BurdenoffEventName union, swap the console.error here to\n // `bus.emit('conversations.error', { ... })`.\n // Tracker: ~/products/dev/stability/vibecontrols-conversations-stability.md Phase 3.7.\n console.error(\"[microfe-conversations] error boundary caught\", {\n message: error.message,\n stack: error.stack,\n componentStack: errorInfo.componentStack,\n });\n }}\n >\n <FeatureFlagProvider\n workspaceId={workspaceId ?? null}\n gateway=\"global\"\n defaultEnabled={true}\n >\n <ConversationsProvider {...props} variant=\"user\">\n <ConversationsUserRoutes />\n </ConversationsProvider>\n </FeatureFlagProvider>\n </ErrorBoundary>\n );\n};\n"],"mappings":";;;;;;;;;;AAuBA,IAAa,KAAqD,MAAU;CAC1E,IAAM,EACJ,qBACA,wBACA,cAAW,KACX,mBACE,GACE,IAAK,GAAO;AA6ElB,QA3EA,QAAgB;AACd,MAAI,CAAC,EAAkB;EAEvB,IAAM,IAAe,GAAG,EAAS,GAAU,OAAO,CAAC;AAoDnD,SADgB,EAjDsB;GACpC,IAAI;GACJ,OAAO,EAAG,uBAAuB,gBAAgB;GACjD,MAAM;GACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;GACjC,OAAO;GACP,OAAO;GACP,UAAU;IACR;KACE,IAAI;KACJ,OAAO,EAAG,uBAAuB,QAAQ;KACzC,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,kCAAkC,oBAAoB;KAChE,MAAM,EAAS,GAAU,OAAO;KAChC,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,iCAAiC,mBAAmB;KAI9D,MAAM,GAAG,EAAS;KAClB,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;KACxB,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM;KACN,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;KACjC,OAAO;KACR;IACD;KACE,IAAI;KACJ,OAAO,EAAG,0BAA0B,WAAW;KAC/C,MAAM,EAAS,GAAU,WAAW;KACpC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;KAC5B,OAAO;KACR;IACF;GACF,CAEqD;IAErD;EAAC;EAAkB;EAAU;EAAG,CAAC,EAEpC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO,EAAG,oCAAoC,sBAAsB;GACpE,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,GAAqB,EAAG,CAAC,EAG3B,kBAAC,GAAD;EACE,UACE,kBAAC,OAAD;GAAK,WAAU;GAAsC,MAAK;aACvD,EACC,iCACA,qEACD;GACG,CAAA;EAER,UAAU,GAAO,MAAc;AAO7B,WAAQ,MAAM,iDAAiD;IAC7D,SAAS,EAAM;IACf,OAAO,EAAM;IACb,gBAAgB,EAAU;IAC3B,CAAC;;YAGJ,kBAAC,GAAD;GACE,aAAa,KAAe;GAC5B,SAAQ;GACR,gBAAgB;aAEhB,kBAAC,GAAD;IAAuB,GAAI;IAAO,SAAQ;cACxC,kBAAC,GAAD,EAA2B,CAAA;IACL,CAAA;GACJ,CAAA;EACR,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary } from \"./components/LazyPageBoundary\";\nimport { lazyRetry } from \"./components/lazyRetry\";\nimport type { ConversationListPageProps } from \"./pages/ConversationListPage\";\n\nconst ConversationListPage = lazyRetry<ConversationListPageProps>(() =>\n import(\"./pages/ConversationListPage\").then((m) => ({\n default: m.ConversationListPage,\n })),\n);\nconst ConversationsLandingPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsLandingPage\").then((m) => ({\n default: m.ConversationsLandingPage,\n })),\n);\n\nconst NewConversationPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsSettingsPage\").then((m) => ({\n default: m.ConversationsSettingsPage,\n })),\n);\n\n/**\n * User-facing routes for the Conversations microfrontend.\n *\n * Routes (relative to basePath, e.g., /conversations):\n * - / -> Inbox (two-pane shell, no conversation selected)\n * - /:conversationId -> Inbox with conversation detail in right pane\n * - /list -> Legacy standalone list (kept for deep-linked callers)\n * - /new -> Create new conversation\n * - /settings -> Personal notification settings\n *\n * Both `/` and `/:conversationId` mount `ConversationsLandingPage`. The page\n * reads `useParams().conversationId` and renders the `ConversationDetailPage`\n * in the right pane when set, otherwise the inbox empty state. This keeps\n * the two-pane experience consistent across deep links and sidebar clicks\n * — no full-page nav happens when a user picks a conversation on md+ screens.\n */\nexport function ConversationsUserRoutes() {\n return (\n <LazyPageBoundary source=\"user\">\n <Routes>\n <Route index element={<ConversationsLandingPage />} />\n <Route path=\"list\" element={<ConversationListPage />} />\n <Route path=\"new\" element={<NewConversationPage />} />\n <Route path=\"settings\" element={<ConversationsSettingsPage />} />\n <Route path=\":conversationId\" element={<ConversationsLandingPage />} />\n <Route path=\"*\" element={<Navigate to=\".\" replace />} />\n </Routes>\n </LazyPageBoundary>\n );\n}\n"],"mappings":";;;;;AAMA,IAAM,IAAuB,QAC3B,OAAO,mCAAgC,MAAM,OAAO,EAClD,SAAS,EAAE,sBACZ,EAAE,CACJ,EACK,IAA2B,QAC/B,OAAO,uCAAoC,MAAM,OAAO,EACtD,SAAS,EAAE,0BACZ,EAAE,CACJ,EAEK,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EACjD,SAAS,EAAE,qBACZ,EAAE,CACJ,EACK,IAA4B,QAChC,OAAO,wCAAqC,MAAM,OAAO,EACvD,SAAS,EAAE,2BACZ,EAAE,CACJ;AAkBD,SAAgB,IAA0B;AACxC,QACE,kBAAC,GAAD;EAAkB,QAAO;YACvB,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAO,OAAA;IAAM,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAO,SAAS,kBAAC,GAAD,EAAwB,CAAA;IAAI,CAAA;GACxD,kBAAC,GAAD;IAAO,MAAK;IAAM,SAAS,kBAAC,GAAD,EAAuB,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAW,SAAS,kBAAC,GAAD,EAA6B,CAAA;IAAI,CAAA;GACjE,kBAAC,GAAD;IAAO,MAAK;IAAkB,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACvE,kBAAC,GAAD;IAAO,MAAK;IAAI,SAAS,kBAAC,GAAD;KAAU,IAAG;KAAI,SAAA;KAAU,CAAA;IAAI,CAAA;GACjD,EAAA,CAAA;EACQ,CAAA"}
1
+ {"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary } from \"./components/LazyPageBoundary\";\nimport { lazyRetry } from \"./components/lazyRetry\";\nimport type { ConversationListPageProps } from \"./pages/ConversationListPage\";\n\nconst ConversationListPage = lazyRetry<ConversationListPageProps>(() =>\n import(\"./pages/ConversationListPage\").then((m) => ({\n default: m.ConversationListPage,\n })),\n);\nconst ConversationsLandingPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsLandingPage\").then((m) => ({\n default: m.ConversationsLandingPage,\n })),\n);\n\nconst NewConversationPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsSettingsPage\").then((m) => ({\n default: m.ConversationsSettingsPage,\n })),\n);\n\n/**\n * User-facing routes for the Conversations microfrontend.\n *\n * Routes (relative to basePath, e.g., /conversations):\n * - / -> Inbox (two-pane shell, no conversation selected)\n * - /:conversationId -> Inbox with conversation detail in right pane\n * - /list -> Legacy standalone list (kept for deep-linked callers)\n * - /new -> Legacy create route; backward-compat only.\n * Redirects to `..?new=true` so the inbox opens\n * the create dialog in-place. Kept registered\n * for deep links that may still exist in the\n * wild — the new canonical UX is the\n * `?new=true` query param on the landing page.\n * - /settings -> Personal notification settings\n *\n * Both `/` and `/:conversationId` mount `ConversationsLandingPage`. The page\n * reads `useParams().conversationId` and renders the `ConversationDetailPage`\n * in the right pane when set, otherwise the inbox empty state. This keeps\n * the two-pane experience consistent across deep links and sidebar clicks\n * — no full-page nav happens when a user picks a conversation on md+ screens.\n */\nexport function ConversationsUserRoutes() {\n return (\n <LazyPageBoundary source=\"user\">\n <Routes>\n <Route index element={<ConversationsLandingPage />} />\n <Route path=\"list\" element={<ConversationListPage />} />\n <Route path=\"new\" element={<NewConversationPage />} />\n <Route path=\"settings\" element={<ConversationsSettingsPage />} />\n <Route path=\":conversationId\" element={<ConversationsLandingPage />} />\n <Route path=\"*\" element={<Navigate to=\".\" replace />} />\n </Routes>\n </LazyPageBoundary>\n );\n}\n"],"mappings":";;;;;AAMA,IAAM,IAAuB,QAC3B,OAAO,mCAAgC,MAAM,OAAO,EAClD,SAAS,EAAE,sBACZ,EAAE,CACJ,EACK,IAA2B,QAC/B,OAAO,uCAAoC,MAAM,OAAO,EACtD,SAAS,EAAE,0BACZ,EAAE,CACJ,EAEK,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EACjD,SAAS,EAAE,qBACZ,EAAE,CACJ,EACK,IAA4B,QAChC,OAAO,wCAAqC,MAAM,OAAO,EACvD,SAAS,EAAE,2BACZ,EAAE,CACJ;AAuBD,SAAgB,IAA0B;AACxC,QACE,kBAAC,GAAD;EAAkB,QAAO;YACvB,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAO,OAAA;IAAM,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAO,SAAS,kBAAC,GAAD,EAAwB,CAAA;IAAI,CAAA;GACxD,kBAAC,GAAD;IAAO,MAAK;IAAM,SAAS,kBAAC,GAAD,EAAuB,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAW,SAAS,kBAAC,GAAD,EAA6B,CAAA;IAAI,CAAA;GACjE,kBAAC,GAAD;IAAO,MAAK;IAAkB,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACvE,kBAAC,GAAD;IAAO,MAAK;IAAI,SAAS,kBAAC,GAAD;KAAU,IAAG;KAAI,SAAA;KAAU,CAAA;IAAI,CAAA;GACjD,EAAA,CAAA;EACQ,CAAA"}
@@ -1,185 +1,189 @@
1
1
  import { useConversations as e } from "../providers/ConversationsProvider.js";
2
- import { joinPath as t } from "../utils/navigation.js";
3
- import { useTr as n } from "../utils/i18n.js";
4
- import { cn as r } from "../utils/cn.js";
5
- import { useConversationsStore as i } from "../store/conversationsStore.js";
6
- import { useCallback as a, useMemo as o } from "react";
7
- import { ArrowLeft as s, MessageSquarePlus as c, Search as l, X as u } from "lucide-react";
8
- import { useLocation as d, useNavigate as f, useParams as p } from "react-router-dom";
9
- import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
- import { Tabs as g } from "@burdenoff/fe-libs/chrome";
11
- import { Button as _ } from "@burdenoff/fe-libs/ui";
2
+ import { useTr as t } from "../utils/i18n.js";
3
+ import { cn as n } from "../utils/cn.js";
4
+ import { useConversationsStore as r } from "../store/conversationsStore.js";
5
+ import { useCallback as i, useMemo as a } from "react";
6
+ import { ArrowLeft as o, MessageSquarePlus as s, Search as c, X as l } from "lucide-react";
7
+ import { useLocation as u, useNavigate as d, useParams as f } from "react-router-dom";
8
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
9
+ import { Tabs as h } from "@burdenoff/fe-libs/chrome";
10
+ import { Button as g } from "@burdenoff/fe-libs/ui";
12
11
  //#region src/conversations/components/InboxLayout.tsx
13
- function v({ activeTab: v, onTabChange: b, archivedEnabled: x = !0, searchQuery: S, onSearchChange: C, filterChips: w, listSlot: T, detailSlot: E, emptyDetailSlot: D }) {
14
- let O = f(), k = n(), { basePath: A } = e(), { setActiveConversationId: j } = i(), M = p(), N = d(), P = !!M.conversationId && !!E, F = o(() => {
12
+ function _({ activeTab: _, onTabChange: y, archivedEnabled: b = !0, searchQuery: x, onSearchChange: S, filterChips: C, listSlot: w, detailSlot: T, emptyDetailSlot: E }) {
13
+ let D = d(), O = t(), { basePath: k } = e(), { setActiveConversationId: A } = r(), j = f(), M = u(), N = !!j.conversationId && !!T, P = a(() => {
15
14
  let e = [
16
15
  {
17
16
  id: "all",
18
- label: k("conversations.tabs.all", "All")
17
+ label: O("conversations.tabs.all", "All")
19
18
  },
20
19
  {
21
20
  id: "direct",
22
- label: k("conversations.tabs.direct", "Direct")
21
+ label: O("conversations.tabs.direct", "Direct")
23
22
  },
24
23
  {
25
24
  id: "groups",
26
- label: k("conversations.tabs.groups", "Groups")
25
+ label: O("conversations.tabs.groups", "Groups")
27
26
  },
28
27
  {
29
28
  id: "channels",
30
- label: k("conversations.tabs.channels", "Channels")
29
+ label: O("conversations.tabs.channels", "Channels")
31
30
  }
32
31
  ];
33
- return x && e.push({
32
+ return b && e.push({
34
33
  id: "archived",
35
- label: k("conversations.tabs.archived", "Archived")
34
+ label: O("conversations.tabs.archived", "Archived")
36
35
  }), e;
37
- }, [x, k]), I = a(() => {
38
- O(t(A || "/conversations", "new"));
39
- }, [A, O]), L = a(() => {
40
- j(null), O(`${A || "/conversations"}${N.search}`);
36
+ }, [b, O]), F = i(() => {
37
+ let e = k || "/conversations", t = new URLSearchParams(M.search);
38
+ t.set("new", "true"), D(`${e}?${t.toString()}`);
41
39
  }, [
42
- A,
43
- N.search,
44
- O,
45
- j
40
+ k,
41
+ M.search,
42
+ D
43
+ ]), I = i(() => {
44
+ A(null), D(`${k || "/conversations"}${M.search}`);
45
+ }, [
46
+ k,
47
+ M.search,
48
+ D,
49
+ A
46
50
  ]);
47
- return /* @__PURE__ */ h("div", {
51
+ return /* @__PURE__ */ m("div", {
48
52
  "data-tour": "conversations-inbox",
49
53
  className: "flex h-full w-full min-h-0 min-w-0 overflow-hidden bg-bg-canvas",
50
- children: [/* @__PURE__ */ h("aside", {
51
- "aria-label": k("conversations.listPaneLabel", "Conversations list"),
52
- className: r("flex flex-col min-h-0 border-r border-border-default bg-bg-surface", "w-full md:w-[320px] md:flex-shrink-0", P ? "hidden md:flex" : "flex"),
53
- children: [/* @__PURE__ */ h("div", {
54
+ children: [/* @__PURE__ */ m("aside", {
55
+ "aria-label": O("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", N ? "hidden md:flex" : "flex"),
57
+ children: [/* @__PURE__ */ m("div", {
54
58
  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",
55
59
  children: [
56
- /* @__PURE__ */ h("div", {
60
+ /* @__PURE__ */ m("div", {
57
61
  className: "flex items-center justify-between gap-2",
58
- children: [/* @__PURE__ */ m("h1", {
62
+ children: [/* @__PURE__ */ p("h1", {
59
63
  className: "text-base font-semibold text-text-primary",
60
- children: k("conversations.title", "Conversations")
61
- }), /* @__PURE__ */ h(_, {
64
+ children: O("conversations.title", "Conversations")
65
+ }), /* @__PURE__ */ m(g, {
62
66
  "data-tour": "conversation-new-button",
63
67
  type: "button",
64
68
  size: "sm",
65
- onClick: I,
66
- "aria-label": k("conversations.createNewConversation", "Create a new conversation"),
67
- children: [/* @__PURE__ */ m(c, { size: 14 }), /* @__PURE__ */ m("span", {
69
+ onClick: F,
70
+ "aria-label": O("conversations.createNewConversation", "Create a new conversation"),
71
+ children: [/* @__PURE__ */ p(s, { size: 14 }), /* @__PURE__ */ p("span", {
68
72
  className: "hidden sm:inline",
69
- children: k("conversations.newConversation", "New")
73
+ children: O("conversations.newConversation", "New")
70
74
  })]
71
75
  })]
72
76
  }),
73
- /* @__PURE__ */ h("div", {
77
+ /* @__PURE__ */ m("div", {
74
78
  className: "relative",
75
79
  children: [
76
- /* @__PURE__ */ m(l, {
80
+ /* @__PURE__ */ p(c, {
77
81
  size: 14,
78
82
  className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary",
79
83
  "aria-hidden": "true"
80
84
  }),
81
- /* @__PURE__ */ m("input", {
85
+ /* @__PURE__ */ p("input", {
82
86
  "data-tour": "conversation-search",
83
87
  type: "search",
84
- value: S,
85
- onChange: (e) => C(e.target.value),
86
- placeholder: k("conversations.search", "Search conversations..."),
87
- "aria-label": k("conversations.searchAria", "Search conversations"),
88
- className: r("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")
88
+ value: x,
89
+ onChange: (e) => S(e.target.value),
90
+ placeholder: O("conversations.search", "Search conversations..."),
91
+ "aria-label": O("conversations.searchAria", "Search conversations"),
92
+ 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")
89
93
  }),
90
- S.length > 0 && /* @__PURE__ */ m("button", {
94
+ x.length > 0 && /* @__PURE__ */ p("button", {
91
95
  type: "button",
92
- onClick: () => C(""),
93
- "aria-label": k("conversations.clearSearch", "Clear conversation search"),
96
+ onClick: () => S(""),
97
+ "aria-label": O("conversations.clearSearch", "Clear conversation search"),
94
98
  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",
95
- children: /* @__PURE__ */ m(u, { size: 12 })
99
+ children: /* @__PURE__ */ p(l, { size: 12 })
96
100
  })
97
101
  ]
98
102
  }),
99
- /* @__PURE__ */ m("div", {
103
+ /* @__PURE__ */ p("div", {
100
104
  "data-tour": "conversations-tabs",
101
- children: /* @__PURE__ */ m(g, {
102
- items: F,
103
- activeTabId: v,
104
- onTabChange: (e) => b(e),
105
+ children: /* @__PURE__ */ p(h, {
106
+ items: P,
107
+ activeTabId: _,
108
+ onTabChange: (e) => y(e),
105
109
  variant: "underline",
106
110
  size: "sm"
107
111
  })
108
112
  }),
109
- w && w.length > 0 && /* @__PURE__ */ m("div", {
113
+ C && C.length > 0 && /* @__PURE__ */ p("div", {
110
114
  "data-tour": "conversations-filter-chips",
111
115
  className: "flex flex-wrap gap-1.5",
112
- children: w.map((e) => /* @__PURE__ */ h("button", {
116
+ children: C.map((e) => /* @__PURE__ */ m("button", {
113
117
  type: "button",
114
118
  onClick: e.onToggle,
115
119
  "aria-pressed": e.active,
116
- className: r("inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors", e.active ? "border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg" : "border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary"),
120
+ className: n("inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors", e.active ? "border-action-primary-border bg-action-primary-bg/10 text-action-primary-bg" : "border-border-default text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary"),
117
121
  children: [e.icon, e.label]
118
122
  }, e.id))
119
123
  })
120
124
  ]
121
- }), /* @__PURE__ */ m("div", {
125
+ }), /* @__PURE__ */ p("div", {
122
126
  "data-tour": "conversations-list-scroll",
123
127
  className: "flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
124
- children: T
128
+ children: w
125
129
  })]
126
- }), /* @__PURE__ */ h("section", {
127
- "aria-label": k("conversations.detailPaneLabel", "Conversation detail"),
128
- className: r("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface", P ? "flex" : "hidden md:flex"),
129
- children: [P && /* @__PURE__ */ m("div", {
130
+ }), /* @__PURE__ */ m("section", {
131
+ "aria-label": O("conversations.detailPaneLabel", "Conversation detail"),
132
+ className: n("flex flex-col min-h-0 flex-1 min-w-0 bg-bg-surface", N ? "flex" : "hidden md:flex"),
133
+ children: [N && /* @__PURE__ */ p("div", {
130
134
  className: "md:hidden border-b border-border-subtle px-3 py-2",
131
- children: /* @__PURE__ */ h(_, {
135
+ children: /* @__PURE__ */ m(g, {
132
136
  variant: "ghost",
133
137
  size: "sm",
134
- onClick: L,
135
- "aria-label": k("conversations.backToInbox", "Back to conversations list"),
136
- children: [/* @__PURE__ */ m(s, { size: 14 }), k("conversations.backToInbox", "Back to inbox")]
138
+ onClick: I,
139
+ "aria-label": O("conversations.backToInbox", "Back to conversations list"),
140
+ children: [/* @__PURE__ */ p(o, { size: 14 }), O("conversations.backToInbox", "Back to inbox")]
137
141
  })
138
- }), /* @__PURE__ */ m("div", {
142
+ }), /* @__PURE__ */ p("div", {
139
143
  className: "flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden",
140
- children: E ?? D ?? /* @__PURE__ */ m(y, {
141
- onNew: I,
142
- title: k("conversations.detailEmpty.title", "Select a conversation"),
143
- description: k("conversations.detailEmpty.description", "Pick a conversation from the list — or start a new one."),
144
- newLabel: k("conversations.detailEmpty.newCta", "Start a new conversation")
144
+ children: T ?? E ?? /* @__PURE__ */ p(v, {
145
+ onNew: F,
146
+ title: O("conversations.detailEmpty.title", "Select a conversation"),
147
+ description: O("conversations.detailEmpty.description", "Pick a conversation from the list — or start a new one."),
148
+ newLabel: O("conversations.detailEmpty.newCta", "Start a new conversation")
145
149
  })
146
150
  })]
147
151
  })]
148
152
  });
149
153
  }
150
- function y({ title: e, description: t, newLabel: n, onNew: r }) {
151
- return /* @__PURE__ */ m("div", {
154
+ function v({ title: e, description: t, newLabel: n, onNew: r }) {
155
+ return /* @__PURE__ */ p("div", {
152
156
  className: "flex h-full items-center justify-center p-8",
153
- children: /* @__PURE__ */ h("div", {
157
+ children: /* @__PURE__ */ m("div", {
154
158
  className: "flex max-w-sm flex-col items-center gap-3 text-center",
155
159
  children: [
156
- /* @__PURE__ */ m("div", {
160
+ /* @__PURE__ */ p("div", {
157
161
  className: "flex size-16 items-center justify-center rounded-full bg-bg-sunken",
158
- children: /* @__PURE__ */ m(c, {
162
+ children: /* @__PURE__ */ p(s, {
159
163
  size: 28,
160
164
  className: "text-text-secondary",
161
165
  "aria-hidden": "true"
162
166
  })
163
167
  }),
164
- /* @__PURE__ */ m("h2", {
168
+ /* @__PURE__ */ p("h2", {
165
169
  className: "text-lg font-semibold text-text-primary",
166
170
  children: e
167
171
  }),
168
- /* @__PURE__ */ m("p", {
172
+ /* @__PURE__ */ p("p", {
169
173
  className: "text-sm text-text-secondary",
170
174
  children: t
171
175
  }),
172
- /* @__PURE__ */ h(_, {
176
+ /* @__PURE__ */ m(g, {
173
177
  type: "button",
174
178
  onClick: r,
175
179
  className: "mt-2",
176
- children: [/* @__PURE__ */ m(c, { size: 14 }), n]
180
+ children: [/* @__PURE__ */ p(s, { size: 14 }), n]
177
181
  })
178
182
  ]
179
183
  })
180
184
  });
181
185
  }
182
186
  //#endregion
183
- export { v as InboxLayout };
187
+ export { _ as InboxLayout };
184
188
 
185
189
  //# 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 { 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\";\nimport { joinPath } from \"../utils/navigation\";\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 navigate(joinPath(basePath || \"/conversations\", \"new\"));\n }, [basePath, 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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"}
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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"}