mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
PR #132 (color themes) only tokenized the high-visibility chrome. Inbox / contacts / pipelines / broadcasts / automations / auth / settings sub-forms still had ~150 hard-coded `violet-*` classes across ~32 files, so picking a non-violet theme felt half-broken — sidebar and header changed colour, but message bubbles, deal cards, and the broadcast wizard all stayed violet. This is the mechanical follow-up: replace every `violet-N(/N)?` with `primary(/N)?` so the theme picker themes the whole app. Two non-trivial swaps inside the pass: - `text-white` on a `bg-primary` button → `text-primary-foreground`. Emerald and Amber define a *dark* `--primary-foreground` because their primaries are bright; `text-white` over those would tank contrast. - `bg-primary hover:bg-primary` (hover identical to base — original code had this latent bug from `hover:bg-violet-500`) → adds `/90` on hover so the lift is visible. Test update: - `broadcast-status.test.ts` asserted shade numbers (`bg-X-N/10`). Relaxed the regex to also accept token form (`bg-X/10`) since brand-accent status badges now ride the active theme. Files touched: 49. Lint clean (existing warnings only), 173/173 tests, typecheck + build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useCallback, KeyboardEvent } from "react";
|
|
import { Send, LayoutTemplate } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { ReplyQuote } from "./reply-quote";
|
|
|
|
interface ReplyDraft {
|
|
/** Internal UUID of the message being replied to — sent back through onSend. */
|
|
id: string;
|
|
authorLabel: string;
|
|
preview: string;
|
|
}
|
|
|
|
interface MessageComposerProps {
|
|
conversationId: string;
|
|
sessionExpired: boolean;
|
|
onSend: (text: string, replyToId?: string) => void;
|
|
onOpenTemplates: () => void;
|
|
replyTo?: ReplyDraft | null;
|
|
onClearReply?: () => void;
|
|
}
|
|
|
|
export function MessageComposer({
|
|
conversationId,
|
|
sessionExpired,
|
|
onSend,
|
|
onOpenTemplates,
|
|
replyTo,
|
|
onClearReply,
|
|
}: MessageComposerProps) {
|
|
const [text, setText] = useState("");
|
|
const [sending, setSending] = useState(false);
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
const adjustHeight = useCallback(() => {
|
|
const el = textareaRef.current;
|
|
if (!el) return;
|
|
el.style.height = "auto";
|
|
// Max 4 lines (~96px)
|
|
el.style.height = `${Math.min(el.scrollHeight, 96)}px`;
|
|
}, []);
|
|
|
|
const handleSend = useCallback(async () => {
|
|
const trimmed = text.trim();
|
|
if (!trimmed || sending || sessionExpired) return;
|
|
|
|
setSending(true);
|
|
try {
|
|
onSend(trimmed, replyTo?.id);
|
|
setText("");
|
|
if (textareaRef.current) {
|
|
textareaRef.current.style.height = "auto";
|
|
}
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
}, [text, sending, sessionExpired, onSend, replyTo?.id]);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSend();
|
|
}
|
|
},
|
|
[handleSend]
|
|
);
|
|
|
|
const handleChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
setText(e.target.value);
|
|
adjustHeight();
|
|
},
|
|
[adjustHeight]
|
|
);
|
|
|
|
return (
|
|
<div className="border-t border-slate-800 bg-slate-900 p-3">
|
|
{replyTo && (
|
|
<div className="mb-2">
|
|
<ReplyQuote
|
|
authorLabel={replyTo.authorLabel}
|
|
preview={replyTo.preview}
|
|
onDismiss={onClearReply}
|
|
/>
|
|
</div>
|
|
)}
|
|
{sessionExpired && (
|
|
<div className="mb-2 flex items-center justify-between rounded-lg bg-amber-500/10 px-3 py-2">
|
|
<p className="text-xs text-amber-400">
|
|
24-hour session expired. Use a template to re-engage.
|
|
</p>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 text-xs text-amber-400 hover:text-amber-300"
|
|
onClick={onOpenTemplates}
|
|
>
|
|
<LayoutTemplate className="mr-1 h-3 w-3" />
|
|
Templates
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-9 w-9 shrink-0 p-0 text-slate-400 hover:text-white"
|
|
onClick={onOpenTemplates}
|
|
title="Send template"
|
|
>
|
|
<LayoutTemplate className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<textarea
|
|
ref={textareaRef}
|
|
value={text}
|
|
onChange={handleChange}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder={
|
|
sessionExpired
|
|
? "Session expired - use a template"
|
|
: "Type a message... (Shift+Enter for new line)"
|
|
}
|
|
disabled={sessionExpired}
|
|
rows={1}
|
|
className={cn(
|
|
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50",
|
|
sessionExpired && "cursor-not-allowed opacity-50"
|
|
)}
|
|
/>
|
|
|
|
<Button
|
|
size="sm"
|
|
className="h-9 w-9 shrink-0 bg-primary p-0 hover:bg-primary/90 disabled:opacity-40"
|
|
disabled={!text.trim() || sessionExpired || sending}
|
|
onClick={handleSend}
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Hint sits outside the flex row so its height doesn't push
|
|
`items-end` buttons below the textarea. Indented to line up
|
|
under the textarea left edge (w-9 button + gap-2 = 44px). */}
|
|
<p className="mt-1 pl-11 text-[10px] text-slate-600">
|
|
Type '/' for quick replies
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|