@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.
@@ -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-destructive/10",
20
- children: /* @__PURE__ */ s(i, { className: "size-8 text-destructive" })
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-foreground",
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-muted-foreground max-w-md",
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-foreground hover:bg-primary/90 transition-colors",
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-muted-foreground hover:text-foreground select-none",
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-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all",
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":"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"}
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":";;;;;;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,iCAAkC,CAAA;IACvD,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"}
@@ -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-muted font-medium text-muted-foreground", r),
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-background",
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-muted-foreground",
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-foreground",
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-muted-foreground",
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-muted-foreground",
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-muted font-medium text-muted-foreground\",\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-primary/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-primary/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 bg-background\"\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-muted-foreground\">\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-foreground\">\n {member.name}\n </span>\n {member.email ? (\n <span className=\"truncate text-xs text-muted-foreground\">\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-muted-foreground\">\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":";;;;;;;;AAqCA,SAAS,EAAS,GAAsB;CACtC,IAAM,IAAQ,EAAK,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAGtD,QAFI,EAAM,WAAW,IAAU,MAC3B,EAAM,WAAW,IAAU,EAAM,GAAI,MAAM,GAAG,EAAE,CAAC,aAAa,GAC3D,GAAG,EAAM,GAAI,MAAM,KAAK,EAAM,EAAM,SAAS,GAAI,MAAM,KAAK,aAAa;;AAQlF,SAAS,EAAa,EAAE,WAAQ,UAAO,QAA2B;CAChE,IAAM,IAAY,MAAS,OAAO,uBAAuB;AAUzD,QATI,EAAO,SAEP,kBAAC,OAAD;EACE,KAAK,EAAO;EACZ,KAAI;EACJ,WAAW,EAAG,6BAA6B,EAAU;EACrD,CAAA,GAIJ,kBAAC,QAAD;EACE,eAAY;EACZ,WAAW,EACT,mGACA,EACD;YAEA,EAAS,EAAO,KAAK;EACjB,CAAA;;AAeX,SAAgB,EAAa,EAC3B,aACA,UACA,aACA,YAAS,IACT,gBAAa,EAAE,IACK;CACpB,IAAM,IAAK,GAAO,EACZ,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,EAAE,YAAS,eAAY,EAAsB,EAAM,EAEnD,IAAY,QAAc;EAC9B,IAAM,IAAM,IAAI,IAAY,EAAW;AACvC,OAAK,IAAM,KAAU,EACnB,GAAI,IAAI,EAAO,GAAG;AAEpB,SAAO;IACN,CAAC,GAAY,EAAS,CAAC,EAEpB,IAAiB,QACf,EAAQ,QAAQ,MAAW,CAAC,EAAU,IAAI,EAAO,GAAG,CAAC,EAC3D,CAAC,GAAS,EAAU,CACrB,EAEK,KAAgB,MAAqC;AAEzD,EADA,EAAM,EAAO,EACb,EAAS,GAAG;;AAGd,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,EAAS,SAAS,KACjB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAS,KAAK,MACb,kBAAC,QAAD;KAEE,WAAU;eAFZ;MAIE,kBAAC,GAAD;OAAsB;OAAQ,MAAK;OAAO,CAAA;MACzC,EAAO;MACR,kBAAC,UAAD;OACE,MAAK;OACL,cAAY,EACV,mCACA,qBACD;OACD,eAAe,EAAS,EAAO,GAAG;OAClC,WAAU;iBAEV,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA;OACR,CAAA;MACJ;OAhBA,EAAO,GAgBP,CACP;IACE,CAAA;GAGR,kBAAC,GAAD;IACE,cAAc;IACd,WAAU;cAFZ,CAIE,kBAAC,GAAD;KACE,OAAO;KACP,eAAe;KACf,aAAa,EACX,yCACA,mCACD;KACD,CAAA,EACF,kBAAC,GAAD,EAAA,UAAA;KACG,KAAW,EAAe,WAAW,IACpC,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAG,qCAAqC,mBAAmB;MACxD,CAAA,GACJ;KAEH,CAAC,KAAW,EAAe,WAAW,IACrC,kBAAC,GAAD,EAAA,UACG,EAAM,MAAM,GACT,EAAG,uCAAuC,oBAAoB,GAC9D,EACE,mCACA,0BACD,EACQ,CAAA,GACb;KAEH,EAAe,SAAS,IACvB,kBAAC,GAAD;MAAc,SAAS,EAAG,yBAAyB,UAAU;gBAC1D,EAAe,KAAK,MACnB,kBAAC,GAAD;OAEE,OAAO,GAAG,EAAO,KAAK,GAAG,EAAO,SAAS,GAAG,GAAG,EAAO;OACtD,gBAAgB,EAAa,EAAO;OACpC,WAAU;iBAJZ,CAME,kBAAC,GAAD,EAAsB,WAAU,CAAA,EAChC,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO;SACH,CAAA,EACN,EAAO,QACN,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO;SACH,CAAA,GACL,KACC;UACK;SAhBP,EAAO,GAgBA,CACd;MACW,CAAA,GACb;KACQ,EAAA,CAAA,CACN;;GAET,KAAU,EAAS,SAAS,IAC3B,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,yCACA,0FACD;IACC,CAAA,GACF;GACA"}
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-muted-foreground"
18
+ className: "text-text-secondary"
19
19
  });
20
20
  case "DELIVERED": return /* @__PURE__ */ m(i, {
21
21
  size: 12,
22
- className: "text-muted-foreground"
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-destructive font-medium",
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-muted-foreground bg-muted/50 px-3 py-1 rounded-full",
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-muted text-muted-foreground"),
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-foreground",
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-foreground rounded-tr-md" : "bg-muted/70 text-foreground rounded-tl-md"),
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-foreground hover:bg-primary-foreground/15" : "bg-background/50 text-foreground hover:bg-background/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-background/40",
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-muted/50 border-border text-muted-foreground hover:bg-muted"),
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-muted-foreground",
135
+ className: "text-[10px] text-text-secondary",
136
136
  children: _(U)
137
137
  }),
138
138
  H && /* @__PURE__ */ m("span", {
139
- className: "text-[10px] text-muted-foreground italic",
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-muted/80",
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-foreground hover:bg-muted/80",
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-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
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-foreground",
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-muted/30"),
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-muted text-muted-foreground"),
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-foreground",
140
+ className: "text-sm font-medium text-text-primary",
141
141
  children: e.label
142
142
  }), /* @__PURE__ */ _("p", {
143
- className: "text-xs text-muted-foreground mt-0.5",
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-foreground",
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-muted-foreground hover:text-foreground hover:bg-muted/30"),
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-muted-foreground",
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-foreground",
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-destructive",
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-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
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-foreground",
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-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none"
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-foreground",
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-destructive",
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-muted-foreground",
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-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2",
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-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors",
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-foreground hover:bg-primary/90 shadow-sm" : "bg-muted text-muted-foreground cursor-not-allowed"),
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
  ]