Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function SidebarControl() {
"pointer-events-auto",
isSidebarVisible &&
stageBackdropVariant &&
"[:hover,[data-pressed]]:bg-white/15 focus-visible:ring-white/90 focus-visible:ring-offset-blue-700 [&_svg]:stroke-white/90! [&_svg]:opacity-100! [&_svg]:hover:stroke-white!",
"[:hover,[data-pressed]]:bg-white/15 focus-visible:ring-white/90 focus-visible:ring-offset-[var(--stage-art-bottom)] [&_svg]:stroke-white/90! [&_svg]:opacity-100! [&_svg]:hover:stroke-white!",
)}
aria-label="Toggle main sidebar"
/>
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/components/SidebarStageBackdrop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
resolveEnvironmentIdentificationPillLabel,
resolveSidebarStageBackdropVariant,
StageBackdropArt,
StageBackdropButtonArt,
} from "./SidebarStageBackdrop";

describe("SidebarStageBackdrop", () => {
Expand Down Expand Up @@ -37,4 +38,26 @@ describe("SidebarStageBackdrop", () => {
expect(new Set(ids).size).toBe(ids.length);
},
);

it("paints each artwork variant with theme-owned color tokens", () => {
const nightlyMarkup = renderToStaticMarkup(<StageBackdropArt variant="nightly" />);
const devMarkup = renderToStaticMarkup(<StageBackdropArt variant="dev" />);

expect(nightlyMarkup).toContain("var(--stage-night-bottom)");
expect(nightlyMarkup).toContain("var(--stage-night-line)");
expect(devMarkup).toContain("var(--stage-art-bottom)");
expect(devMarkup).toContain("var(--stage-art-line)");
expect(nightlyMarkup).not.toMatch(/#[0-9a-f]{3,8}/i);
expect(devMarkup).not.toMatch(/#[0-9a-f]{3,8}/i);
});

it.each([
["nightly", "96 0 8192 96"],
["dev", "64 0 8192 96"],
] as const)("uses the compact %s crop inside the send button", (variant, viewBox) => {
const markup = renderToStaticMarkup(<StageBackdropButtonArt variant={variant} />);

expect(markup).toContain(`viewBox="${viewBox}"`);
expect(markup).toContain(`stage-${variant === "dev" ? "blueprint" : "nightly"}`);
});
});
119 changes: 84 additions & 35 deletions apps/web/src/components/SidebarStageBackdrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVar
return variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />;
}

export function StageBackdropButtonArt({ variant }: { variant: SidebarStageBackdropVariant }) {
return variant === "nightly" ? <NightlySkyArt compact /> : <DevBlueprintArt compact />;
}

const NIGHTLY_STARS: ReadonlyArray<{
cx: number;
cy: number;
Expand Down Expand Up @@ -93,7 +97,7 @@ const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [
{ x: 246, y: 26 },
];

function NightlySkyArt() {
function NightlySkyArt({ compact = false }: { compact?: boolean }) {
const idPrefix = useId().replaceAll(":", "");
const skyId = `${idPrefix}-stage-night-sky`;
const glowId = `${idPrefix}-stage-night-glow`;
Expand All @@ -104,10 +108,10 @@ function NightlySkyArt() {

return (
<svg
className="h-full w-full"
className="stage-art stage-nightly h-full w-full"
fill="none"
preserveAspectRatio="xMinYMin slice"
viewBox={STAGE_BACKDROP_VIEW_BOX}
viewBox={compact ? "96 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
Expand All @@ -120,9 +124,9 @@ function NightlySkyArt() {
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
>
<stop stopColor="#07152F" />
<stop offset="0.5" stopColor="#151443" />
<stop offset="1" stopColor="#32155B" />
<stop style={{ stopColor: "var(--stage-night-bottom)" }} />
<stop offset="0.5" style={{ stopColor: "var(--stage-night-mid)" }} />
<stop offset="1" style={{ stopColor: "var(--stage-night-top)" }} />
</linearGradient>
<radialGradient
id={glowId}
Expand All @@ -132,20 +136,28 @@ function NightlySkyArt() {
gradientTransform="translate(216 18) rotate(137) scale(120 84)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#5165D8" stopOpacity="0.4" />
<stop offset="0.5" stopColor="#283075" stopOpacity="0.16" />
<stop offset="1" stopColor="#111635" stopOpacity="0" />
<stop style={{ stopColor: "var(--stage-night-highlight)" }} stopOpacity="0.4" />
<stop
offset="0.5"
style={{ stopColor: "var(--stage-night-secondary)" }}
stopOpacity="0.16"
/>
<stop offset="1" style={{ stopColor: "var(--stage-night-bottom)" }} stopOpacity="0" />
</radialGradient>
<linearGradient id={cloudId} x1="0" y1="60" x2="288" y2="96" gradientUnits="userSpaceOnUse">
<stop stopColor="#4EA4FF" stopOpacity="0.5" />
<stop offset="0.52" stopColor="#696FEA" stopOpacity="0.62" />
<stop offset="1" stopColor="#A85BEA" stopOpacity="0.5" />
<stop style={{ stopColor: "var(--stage-night-highlight)" }} stopOpacity="0.5" />
<stop
offset="0.52"
style={{ stopColor: "var(--stage-night-secondary)" }}
stopOpacity="0.62"
/>
<stop offset="1" style={{ stopColor: "var(--stage-night-tertiary)" }} stopOpacity="0.5" />
</linearGradient>
<filter id={softId} x="-24" y="-24" width="336" height="144" filterUnits="userSpaceOnUse">
<feGaussianBlur stdDeviation="4" />
</filter>
<pattern id={starsId} width="288" height="96" patternUnits="userSpaceOnUse">
<g fill="#E4EAFF">
<g style={{ fill: "var(--stage-night-line)" }}>
{NIGHTLY_STARS.map((star) => (
<circle
key={`${star.cx}-${star.cy}`}
Expand All @@ -156,7 +168,12 @@ function NightlySkyArt() {
/>
))}
</g>
<g stroke="#C8D7FF" strokeLinecap="round" strokeOpacity="0.7" strokeWidth="0.6">
<g
style={{ stroke: "var(--stage-night-line)" }}
strokeLinecap="round"
strokeOpacity="0.7"
strokeWidth="0.6"
>
{NIGHTLY_SPARKLES.map((sparkle) => (
<g key={`${sparkle.x}-${sparkle.y}`}>
<path d={`M${sparkle.x - 1.5} ${sparkle.y}H${sparkle.x + 1.5}`} />
Expand Down Expand Up @@ -191,7 +208,7 @@ function NightlySkyArt() {
);
}

function DevBlueprintArt() {
function DevBlueprintArt({ compact = false }: { compact?: boolean }) {
const idPrefix = useId().replaceAll(":", "");
const paperId = `${idPrefix}-stage-bp-paper`;
const glowId = `${idPrefix}-stage-bp-glow`;
Expand All @@ -205,10 +222,10 @@ function DevBlueprintArt() {

return (
<svg
className="stage-blueprint h-full w-full"
className="stage-art stage-blueprint h-full w-full"
fill="none"
preserveAspectRatio="xMinYMin slice"
viewBox={STAGE_BACKDROP_VIEW_BOX}
viewBox={compact ? "64 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
Expand All @@ -221,9 +238,9 @@ function DevBlueprintArt() {
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
>
<stop style={{ stopColor: "var(--stage-bp-bottom)" }} />
<stop offset="0.5" style={{ stopColor: "var(--stage-bp-mid)" }} />
<stop offset="1" style={{ stopColor: "var(--stage-bp-top)" }} />
<stop style={{ stopColor: "var(--stage-art-bottom)" }} />
<stop offset="0.5" style={{ stopColor: "var(--stage-art-mid)" }} />
<stop offset="1" style={{ stopColor: "var(--stage-art-top)" }} />
</linearGradient>
<radialGradient
id={glowId}
Expand All @@ -233,9 +250,13 @@ function DevBlueprintArt() {
gradientTransform="translate(216 14) rotate(137) scale(120 84)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#D4F6FF" stopOpacity="0.4" />
<stop offset="0.52" stopColor="#65C8FF" stopOpacity="0.16" />
<stop offset="1" stopColor="#276AF1" stopOpacity="0" />
<stop style={{ stopColor: "var(--stage-art-highlight)" }} stopOpacity="0.4" />
<stop
offset="0.52"
style={{ stopColor: "var(--stage-art-secondary)" }}
stopOpacity="0.16"
/>
<stop offset="1" style={{ stopColor: "var(--stage-art-bottom)" }} stopOpacity="0" />
</radialGradient>
<radialGradient
id={celesteGlowId}
Expand All @@ -245,9 +266,13 @@ function DevBlueprintArt() {
gradientTransform="translate(474 44) rotate(166) scale(156 92)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#D2FFFF" stopOpacity="0.34" />
<stop offset="0.5" stopColor="#48DCF5" stopOpacity="0.18" />
<stop offset="1" stopColor="#277EF1" stopOpacity="0" />
<stop style={{ stopColor: "var(--stage-art-highlight)" }} stopOpacity="0.34" />
<stop
offset="0.5"
style={{ stopColor: "var(--stage-art-secondary)" }}
stopOpacity="0.18"
/>
<stop offset="1" style={{ stopColor: "var(--stage-art-bottom)" }} stopOpacity="0" />
</radialGradient>
<radialGradient
id={violetGlowId}
Expand All @@ -257,20 +282,34 @@ function DevBlueprintArt() {
gradientTransform="translate(704 18) rotate(145) scale(132 88)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#D9D8FF" stopOpacity="0.3" />
<stop offset="0.52" stopColor="#7C8BFF" stopOpacity="0.14" />
<stop offset="1" stopColor="#3155DF" stopOpacity="0" />
<stop style={{ stopColor: "var(--stage-art-highlight)" }} stopOpacity="0.3" />
<stop
offset="0.52"
style={{ stopColor: "var(--stage-art-tertiary)" }}
stopOpacity="0.14"
/>
<stop offset="1" style={{ stopColor: "var(--stage-art-bottom)" }} stopOpacity="0" />
</radialGradient>
<pattern id={minorGridId} width="8" height="8" patternUnits="userSpaceOnUse">
<path d="M8 0H0V8" stroke="#EAF6FF" strokeOpacity="0.14" strokeWidth="0.5" />
<path
d="M8 0H0V8"
style={{ stroke: "var(--stage-art-line)" }}
strokeOpacity="0.14"
strokeWidth="0.5"
/>
</pattern>
<pattern id={majorGridId} width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M32 0H0V32" stroke="#EAF6FF" strokeOpacity="0.26" strokeWidth="0.6" />
<path
d="M32 0H0V32"
style={{ stroke: "var(--stage-art-line)" }}
strokeOpacity="0.26"
strokeWidth="0.6"
/>
</pattern>
<pattern id={rulerId} width="32" height="6" patternUnits="userSpaceOnUse">
<path
d="M4 0V2.5M12 0V2.5M20 0V4M28 0V2.5"
stroke="#DDF7FF"
style={{ stroke: "var(--stage-art-line)" }}
strokeOpacity="0.5"
strokeWidth="0.5"
/>
Expand All @@ -281,7 +320,12 @@ function DevBlueprintArt() {
<rect width="768" height="96" fill={`url(#${violetGlowId})`} />
</pattern>
<pattern id={annotationsId} width="768" height="96" patternUnits="userSpaceOnUse">
<g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.6" strokeWidth="0.7">
<g
style={{ stroke: "var(--stage-art-line)" }}
strokeLinecap="round"
strokeOpacity="0.6"
strokeWidth="0.7"
>
<path d="M180 64H264" strokeDasharray="5 4" />
<path d="M180 61V67M264 61V67" />
<path d="M276 10V44" strokeDasharray="4 4" strokeOpacity="0.5" />
Expand All @@ -294,7 +338,12 @@ function DevBlueprintArt() {
<path d="M590 67V73M724 67V73" strokeOpacity="0.55" />
</g>

<g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.55" strokeWidth="0.6">
<g
style={{ stroke: "var(--stage-art-line)" }}
strokeLinecap="round"
strokeOpacity="0.55"
strokeWidth="0.6"
>
<g>
<path d="M34 60L38 64M38 60L34 64" />
</g>
Expand All @@ -318,7 +367,7 @@ function DevBlueprintArt() {
</g>
</g>

<g stroke="#DDF7FF" strokeOpacity="0.35" strokeWidth="0.6">
<g style={{ stroke: "var(--stage-art-line)" }} strokeOpacity="0.35" strokeWidth="0.6">
<circle cx="196" cy="38" r="13" strokeDasharray="3.5 4" />
<path d="M196 33V43M191 38H201" strokeOpacity="0.6" strokeWidth="0.4" />
<circle cx="414" cy="64" r="10" strokeDasharray="2.5 3.5" />
Expand Down
59 changes: 55 additions & 4 deletions apps/web/src/components/chat/ComposerPrimaryActions.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vite-plus/test";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";

const stageArtworkState = vi.hoisted(() => ({
mode: "none" as "artwork" | "none",
variant: null as "nightly" | "dev" | null,
}));

vi.mock("~/hooks/useSettings", () => ({
useEnvironmentIdentificationMode: () => "none",
useEnvironmentIdentificationMode: () => stageArtworkState.mode,
}));
vi.mock("../SidebarStageBackdrop", () => ({
StageBackdropButtonArt: () => null,
useSidebarStageBackdropVariant: () => null,
StageBackdropButtonArt: ({ variant }: { variant: string }) => `stage-${variant}`,
useSidebarStageBackdropVariant: (enabled = true) => (enabled ? stageArtworkState.variant : null),
}));

import { ComposerPrimaryActions, formatPendingPrimaryActionLabel } from "./ComposerPrimaryActions";
Expand Down Expand Up @@ -60,6 +65,32 @@ function renderStandaloneStop() {
);
}

function renderSendButton() {
return renderToStaticMarkup(
createElement(ComposerPrimaryActions, {
compact: true,
pendingAction: null,
isRunning: false,
showPlanFollowUpPrompt: false,
promptHasText: true,
isSendBusy: false,
sendDisabledReason: null,
isConnecting: false,
isEnvironmentUnavailable: false,
isPreparingWorktree: false,
hasSendableContent: true,
onPreviousPendingQuestion: () => {},
onInterrupt: () => {},
onImplementPlanInNewThread: () => {},
}),
);
}

afterEach(() => {
stageArtworkState.mode = "none";
stageArtworkState.variant = null;
});

describe("formatPendingPrimaryActionLabel", () => {
it("returns 'Submitting...' while responding", () => {
expect(
Expand Down Expand Up @@ -164,4 +195,24 @@ describe("ComposerPrimaryActions", () => {
expect(renderStandaloneStop()).toContain("size-8 sm:h-8 sm:w-8");
expect(renderStandaloneStop()).not.toContain("sm:size-7");
});

it("renders stage artwork inside the send button when artwork identification is active", () => {
stageArtworkState.mode = "artwork";
stageArtworkState.variant = "nightly";

const markup = renderSendButton();

expect(markup).toContain("stage-nightly");
expect(markup).toContain("bg-transparent text-white");
expect(markup).not.toContain("bg-message-action text-message-action-foreground");
});

it("keeps the normal send-button fill when artwork identification is inactive", () => {
stageArtworkState.variant = "nightly";

const markup = renderSendButton();

expect(markup).not.toContain("stage-nightly");
expect(markup).toContain("bg-message-action text-message-action-foreground");
});
});
Loading
Loading