wacrm/src/components/inbox/reply-quote.tsx
Arnas Donauskas 1474657d03
fix(theme): light-mode polish from review pass
Self-review of the light/dark sweep surfaced neutral colors that lived
in raw inline-style / <style jsx> / gradient values (so the className
sweep correctly skipped them) and a few contrast/affordance nits:

- reply-quote: adapt to the outbound (primary) bubble via onPrimary so
  the quote/author stay legible on the violet fill in light mode; wire
  it from message-bubble.
- dashboard SVG chrome -> tokens: conversations-chart gridline/crosshair,
  pipeline-donut background ring, automation-builder dot-grid gradient.
- members-tab viewer chip: bg-card -> bg-transparent (outline) so it
  doesn't blend into a card surface in light mode.
- tag-manager / pipeline-settings color-swatch selection rings: raw
  slate/white -> var(--popover)/var(--foreground) so they flip.
- pipeline-board scrollbar (<style jsx>): slate rgb -> var tokens.
- activity-feed zebra stripe: bg-card/40 -> bg-muted/40 (survives light).
- forgot-password / import-modal: restore real hover (dead no-op hovers).
- themed-toaster: gate theme via useSyncExternalStore to avoid a
  hydration mismatch on sonner's theme attribute (no setState-in-effect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:28:22 +03:00

96 lines
3.1 KiB
TypeScript

"use client";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import type { Message } from "@/types";
interface ReplyQuoteProps {
/** Sender label of the quoted message: "You" for our own messages,
* contact name for customer-sent messages. Caller resolves this — the
* quote component doesn't see the parent Message. */
authorLabel: string;
/** Compact text preview. Falls back to a placeholder for media types. */
preview: string;
/** Present → renders the composer-chip variant with an X button. Absent →
* renders the embedded-in-bubble variant. */
onDismiss?: () => void;
/** True when embedded inside an outbound (primary-filled) bubble, so the
* quote must read against the primary surface rather than the neutral
* foreground — otherwise it goes low-contrast in light mode. */
onPrimary?: boolean;
}
export function ReplyQuote({
authorLabel,
preview,
onDismiss,
onPrimary = false,
}: ReplyQuoteProps) {
const isChip = !!onDismiss;
return (
<div
className={cn(
"flex items-start gap-2 border-l-2 px-2 py-1",
onPrimary ? "border-primary-foreground/50" : "border-primary",
isChip
? "rounded-md bg-muted/80"
: onPrimary
? "mb-1.5 rounded-md bg-primary-foreground/15"
: "mb-1.5 rounded-md bg-background/20",
)}
>
<div className="min-w-0 flex-1 overflow-hidden">
<div
className={cn(
"truncate text-[11px] font-medium",
onPrimary ? "text-primary-foreground" : "text-primary",
)}
>
{authorLabel}
</div>
{/* Wrap the preview instead of truncating to a single line.
* `truncate` (white-space: nowrap) forced the quote onto one
* impossibly-wide line and — because the parent flex chain
* lacked `min-w-0` at every step — pushed the entire inbox
* layout wider, shoving the contact sidebar off-screen.
* `break-words` also wraps long URLs that have no whitespace
* to break on. Issue #165. */}
<div className="whitespace-pre-wrap break-words text-xs text-foreground/80">
{preview}
</div>
</div>
{onDismiss && (
<button
type="button"
onClick={onDismiss}
aria-label="Cancel reply"
className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
>
<X className="h-3.5 w-3.5" />
</button>
)}
</div>
);
}
/** Build the one-line preview text shown inside a reply quote. */
export function buildReplyPreview(message: Message): string {
if (message.content_text) return message.content_text;
switch (message.content_type) {
case "image":
return "[Image]";
case "video":
return "[Video]";
case "audio":
return "[Audio]";
case "document":
return "[Document]";
case "location":
return "[Location]";
case "template":
return "[Template]";
default:
return "[Message]";
}
}