mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
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>
This commit is contained in:
parent
4aa329b527
commit
1474657d03
@ -119,7 +119,7 @@ export default function ForgotPasswordPage() {
|
||||
|
||||
<Link
|
||||
href="/login"
|
||||
className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-muted-foreground"
|
||||
className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to sign in
|
||||
|
||||
@ -587,7 +587,7 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="relative flex-1 overflow-y-auto">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle,#1e293b_1px,transparent_1px)] [background-size:20px_20px] pointer-events-none" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle,var(--border)_1px,transparent_1px)] [background-size:20px_20px] pointer-events-none" />
|
||||
<div className="relative mx-auto flex max-w-2xl flex-col items-center gap-0 px-4 py-10">
|
||||
<ResourcesProvider>
|
||||
<TriggerCard
|
||||
|
||||
@ -463,7 +463,7 @@ export function ImportModal({
|
||||
) : (
|
||||
<>
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted/80 ring-1 ring-border/80 transition-colors group-hover:bg-muted">
|
||||
<Upload className="size-5 text-muted-foreground group-hover:text-muted-foreground" />
|
||||
<Upload className="size-5 text-muted-foreground group-hover:text-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Click to choose a CSV file
|
||||
|
||||
@ -85,9 +85,10 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
||||
{visible.map((it, i) => {
|
||||
const theme = KIND_THEME[it.kind]
|
||||
const Icon = theme.icon
|
||||
// Alternating row background for scanability — dark-theme
|
||||
// translation of the spec's white / #f9fafb stripes.
|
||||
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-card/40'
|
||||
// Alternating row background for scanability. bg-muted/40
|
||||
// keeps the stripe visible in both light and dark modes
|
||||
// (bg-card/40 vanishes against a white card surface in light).
|
||||
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-muted/40'
|
||||
const row = (
|
||||
<div className="flex items-center gap-3 px-5 py-2.5">
|
||||
<span
|
||||
|
||||
@ -207,7 +207,7 @@ function LineSvg({
|
||||
x2={VB_W - PADDING.right}
|
||||
y1={y}
|
||||
y2={y}
|
||||
stroke="rgb(30 41 59)"
|
||||
stroke="var(--border)"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<text
|
||||
@ -265,7 +265,7 @@ function LineSvg({
|
||||
x2={hoverX}
|
||||
y1={PADDING.top}
|
||||
y2={PADDING.top + chartH}
|
||||
stroke="rgb(71 85 105)"
|
||||
stroke="var(--muted-foreground)"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<circle cx={hoverX} cy={yFor(data[hover.idx].incoming)} r={3.5} fill="#3b82f6" />
|
||||
|
||||
@ -98,7 +98,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
|
||||
<div className="flex items-center justify-center">
|
||||
<svg viewBox={`0 0 ${size} ${size}`} className="h-48 w-48" role="img" aria-label="Pipeline value by stage">
|
||||
{/* background ring */}
|
||||
<circle cx={cx} cy={cy} r={r} fill="none" stroke="rgb(30 41 59)" strokeWidth={ringWidth} />
|
||||
<circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--muted)" strokeWidth={ringWidth} />
|
||||
{segments.map((seg) => (
|
||||
<path
|
||||
key={seg.id}
|
||||
|
||||
@ -269,7 +269,11 @@ export function MessageBubble({
|
||||
)}
|
||||
>
|
||||
{reply && (
|
||||
<ReplyQuote authorLabel={reply.authorLabel} preview={reply.preview} />
|
||||
<ReplyQuote
|
||||
authorLabel={reply.authorLabel}
|
||||
preview={reply.preview}
|
||||
onPrimary={isAgent}
|
||||
/>
|
||||
)}
|
||||
<MessageContent message={message} />
|
||||
<div
|
||||
|
||||
@ -14,25 +14,38 @@ interface ReplyQuoteProps {
|
||||
/** 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 border-primary px-2 py-1",
|
||||
"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"
|
||||
: "mb-1.5 rounded-md bg-background/20",
|
||||
: 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="truncate text-[11px] font-medium text-primary">
|
||||
<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.
|
||||
|
||||
@ -164,7 +164,7 @@ export function PipelineBoard({
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.pipeline-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(51 65 85) transparent;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
.pipeline-scroll::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
@ -173,11 +173,11 @@ export function PipelineBoard({
|
||||
background: transparent;
|
||||
}
|
||||
.pipeline-scroll::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(51 65 85);
|
||||
background-color: var(--border);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.pipeline-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgb(71 85 105);
|
||||
background-color: var(--muted-foreground);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
@ -292,7 +292,9 @@ export function PipelineSettings({
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderColor:
|
||||
newStageColor === color ? "white" : "transparent",
|
||||
newStageColor === color
|
||||
? "var(--foreground)"
|
||||
: "transparent",
|
||||
}}
|
||||
aria-label={`Pick color ${color}`}
|
||||
/>
|
||||
@ -450,7 +452,8 @@ function ColorSwatch({
|
||||
className="h-5 w-5 rounded-full border-2 transition-transform hover:scale-110"
|
||||
style={{
|
||||
backgroundColor: c,
|
||||
borderColor: c === value ? "white" : "transparent",
|
||||
borderColor:
|
||||
c === value ? "var(--foreground)" : "transparent",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -115,7 +115,9 @@ const ROLE_CHIP: Record<
|
||||
viewer: {
|
||||
icon: UserIcon,
|
||||
label: 'Viewer',
|
||||
className: 'border-border bg-card text-muted-foreground',
|
||||
// Outline-only so it stays quieter than the filled Agent chip in
|
||||
// both modes — bg-card would blend into a card surface in light mode.
|
||||
className: 'border-border bg-transparent text-muted-foreground',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -241,7 +241,7 @@ export function TagManager() {
|
||||
className="relative size-8 rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-popover"
|
||||
style={{
|
||||
backgroundColor: color.value,
|
||||
boxShadow: selectedColor === color.value ? `0 0 0 2px rgb(15 23 42), 0 0 0 4px ${color.value}` : 'none',
|
||||
boxShadow: selectedColor === color.value ? `0 0 0 2px var(--popover), 0 0 0 4px ${color.value}` : 'none',
|
||||
}}
|
||||
title={color.name}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
import { useTheme } from "@/hooks/use-theme";
|
||||
import { DEFAULT_MODE } from "@/lib/themes";
|
||||
|
||||
// Returns false during SSR and the first hydration render, true after —
|
||||
// the sanctioned (warning-free, no setState-in-effect) way to diverge
|
||||
// server vs client. Lets us match the server-rendered default on first
|
||||
// paint, then adopt the real mode.
|
||||
const noopSubscribe = () => () => {};
|
||||
function useIsClient() {
|
||||
return useSyncExternalStore(
|
||||
noopSubscribe,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toaster wrapper that tracks the active light/dark mode.
|
||||
@ -11,12 +26,18 @@ import { useTheme } from "@/hooks/use-theme";
|
||||
* current mode and hand it to sonner. Colors are driven off the same
|
||||
* CSS tokens as the rest of the app, so a toast looks at home in
|
||||
* either mode without a second palette to maintain.
|
||||
*
|
||||
* The theme is gated behind `useIsClient`: the server renders
|
||||
* DEFAULT_MODE, so first client paint must too, otherwise a light-mode
|
||||
* user hydrates with a different sonner `theme` attribute than the
|
||||
* server emitted and React logs a hydration mismatch.
|
||||
*/
|
||||
export function ThemedToaster() {
|
||||
const { mode } = useTheme();
|
||||
const isClient = useIsClient();
|
||||
return (
|
||||
<Toaster
|
||||
theme={mode}
|
||||
theme={isClient ? mode : DEFAULT_MODE}
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user