@burdenoff/microfe-conversations 2026.608.1 → 2026.624.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.
- package/dist/conversations/components/LazyPageBoundary.js +7 -7
- package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
- package/dist/conversations/components/MemberPicker.js +8 -8
- package/dist/conversations/components/MemberPicker.js.map +1 -1
- package/dist/conversations/components/MessageBubble.js +18 -18
- package/dist/conversations/components/MessageBubble.js.map +1 -1
- package/dist/conversations/components/NewConversationDialog.js +19 -19
- package/dist/conversations/components/NewConversationDialog.js.map +1 -1
- package/dist/conversations/pages/AdminConversationsLandingPage.js +9 -9
- package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js +38 -38
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +35 -35
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +15 -15
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,32 +16,32 @@ function u({ errorMessage: t, onRetry: n }) {
|
|
|
16
16
|
className: "flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6",
|
|
17
17
|
children: [
|
|
18
18
|
/* @__PURE__ */ s("div", {
|
|
19
|
-
className: "p-3 rounded-full bg-
|
|
20
|
-
children: /* @__PURE__ */ s(i, { className: "size-8 text-
|
|
19
|
+
className: "p-3 rounded-full bg-status-error-bg/10",
|
|
20
|
+
children: /* @__PURE__ */ s(i, { className: "size-8 text-status-error-text" })
|
|
21
21
|
}),
|
|
22
22
|
/* @__PURE__ */ c("div", {
|
|
23
23
|
className: "space-y-2",
|
|
24
24
|
children: [/* @__PURE__ */ s("h3", {
|
|
25
|
-
className: "text-lg font-semibold text-
|
|
25
|
+
className: "text-lg font-semibold text-text-primary",
|
|
26
26
|
children: r("conversations.failedToLoadPage", "Failed to load page")
|
|
27
27
|
}), /* @__PURE__ */ s("p", {
|
|
28
|
-
className: "text-sm text-
|
|
28
|
+
className: "text-sm text-text-secondary max-w-md",
|
|
29
29
|
children: r("conversations.pageChunkLoadFailed", "The page chunk could not be loaded. This may be due to a network issue.")
|
|
30
30
|
})]
|
|
31
31
|
}),
|
|
32
32
|
/* @__PURE__ */ c("button", {
|
|
33
33
|
type: "button",
|
|
34
34
|
onClick: n,
|
|
35
|
-
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-
|
|
35
|
+
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors",
|
|
36
36
|
children: [/* @__PURE__ */ s(o, { className: "size-4" }), r("conversations.tryAgain", "Try Again")]
|
|
37
37
|
}),
|
|
38
38
|
t && /* @__PURE__ */ c("details", {
|
|
39
39
|
className: "mt-2 max-w-md w-full text-left",
|
|
40
40
|
children: [/* @__PURE__ */ s("summary", {
|
|
41
|
-
className: "cursor-pointer text-xs text-
|
|
41
|
+
className: "cursor-pointer text-xs text-text-secondary hover:text-text-primary select-none",
|
|
42
42
|
children: r("conversations.errorDetails", "Error details")
|
|
43
43
|
}), /* @__PURE__ */ s("pre", {
|
|
44
|
-
className: "mt-2 p-2 rounded bg-
|
|
44
|
+
className: "mt-2 p-2 rounded bg-bg-sunken text-[11px] text-text-secondary whitespace-pre-wrap break-all",
|
|
45
45
|
children: t
|
|
46
46
|
})]
|
|
47
47
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import { Component, Suspense, type ErrorInfo, type ReactNode } from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-muted-foreground max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-muted-foreground hover:text-foreground select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":";;;;;;AAQA,SAAS,IAAa;AACpB,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAS,WAAU,oCAAqC,CAAA;EACpD,CAAA;;AAIV,SAAS,EAAY,EACnB,iBACA,cAIC;CACD,IAAM,IAAK,GAAO;AAElB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAe,WAAU,2BAA4B,CAAA;IACjD,CAAA;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,kCAAkC,sBAAsB;KACzD,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,qCACA,0EACD;KACC,CAAA,CACA;;GACN,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAC/B,EAAG,0BAA0B,YAAY,CACnC;;GACR,KACC,kBAAC,WAAD;IAAS,WAAU;cAAnB,CACE,kBAAC,WAAD;KAAS,WAAU;eAChB,EAAG,8BAA8B,gBAAgB;KAC1C,CAAA,EACV,kBAAC,OAAD;KAAK,WAAU;eACZ;KACG,CAAA,CACE;;GAER;;;AAuBV,IAAM,IAAqB;AAE3B,SAAS,EAAc,GAAyB;AAC9C,QAAO,GAAG,IAAqB,KAAU;;AAG3C,SAAS,EAAgB,GAAsB;AAC7C,KAAI;AAEF,SADI,OAAO,iBAAmB,MAAoB,KAC3C,eAAe,QAAQ,EAAI,KAAK;SACjC;AACN,SAAO;;;AAIX,SAAS,EAAe,GAAmB;AACzC,KAAI;AACF,MAAI,OAAO,iBAAmB,IAAa;AAC3C,iBAAe,QAAQ,GAAK,IAAI;SAC1B;;AAKV,SAAS,EAAiB,GAAmB;AAC3C,KAAI;AACF,MAAI,OAAO,iBAAmB,IAAa;AAC3C,iBAAe,WAAW,EAAI;SACxB;;AAsBV,IAAM,IAAN,cAAgC,EAG9B;CACA,QAAgC;EAC9B,UAAU;EACV,OAAO;EACP,UAAU;EACX;CAED,OAAO,yBAAyB,GAAc;AAC5C,SAAO;GAAE,UAAU;GAAM;GAAO;;CAGlC,oBAAoB;AAIlB,EAAK,KAAK,MAAM,YACd,EAAiB,EAAc,KAAK,MAAM,OAAO,CAAC;;CAItD,mBACE,GACA,GACA;AAGA,EAAI,EAAU,YAAY,CAAC,KAAK,MAAM,YACpC,EAAiB,EAAc,KAAK,MAAM,OAAO,CAAC;;CAItD,kBAAkB,GAAc,GAAsB;EACpD,IAAM,IAAM,KAAK,MAAM,SACnB,kBAAkB,KAAK,MAAM,OAAO,KACpC;AAKJ,UAAQ,MAAM,GAAG,EAAI,0BAA0B,GAAO,EAAU;;CAGlE,oBAAoB;AAClB,MAAI,EAAiB,KAAK,MAAM,MAAM,EAAE;GAatC,IAAM,IAAU,EAAc,KAAK,MAAM,OAAO;AAChD,OAAI,EAAgB,EAAQ,CAC1B;AAGF,GADA,EAAe,EAAQ,EACnB,OAAO,SAAW,OACpB,OAAO,SAAS,QAAQ;AAE1B;;AAEF,OAAK,UAAU,OAAO;GACpB,UAAU;GACV,OAAO;GACP,UAAU,EAAE,WAAW;GACxB,EAAE;;CAGL,SAAS;AAeP,SAdI,KAAK,MAAM,WAQX,kBAAC,GAAD;GACE,cAAc,KAAK,MAAM,OAAO,WAAW;GAC3C,SAAS,KAAK;GACd,CAAA,GAGC,KAAK,MAAM,SAAS,KAAK,MAAM,SAAS;;;AAenD,SAAgB,EAAiB,EAC/B,aACA,aAIC;AACD,QACE,kBAAC,GAAD;EAA2B;aACvB,MACA,kBAAC,GAAD;GAAyB,UAAU,kBAAC,GAAD,EAAc,CAAA;GAC9C;GACQ,EAFI,EAEJ;EAEK,CAAA"}
|
|
1
|
+
{"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import { Component, Suspense, type ErrorInfo, type ReactNode } from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-status-error-bg/10\">\n <AlertTriangle className=\"size-8 text-status-error-text\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-text-secondary max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-text-secondary hover:text-text-primary select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-bg-sunken text-[11px] text-text-secondary whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ function h({ member: e, size: n = "md" }) {
|
|
|
18
18
|
className: t("rounded-full object-cover", r)
|
|
19
19
|
}) : /* @__PURE__ */ o("span", {
|
|
20
20
|
"aria-hidden": "true",
|
|
21
|
-
className: t("inline-flex items-center justify-center rounded-full bg-
|
|
21
|
+
className: t("inline-flex items-center justify-center rounded-full bg-bg-sunken font-medium text-text-secondary", r),
|
|
22
22
|
children: m(e.name)
|
|
23
23
|
});
|
|
24
24
|
}
|
|
@@ -36,7 +36,7 @@ function g({ selected: t, onAdd: m, onRemove: g, single: _ = !1, excludeIds: v =
|
|
|
36
36
|
t.length > 0 && /* @__PURE__ */ o("div", {
|
|
37
37
|
className: "flex flex-wrap gap-2",
|
|
38
38
|
children: t.map((e) => /* @__PURE__ */ s("span", {
|
|
39
|
-
className: "inline-flex items-center gap-1.5 rounded-full bg-primary/10 py-1 pl-1 pr-2 text-xs font-medium text-primary",
|
|
39
|
+
className: "inline-flex items-center gap-1.5 rounded-full bg-action-primary-bg/10 py-1 pl-1 pr-2 text-xs font-medium text-primary",
|
|
40
40
|
children: [
|
|
41
41
|
/* @__PURE__ */ o(h, {
|
|
42
42
|
member: e,
|
|
@@ -47,7 +47,7 @@ function g({ selected: t, onAdd: m, onRemove: g, single: _ = !1, excludeIds: v =
|
|
|
47
47
|
type: "button",
|
|
48
48
|
"aria-label": y("conversations.removeParticipant", "Remove participant"),
|
|
49
49
|
onClick: () => g(e.id),
|
|
50
|
-
className: "rounded-full p-0.5 hover:bg-primary/20",
|
|
50
|
+
className: "rounded-full p-0.5 hover:bg-action-primary-bg/20",
|
|
51
51
|
children: /* @__PURE__ */ o(a, { size: 12 })
|
|
52
52
|
})
|
|
53
53
|
]
|
|
@@ -55,14 +55,14 @@ function g({ selected: t, onAdd: m, onRemove: g, single: _ = !1, excludeIds: v =
|
|
|
55
55
|
}),
|
|
56
56
|
/* @__PURE__ */ s(c, {
|
|
57
57
|
shouldFilter: !1,
|
|
58
|
-
className: "rounded-lg border border-border bg-
|
|
58
|
+
className: "rounded-lg border border-border-subtle bg-bg-surface",
|
|
59
59
|
children: [/* @__PURE__ */ o(d, {
|
|
60
60
|
value: b,
|
|
61
61
|
onValueChange: x,
|
|
62
62
|
placeholder: y("conversations.memberSearchPlaceholder", "Search members by name or email…")
|
|
63
63
|
}), /* @__PURE__ */ s(p, { children: [
|
|
64
64
|
C && T.length === 0 ? /* @__PURE__ */ o("div", {
|
|
65
|
-
className: "py-6 text-center text-sm text-
|
|
65
|
+
className: "py-6 text-center text-sm text-text-secondary",
|
|
66
66
|
children: y("conversations.memberSearchLoading", "Loading members…")
|
|
67
67
|
}) : null,
|
|
68
68
|
!C && T.length === 0 ? /* @__PURE__ */ o(l, { children: b.trim() ? y("conversations.memberSearchNoResults", "No members found.") : y("conversations.memberSearchEmpty", "No more members to add.") }) : null,
|
|
@@ -75,10 +75,10 @@ function g({ selected: t, onAdd: m, onRemove: g, single: _ = !1, excludeIds: v =
|
|
|
75
75
|
children: [/* @__PURE__ */ o(h, { member: e }), /* @__PURE__ */ s("span", {
|
|
76
76
|
className: "flex min-w-0 flex-col",
|
|
77
77
|
children: [/* @__PURE__ */ o("span", {
|
|
78
|
-
className: "truncate text-sm text-
|
|
78
|
+
className: "truncate text-sm text-text-primary",
|
|
79
79
|
children: e.name
|
|
80
80
|
}), e.email ? /* @__PURE__ */ o("span", {
|
|
81
|
-
className: "truncate text-xs text-
|
|
81
|
+
className: "truncate text-xs text-text-secondary",
|
|
82
82
|
children: e.email
|
|
83
83
|
}) : null]
|
|
84
84
|
})]
|
|
@@ -87,7 +87,7 @@ function g({ selected: t, onAdd: m, onRemove: g, single: _ = !1, excludeIds: v =
|
|
|
87
87
|
] })]
|
|
88
88
|
}),
|
|
89
89
|
_ && t.length > 0 ? /* @__PURE__ */ o("p", {
|
|
90
|
-
className: "text-xs text-
|
|
90
|
+
className: "text-xs text-text-secondary",
|
|
91
91
|
children: y("conversations.directMessageSingleHint", "A direct message has exactly one recipient. Selecting another replaces the current one.")
|
|
92
92
|
}) : null
|
|
93
93
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemberPicker.js","names":[],"sources":["../../../src/conversations/components/MemberPicker.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@burdenoff/fe-libs/ui\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport {\n useWorkspaceDirectory,\n type WorkspaceDirectoryMember,\n} from \"../hooks/useConversationQueries\";\n\nexport interface MemberPickerProps {\n /** Currently selected members (controlled by the parent). */\n selected: WorkspaceDirectoryMember[];\n /** Add a member to the selection. */\n onAdd: (member: WorkspaceDirectoryMember) => void;\n /** Remove a member from the selection by user id. */\n onRemove: (userId: string) => void;\n /**\n * Single-select mode: only one member may be chosen at a time (e.g. for a\n * direct message). Defaults to multi-select.\n */\n single?: boolean;\n /**\n * Extra user ids to hide from the suggestion list (e.g. the current user).\n * Selected members are always hidden.\n */\n excludeIds?: string[];\n}\n\n/** Two-letter initials derived from a member's display name. */\nfunction initials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return `${parts[0]![0] ?? \"\"}${parts[parts.length - 1]![0] ?? \"\"}`.toUpperCase();\n}\n\ninterface MemberAvatarProps {\n member: WorkspaceDirectoryMember;\n size?: \"sm\" | \"md\";\n}\n\nfunction MemberAvatar({ member, size = \"md\" }: MemberAvatarProps) {\n const dimension = size === \"sm\" ? \"size-5 text-[10px]\" : \"size-8 text-xs\";\n if (member.avatar) {\n return (\n <img\n src={member.avatar}\n alt=\"\"\n className={cn(\"rounded-full object-cover\", dimension)}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"inline-flex items-center justify-center rounded-full bg-
|
|
1
|
+
{"version":3,"file":"MemberPicker.js","names":[],"sources":["../../../src/conversations/components/MemberPicker.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@burdenoff/fe-libs/ui\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport {\n useWorkspaceDirectory,\n type WorkspaceDirectoryMember,\n} from \"../hooks/useConversationQueries\";\n\nexport interface MemberPickerProps {\n /** Currently selected members (controlled by the parent). */\n selected: WorkspaceDirectoryMember[];\n /** Add a member to the selection. */\n onAdd: (member: WorkspaceDirectoryMember) => void;\n /** Remove a member from the selection by user id. */\n onRemove: (userId: string) => void;\n /**\n * Single-select mode: only one member may be chosen at a time (e.g. for a\n * direct message). Defaults to multi-select.\n */\n single?: boolean;\n /**\n * Extra user ids to hide from the suggestion list (e.g. the current user).\n * Selected members are always hidden.\n */\n excludeIds?: string[];\n}\n\n/** Two-letter initials derived from a member's display name. */\nfunction initials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return `${parts[0]![0] ?? \"\"}${parts[parts.length - 1]![0] ?? \"\"}`.toUpperCase();\n}\n\ninterface MemberAvatarProps {\n member: WorkspaceDirectoryMember;\n size?: \"sm\" | \"md\";\n}\n\nfunction MemberAvatar({ member, size = \"md\" }: MemberAvatarProps) {\n const dimension = size === \"sm\" ? \"size-5 text-[10px]\" : \"size-8 text-xs\";\n if (member.avatar) {\n return (\n <img\n src={member.avatar}\n alt=\"\"\n className={cn(\"rounded-full object-cover\", dimension)}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"inline-flex items-center justify-center rounded-full bg-bg-sunken font-medium text-text-secondary\",\n dimension,\n )}\n >\n {initials(member.name)}\n </span>\n );\n}\n\n/**\n * MemberPicker — search-as-you-type workspace-member selector.\n *\n * Lists the current workspace's members via the conversations\n * `workspaceDirectory` query (server-side search, debounced in\n * {@link useWorkspaceDirectory}) and renders each as an avatar + name + email\n * row inside the canonical `Command` palette from `@burdenoff/fe-libs/ui`.\n *\n * Selection is controlled by the parent: the picker reports adds/removes by\n * user id so the host owns the `participantIds` it ultimately submits.\n */\nexport function MemberPicker({\n selected,\n onAdd,\n onRemove,\n single = false,\n excludeIds = [],\n}: MemberPickerProps) {\n const tr = useTr();\n const [input, setInput] = useState(\"\");\n const { members, loading } = useWorkspaceDirectory(input);\n\n const hiddenIds = useMemo(() => {\n const ids = new Set<string>(excludeIds);\n for (const member of selected) {\n ids.add(member.id);\n }\n return ids;\n }, [excludeIds, selected]);\n\n const visibleMembers = useMemo(\n () => members.filter((member) => !hiddenIds.has(member.id)),\n [members, hiddenIds],\n );\n\n const handleSelect = (member: WorkspaceDirectoryMember) => {\n onAdd(member);\n setInput(\"\");\n };\n\n return (\n <div className=\"space-y-2\">\n {selected.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {selected.map((member) => (\n <span\n key={member.id}\n className=\"inline-flex items-center gap-1.5 rounded-full bg-action-primary-bg/10 py-1 pl-1 pr-2 text-xs font-medium text-primary\"\n >\n <MemberAvatar member={member} size=\"sm\" />\n {member.name}\n <button\n type=\"button\"\n aria-label={tr(\n \"conversations.removeParticipant\",\n \"Remove participant\",\n )}\n onClick={() => onRemove(member.id)}\n className=\"rounded-full p-0.5 hover:bg-action-primary-bg/20\"\n >\n <X size={12} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <Command\n shouldFilter={false}\n className=\"rounded-lg border border-border-subtle bg-bg-surface\"\n >\n <CommandInput\n value={input}\n onValueChange={setInput}\n placeholder={tr(\n \"conversations.memberSearchPlaceholder\",\n \"Search members by name or email…\",\n )}\n />\n <CommandList>\n {loading && visibleMembers.length === 0 ? (\n <div className=\"py-6 text-center text-sm text-text-secondary\">\n {tr(\"conversations.memberSearchLoading\", \"Loading members…\")}\n </div>\n ) : null}\n\n {!loading && visibleMembers.length === 0 ? (\n <CommandEmpty>\n {input.trim()\n ? tr(\"conversations.memberSearchNoResults\", \"No members found.\")\n : tr(\n \"conversations.memberSearchEmpty\",\n \"No more members to add.\",\n )}\n </CommandEmpty>\n ) : null}\n\n {visibleMembers.length > 0 ? (\n <CommandGroup heading={tr(\"conversations.members\", \"Members\")}>\n {visibleMembers.map((member) => (\n <CommandItem\n key={member.id}\n value={`${member.name} ${member.email ?? \"\"} ${member.id}`}\n onSelect={() => handleSelect(member)}\n className=\"gap-2\"\n >\n <MemberAvatar member={member} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm text-text-primary\">\n {member.name}\n </span>\n {member.email ? (\n <span className=\"truncate text-xs text-text-secondary\">\n {member.email}\n </span>\n ) : null}\n </span>\n </CommandItem>\n ))}\n </CommandGroup>\n ) : null}\n </CommandList>\n </Command>\n\n {single && selected.length > 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {tr(\n \"conversations.directMessageSingleHint\",\n \"A direct message has exactly one recipient. Selecting another replaces the current one.\",\n )}\n </p>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -15,18 +15,18 @@ function v({ status: e, failedLabel: t }) {
|
|
|
15
15
|
case "SENDING": return /* @__PURE__ */ m("span", { className: "size-3 rounded-full border border-muted-foreground animate-pulse" });
|
|
16
16
|
case "SENT": return /* @__PURE__ */ m(r, {
|
|
17
17
|
size: 12,
|
|
18
|
-
className: "text-
|
|
18
|
+
className: "text-text-secondary"
|
|
19
19
|
});
|
|
20
20
|
case "DELIVERED": return /* @__PURE__ */ m(i, {
|
|
21
21
|
size: 12,
|
|
22
|
-
className: "text-
|
|
22
|
+
className: "text-text-secondary"
|
|
23
23
|
});
|
|
24
24
|
case "READ": return /* @__PURE__ */ m(i, {
|
|
25
25
|
size: 12,
|
|
26
26
|
className: "text-primary"
|
|
27
27
|
});
|
|
28
28
|
case "FAILED": return /* @__PURE__ */ m("span", {
|
|
29
|
-
className: "text-[10px] text-
|
|
29
|
+
className: "text-[10px] text-status-error-text font-medium",
|
|
30
30
|
children: t
|
|
31
31
|
});
|
|
32
32
|
default: return null;
|
|
@@ -43,7 +43,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
43
43
|
return G ? /* @__PURE__ */ m("div", {
|
|
44
44
|
className: "flex justify-center my-2 px-4",
|
|
45
45
|
children: /* @__PURE__ */ m("span", {
|
|
46
|
-
className: "text-xs text-
|
|
46
|
+
className: "text-xs text-text-secondary bg-bg-sunken/50 px-3 py-1 rounded-full",
|
|
47
47
|
children: L
|
|
48
48
|
})
|
|
49
49
|
}) : /* @__PURE__ */ h("div", {
|
|
@@ -52,7 +52,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
52
52
|
className: e("group flex gap-2 px-4 py-1", r ? "flex-row-reverse" : "flex-row"),
|
|
53
53
|
children: [
|
|
54
54
|
i ? /* @__PURE__ */ m("div", {
|
|
55
|
-
className: e("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", r ? "bg-primary/10 text-primary" : "bg-
|
|
55
|
+
className: e("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", r ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
|
|
56
56
|
children: I.avatarUrl ? /* @__PURE__ */ m("img", {
|
|
57
57
|
src: I.avatarUrl,
|
|
58
58
|
alt: I.name,
|
|
@@ -65,7 +65,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
65
65
|
b && !r && /* @__PURE__ */ h("div", {
|
|
66
66
|
className: "flex items-center gap-1.5 mb-0.5",
|
|
67
67
|
children: [/* @__PURE__ */ m("span", {
|
|
68
|
-
className: "text-xs font-semibold text-
|
|
68
|
+
className: "text-xs font-semibold text-text-primary",
|
|
69
69
|
children: I.name
|
|
70
70
|
}), W && /* @__PURE__ */ m(l, {
|
|
71
71
|
size: 10,
|
|
@@ -73,15 +73,15 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
73
73
|
})]
|
|
74
74
|
}),
|
|
75
75
|
/* @__PURE__ */ h("div", {
|
|
76
|
-
className: e("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", r ? "bg-primary text-primary-
|
|
76
|
+
className: e("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", r ? "bg-action-primary-bg text-action-primary-text rounded-tr-md" : "bg-bg-sunken/70 text-text-primary rounded-tl-md"),
|
|
77
77
|
children: [/* @__PURE__ */ m("p", {
|
|
78
78
|
className: "whitespace-pre-wrap break-words",
|
|
79
79
|
children: L
|
|
80
80
|
}), V.length > 0 && /* @__PURE__ */ m("div", {
|
|
81
81
|
className: "mt-2 grid gap-2",
|
|
82
82
|
children: V.map((t) => {
|
|
83
|
-
let n = e("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", r ? "bg-primary-foreground/10 text-primary-
|
|
84
|
-
className: "flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-
|
|
83
|
+
let n = e("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", r ? "bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15" : "bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70"), i = /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m("div", {
|
|
84
|
+
className: "flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/40",
|
|
85
85
|
children: t.thumbnail && y(t.mimeType) ? /* @__PURE__ */ m("img", {
|
|
86
86
|
src: t.thumbnail,
|
|
87
87
|
alt: t.fileName,
|
|
@@ -121,7 +121,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
121
121
|
children: B.map((n) => /* @__PURE__ */ h("button", {
|
|
122
122
|
type: "button",
|
|
123
123
|
onClick: () => w?.(t.id, n.emoji),
|
|
124
|
-
className: e("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", n.currentUserReacted ? "bg-primary/10 border-primary/30 text-primary" : "bg-
|
|
124
|
+
className: e("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", n.currentUserReacted ? "bg-action-primary-bg/10 border-primary/30 text-primary" : "bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken"),
|
|
125
125
|
children: [/* @__PURE__ */ m("span", { children: n.emoji }), /* @__PURE__ */ m("span", {
|
|
126
126
|
className: "font-medium",
|
|
127
127
|
children: n.count
|
|
@@ -132,11 +132,11 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
132
132
|
className: "flex items-center gap-1.5 mt-0.5 px-1",
|
|
133
133
|
children: [
|
|
134
134
|
/* @__PURE__ */ m("span", {
|
|
135
|
-
className: "text-[10px] text-
|
|
135
|
+
className: "text-[10px] text-text-secondary",
|
|
136
136
|
children: _(U)
|
|
137
137
|
}),
|
|
138
138
|
H && /* @__PURE__ */ m("span", {
|
|
139
|
-
className: "text-[10px] text-
|
|
139
|
+
className: "text-[10px] text-text-secondary italic",
|
|
140
140
|
children: F("conversations.edited", "edited")
|
|
141
141
|
}),
|
|
142
142
|
r && /* @__PURE__ */ m(v, {
|
|
@@ -156,7 +156,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
156
156
|
onBlur: (e) => {
|
|
157
157
|
e.currentTarget.contains(e.relatedTarget) || N(!1);
|
|
158
158
|
},
|
|
159
|
-
className: e("relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", K && "sm:opacity-100"),
|
|
159
|
+
className: e("relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", K && "sm:opacity-100"),
|
|
160
160
|
children: [
|
|
161
161
|
t.permissions.canReact && /* @__PURE__ */ m(x, {
|
|
162
162
|
icon: /* @__PURE__ */ m(d, { size: 14 }),
|
|
@@ -190,7 +190,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
190
190
|
}
|
|
191
191
|
}),
|
|
192
192
|
O && /* @__PURE__ */ m("div", {
|
|
193
|
-
className: "absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg",
|
|
193
|
+
className: "absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg",
|
|
194
194
|
children: [
|
|
195
195
|
"👍",
|
|
196
196
|
"❤️",
|
|
@@ -202,19 +202,19 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
202
202
|
onClick: () => {
|
|
203
203
|
w?.(t.id, e), k(!1);
|
|
204
204
|
},
|
|
205
|
-
className: "rounded-md px-2 py-1 text-sm hover:bg-
|
|
205
|
+
className: "rounded-md px-2 py-1 text-sm hover:bg-bg-sunken/80",
|
|
206
206
|
"aria-label": `${F("conversations.reactWith", "React with")} ${e}`,
|
|
207
207
|
children: e
|
|
208
208
|
}, e))
|
|
209
209
|
}),
|
|
210
210
|
A && t.permissions.canPin && D && /* @__PURE__ */ m("div", {
|
|
211
|
-
className: "absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg",
|
|
211
|
+
className: "absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg",
|
|
212
212
|
children: /* @__PURE__ */ h("button", {
|
|
213
213
|
type: "button",
|
|
214
214
|
onClick: () => {
|
|
215
215
|
D(t.id, t.isPinned), j(!1);
|
|
216
216
|
},
|
|
217
|
-
className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-
|
|
217
|
+
className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken/80",
|
|
218
218
|
children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? F("conversations.unpinMessage", "Unpin message") : F("conversations.pinMessage", "Pin message")]
|
|
219
219
|
})
|
|
220
220
|
})
|
|
@@ -230,7 +230,7 @@ function x({ icon: e, label: t, onClick: n, pressed: r }) {
|
|
|
230
230
|
"aria-label": t,
|
|
231
231
|
"aria-pressed": r,
|
|
232
232
|
title: t,
|
|
233
|
-
className: "flex min-h-9 min-w-9 items-center justify-center rounded-md text-
|
|
233
|
+
className: "flex min-h-9 min-w-9 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-bg-sunken/80 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
234
234
|
children: e
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"size-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-muted-foreground\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-muted-foreground\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-destructive font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const [showActions, setShowActions] = useState(false);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n const actionsVisible = showActions || showReactionPicker || showMoreMenu;\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-foreground\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-primary text-primary-foreground rounded-tr-md\"\n : \"bg-muted/70 text-foreground rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15\"\n : \"bg-background/50 text-foreground hover:bg-background/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-muted/50 border-border text-muted-foreground hover:bg-muted\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-muted-foreground\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-muted-foreground italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n <div\n data-tour=\"conversation-message-actions\"\n role=\"toolbar\"\n aria-label={tr(\"conversations.messageActions\", \"Message actions\")}\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"sm:opacity-100\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-muted/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"flex min-h-9 min-w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"size-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-text-secondary\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-text-secondary\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-status-error-text font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const [showActions, setShowActions] = useState(false);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n const actionsVisible = showActions || showReactionPicker || showMoreMenu;\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-text-secondary bg-bg-sunken/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-text-primary\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-action-primary-bg text-action-primary-text rounded-tr-md\"\n : \"bg-bg-sunken/70 text-text-primary rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15\"\n : \"bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-action-primary-bg/10 border-primary/30 text-primary\"\n : \"bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-text-secondary\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-text-secondary italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n <div\n data-tour=\"conversation-message-actions\"\n role=\"toolbar\"\n aria-label={tr(\"conversations.messageActions\", \"Message actions\")}\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"sm:opacity-100\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-bg-sunken/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"flex min-h-9 min-w-9 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-bg-sunken/80 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
|
|
@@ -123,7 +123,7 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
123
123
|
"data-tour": "conversation-type-selector",
|
|
124
124
|
className: "space-y-3",
|
|
125
125
|
children: [/* @__PURE__ */ _("h3", {
|
|
126
|
-
className: "text-sm font-semibold text-
|
|
126
|
+
className: "text-sm font-semibold text-text-primary",
|
|
127
127
|
children: N("conversations.conversationType", "Conversation Type")
|
|
128
128
|
}), /* @__PURE__ */ _("div", {
|
|
129
129
|
className: "grid grid-cols-1 sm:grid-cols-2 gap-3",
|
|
@@ -132,15 +132,15 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
132
132
|
onClick: () => {
|
|
133
133
|
I(e.type), e.type === "DIRECT" && W((e) => e.slice(0, 1));
|
|
134
134
|
},
|
|
135
|
-
className: n("flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150", F === e.type ? "border-primary bg-primary/5 ring-1 ring-primary/20" : "border-border hover:border-muted-foreground/30 hover:bg-
|
|
135
|
+
className: n("flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150", F === e.type ? "border-primary bg-action-primary-bg/5 ring-1 ring-primary/20" : "border-border-subtle hover:border-muted-foreground/30 hover:bg-bg-sunken/30"),
|
|
136
136
|
children: [/* @__PURE__ */ _("div", {
|
|
137
|
-
className: n("p-2 rounded-lg", F === e.type ? "bg-primary/10 text-primary" : "bg-
|
|
137
|
+
className: n("p-2 rounded-lg", F === e.type ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
|
|
138
138
|
children: e.icon
|
|
139
139
|
}), /* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("p", {
|
|
140
|
-
className: "text-sm font-medium text-
|
|
140
|
+
className: "text-sm font-medium text-text-primary",
|
|
141
141
|
children: e.label
|
|
142
142
|
}), /* @__PURE__ */ _("p", {
|
|
143
|
-
className: "text-xs text-
|
|
143
|
+
className: "text-xs text-text-secondary mt-0.5",
|
|
144
144
|
children: e.description
|
|
145
145
|
})] })]
|
|
146
146
|
}, e.type))
|
|
@@ -151,7 +151,7 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
151
151
|
className: "space-y-3",
|
|
152
152
|
children: [
|
|
153
153
|
/* @__PURE__ */ _("h3", {
|
|
154
|
-
className: "text-sm font-semibold text-
|
|
154
|
+
className: "text-sm font-semibold text-text-primary",
|
|
155
155
|
children: N("conversations.visibility", "Visibility")
|
|
156
156
|
}),
|
|
157
157
|
/* @__PURE__ */ _("div", {
|
|
@@ -159,12 +159,12 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
159
159
|
children: P.map((e) => /* @__PURE__ */ v("button", {
|
|
160
160
|
type: "button",
|
|
161
161
|
onClick: () => H(e.value),
|
|
162
|
-
className: n("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all", V === e.value ? "border-primary bg-primary/5 text-primary font-medium" : "border-border text-
|
|
162
|
+
className: n("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all", V === e.value ? "border-primary bg-action-primary-bg/5 text-primary font-medium" : "border-border-subtle text-text-secondary hover:text-text-primary hover:bg-bg-sunken/30"),
|
|
163
163
|
children: [e.icon, e.label]
|
|
164
164
|
}, e.value))
|
|
165
165
|
}),
|
|
166
166
|
/* @__PURE__ */ _("p", {
|
|
167
|
-
className: "text-xs text-
|
|
167
|
+
className: "text-xs text-text-secondary",
|
|
168
168
|
children: P.find((e) => e.value === V)?.description
|
|
169
169
|
})
|
|
170
170
|
]
|
|
@@ -174,12 +174,12 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
174
174
|
className: "space-y-3",
|
|
175
175
|
children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ v("label", {
|
|
176
176
|
htmlFor: "conv-title",
|
|
177
|
-
className: "text-sm font-semibold text-
|
|
177
|
+
className: "text-sm font-semibold text-text-primary",
|
|
178
178
|
children: [
|
|
179
179
|
N("conversations.name", "Name"),
|
|
180
180
|
" ",
|
|
181
181
|
/* @__PURE__ */ _("span", {
|
|
182
|
-
className: "text-
|
|
182
|
+
className: "text-status-error-text",
|
|
183
183
|
children: "*"
|
|
184
184
|
})
|
|
185
185
|
]
|
|
@@ -190,10 +190,10 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
190
190
|
onChange: (e) => R(e.target.value),
|
|
191
191
|
placeholder: F === "CHANNEL" ? N("conversations.channelNamePlaceholder", "e.g., engineering-general") : N("conversations.conversationNamePlaceholder", "e.g., Product Launch Q1"),
|
|
192
192
|
maxLength: 200,
|
|
193
|
-
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-
|
|
193
|
+
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border-subtle bg-bg-surface placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
194
194
|
})] }), /* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
195
195
|
htmlFor: "conv-desc",
|
|
196
|
-
className: "text-sm font-semibold text-
|
|
196
|
+
className: "text-sm font-semibold text-text-primary",
|
|
197
197
|
children: N("conversations.description", "Description")
|
|
198
198
|
}), /* @__PURE__ */ _("textarea", {
|
|
199
199
|
id: "conv-desc",
|
|
@@ -202,19 +202,19 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
202
202
|
placeholder: N("conversations.descriptionPlaceholder", "What is this conversation about?"),
|
|
203
203
|
rows: 2,
|
|
204
204
|
maxLength: 1e3,
|
|
205
|
-
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-
|
|
205
|
+
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border-subtle bg-bg-surface placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none"
|
|
206
206
|
})] })]
|
|
207
207
|
}),
|
|
208
208
|
/* @__PURE__ */ v("section", {
|
|
209
209
|
"data-tour": "conversation-participants",
|
|
210
210
|
className: "space-y-3",
|
|
211
211
|
children: [/* @__PURE__ */ v("h3", {
|
|
212
|
-
className: "text-sm font-semibold text-
|
|
212
|
+
className: "text-sm font-semibold text-text-primary",
|
|
213
213
|
children: [G ? N("conversations.recipient", "Recipient") : N("conversations.participants", "Participants"), Z ? /* @__PURE__ */ _("span", {
|
|
214
|
-
className: "ml-1 text-
|
|
214
|
+
className: "ml-1 text-status-error-text",
|
|
215
215
|
children: "*"
|
|
216
216
|
}) : /* @__PURE__ */ v("span", {
|
|
217
|
-
className: "ml-1.5 text-xs font-normal text-
|
|
217
|
+
className: "ml-1.5 text-xs font-normal text-text-secondary",
|
|
218
218
|
children: [
|
|
219
219
|
"(",
|
|
220
220
|
N("conversations.optional", "optional"),
|
|
@@ -230,7 +230,7 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
230
230
|
})]
|
|
231
231
|
}),
|
|
232
232
|
A && /* @__PURE__ */ _("div", {
|
|
233
|
-
className: "text-xs text-
|
|
233
|
+
className: "text-xs text-status-error-text rounded-lg border border-status-error-border/30 bg-status-error-bg/10 px-3 py-2",
|
|
234
234
|
children: w(A)
|
|
235
235
|
})
|
|
236
236
|
]
|
|
@@ -238,13 +238,13 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
238
238
|
/* @__PURE__ */ v(S, { children: [/* @__PURE__ */ _("button", {
|
|
239
239
|
type: "button",
|
|
240
240
|
onClick: () => Q(!1),
|
|
241
|
-
className: "px-4 py-2 text-sm font-medium rounded-lg text-
|
|
241
|
+
className: "px-4 py-2 text-sm font-medium rounded-lg text-text-secondary hover:text-text-primary hover:bg-bg-sunken/50 transition-colors",
|
|
242
242
|
children: N("conversations.cancel", "Cancel")
|
|
243
243
|
}), /* @__PURE__ */ _("button", {
|
|
244
244
|
type: "button",
|
|
245
245
|
onClick: () => void ie(),
|
|
246
246
|
disabled: !$ || k,
|
|
247
|
-
className: n("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !k ? "bg-primary text-primary-
|
|
247
|
+
className: n("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !k ? "bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 shadow-sm" : "bg-bg-sunken text-text-secondary cursor-not-allowed"),
|
|
248
248
|
children: k ? N("conversations.creating", "Creating...") : N("conversations.createConversation", "Create Conversation")
|
|
249
249
|
})] })
|
|
250
250
|
]
|