fix(frontend): render project watermark icons full-size above the composer with detail-preserving grayscale — #353 (QA verte)
Cause: the monochrome-first treatment from #351 flattened the emoji artwork through an alpha mask (transparent fill + zero-offset text-shadow), dropping internal details (the ghost lost its tongue and eyes), and the watermark box did not occupy the full space available above the chat composer. Fix: new .project-cell-illustration-area container (container-type: size) wrapping the scroll zone so the absolute watermark spans the whole region between the agent toolbar and the composer; glyph keeps the OS full-colour emoji artwork as source and is neutralised with a non-destructive CSS filter (grayscale(1) contrast(0.92)) so internal tones and details stay visible in black and white; glyph sized to min(100cqw - 8px, 100cqh - 12px); text-variation selector U+FE0E stripping removed (full emoji preserved) and project-theme tint plumbing (accessibleProjectAccent/onProjectThemeChanged) dropped as superseded by grayscale; empty label and overlay z-layering untouched. QA verte reconfirmée par Git: npm test -- CustomAgentChatView.test.tsx projectIcons.test.tsx 96/96, npm run typecheck OK, npm run build OK, npm test 1396/1396, git diff --check OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -15,7 +15,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
type ClipboardEvent,
|
||||
type CSSProperties,
|
||||
type KeyboardEvent,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type RefObject,
|
||||
@ -46,11 +45,6 @@ import {
|
||||
} from "@/features/projects/projectCliInteractionActivity";
|
||||
import { Button, EntryTimestamp, Spinner, cn, normalizeTimestamp } from "@/shared";
|
||||
import { onProjectIconChanged, ProjectIcon } from "@/features/projects/projectIcons";
|
||||
import {
|
||||
accessibleProjectAccent,
|
||||
onProjectThemeChanged,
|
||||
validThemeColor,
|
||||
} from "@/features/projects/projectTheme";
|
||||
import type {
|
||||
ChatAttachmentInput,
|
||||
ChatDraftScope,
|
||||
@ -1077,7 +1071,6 @@ export function CustomAgentChatView({
|
||||
const [stopTerminalOverride, setStopTerminalOverride] = useState(false);
|
||||
const [error, setError] = useState<ChatErrorState | null>(null);
|
||||
const [projectIcon, setProjectIcon] = useState<ProjectIconRef | null>(null);
|
||||
const [projectThemeColor, setProjectThemeColor] = useState<string | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const composerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const chatRootRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -1135,21 +1128,14 @@ export function CustomAgentChatView({
|
||||
void project?.readProjectSettings(projectId).then((settings) => {
|
||||
if (!cancelled) {
|
||||
setProjectIcon(settings.icon ?? null);
|
||||
setProjectThemeColor(validThemeColor(settings.themeColor));
|
||||
}
|
||||
}).catch(() => undefined);
|
||||
const unsubscribeIcon = onProjectIconChanged((change) => {
|
||||
if (change.projectId === projectId) setProjectIcon(change.icon);
|
||||
});
|
||||
const unsubscribeTheme = onProjectThemeChanged((change) => {
|
||||
if (change.projectId === projectId) {
|
||||
setProjectThemeColor(validThemeColor(change.themeColor));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribeIcon();
|
||||
unsubscribeTheme();
|
||||
};
|
||||
}, [project, projectId]);
|
||||
|
||||
@ -2789,28 +2775,6 @@ export function CustomAgentChatView({
|
||||
data-testid="custom-agent-chat-view"
|
||||
className="custom-agent-chat-view relative z-0 flex h-full min-h-0 flex-col overflow-hidden bg-surface text-content"
|
||||
>
|
||||
<div
|
||||
data-testid="project-icon-watermark"
|
||||
data-color-mode={projectThemeColor ? "project" : "monochrome"}
|
||||
aria-hidden="true"
|
||||
className="project-cell-illustration pointer-events-none absolute z-0 flex items-center justify-center overflow-hidden"
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
"--project-illustration-tint": projectThemeColor
|
||||
? accessibleProjectAccent(projectThemeColor)
|
||||
: "var(--color-muted, #9aa7b4)",
|
||||
} as CSSProperties}
|
||||
>
|
||||
<ProjectIcon
|
||||
icon={projectIcon}
|
||||
fallbackLabel={projectFallbackLabel}
|
||||
surface="watermark"
|
||||
size="100%"
|
||||
decorative
|
||||
className="project-cell-illustration__glyph"
|
||||
style={{ fontSize: "min(88cqw, 88cqh)" }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label={t("custom.actions")}
|
||||
@ -2978,76 +2942,93 @@ export function CustomAgentChatView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
data-testid="custom-agent-chat-scroll"
|
||||
className="relative z-10 flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
|
||||
onScroll={updateLatestVisibility}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<AnnouncementsPreview
|
||||
requester={agentId}
|
||||
fallbackActive={delegationActive}
|
||||
fallbackText={delegationFallbackText}
|
||||
<div className="project-cell-illustration-area relative z-10 flex min-h-0 flex-1 basis-0 overflow-hidden">
|
||||
<div
|
||||
data-testid="project-icon-watermark"
|
||||
aria-hidden="true"
|
||||
className="project-cell-illustration pointer-events-none absolute z-0 flex items-center justify-center overflow-hidden"
|
||||
style={{ pointerEvents: "none" }}
|
||||
>
|
||||
<ProjectIcon
|
||||
icon={projectIcon}
|
||||
fallbackLabel={projectFallbackLabel}
|
||||
surface="watermark"
|
||||
size="100%"
|
||||
decorative
|
||||
className="project-cell-illustration__glyph"
|
||||
/>
|
||||
</div>
|
||||
{opening && displayTurns.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Spinner size={14} />
|
||||
<span>
|
||||
{externalSessionId ? t("custom.resume") : t("custom.opening")}
|
||||
</span>
|
||||
</div>
|
||||
) : unavailableLinkedTranscript ? (
|
||||
<div className="m-auto flex max-w-md flex-col items-center gap-3 rounded-md border border-warning/40 bg-warning/10 p-4 text-center">
|
||||
<p className="text-sm font-medium text-content">
|
||||
{tr("La conversation liée n’a pas pu être restaurée.", "The linked conversation could not be restored.")}
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
{unavailableLinkedTranscript === "conversationNotFound"
|
||||
? tr("Cette conversation est introuvable dans le stockage disponible.", "This conversation cannot be found in available storage.")
|
||||
: tr("Sa transcription n’est plus disponible après la reprise.", "Its transcript is no longer available after restoration.")}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
disabled={opening}
|
||||
onClick={retryUnavailableTranscript}
|
||||
>
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={unlinkUnavailableConversation}>
|
||||
{tr("Délier la conversation", "Unlink conversation")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : displayTurns.length === 0 ? (
|
||||
<p className="project-cell-empty-label text-xs text-muted">
|
||||
{t("custom.empty")}
|
||||
</p>
|
||||
) : (
|
||||
displayTurns.map((turn, index) => (
|
||||
<ChatBubble
|
||||
key={index}
|
||||
turn={turn}
|
||||
requesterName={agentName}
|
||||
sessionLabel={sessionLabel}
|
||||
expandedProgressKeys={expandedProgressKeys}
|
||||
onToggleProgressExpanded={(key) =>
|
||||
setExpandedProgressKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
})
|
||||
}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
data-testid="custom-agent-chat-scroll"
|
||||
className="relative z-10 flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
|
||||
onScroll={updateLatestVisibility}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<AnnouncementsPreview
|
||||
requester={agentId}
|
||||
fallbackActive={delegationActive}
|
||||
fallbackText={delegationFallbackText}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{opening && displayTurns.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Spinner size={14} />
|
||||
<span>
|
||||
{externalSessionId ? t("custom.resume") : t("custom.opening")}
|
||||
</span>
|
||||
</div>
|
||||
) : unavailableLinkedTranscript ? (
|
||||
<div className="m-auto flex max-w-md flex-col items-center gap-3 rounded-md border border-warning/40 bg-warning/10 p-4 text-center">
|
||||
<p className="text-sm font-medium text-content">
|
||||
{tr("La conversation liée n’a pas pu être restaurée.", "The linked conversation could not be restored.")}
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
{unavailableLinkedTranscript === "conversationNotFound"
|
||||
? tr("Cette conversation est introuvable dans le stockage disponible.", "This conversation cannot be found in available storage.")
|
||||
: tr("Sa transcription n’est plus disponible après la reprise.", "Its transcript is no longer available after restoration.")}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
disabled={opening}
|
||||
onClick={retryUnavailableTranscript}
|
||||
>
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={unlinkUnavailableConversation}>
|
||||
{tr("Délier la conversation", "Unlink conversation")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : displayTurns.length === 0 ? (
|
||||
<p className="project-cell-empty-label text-xs text-muted">
|
||||
{t("custom.empty")}
|
||||
</p>
|
||||
) : (
|
||||
displayTurns.map((turn, index) => (
|
||||
<ChatBubble
|
||||
key={index}
|
||||
turn={turn}
|
||||
requesterName={agentName}
|
||||
sessionLabel={sessionLabel}
|
||||
expandedProgressKeys={expandedProgressKeys}
|
||||
onToggleProgressExpanded={(key) =>
|
||||
setExpandedProgressKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
})
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user