feat(studio): graphite colour system with a light theme that passes contrast (#4836)

* feat(studio): graphite colour system with a light theme that passes contrast

Studio's tokens take the Graphite values: a lifted dark, a light set behind
data-theme="paper", the player dark in both. Every text role clears 4.5:1 and
every icon and ring 3:1 on the fills it sits on, held by the contrast ratchet.

* fix(studio): every studio surface follows the light theme and passes contrast

The dock no longer inherits dockview's forced dark scheme, popovers sit on the
raised surface, stock white, red and amber text map to roles, the keyframe
diamonds and track headers use roles, and the contrast ratchet measures every
pair in both themes (150 pairs).

* fix(studio): stock white, red and amber stay colours; studio text and fills use roles

Round-two review: white, red and amber are colour content as well as text, so
they keep Tailwind's values and hosts keep their palettes. Text, toasts, the
feedback card, the FX popover, destructive buttons, category labels and the dock
take Graphite roles instead; the contrast gate rejects an unparseable colour.

* fix(studio): every light-theme ink, icon, dot and focus ring holds its contrast

Stock white, red, amber and file-type hex left in Studio chrome now use roles or a light-dark pair; the file tree icons carry a contrast test.

* chore(studio): drop two comments the names already carry

* fix(studio): red marks, teal outlines and colour curves hold contrast in both themes

Danger graphics use the danger ink, file tree outlines use the accent ink, the colour-curve well stays dark like the scopes, and six timeline token pins come back.

* fix(studio): invalid readouts, the curve focus ring and editor dots stay distinct in both themes

The invalid readout border is dashed as well as red, the curve graph's ring sits inside its dark well, the radius dots and ease handle centre follow the theme.

* refactor(studio): ease and radius editors use colour classes; the dashed invalid border has a test

Simplify pass: the ease editor's inline accent styles become stroke and fill classes, the radius dot class is computed once, the ratio check is one expression, and the alias table is named for what it holds.
This commit is contained in:
Miguel Ángel
2026-10-01 07:21:05 -07:00
committed by GitHub
parent 3d8325e7a4
commit 658f22bb7f
123 changed files with 1111 additions and 790 deletions
@@ -250,7 +250,7 @@ export const CaptionAnimationPanel = memo(function CaptionAnimationPanel() {
<div className="flex flex-col h-full min-h-0">
{gated && (
<div className="shrink-0 mx-3 mt-2 px-2 py-1.5 rounded-sm border border-amber-500/30 bg-amber-500/10">
<p className="text-2xs text-amber-300/90 leading-snug">
<p className="text-2xs text-warning-ink leading-snug">
Animation editing isn&apos;t applied to playback or saved yet, so these controls are
disabled.
</p>
@@ -292,7 +292,7 @@ export const CaptionAnimationPanel = memo(function CaptionAnimationPanel() {
onClick={handleApplyToAll}
disabled={gated}
title={gated ? "Disabled until animation editing is applied to playback" : undefined}
className="w-full py-1.5 rounded-sm border border-neutral-700 text-2xs text-neutral-300 hover:border-studio-accent/50 hover:text-studio-accent transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-neutral-700 disabled:hover:text-neutral-300"
className="w-full py-1.5 rounded-sm border border-neutral-700 text-2xs text-neutral-300 hover:border-studio-accent/50 hover:text-accent-ink transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-neutral-700 disabled:hover:text-neutral-300"
>
Apply to all groups
</button>
@@ -227,7 +227,7 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
className={[
"flex-1 py-0.5 rounded-sm text-2xs font-medium transition-colors",
activeTab === "style"
? "bg-studio-accent/20 text-studio-accent border border-studio-accent/50"
? "bg-studio-accent/20 text-accent-ink border border-studio-accent/50"
: "text-neutral-500 border border-neutral-800 hover:text-neutral-300 hover:border-neutral-600",
].join(" ")}
>
@@ -241,7 +241,7 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
className={[
"flex-1 py-0.5 rounded-sm text-2xs font-medium transition-colors",
activeTab === "animation"
? "bg-studio-accent/20 text-studio-accent border border-studio-accent/50"
? "bg-studio-accent/20 text-accent-ink border border-studio-accent/50"
: "text-neutral-500 border border-neutral-800 hover:text-neutral-300 hover:border-neutral-600",
].join(" ")}
>
@@ -109,8 +109,8 @@ export const CaptionTimeline = memo(function CaptionTimeline({
tabIndex={0}
aria-label={`Caption word "${seg.text}"`}
aria-pressed={isSelected}
className={`absolute top-1 bottom-1 rounded flex items-center overflow-hidden cursor-pointer focus-visible:ring-1 focus-visible:ring-white outline-hidden${
isSelected ? " ring-1 ring-white/50 z-10" : ""
className={`absolute top-1 bottom-1 rounded flex items-center overflow-hidden cursor-pointer focus-visible:ring-1 focus-visible:ring-text-0 outline-hidden${
isSelected ? " ring-1 ring-text-0/50 z-10" : ""
}`}
style={{
left,
@@ -135,7 +135,7 @@ export const CaptionTimeline = memo(function CaptionTimeline({
{/* Playhead — correlates blocks with the current frame */}
<div
className="absolute top-0 bottom-0 w-px bg-white/70 pointer-events-none z-20"
className="absolute top-0 bottom-0 w-px bg-text-0/70 pointer-events-none z-20"
style={{ left: playheadLeft }}
aria-hidden="true"
/>
@@ -138,7 +138,7 @@ export function AskAgentModal({
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to copy
</span>
<button
className="px-4 py-1.5 rounded-lg bg-studio-accent/90 text-xs font-medium text-neutral-950 hover:bg-studio-accent disabled:opacity-40 disabled:cursor-not-allowed"
className="px-4 py-1.5 rounded-lg bg-accent text-xs font-medium text-on-accent hover:bg-accent-hover disabled:opacity-40 disabled:cursor-not-allowed"
disabled={!value.trim()}
onClick={handleSubmit}
>
+35 -35
View File
@@ -75,11 +75,11 @@ export function LintModal({
<div className="flex items-center gap-3">
{hasIssues ? (
<div className="w-8 h-8 rounded-full bg-red-500/10 flex items-center justify-center">
<WarningIcon size={18} className="text-red-400" weight="fill" />
<WarningIcon size={18} className="text-danger-ink" weight="fill" />
</div>
) : (
<div className="w-8 h-8 rounded-full bg-studio-accent/10 flex items-center justify-center">
<CheckCircleIcon size={18} className="text-studio-accent" weight="fill" />
<CheckCircleIcon size={18} className="text-accent-ink" weight="fill" />
</div>
)}
<div>
@@ -107,10 +107,10 @@ export function LintModal({
onClick={handleCopyToAgent}
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors active:scale-[0.98] ${
copied
? "bg-green-600 text-white"
? "bg-accent text-on-accent"
: copyFailed
? "bg-red-600 text-white"
: "bg-studio-accent hover:bg-studio-accent/80 text-white"
? "bg-danger text-on-danger"
: "bg-accent hover:bg-accent-hover text-on-accent"
}`}
>
{copied
@@ -128,41 +128,41 @@ export function LintModal({
</div>
)}
{errors.map((f, i) => (
<div key={`e-${i}`} className="py-3 border-b border-neutral-800/50 last:border-0">
<div className="flex items-start gap-2">
<WarningIcon size={14} className="text-red-400 shrink-0 mt-0.5" weight="fill" />
<div className="min-w-0">
<p className="text-sm text-neutral-200">{f.message}</p>
{f.file && <p className="text-xs text-neutral-600 font-mono mt-0.5">{f.file}</p>}
{f.fixHint && (
<div className="flex items-start gap-1 mt-1.5">
<CaretRightIcon size={10} className="text-studio-accent shrink-0 mt-0.5" />
<p className="text-xs text-studio-accent">{f.fixHint}</p>
</div>
)}
</div>
</div>
</div>
<LintFindingRow key={`e-${i}`} finding={f} tone="error" />
))}
{warnings.map((f, i) => (
<div key={`w-${i}`} className="py-3 border-b border-neutral-800/50 last:border-0">
<div className="flex items-start gap-2">
<WarningIcon size={14} className="text-amber-400 shrink-0 mt-0.5" />
<div className="min-w-0">
<p className="text-sm text-neutral-300">{f.message}</p>
{f.file && <p className="text-xs text-neutral-600 font-mono mt-0.5">{f.file}</p>}
{f.fixHint && (
<div className="flex items-start gap-1 mt-1.5">
<CaretRightIcon size={10} className="text-studio-accent shrink-0 mt-0.5" />
<p className="text-xs text-studio-accent">{f.fixHint}</p>
</div>
)}
</div>
</div>
</div>
<LintFindingRow key={`w-${i}`} finding={f} tone="warning" />
))}
</div>
</div>
</div>
);
}
function LintFindingRow({ finding, tone }: { finding: LintFinding; tone: "error" | "warning" }) {
return (
<div className="py-3 border-b border-border last:border-0">
<div className="flex items-start gap-2">
<WarningIcon
size={14}
className={`${tone === "error" ? "text-danger-ink" : "text-warning-ink"} shrink-0 mt-0.5`}
weight={tone === "error" ? "fill" : undefined}
/>
<div className="min-w-0">
<p className={`text-sm ${tone === "error" ? "text-text-0" : "text-text-2"}`}>
{finding.message}
</p>
{finding.file && (
<p className="text-xs text-text-muted font-mono mt-0.5">{finding.file}</p>
)}
{finding.fixHint && (
<div className="flex items-start gap-1 mt-1.5">
<CaretRightIcon size={10} className="text-accent-ink shrink-0 mt-0.5" />
<p className="text-xs text-accent-ink">{finding.fixHint}</p>
</div>
)}
</div>
</div>
</div>
);
}
@@ -39,7 +39,7 @@ export class StudioErrorBoundary extends Component<Props, State> {
<div className="mt-2 flex items-center gap-2">
<button
onClick={() => this.setState({ error: null })}
className="rounded-md bg-studio-accent px-5 py-2 text-sm font-medium text-neutral-950 transition-[filter] hover:brightness-110 active:scale-[0.98]"
className="rounded-md bg-accent px-5 py-2 text-sm font-medium text-on-accent transition-colors hover:bg-accent-hover active:scale-[0.98]"
>
Try again
</button>
@@ -139,16 +139,16 @@ it("shows Inspector pressed and filled only when on", () => {
const host = mount({ inspectorButtonActive: true });
const on = query(host, '[aria-label="Inspector"]');
expect(on.getAttribute("aria-pressed")).toBe("true");
expect(hasToken(on.className, "text-accent")).toBe(true);
expect(hasToken(on.className, "bg-hover")).toBe(true);
expect(hasToken(on.className, "text-accent-ink")).toBe(true);
expect(hasToken(on.className, "bg-on")).toBe(true);
act(() => mounted?.root.unmount());
mounted?.host.remove();
mounted = null;
const off = query(mount(), '[aria-label="Inspector"]');
expect(off.getAttribute("aria-pressed")).toBe("false");
expect(hasToken(off.className, "text-accent")).toBe(false);
expect(hasToken(off.className, "bg-hover")).toBe(false);
expect(hasToken(off.className, "text-accent-ink")).toBe(false);
expect(hasToken(off.className, "bg-on")).toBe(false);
});
it("keeps Capture a real download link rather than a button", () => {
@@ -126,7 +126,8 @@ export function StudioHeader({
aria-pressed={inspectorButtonActive}
className={cn(
"h-full rounded-none",
inspectorButtonActive && "bg-hover text-accent enabled:hover:text-accent",
inspectorButtonActive &&
"bg-on text-accent-ink enabled:hover:bg-on-hover enabled:hover:text-accent-ink",
)}
icon={<InspectorIcon size={16} />}
onClick={() => {
+7 -16
View File
@@ -14,23 +14,10 @@ export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastPr
className={`motion-reduce:animate-none ${leaving ? "hf-toast-exit" : "hf-toast-enter"}`}
>
<div
className="relative flex max-w-[min(420px,calc(100vw-48px))] items-center gap-3 overflow-hidden rounded-2xl py-3 pl-4 pr-2 text-[12px]"
style={{
background: isError
? "linear-gradient(135deg, rgba(127,29,29,0.55), rgba(80,10,10,0.45))"
: "linear-gradient(135deg, rgba(38,38,38,0.55), rgba(23,23,23,0.45))",
backdropFilter: "blur(16px) saturate(1.6)",
WebkitBackdropFilter: "blur(16px) saturate(1.6)",
border: `1px solid ${isError ? "rgba(239,68,68,0.18)" : "rgba(255,255,255,0.08)"}`,
boxShadow: [
"0 8px 32px rgba(0,0,0,0.35)",
`inset 0 1px 0 ${isError ? "rgba(239,68,68,0.12)" : "rgba(255,255,255,0.06)"}`,
`inset 0 -1px 0 rgba(0,0,0,0.15)`,
].join(", "),
}}
className={`relative flex max-w-[min(420px,calc(100vw-48px))] items-center gap-3 overflow-hidden rounded-2xl py-3 pl-4 pr-2 text-[12px] ${toastSurface(isError)}`}
>
<span
className={`min-w-0 wrap-break-word leading-5 ${isError ? "text-red-200" : "text-neutral-200"}`}
className={`min-w-0 wrap-break-word leading-5 ${isError ? "text-danger-ink" : "text-text-0"}`}
>
{message}
</span>
@@ -38,7 +25,7 @@ export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastPr
<button
type="button"
onClick={onDismiss}
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-300"
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-hover hover:text-neutral-300"
aria-label="Dismiss"
>
<svg
@@ -57,3 +44,7 @@ export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastPr
</div>
);
}
export function toastSurface(isError: boolean): string {
return `border shadow-popover backdrop-blur-md ${isError ? "border-danger/40 bg-raised" : "border-border bg-raised/95"}`;
}
@@ -81,7 +81,7 @@ export function TimelineToolPicker({ showSelectAroundPlayhead = true }) {
shortcut={row.shortcut}
onClick={row.run}
data-active={row.active || undefined}
className={row.active ? "bg-neutral-800 text-white" : undefined}
className={row.active ? "bg-press text-text-0" : undefined}
>
<span className="flex items-center gap-2">
{row.icon}
@@ -307,11 +307,11 @@ export function TimelineToolbar({
className={
!onToggleKeyframe
? flatDisabled
: `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${
: `${flatBtn} active:scale-[0.98] hover:bg-hover ${
keyframeState === "active"
? "text-studio-accent"
? "text-accent-ink"
: keyframeState === "inactive"
? "text-neutral-400 hover:text-studio-accent"
? "text-neutral-400 hover:text-accent-ink"
: "text-neutral-600 hover:text-neutral-400"
}`
}
@@ -342,9 +342,9 @@ export function TimelineToolbar({
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
aria-label="Auto-record manual edits as keyframes"
aria-pressed={autoKeyframeEnabled}
className={`${flatBtn} active:scale-[0.98] hover:bg-white/6 ${
className={`${flatBtn} active:scale-[0.98] hover:bg-hover ${
autoKeyframeEnabled
? "text-red-400 hover:text-red-300"
? "text-danger-ink"
: "text-neutral-600 hover:text-neutral-400"
}`}
>
@@ -442,7 +442,7 @@ export function TimelineToolbar({
}}
className={
canAdd
? `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-[#22c55e] active:scale-[0.98]`
? `${flatBtn} text-text-2 hover:bg-hover hover:text-accent-ink active:scale-[0.98]`
: flatDisabled
}
>
@@ -479,8 +479,8 @@ export function TimelineToolbar({
onClick={() => setThumbnailMode(thumbnailsVisible ? "hidden" : "adaptive")}
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
thumbnailsVisible
? "bg-studio-accent/10 text-studio-accent"
: "text-neutral-400 hover:bg-white/6 hover:text-neutral-200"
? "bg-studio-accent/10 text-accent-ink"
: "text-neutral-400 hover:bg-hover hover:text-neutral-200"
}`}
>
<Image size={16} aria-hidden="true" />
@@ -494,8 +494,8 @@ export function TimelineToolbar({
aria-pressed={zoomMode === "fit"}
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
zoomMode === "fit"
? "bg-studio-accent/10 text-studio-accent"
: "text-neutral-400 hover:bg-white/6 hover:text-neutral-200"
? "bg-studio-accent/10 text-accent-ink"
: "text-neutral-400 hover:bg-hover hover:text-neutral-200"
}`}
>
<ArrowsOutLineHorizontal size={16} aria-hidden="true" />
@@ -531,7 +531,7 @@ export function TimelineToolbar({
}}
// h-6 on the input is the 24x24 WCAG 2.2 (2.5.8) target: the visible
// track stays 2px and the thumb 10px, only the pointer box grows.
className="mx-1 h-6 w-[96px] cursor-pointer appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-neutral-700 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0a0a0a,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
className="mx-1 h-6 w-[96px] cursor-pointer appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-neutral-700 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-text-0 [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0a0a0a,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
/>
<Tooltip label="Zoom in">
<button
+1 -1
View File
@@ -301,7 +301,7 @@ function WindowMenu() {
onClick={() => togglePanel(id)}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-neutral-300 hover:bg-neutral-800"
>
<span className="w-3 text-panel-accent">{openPanels.has(id) ? "✓" : ""}</span>
<span className="w-3 text-accent-ink">{openPanels.has(id) ? "✓" : ""}</span>
{PANEL_DEFINITIONS[id].title}
</button>
))}
+9 -1
View File
@@ -1,9 +1,17 @@
@import "dockview-react/dist/styles/dockview.css";
.dv-shell:has(.hf-dock) {
/* dockview's runtime stylesheet pins `color-scheme: dark` on its theme class, which sits on
* the shell around the dock. */
color-scheme: inherit;
}
.hf-dock {
--dv-group-view-background-color: var(--panel-card-bg);
--dv-tabs-and-actions-container-background-color: var(--panel-strip-bg);
--dv-tabs-and-actions-container-height: 36px;
--dv-icon-hover-background-color: var(--color-hover);
--dv-tabs-container-scrollbar-color: var(--color-text-muted);
/* The fill paints the shown tab, so every tab state is transparent. */
--dv-activegroup-visiblepanel-tab-background-color: transparent;
--dv-activegroup-hiddenpanel-tab-background-color: transparent;
@@ -178,7 +186,7 @@
}
.dv-groupview:hover .hf-dock-strip-actions {
opacity: 0.5;
opacity: 0.8;
}
.hf-dock-strip-actions:hover,
@@ -201,7 +201,7 @@ export const AnimationCard = memo(function AnimationCard({
className="flex w-full items-center gap-2 py-1.5 active:scale-[0.99]"
>
<span
className="rounded-sm bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-panel-accent"
className="rounded-sm bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-accent-ink"
title={METHOD_TOOLTIPS[animation.method]}
>
{methodLabel}
@@ -354,7 +354,7 @@ export const AnimationCard = memo(function AnimationCard({
{animation.method === "fromTo" && (
<div className="space-y-1">
<p className="text-[9px] font-semibold uppercase tracking-wider text-orange-400/70">
<p className="text-[9px] font-semibold uppercase tracking-wider text-warning-ink">
From
</p>
<div className="space-y-1.5">
@@ -378,14 +378,14 @@ export const AnimationCard = memo(function AnimationCard({
onAdd={(prop) => onAddFromProperty?.(animation.id, prop)}
onOpen={() => setAddingFromProp(true)}
onClose={() => setAddingFromProp(false)}
buttonClassName="text-[11px] font-medium text-orange-400/70 transition-colors hover:text-orange-300"
buttonClassName="text-[11px] font-medium text-warning-ink transition-colors hover:text-text-0"
/>
</div>
</div>
)}
{animation.method === "fromTo" && Object.keys(animation.properties).length > 0 && (
<p className="text-[9px] font-semibold uppercase tracking-wider text-panel-accent/70">
<p className="text-[9px] font-semibold uppercase tracking-wider text-accent-ink">
To
</p>
)}
@@ -455,7 +455,7 @@ export const AnimationCard = memo(function AnimationCard({
<button
type="button"
onClick={() => onDeleteAnimation(animation.id)}
className="ml-auto text-[11px] font-medium text-red-400 transition-colors hover:text-red-300"
className="ml-auto text-[11px] font-medium rounded-sm text-danger-ink transition-colors hover:bg-danger/15"
title="Remove this animation"
>
Remove
@@ -46,7 +46,7 @@ function RemoveButton({ onClick, title }: { onClick: () => void; title: string }
<button
type="button"
onClick={onClick}
className="relative shrink-0 rounded-sm p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400 active:scale-[0.95]"
className="relative shrink-0 rounded-sm p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-danger-ink active:scale-[0.95]"
title={title}
aria-label={title}
>
@@ -170,7 +170,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
<div className="text-[10px] text-neutral-500">This block has no editable parameters.</div>
)}
{!fileManager && params.length > 0 && (
<div className="text-[10px] text-amber-400/90">
<div className="text-[10px] text-warning-ink">
Block params can't be edited here — no project file access.
</div>
)}
@@ -189,12 +189,12 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
</div>
)}
{commitState.tone === "saved" && (
<div className="text-[10px] text-emerald-500/90" role="status">
<div className="text-[10px] text-accent-ink" role="status">
Saved to {compositionPath}
</div>
)}
{commitState.tone === "error" && (
<div className="text-[10px] text-red-400" role="alert">
<div className="text-[10px] text-danger-ink" role="alert">
{commitState.message}
</div>
)}
@@ -63,6 +63,9 @@ export function BorderRadiusEditor({
}, [linked, uniform, tl, onCommit]);
const path = buildRoundedRectPath(PREVIEW_W, PREVIEW_H, sTL, sTR, sBR, sBL);
const dotFill = linked
? "fill-[light-dark(oklch(0.5_0.2_260),#3b82f6)]"
: "fill-[light-dark(oklch(0.55_0.2_294),#a78bfa)]";
return (
<div className="space-y-3">
@@ -73,21 +76,11 @@ export function BorderRadiusEditor({
viewBox={`0 0 ${PREVIEW_W} ${PREVIEW_H}`}
className="shrink-0"
>
<path
d={path}
fill="rgba(255,255,255,0.06)"
stroke="rgba(255,255,255,0.24)"
strokeWidth={1.5}
/>
<circle cx={sTL} cy={sTL} r={3} fill={linked ? "#3b82f6" : "#a78bfa"} />
<circle cx={PREVIEW_W - sTR} cy={sTR} r={3} fill={linked ? "#3b82f6" : "#a78bfa"} />
<circle
cx={PREVIEW_W - sBR}
cy={PREVIEW_H - sBR}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
/>
<circle cx={sBL} cy={PREVIEW_H - sBL} r={3} fill={linked ? "#3b82f6" : "#a78bfa"} />
<path d={path} className="fill-text-0/[0.06] stroke-text-0/25" strokeWidth={1.5} />
<circle cx={sTL} cy={sTL} r={3} className={dotFill} />
<circle cx={PREVIEW_W - sTR} cy={sTR} r={3} className={dotFill} />
<circle cx={PREVIEW_W - sBR} cy={PREVIEW_H - sBR} r={3} className={dotFill} />
<circle cx={sBL} cy={PREVIEW_H - sBL} r={3} className={dotFill} />
</svg>
<button
@@ -257,7 +257,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
{hasDelete && (
<button
type="button"
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-danger-ink hover:bg-neutral-800 cursor-pointer text-left"
onPointerDown={(e) => {
if (e.button !== 0) return;
e.preventDefault();
@@ -29,7 +29,7 @@ export function ComputedTweenNotice({
<button
type="button"
onClick={onUnroll}
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-panel-accent hover:bg-neutral-800"
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-accent-ink hover:bg-neutral-800"
title="Rewrite the helper/loop into explicit tweens so this keyframe edits directly"
>
Unroll to edit
@@ -38,7 +38,7 @@ export function DomEditRotateHandle({
aria-label="Rotate selection"
onPointerDown={onStartRotate}
>
<span className="pointer-events-none flex h-[18px] w-[18px] items-center justify-center rounded-full border border-studio-accent/70 bg-studio-surface text-studio-accent shadow-[0_0_3px_rgba(0,0,0,0.45)]">
<span className="pointer-events-none flex h-[18px] w-[18px] items-center justify-center rounded-full border border-studio-accent/70 bg-studio-surface text-accent-ink shadow-[0_0_3px_rgba(0,0,0,0.45)]">
<svg
width="11"
height="11"
@@ -42,7 +42,6 @@ const VMIN = -HR / S; // bottom of visible view (undershoot headroom)
// of snapping to the view edge; the handle DOT is still clampView'd into view.
const DRAG_VMAX = 2;
const DRAG_VMIN = -1;
const ACCENT = "#3CE6AC";
// Figma-style ease-type dropdown: the current ease (glyph + name) as a button
// that opens the preset grid in a popover. This is where a preset is selected —
@@ -130,7 +129,7 @@ function EaseTypeDropdown({
aria-expanded={open}
aria-controls={open ? menuId : undefined}
onClick={() => setOpen((prev) => !prev)}
className="flex w-full items-center gap-2 rounded-md border border-white/10 bg-black/20 px-2 py-1.5 text-left transition-colors hover:border-white/20"
className="flex w-full items-center gap-2 rounded-md border border-border bg-input px-2 py-1.5 text-left transition-colors hover:border-border-strong"
>
<MiniCurveSvg ease={ease} active size={16} />
<span className="text-[11px] text-neutral-200">{label}</span>
@@ -151,7 +150,7 @@ function EaseTypeDropdown({
role="menu"
aria-label={`${MODE_LABELS[kind]} ease presets`}
onKeyDown={handleMenuKeyDown}
className="absolute inset-x-0 top-full z-20 mt-1 rounded-md border border-white/10 bg-neutral-900 p-2 shadow-xl"
className="absolute inset-x-0 top-full z-20 mt-1 rounded-md border border-border bg-raised p-2 shadow-xl"
>
<EasePresetGrid
kind={kind}
@@ -407,7 +406,7 @@ export function EaseCurveSection({
{showGraph ? (
<>
<div
className="mx-auto overflow-hidden rounded-md border border-white/5 bg-black/20"
className="mx-auto overflow-hidden rounded-md border border-border bg-input"
style={{ aspectRatio: `${SVGW} / ${SVGH}`, width: "100%", maxWidth: 230 }}
>
<svg
@@ -429,7 +428,7 @@ export function EaseCurveSection({
y1={top}
x2={xToSvg(q)}
y2={bottom}
stroke="white"
className="stroke-text-0"
strokeOpacity="0.05"
strokeWidth="1"
/>
@@ -441,7 +440,7 @@ export function EaseCurveSection({
y1={yToSvg(q)}
x2={right}
y2={yToSvg(q)}
stroke="white"
className="stroke-text-0"
strokeOpacity="0.05"
strokeWidth="1"
/>
@@ -453,7 +452,7 @@ export function EaseCurveSection({
width={S}
height={bottom - top}
fill="none"
stroke="white"
className="stroke-text-0"
strokeOpacity="0.1"
strokeWidth="1"
/>
@@ -463,7 +462,7 @@ export function EaseCurveSection({
y1={a0.y}
x2={a1.x}
y2={a1.y}
stroke="white"
className="stroke-text-0"
strokeOpacity="0.08"
strokeWidth="1"
strokeDasharray="3 4"
@@ -476,8 +475,7 @@ export function EaseCurveSection({
y1={a0.y}
x2={p1.x}
y2={p1.y}
stroke={ACCENT}
strokeOpacity="0.5"
className="stroke-accent-ink"
strokeWidth="1.5"
/>
<line
@@ -485,8 +483,7 @@ export function EaseCurveSection({
y1={a1.y}
x2={p2.x}
y2={p2.y}
stroke={ACCENT}
strokeOpacity="0.5"
className="stroke-accent-ink"
strokeWidth="1.5"
/>
</>
@@ -495,13 +492,13 @@ export function EaseCurveSection({
<path
d={curvePath}
fill="none"
stroke={ACCENT}
className="stroke-accent-ink"
strokeWidth="2.5"
strokeLinecap="round"
/>
{/* Anchors at (0,0) and (1,1) */}
<circle cx={a0.x} cy={a0.y} r="3" fill={ACCENT} />
<circle cx={a1.x} cy={a1.y} r="3" fill={ACCENT} />
<circle cx={a0.x} cy={a0.y} r="3" className="fill-accent-ink" />
<circle cx={a1.x} cy={a1.y} r="3" className="fill-accent-ink" />
{/* Draggable control handles (large transparent hit area + visible dot) */}
{showHandles &&
[["p1", p1] as const, ["p2", p2] as const].map(([key, pt]) => (
@@ -518,7 +515,7 @@ export function EaseCurveSection({
aria-valuemax={1}
aria-valuenow={key === "p1" ? x1 : x2}
aria-valuetext={`x ${key === "p1" ? x1 : x2}, y ${key === "p1" ? y1 : y2}`}
className="cursor-grab stroke-transparent outline-hidden active:cursor-grabbing focus-visible:stroke-white focus-visible:stroke-[2px]"
className="cursor-grab stroke-transparent outline-hidden active:cursor-grabbing focus-visible:stroke-text-0 focus-visible:stroke-[2px]"
onPointerDown={(e) => handlePointerDown(key, e)}
onKeyDown={(event) => handleKeyDown(key, event)}
onPointerEnter={() => setHover(key)}
@@ -528,10 +525,8 @@ export function EaseCurveSection({
cx={pt.x}
cy={pt.y}
r={hover === key || draggingRef.current === key ? 7 : 5.5}
fill="#0a0a1a"
stroke={ACCENT}
strokeWidth="2.5"
className="pointer-events-none transition-[r]"
className="pointer-events-none fill-input stroke-accent-ink transition-[r]"
/>
</g>
))}
@@ -59,7 +59,7 @@ export const EasePresetGrid = function EasePresetGrid({
<MiniCurveSvg ease={preset.ease} active={isActive} />
<span
className={`text-center text-[8px] leading-none ${
isActive ? "text-panel-accent" : "text-neutral-500"
isActive ? "text-accent-ink" : "text-neutral-500"
}`}
>
{preset.label}
@@ -80,7 +80,7 @@ export function EaseModeToggle({
}) {
return (
<div
className="mb-2 grid grid-cols-3 rounded-md bg-black/20 p-0.5"
className="mb-2 grid grid-cols-3 rounded-md bg-press p-0.5"
role="radiogroup"
aria-label="Ease editor mode"
>
@@ -93,17 +93,17 @@ export function EaseBezierField({
}
}}
onBlur={(event) => commit(event.currentTarget.value)}
className={`w-full rounded border bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden ${
className={`w-full rounded border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden ${
error
? "border-red-500/70 focus:border-red-400"
: "border-white/10 focus:border-panel-accent/50"
? "border-danger-ink/70 focus:border-danger-ink"
: "border-border focus:border-panel-accent/50"
}`}
/>
</div>
<p
id={errorId}
aria-live="polite"
className={`mt-1 text-[9px] text-red-400 ${error ? "" : "sr-only"}`}
className={`mt-1 text-[9px] text-danger-ink ${error ? "" : "sr-only"}`}
>
{error ?? "Valid bezier values"}
</p>
@@ -183,7 +183,7 @@ export function SpringBounceField({
const bounce = parseSpringBounce(`spring(${value})`);
if (bounce !== null) onCommit(`spring(${round2(bounce)})`);
}}
className="w-16 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
className="w-16 rounded-sm border border-border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
/>
</div>
);
@@ -207,7 +207,7 @@ export function WiggleField({
min={1}
step={1}
onCommit={(value) => commitWiggle(onCommit, value, config.type, amplitude)}
className="w-14 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
className="w-14 rounded-sm border border-border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
/>
</div>
<div className="flex items-center gap-1">
@@ -220,7 +220,7 @@ export function WiggleField({
commitWiggle(onCommit, config.wiggles, event.currentTarget.value, amplitude);
}
}}
className="rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
className="rounded-sm border border-border bg-input px-1.5 py-1 text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
>
{WIGGLE_TYPES.map((type) => (
<option key={type} value={type}>
@@ -238,7 +238,7 @@ export function WiggleField({
max={1}
step={0.01}
onCommit={(value) => commitWiggle(onCommit, config.wiggles, config.type, value)}
className="w-16 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
className="w-16 rounded-sm border border-border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
/>
</div>
</div>
@@ -263,7 +263,7 @@ export const FileTree = memo(function FileTree({
<div
className={`flex-1 overflow-y-auto py-1 transition-colors ${
dragOverFolder === ""
? "bg-[#3CE6AC]/5 outline-solid outline-1 outline-[#3CE6AC]/30 -outline-offset-1"
? "bg-accent/5 outline-solid outline-1 outline-accent-ink -outline-offset-1"
: ""
}`}
onContextMenu={handleRootContextMenu}
@@ -21,34 +21,55 @@ import {
const SZ = 14;
const W = "duotone" as const;
type Icon = typeof File;
// Brand hues; each light half is darkened to hold 3:1 on a hovered or selected row.
const RED = "text-[light-dark(oklch(0.59_0.194_35),oklch(0.71_0.194_35))]";
const BLUE = "text-[light-dark(oklch(0.56_0.14_253),oklch(0.69_0.14_253))]";
const GREEN = "text-[light-dark(oklch(0.53_0.192_150),#22C55E)]";
const GRAY = "text-[light-dark(oklch(0.56_0.019_261),#9CA3AF)]";
export const DIM = "text-[light-dark(oklch(0.55_0.023_264),oklch(0.69_0.023_264))]";
const ICONS = new Map<string, [Icon, string]>([
["html", [FileHtml, RED]],
["css", [FileCss, "text-[light-dark(oklch(0.5_0.2_266),oklch(0.71_0.2_266))]"]],
["js", [FileJs, "text-[light-dark(oklch(0.56_0.156_101),#F0DB4F)]"]],
["jsx", [FileJsx, "text-[light-dark(oklch(0.55_0.117_219),#61DAFB)]"]],
["ts", [FileTs, BLUE]],
["tsx", [FileTsx, BLUE]],
["json", [FileCode, "text-[light-dark(oklch(0.54_0.182_152),#4ADE80)]"]],
["svg", [FileSvg, "text-[light-dark(oklch(0.58_0.187_48),#F97316)]"]],
["md", [FileMd, GRAY]],
["txt", [FileTxt, GRAY]],
["png", [FilePng, GREEN]],
["jpg", [FileJpg, GREEN]],
["webp", [PhImage, GREEN]],
["mp4", [FileVideo, "text-[light-dark(oklch(0.59_0.2_304),oklch(0.71_0.2_304))]"]],
["mp3", [Waveform, "text-[light-dark(oklch(0.53_0.159_165),#3CE6AC)]"]],
["woff", [TextAa, DIM]],
]);
const ALIAS = new Map(
Object.entries({
mjs: "js",
cjs: "js",
mts: "ts",
mdx: "md",
jpeg: "jpg",
gif: "webp",
ico: "webp",
webm: "mp4",
mov: "mp4",
wav: "mp3",
ogg: "mp3",
m4a: "mp3",
woff2: "woff",
ttf: "woff",
otf: "woff",
}),
);
export function FileIcon({ path }: { path: string }) {
const ext = path.split(".").pop()?.toLowerCase() ?? "";
const c = "shrink-0";
if (ext === "html") return <FileHtml size={SZ} weight={W} color="#E44D26" className={c} />;
if (ext === "css") return <FileCss size={SZ} weight={W} color="#264DE4" className={c} />;
if (ext === "js" || ext === "mjs" || ext === "cjs")
return <FileJs size={SZ} weight={W} color="#F0DB4F" className={c} />;
if (ext === "jsx") return <FileJsx size={SZ} weight={W} color="#61DAFB" className={c} />;
if (ext === "ts" || ext === "mts")
return <FileTs size={SZ} weight={W} color="#3178C6" className={c} />;
if (ext === "tsx") return <FileTsx size={SZ} weight={W} color="#3178C6" className={c} />;
if (ext === "json") return <FileCode size={SZ} weight={W} color="#4ADE80" className={c} />;
if (ext === "svg") return <FileSvg size={SZ} weight={W} color="#F97316" className={c} />;
if (ext === "md" || ext === "mdx")
return <FileMd size={SZ} weight={W} color="#9CA3AF" className={c} />;
if (ext === "txt") return <FileTxt size={SZ} weight={W} color="#9CA3AF" className={c} />;
if (ext === "png") return <FilePng size={SZ} weight={W} color="#22C55E" className={c} />;
if (ext === "jpg" || ext === "jpeg")
return <FileJpg size={SZ} weight={W} color="#22C55E" className={c} />;
if (ext === "webp" || ext === "gif" || ext === "ico")
return <PhImage size={SZ} weight={W} color="#22C55E" className={c} />;
if (ext === "mp4" || ext === "webm" || ext === "mov")
return <FileVideo size={SZ} weight={W} color="#A855F7" className={c} />;
if (ext === "mp3" || ext === "wav" || ext === "ogg" || ext === "m4a")
return <Waveform size={SZ} weight={W} color="#3CE6AC" className={c} />;
if (ext === "woff" || ext === "woff2" || ext === "ttf" || ext === "otf")
return <TextAa size={SZ} weight={W} color="#6B7280" className={c} />;
return <File size={SZ} weight={W} color="#6B7280" className={c} />;
const [Glyph, color] = ICONS.get(ALIAS.get(ext) ?? ext) ?? [File, DIM];
return <Glyph size={SZ} weight={W} className={`shrink-0 ${color}`} />;
}
// ── Tree Types ──
@@ -16,6 +16,7 @@ import {
type TreeNode,
type ContextMenuState,
type InlineInputState,
DIM,
} from "./FileTreeIcons";
export type { ContextMenuState, InlineInputState };
@@ -173,7 +174,7 @@ export function ContextMenu({
<div className="border-t border-neutral-700 my-1" />
<button
role="menuitem"
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-900/30 focus-visible:bg-red-900/30 active:bg-red-900/50 outline-hidden cursor-pointer text-left"
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-danger-ink hover:bg-danger/25 focus-visible:bg-danger/25 active:bg-danger/25 outline-hidden cursor-pointer text-left"
onClick={() => {
onDelete(state.targetPath);
onClose();
@@ -263,7 +264,7 @@ export function InlineInput({
style={{ paddingLeft: `${8 + depth * 12 + (isFolder ? 0 : 14)}px` }}
>
{isFolder ? (
<FolderSimple size={SZ_ICON} weight="duotone" color="#6B7280" className="shrink-0" />
<FolderSimple size={SZ_ICON} weight="duotone" className={`shrink-0 ${DIM}`} />
) : (
<FileIcon path={value} />
)}
@@ -279,12 +280,12 @@ export function InlineInput({
onBlur={handleBlur}
aria-invalid={error ? true : undefined}
className={`w-full min-w-0 bg-neutral-800 text-neutral-200 text-xs px-1.5 py-0.5 rounded border outline-hidden ${
error ? "border-red-500/70" : "border-neutral-600 focus:border-[#3CE6AC]"
error ? "border-danger-ink" : "border-neutral-600 focus:border-accent-ink"
}`}
spellCheck={false}
/>
{error && (
<div className="mt-0.5 text-[10px] text-red-400" role="alert">
<div className="mt-0.5 text-[10px] text-danger-ink" role="alert">
{error}
</div>
)}
@@ -344,13 +345,13 @@ export function DeleteConfirm({
<div className="flex gap-1.5">
<button
onClick={onCancel}
className="flex-1 px-2 py-1 rounded-sm bg-neutral-700 text-neutral-300 hover:bg-neutral-600 transition-colors"
className="flex-1 px-2 py-1 rounded-sm bg-hover text-neutral-300 hover:bg-press transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="flex-1 px-2 py-1 rounded-sm bg-red-900/60 text-red-300 hover:bg-red-800/60 transition-colors"
className="flex-1 px-2 py-1 rounded-sm bg-danger/15 text-danger-ink hover:bg-danger/25 transition-colors"
>
Delete
</button>
@@ -419,7 +420,7 @@ export const TreeFile = memo(function TreeFile({
<span className="truncate flex-1">{node.name}</span>
{lintInfo && lintInfo.count > 0 && (
<span
className="shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400 text-center mr-1"
className="shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-warning-ink text-center mr-1"
title={lintInfo.messages.join("\n")}
>
{lintInfo.count}
@@ -505,7 +506,9 @@ export const TreeFolder = memo(function TreeFolder({
}}
onDragLeave={onDragLeave}
className={`w-full flex items-center gap-1.5 px-2.5 py-1 min-h-7 text-left text-xs text-neutral-400 hover:bg-neutral-800/30 hover:text-neutral-300 transition-colors ${
isDragOver ? "bg-[#3CE6AC]/10 outline-solid outline-1 outline-[#3CE6AC]/40" : ""
isDragOver
? "bg-accent/10 outline-solid outline-1 outline-accent-ink -outline-offset-1"
: ""
}`}
style={{ paddingLeft: `${8 + depth * 12}px` }}
>
@@ -43,7 +43,7 @@ export function GestureRecordPanelButton({
}}
className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${
recording
? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse"
? "bg-red-500/15 text-danger-ink border border-red-500/30 animate-pulse"
: "bg-panel-input text-panel-text-2 hover:bg-panel-hover border border-panel-border"
}`}
>
@@ -77,8 +77,8 @@ export function GestureRecordBadge({
title={label}
className={`pointer-events-auto absolute z-20 flex h-7 w-7 items-center justify-center rounded-full border shadow-lg transition-colors ${
recording
? "border-red-400/60 bg-red-500 text-white animate-pulse"
: "border-studio-accent/60 bg-neutral-950 text-studio-accent hover:bg-neutral-900"
? "border-danger/60 bg-danger text-on-danger animate-pulse"
: "border-studio-accent/60 bg-neutral-950 text-accent-ink hover:bg-neutral-900"
}`}
style={{
left: Math.max(0, rect.left + rect.width + 8),
@@ -3,7 +3,7 @@ import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimation
const STYLES = {
classic: {
method:
"rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 py-1.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white",
"rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 py-1.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0",
cancel: "px-1.5 text-[11px] text-neutral-500 hover:text-neutral-300",
trigger: "text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200",
},
@@ -24,13 +24,13 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
return (
<Section title="Animation" icon={<Film size={15} />}>
{multipleTimelines && (
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss
— consolidate into a single timeline to enable editing.
</p>
)}
{unsupportedTimelinePattern && (
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
This timeline uses a computed key (window.__timelines[variable]) the editor can&apos;t
resolve statically. Use a string-literal key (window.__timelines[&quot;id&quot;]) or a
variable declaration (const tl = gsap.timeline()) to enable editing.
@@ -77,7 +77,7 @@ export function InlineTextToolbar({
data-inline-text-toolbar="true"
role="toolbar"
aria-label="Text formatting"
className="pointer-events-auto fixed z-200 flex items-center gap-1 rounded-lg border border-white/10 bg-[#15171c] p-1 shadow-[0_8px_24px_rgba(0,0,0,0.55)]"
className="pointer-events-auto fixed z-200 flex items-center gap-1 rounded-lg border border-border bg-raised p-1 shadow-[0_8px_24px_rgba(0,0,0,0.55)]"
style={{
left: placement.left,
top: placement.top,
@@ -98,12 +98,12 @@ export function InlineTextToolbar({
onClick={(event) => event.stopPropagation()}
>
<label
className="group relative flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-white/10"
className="group relative flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-hover"
title="Text colour"
>
<span
aria-hidden="true"
className="h-4 w-4 rounded-full border-2 border-white/25 transition-transform duration-150 group-hover:scale-110 group-active:scale-95"
className="h-4 w-4 rounded-full border-2 border-border-strong transition-transform duration-150 group-hover:scale-110 group-active:scale-95"
// `background` maps a gradient to the PADDING box and then repeats it
// to fill the border box, so the 1px border shows the strip either
// side of the tile: the end colour on the left, the start colour on
@@ -202,7 +202,7 @@ function ToolbarToggle({
aria-label={label}
aria-pressed={on}
className={`flex h-6 w-6 items-center justify-center rounded-md text-xs ${
on ? "bg-studio-accent/20 text-studio-accent" : "text-white/70 hover:bg-white/10"
on ? "bg-studio-accent/20 text-accent-ink" : "text-text-2 hover:bg-hover"
}`}
style={{
fontWeight: bold ? 700 : 400,
@@ -67,7 +67,7 @@ export function InspectorHeaderActions({
}}
className={`flex h-6 w-6 items-center justify-center rounded transition-colors ${
copied
? "text-studio-accent"
? "text-accent-ink"
: "text-neutral-500 hover:bg-neutral-800 hover:text-neutral-300"
}`}
title={copied ? "Copied!" : "Copy element info to clipboard"}
@@ -19,8 +19,11 @@ export const KeyframeDiamond = memo(function KeyframeDiamond({
isHold = false,
}: KeyframeDiamondProps) {
const isFilled = state === "active";
const opacity = state === "ghost" ? 0.25 : state === "inactive" ? 0.6 : 1;
const color = state === "active" ? "#3CE6AC" : "#a3a3a3";
const color = {
active: "text-accent",
inactive: "text-text-2 hover:text-text-0",
ghost: "text-text-off hover:text-text-0",
}[state];
return (
<button
@@ -29,8 +32,7 @@ export const KeyframeDiamond = memo(function KeyframeDiamond({
e.stopPropagation();
onClick();
}}
className="relative shrink-0 p-0.5 transition-opacity hover:opacity-100 before:absolute before:-inset-1.5 before:content-['']"
style={{ color, opacity }}
className={`relative shrink-0 p-0.5 transition-colors before:absolute before:-inset-1.5 before:content-[''] ${color}`}
title={title}
aria-label={title}
aria-pressed={state === "active"}
@@ -82,11 +82,11 @@ function ArrowLeft({ disabled }: { disabled: boolean }) {
height="10"
viewBox="0 0 6 10"
fill="none"
style={{ opacity: disabled ? 0.25 : 1 }}
className={disabled ? "text-text-off" : "text-text-2"}
>
<path
d="M5 1L1 5L5 9"
stroke="#a3a3a3"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
@@ -102,11 +102,11 @@ function ArrowRight({ disabled }: { disabled: boolean }) {
height="10"
viewBox="0 0 6 10"
fill="none"
style={{ opacity: disabled ? 0.25 : 1 }}
className={disabled ? "text-text-off" : "text-text-2"}
>
<path
d="M1 1L5 5L1 9"
stroke="#a3a3a3"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
@@ -458,7 +458,7 @@ export const LayersPanel = memo(function LayersPanel() {
isDragged
? "opacity-40"
: selected
? "bg-panel-accent/14 text-panel-accent"
? "bg-panel-accent/14 text-accent-ink"
: "text-panel-text-2 hover:bg-panel-hover/40 hover:text-panel-text-1"
} ${dragKey ? "cursor-grabbing" : "cursor-pointer"}`}
style={{ paddingLeft: 8 + layer.depth * 16 }}
@@ -494,9 +494,9 @@ export const LayersPanel = memo(function LayersPanel() {
<span
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded text-[8px] font-bold uppercase ${
selected
? "bg-panel-accent/18 text-panel-accent"
? "bg-panel-accent/18 text-accent-ink"
: isCompHost
? "bg-panel-accent/40 text-panel-accent"
? "bg-on text-accent-ink"
: "bg-panel-hover text-panel-text-4"
}`}
>
@@ -83,7 +83,7 @@ export function PromotableControl({
<span
// Sits above the row (not on top of top-0) so it clears a value that
// renders flush to the row's right edge, e.g. flat Font/Color rows.
className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded-sm bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded-sm bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-accent-ink"
title={`Bound to variable "${promote.boundId}"`}
>
◆ {promote.boundId}
@@ -97,7 +97,7 @@ export function PromotableControl({
e.stopPropagation();
promote.promote();
}}
className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded-sm bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded-sm bg-raised/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-accent-ink hover:opacity-100"
>
◇ var
</button>
@@ -24,7 +24,7 @@ function FlatEmptyState() {
<div className="mt-2 flex w-full flex-col gap-1.5">
<span className="flex items-center justify-between rounded-lg border border-panel-border bg-panel-bg px-3 py-2">
<span className="flex items-center gap-2 text-[11px] text-panel-text-2">
<span className="text-panel-danger">●</span>
<span className="text-danger-ink">●</span>
Record a gesture
</span>
<span className="rounded-sm border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
@@ -33,7 +33,7 @@ function FlatEmptyState() {
</span>
<span className="flex items-center justify-between rounded-lg border border-panel-border bg-panel-bg px-3 py-2">
<span className="flex items-center gap-2 text-[11px] text-panel-text-2">
<span className="text-panel-accent">✦</span>
<span className="text-accent-ink">✦</span>
Describe a change to the agent
</span>
<span className="rounded-sm border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
@@ -50,7 +50,7 @@ function elementKindGlyph(element: DomEditSelection): { glyph: string; className
return { glyph: "◆", className: "bg-panel-media/10 text-panel-media" };
}
if (element.textFields?.length > 0) {
return { glyph: "T", className: "bg-panel-accent/10 text-panel-accent" };
return { glyph: "T", className: "bg-panel-accent/10 text-accent-ink" };
}
return { glyph: "▦", className: "bg-panel-container/10 text-panel-container" };
}
@@ -74,7 +74,7 @@ function FlatMultiSelectState({
return (
<div className="flex flex-col gap-3 px-4 py-3">
<div className="flex items-center gap-3 rounded-xl border border-panel-border bg-panel-surface p-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-panel-accent/10 text-panel-accent">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-panel-accent/10 text-accent-ink">
<Layers size={16} />
</span>
<div className="min-w-0 flex-1">
@@ -40,7 +40,7 @@ export function PropertyPanelFlatFooter({
height="13"
viewBox="0 0 16 16"
fill="currentColor"
className="text-panel-accent"
className="text-accent-ink"
>
<path d="M8 1l1.4 4.6L14 7l-4.6 1.4L8 13l-1.4-4.6L2 7l4.6-1.4z" />
</svg>
@@ -57,7 +57,7 @@ export function PropertyPanelFlatFooter({
track("button", "Gesture recording");
onToggleRecording();
}}
className={recording ? "text-panel-danger animate-pulse" : "text-panel-danger"}
className={recording ? "text-danger-ink animate-pulse" : "text-danger-ink"}
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
{recording ? (
@@ -38,7 +38,7 @@ describe("PropertyPanelFlatHeader", () => {
expect(host.textContent).toContain("Mono Label");
expect(host.textContent).toContain(".mono-label · div");
const icon = host.querySelector('[data-flat-header-icon="true"]');
expect(icon?.className).toContain("text-panel-accent");
expect(icon?.className).toContain("text-accent-ink");
act(() => root.unmount());
});
@@ -4,7 +4,7 @@ import { ClipboardList, Film, Square, Type, X } from "../../icons/SystemIcons";
const ICON_BY_KIND = { text: Type, media: Film, other: Square } as const;
const ICON_COLOR_BY_KIND = {
text: "text-panel-accent",
text: "text-accent-ink",
media: "text-panel-media",
other: "text-panel-container",
} as const;
@@ -92,7 +92,7 @@ export function PropertyPanelFlatHeader({
track("button", "Copy element info");
onCopy();
}}
className={copied ? "text-panel-accent" : undefined}
className={copied ? "text-accent-ink" : undefined}
>
<ClipboardList size={13} />
</button>
@@ -73,7 +73,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
type="button"
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
motionPathArmed
? "bg-studio-accent/20 text-studio-accent"
? "bg-studio-accent/20 text-accent-ink"
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
}`}
onClick={() => setMotionPathArmed(!motionPathArmed)}
@@ -99,7 +99,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
type="button"
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
visible
? "bg-studio-accent/20 text-studio-accent"
? "bg-studio-accent/20 text-accent-ink"
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
}`}
onClick={toggle}
@@ -115,7 +115,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
type="button"
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
prefs.snapEnabled
? "bg-studio-accent/20 text-studio-accent"
? "bg-studio-accent/20 text-accent-ink"
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
}`}
onClick={toggleSnap}
@@ -131,7 +131,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
type="button"
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
prefs.gridVisible
? "bg-studio-accent/20 text-studio-accent"
? "bg-studio-accent/20 text-accent-ink"
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
}`}
onClick={toggleGrid}
@@ -164,7 +164,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
{gridPopoverOpen && (
<div
ref={popoverRef}
className="absolute right-0 top-full mt-1 rounded-lg bg-neutral-800 border border-neutral-700 p-3 shadow-xl min-w-[180px]"
className="absolute right-0 top-full mt-1 rounded-lg bg-raised border border-border p-3 shadow-xl min-w-[180px]"
>
<label className="flex items-center justify-between text-xs text-white/80 mb-2">
<span>Grid spacing</span>
@@ -139,7 +139,7 @@ export function TimelineFxPopover({
ref={rootRef}
role="dialog"
aria-label="Effects"
className="z-200 flex flex-col overflow-hidden rounded-md border border-white/10 bg-[#1b1b1f] p-2 shadow-xl"
className="z-200 flex flex-col overflow-hidden rounded-md border border-border bg-raised p-2 shadow-xl"
style={clampedStyle(anchorRect)}
onKeyDown={onKeyDown}
onPointerDown={(event) => event.stopPropagation()}
@@ -165,10 +165,10 @@ export function TimelineFxPopover({
onAuditionTracked={(id) => trackPresetAuditioned(id, { trackKind })}
/>
</div>
<div className="mt-2 flex shrink-0 items-center justify-between border-t border-white/10 pt-2 text-[10px] text-white/55">
<div className="mt-2 flex shrink-0 items-center justify-between border-t border-border pt-2 text-[10px] text-text-muted">
<button
type="button"
className="hover:text-white"
className="hover:text-text-0"
onClick={() => {
onClose();
onOpenRack();
@@ -178,7 +178,7 @@ export function TimelineFxPopover({
</button>
<button
type="button"
className="hover:text-white"
className="hover:text-text-0"
onClick={() => {
onClose();
onOpenRack();
@@ -314,7 +314,7 @@ export function Transform3DCube({
aria-label="Keyframe 3D transform"
aria-pressed={keyframed}
className={`absolute left-1.5 top-1.5 rounded p-0.5 hover:bg-neutral-800 ${
keyframed ? "text-[#5ff0bf]" : "text-neutral-500 hover:text-neutral-200"
keyframed ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200"
}`}
>
<svg
@@ -56,7 +56,7 @@ export function MiniCurveSvg({
<path
d={d}
fill="none"
stroke={active ? "#3CE6AC" : "#737373"}
className={active ? "stroke-accent-ink" : "stroke-text-muted"}
strokeWidth="1.5"
strokeLinecap="round"
/>
@@ -477,7 +477,7 @@ export function CurveGraph({
onSettle();
}
}}
className="mx-auto aspect-square w-full max-w-[200px] touch-none rounded-sm border border-panel-border-input bg-black/20 outline-hidden focus:ring-1 focus:ring-panel-accent"
className="mx-auto aspect-square w-full max-w-[200px] touch-none rounded-sm scheme-dark border border-panel-border-input bg-bg-0 outline-hidden focus:ring-1 focus:ring-inset focus:ring-panel-accent"
>
<defs>
<linearGradient id={`hf-hue-axis-${tab.key}`}>
@@ -491,7 +491,7 @@ export function CurveGraph({
</linearGradient>
</defs>
{[0.25, 0.5, 0.75].map((ratio) => (
<g key={ratio} stroke="rgba(255,255,255,0.08)" strokeWidth="0.5">
<g key={ratio} className="stroke-text-0/10" strokeWidth="0.5">
<line
x1={GRAPH_PADDING}
y1={GRAPH_PADDING + ratio * (GRAPH_SIZE - GRAPH_PADDING * 2)}
@@ -431,7 +431,7 @@ export function ColorGradingControls({
/>
</div>
{lutImportError && (
<div className="text-[10px] text-red-400" role="alert">
<div className="text-[10px] text-danger-ink" role="alert">
{lutImportError}
</div>
)}
@@ -14,11 +14,11 @@ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
function StatusPill({ status }: { status: RuntimeColorGradingStatus }) {
const dotClass =
status.state === "active"
? "bg-emerald-400"
? "bg-accent"
: status.state === "pending"
? "bg-amber-300"
? "bg-warning-ink"
: status.state === "unavailable"
? "bg-red-400"
? "bg-danger-ink"
: "bg-panel-text-5";
return (
<div
@@ -44,18 +44,18 @@ function HdrMediaWarning({ metadata }: { metadata: MediaMetadata | null }) {
.join(" · ");
return (
<div className="mb-3 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] leading-4 text-amber-100">
<div className="mb-3 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] leading-4 text-warning-ink">
<div className="mb-1 flex min-w-0 items-center justify-between gap-2">
<span className="font-semibold">{metadata.color.label} source</span>
<span className="rounded-sm bg-amber-400/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
<span className="rounded-sm bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-warning-ink">
SDR preview
</span>
</div>
<p className="text-amber-100/80">
<p className="text-text-2">
These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
is not true HDR color grading yet.
</p>
{details && <p className="mt-1 truncate text-[10px] text-amber-100/55">{details}</p>}
{details && <p className="mt-1 truncate text-[10px] text-text-muted">{details}</p>}
</div>
);
}
@@ -120,7 +120,7 @@ function HoldBeforeButton({
}}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors ${
active
? "bg-studio-accent text-black"
? "bg-studio-accent text-on-accent"
: "text-panel-text-4 hover:bg-panel-hover hover:text-panel-text-1"
} disabled:cursor-not-allowed disabled:opacity-40`}
title="Hold to show original"
@@ -158,7 +158,7 @@ export function ColorGradingSliderControl({
settings.onClick();
}}
className={`relative flex h-5 w-5 shrink-0 items-center justify-center rounded transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40 ${
settings.active ? "text-studio-accent" : "text-panel-text-5"
settings.active ? "text-accent-ink" : "text-panel-text-5"
}`}
title={settings.label}
>
@@ -204,13 +204,13 @@ export function PropertyPanelColorSecondary({
setSampling(false);
}
}}
className="flex min-h-7 items-center gap-1.5 text-[10px] font-medium text-panel-accent hover:text-panel-accent/80 disabled:opacity-50"
className="flex min-h-7 items-center gap-1.5 text-[10px] font-medium text-accent-ink hover:text-accent-ink/80 disabled:opacity-50"
>
<Eyedropper size={12} />
{sampling ? "Capturing frame" : "Sample color from frame"}
</button>
{captureError && (
<p role="alert" className="text-[9px] leading-4 text-red-300">
<p role="alert" className="text-[9px] leading-4 text-danger-ink">
{captureError}
</p>
)}
@@ -221,7 +221,7 @@ export function PropertyPanelColorSecondary({
type="button"
aria-pressed={!showMatte}
onClick={() => setShowMatte(false)}
className={!showMatte ? "text-panel-accent" : "text-panel-text-4"}
className={!showMatte ? "text-accent-ink" : "text-panel-text-4"}
>
Source
</button>
@@ -229,7 +229,7 @@ export function PropertyPanelColorSecondary({
type="button"
aria-pressed={showMatte}
onClick={() => setShowMatte(true)}
className={showMatte ? "text-panel-accent" : "text-panel-text-4"}
className={showMatte ? "text-accent-ink" : "text-panel-text-4"}
>
Selection matte
</button>
@@ -131,7 +131,7 @@ export function ImageFillField({
className={`inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors ${
disabled || uploading
? "cursor-not-allowed text-neutral-600"
: "cursor-pointer hover:border-neutral-600 hover:text-white"
: "cursor-pointer hover:border-neutral-600 hover:text-text-0"
}`}
>
<Plus size={12} className="shrink-0" />
@@ -151,7 +151,7 @@ export function ImageFillField({
/>
</div>
{uploadError && (
<div className="text-[10px] text-red-400" role="alert">
<div className="text-[10px] text-danger-ink" role="alert">
{uploadError}
</div>
)}
@@ -340,7 +340,7 @@ export function GradientField({
track("toggle", "Repeat gradient");
patch({ repeating: e.target.checked });
}}
className="h-4 w-4 rounded-sm border-neutral-700 bg-neutral-950 text-panel-accent focus:ring-panel-accent"
className="h-4 w-4 rounded-sm border-neutral-700 bg-neutral-950 text-accent-ink focus:ring-panel-accent"
/>
Repeat
</label>
@@ -357,7 +357,7 @@ export function GradientField({
})),
});
}}
className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white disabled:cursor-not-allowed disabled:text-neutral-600"
className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0 disabled:cursor-not-allowed disabled:text-neutral-600"
>
<ReverseGradientIcon size={16} />
Reverse
@@ -442,7 +442,7 @@ export function GradientField({
disabled={disabled || parsed.stops.length >= 6}
onClick={() => addStop()}
title={parsed.stops.length >= 6 ? "Maximum 6 stops" : "Add a gradient stop"}
className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white active:scale-[0.98] disabled:cursor-not-allowed disabled:text-neutral-600"
className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0 active:scale-[0.98] disabled:cursor-not-allowed disabled:text-neutral-600"
>
<Plus size={12} />
Add stop
@@ -474,7 +474,7 @@ export function GradientField({
type="button"
disabled={disabled || parsed.stops.length <= 2}
onClick={() => removeStop(index)}
className="mt-[22px] flex h-10 items-center justify-center rounded-lg border border-neutral-700 bg-neutral-950 text-neutral-400 transition-colors hover:border-neutral-600 hover:text-white disabled:cursor-not-allowed disabled:text-neutral-700"
className="mt-[22px] flex h-10 items-center justify-center rounded-lg border border-neutral-700 bg-neutral-950 text-neutral-400 transition-colors hover:border-neutral-600 hover:text-text-0 disabled:cursor-not-allowed disabled:text-text-off"
aria-label={`Remove stop ${index + 1}`}
>
<X size={12} />
@@ -5,9 +5,9 @@ import { Compare, RotateCcw } from "../../icons/SystemIcons";
import type { ColorGradingControllerState } from "./useColorGradingController";
const STATUS_DOT_CLASS: Record<ColorGradingControllerState["runtimeStatus"]["state"], string> = {
active: "bg-emerald-400",
pending: "bg-amber-300",
unavailable: "bg-red-400",
active: "bg-accent",
pending: "bg-warning-ink",
unavailable: "bg-danger-ink",
missing: "bg-panel-text-5",
inactive: "bg-panel-text-5",
};
@@ -81,7 +81,7 @@ describe("FlatColorGradingAccessory", () => {
const dot = host.querySelector('[data-flat-grade-status-dot="true"]');
expect(dot).not.toBeNull();
expect(dot?.getAttribute("title")).toBe("Shader active");
expect(dot?.className).toContain("bg-emerald-400");
expect(dot?.className).toContain("bg-accent");
act(() => root.unmount());
});
@@ -70,23 +70,20 @@ function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
return (
<div
data-flat-grade-hdr-banner="true"
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1.5 text-[10px] leading-4 text-amber-100"
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1.5 text-[10px] leading-4 text-warning-ink"
>
<div className="mb-0.5 flex items-center justify-between gap-2">
<span className="font-semibold">{metadata.color.label} source</span>
<span className="rounded-sm bg-amber-400/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
<span className="rounded-sm bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-warning-ink">
SDR preview
</span>
</div>
<p className="text-amber-100/80">
<p className="text-text-2">
These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
is not true HDR color grading yet.
</p>
{details && (
<p
data-flat-grade-hdr-detail="true"
className="mt-0.5 truncate text-[9px] text-amber-100/55"
>
<p data-flat-grade-hdr-detail="true" className="mt-0.5 truncate text-[9px] text-text-muted">
{details}
</p>
)}
@@ -214,7 +211,7 @@ export function FlatColorGradingSection({
<button
type="button"
onClick={onRequestPresetPreviews}
className="text-[10px] font-medium text-panel-accent hover:text-panel-accent/80"
className="text-[10px] font-medium text-accent-ink hover:text-accent-ink/80"
>
Retry look previews
</button>
@@ -519,7 +516,7 @@ export function FlatColorGradingSection({
track("button", "Apply grade to scope");
onApplyToScope();
}}
className="text-[11px] font-medium text-panel-accent hover:text-panel-accent/80 disabled:cursor-not-allowed disabled:opacity-50"
className="text-[11px] font-medium text-accent-ink hover:text-accent-ink/80 disabled:cursor-not-allowed disabled:opacity-50"
>
{applyBusy ? "Applying" : "Apply"}
</button>
@@ -186,7 +186,7 @@ export function FlatEffectsSection({
track("button", "Customize effect palette");
onCommitColorGrading({ ...grading, palette: fallback });
}}
className="flex min-h-[28px] items-center gap-1 text-[10px] font-medium text-panel-accent hover:text-panel-accent/80"
className="flex min-h-[28px] items-center gap-1 text-[10px] font-medium text-accent-ink hover:text-accent-ink/80"
>
<Plus size={11} /> Custom palette
</button>
@@ -307,7 +307,7 @@ export function FlatEffectsSection({
type="button"
title={`Remove ${selectedEffect.label}`}
onClick={() => removeEffect(selectedEffect)}
className="text-panel-text-4 hover:text-red-300"
className="text-panel-text-4 hover:text-danger-ink"
>
<X size={11} />
</button>
@@ -352,7 +352,7 @@ export function FlatEffectsSection({
data-flat-effects-add-toggle="true"
aria-expanded={catalogOpen}
onClick={() => setCatalogOpen((open) => !open)}
className="flex min-h-[30px] items-center gap-1 text-[10px] font-medium text-panel-accent hover:text-panel-accent/80"
className="flex min-h-[30px] items-center gap-1 text-[10px] font-medium text-accent-ink hover:text-accent-ink/80"
>
<Plus size={11} /> Add effect
</button>
@@ -374,7 +374,7 @@ export function FlatEffectsSection({
onClick={() => setCatalogGroup(group.label)}
className={`min-h-[25px] min-w-0 truncate bg-panel-bg px-2 text-[9px] ${
catalogGroup === group.label
? "font-medium text-panel-accent"
? "font-medium text-accent-ink"
: "text-panel-text-4 hover:text-panel-text-1"
}`}
>
@@ -200,7 +200,7 @@ export function FlatMediaSection({
data-flat-media-remove-bg="true"
disabled={!canRemoveBackground || removeBusy}
onClick={() => void runBackgroundRemoval()}
className="flex items-center gap-1 text-[10px] font-medium text-panel-accent disabled:cursor-not-allowed disabled:opacity-50"
className="flex items-center gap-1 text-[10px] font-medium text-accent-ink disabled:cursor-not-allowed disabled:opacity-50"
title={
canRemoveBackground
? "Remove background and save a transparent asset"
@@ -231,7 +231,7 @@ export function FlatMediaSection({
<div className="h-1 overflow-hidden rounded-full bg-panel-hover">
<div
className={`h-full rounded-full ${
removeProgress.status === "failed" ? "bg-red-400" : "bg-panel-accent"
removeProgress.status === "failed" ? "bg-danger-ink" : "bg-panel-accent"
}`}
style={{ width: `${Math.max(0, Math.min(100, removeProgress.progress))}%` }}
/>
@@ -148,13 +148,13 @@ export function FlatMotionSection({
{showEffects && (
<>
{multipleTimelines && (
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
This file has multiple GSAP timelines. Animation editing is disabled to prevent data
loss — consolidate into a single timeline to enable editing.
</p>
)}
{unsupportedTimelinePattern && (
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
This timeline uses a computed key the editor can&apos;t resolve statically.
</p>
)}
@@ -45,7 +45,7 @@ export function FlatOverlaysSection({
return <div className="py-4 text-center text-[10px] text-panel-text-4">Loading overlays…</div>;
}
if (error) {
return <div className="py-4 text-center text-[10px] text-red-300">{error}</div>;
return <div className="py-4 text-center text-[10px] text-danger-ink">{error}</div>;
}
const busy = adding !== null;
@@ -95,7 +95,7 @@ export function FlatOverlaysSection({
)}
<Plus
size={12}
className="absolute right-1.5 top-1.5 text-white opacity-70 drop-shadow-sm group-hover:text-panel-accent group-hover:opacity-100"
className="absolute right-1.5 top-1.5 text-white opacity-70 drop-shadow-sm group-hover:text-accent-ink group-hover:opacity-100"
/>
</span>
<span className="block truncate px-2 py-1.5 text-[10px] text-panel-text-2">
@@ -60,7 +60,7 @@ describe("FlatRow", () => {
/>,
);
const value = host.querySelector('[data-flat-row-value="true"]');
expect(value?.className).toContain("text-panel-accent");
expect(value?.className).toContain("text-accent-ink");
const reset = host.querySelector<HTMLButtonElement>('[data-flat-row-reset="true"]');
expect(reset).not.toBeNull();
act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
@@ -356,7 +356,7 @@ describe("FlatSlider", () => {
const fill = host.querySelector<HTMLElement>('[data-flat-slider-fill="true"]');
expect(fill?.style.width).toBe("100%");
const knob = host.querySelector<HTMLElement>('[data-flat-slider-knob="true"]');
expect(knob?.className).toContain("bg-white");
expect(knob?.className).toContain("bg-text-0");
act(() => root.unmount());
});
@@ -1172,7 +1172,7 @@ describe("FlatSelectRow", () => {
/>,
);
const select = host.querySelector<HTMLSelectElement>("select");
expect(select?.className).toContain("text-panel-accent");
expect(select?.className).toContain("text-accent-ink");
const reset = host.querySelector<HTMLButtonElement>('[data-flat-select-reset="true"]');
act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onReset).toHaveBeenCalledTimes(1);
@@ -1364,6 +1364,7 @@ describe("FlatSlider — typed value", () => {
const still = host.querySelector<HTMLInputElement>('[data-flat-slider-input="true"]');
expect(still).not.toBeNull();
expect(still?.getAttribute("aria-invalid")).toBe("true");
expect(still?.className).toContain("border-dashed");
act(() => still?.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })));
expect(host.querySelector('[data-flat-slider-input="true"]')).toBeNull();
expect(host.querySelector('[data-flat-slider-value="true"]')?.textContent).toBe("1x");
@@ -537,7 +537,7 @@ export function FlatSlider({
<div
data-flat-slider-knob="true"
className={`absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full ${
tier === "explicitCustom" ? "h-2 w-2 bg-white" : "h-[7px] w-[7px] bg-panel-text-4"
tier === "explicitCustom" ? "h-2 w-2 bg-text-0" : "h-[7px] w-[7px] bg-panel-text-4"
}`}
style={{ left: `${clampedPct}%` }}
/>
@@ -76,7 +76,7 @@ export function FlatSliderReadout({
}}
onBlur={() => commit(false)}
className={`w-11 shrink-0 rounded-[3px] border bg-panel-surface px-1 text-right font-mono text-[10px] text-panel-text-0 outline-none ${
invalid ? "border-red-400" : "border-panel-accent"
invalid ? "border-dashed border-danger-ink" : "border-panel-accent"
}`}
/>
);
@@ -433,7 +433,7 @@ export function FontFamilyField({
type="button"
disabled={disabled || loadingLocalFonts}
onClick={loadBrowserLocalFonts}
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-text-off"
>
{loadingLocalFonts ? "..." : "Local"}
</button>
@@ -442,7 +442,7 @@ export function FontFamilyField({
type="button"
disabled={disabled || importingFonts || !onImportFonts}
onClick={() => fontInputRef.current?.click()}
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-text-off"
>
{importingFonts ? "..." : "Import"}
</button>
@@ -111,7 +111,7 @@ function FxCarveMember({
{/* The lane is where an automated value comes from, and where it is
edited — saying so is the difference between a stale readout and
a pointer to the thing that owns it. */}
{automated ? <span className="text-[#3CE6AC]">A</span> : null}
{automated ? <span className="text-accent-ink">A</span> : null}
</span>
);
})}
@@ -90,7 +90,7 @@ export function AutomationToggle({
type="button"
className={`hf-fx-automate w-[16px] shrink-0 rounded-[3px] border font-mono text-[9px] leading-none ${
automated
? "border-panel-accent text-panel-accent"
? "border-panel-accent text-accent-ink"
: "border-panel-border-input text-panel-text-2 hover:text-panel-text-0"
}`}
aria-pressed={automated}
@@ -237,7 +237,7 @@ export function FxParamRow({
{/* See the enum row above for why the name wraps instead of truncating. */}
<span
className={`hf-fx-label w-[86px] shrink-0 wrap-break-word text-[10px] leading-tight ${
automated ? "text-panel-accent" : "text-panel-text-2"
automated ? "text-accent-ink" : "text-panel-text-2"
}`}
>
{param.label}
@@ -115,7 +115,7 @@ function Fader({
</span>
<span
className={`hf-fx-eq-value font-mono text-[9px] tabular-nums ${
moved ? "text-panel-accent" : "text-panel-text-2"
moved ? "text-accent-ink" : "text-panel-text-2"
}`}
>
{moved ? shown(value) : "0"}
@@ -160,7 +160,7 @@ export function FxEqModule({
<span className="font-mono text-[9px] text-panel-text-2">{bands.length}-band</span>
<button
type="button"
className="hf-fx-remove px-1 text-[11px] text-panel-text-2 hover:text-panel-danger"
className="hf-fx-remove px-1 text-[11px] text-panel-text-2 hover:text-danger-ink"
aria-label="Remove Tone"
disabled={disabled}
onClick={onRemove}
@@ -216,7 +216,7 @@ function FxNodeHeader({
/>
<button
type="button"
className="hf-fx-remove px-1 font-mono text-[11px] text-panel-text-2 hover:text-red-400 disabled:opacity-40"
className="hf-fx-remove px-1 font-mono text-[11px] text-panel-text-2 hover:text-danger-ink disabled:opacity-40"
title="Remove"
disabled={disabled}
onClick={onRemove}
@@ -189,7 +189,7 @@ export function FxPresetRun({
</button>
<button
type="button"
className="hf-fx-preset-run-remove px-1 font-mono text-[11px] text-panel-text-2 hover:text-red-400 disabled:opacity-40"
className="hf-fx-preset-run-remove px-1 font-mono text-[11px] text-panel-text-2 hover:text-danger-ink disabled:opacity-40"
title={`Remove ${preset.label}`}
disabled={disabled}
onClick={onRemoveRun}
@@ -231,7 +231,7 @@ export function MediaSection({
<div className="h-1 overflow-hidden rounded-full bg-panel-border">
<div
className={`h-full rounded-full ${
removeProgress.status === "failed" ? "bg-red-400" : "bg-studio-accent"
removeProgress.status === "failed" ? "bg-danger-ink" : "bg-studio-accent"
}`}
style={{ width: `${Math.max(0, Math.min(100, removeProgress.progress))}%` }}
/>
@@ -234,7 +234,7 @@ export function SegmentedControl({
aria-pressed={option.value === value}
className={`min-w-0 truncate rounded px-2 py-[5px] text-[11px] font-medium transition-colors disabled:cursor-not-allowed ${
option.value === value
? "bg-panel-hover text-white"
? "bg-panel-hover text-text-0"
: "text-panel-text-4 hover:text-panel-text-2"
}`}
>
@@ -323,7 +323,7 @@ function TextFieldEditor({
track("button", "Remove text field");
onRemoveTextField(field.key);
}}
className="inline-flex h-7 shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
className="inline-flex h-7 shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0"
>
Remove
</button>
@@ -474,7 +474,7 @@ export function TextSection({
if (nextKey) setActiveTextFieldKey(nextKey);
});
}}
className="inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
className="inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0"
>
<Plus size={12} className="shrink-0" />
<span className="truncate">Add text</span>
@@ -27,6 +27,6 @@ describe("value tier class maps", () => {
expect(VALUE_TIER_LABEL_CLASS[tier]).toBeTruthy();
expect(VALUE_TIER_VALUE_CLASS[tier]).toBeTruthy();
}
expect(VALUE_TIER_VALUE_CLASS.explicitCustom).toBe("text-panel-accent");
expect(VALUE_TIER_VALUE_CLASS.explicitCustom).toBe("text-accent-ink");
});
});
@@ -24,5 +24,5 @@ export const VALUE_TIER_LABEL_CLASS: Record<PropertyValueTier, string> = {
export const VALUE_TIER_VALUE_CLASS: Record<PropertyValueTier, string> = {
default: "text-panel-text-3",
explicitDefault: "text-panel-text-0",
explicitCustom: "text-panel-accent",
explicitCustom: "text-accent-ink",
};
@@ -1,4 +1,5 @@
import { memo, useState, useCallback, useRef, useEffect } from "react";
import { toastSurface } from "../StudioToast";
import {
trackStudioFeedback,
trackStudioFeedbackShown,
@@ -212,24 +213,11 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
}}
>
<div
className="flex w-[min(340px,calc(100vw-48px))] flex-col gap-2.5 overflow-hidden rounded-2xl px-3.5 py-3"
style={{
background: failed
? "linear-gradient(135deg, rgba(127,29,29,0.55), rgba(80,10,10,0.45))"
: "linear-gradient(135deg, rgba(38,38,38,0.55), rgba(23,23,23,0.45))",
backdropFilter: "blur(16px) saturate(1.6)",
WebkitBackdropFilter: "blur(16px) saturate(1.6)",
border: `1px solid ${failed ? "rgba(239,68,68,0.18)" : "rgba(255,255,255,0.08)"}`,
boxShadow: [
"0 8px 32px rgba(0,0,0,0.35)",
`inset 0 1px 0 ${failed ? "rgba(239,68,68,0.12)" : "rgba(255,255,255,0.06)"}`,
"inset 0 -1px 0 rgba(0,0,0,0.15)",
].join(", "),
}}
className={`flex w-[min(340px,calc(100vw-48px))] flex-col gap-2.5 overflow-hidden rounded-2xl px-3.5 py-3 ${toastSurface(failed)}`}
>
<div className="flex items-start gap-3">
<p
className={`min-w-0 flex-1 text-[12px] leading-5 ${failed ? "text-red-100" : "text-neutral-100"}`}
className={`min-w-0 flex-1 text-[12px] leading-5 ${failed ? "text-danger-ink" : "text-text-0"}`}
>
{step === "thanks" ? "Thanks, that helps." : title}
</p>
@@ -243,7 +231,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
if (req) trackStudioFeedbackInterviewClick({ reason: req.reason });
close("sent");
}}
className="h-6 shrink-0 whitespace-nowrap rounded-full border border-white/20 bg-white/8 px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-white/35 hover:bg-white/16 active:scale-[0.97] motion-reduce:transition-none"
className="h-6 shrink-0 whitespace-nowrap rounded-full border border-border-strong bg-hover px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-text-muted hover:bg-press active:scale-[0.97] motion-reduce:transition-none"
>
Talk to us, 30 min
</a>
@@ -251,7 +239,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
<button
type="button"
onClick={() => close("close")}
className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-white/10 hover:text-neutral-200"
className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-press hover:text-neutral-200"
aria-label="Dismiss"
>
<svg
@@ -270,7 +258,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
{/* The failure text the user already saw, quoted back so the report
carries it without asking them to retype the error. */}
{failed && request.detail && step !== "thanks" && (
<p className="line-clamp-2 text-[11px] leading-4 text-red-200/70">{request.detail}</p>
<p className="line-clamp-2 text-[11px] leading-4 text-danger-ink">{request.detail}</p>
)}
{step === "rating" && (
@@ -283,7 +271,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
key={n}
// A resting fill, so the row reads as eleven controls rather
// than as a line of text that happens to be clickable.
className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-white/4 text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-white/[0.14] hover:text-neutral-100 active:scale-[0.97] has-checked:bg-white/90 has-checked:text-neutral-900 has-focus-visible:ring-1 has-focus-visible:ring-white/50 motion-reduce:transition-none"
className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-hover text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-press hover:text-neutral-100 active:scale-[0.97] has-checked:bg-text-0 has-checked:text-bg-0 has-focus-visible:ring-1 has-focus-visible:ring-ring motion-reduce:transition-none"
>
<input
type="radio"
@@ -328,8 +316,8 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
// attached to nothing.
className={`h-6 rounded-full border px-2.5 text-[11px] transition-[background-color,border-color,color,transform] duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none ${
failed
? "border-red-300/20 bg-red-100/6 text-red-100/80 hover:border-red-200/70 hover:bg-red-100/25 hover:text-white"
: "border-white/10 bg-white/4 text-neutral-300 hover:border-white/60 hover:bg-white/25 hover:text-white"
? "border-danger/30 bg-danger/10 text-danger-ink hover:border-danger/60 hover:bg-danger/20"
: "border-border bg-hover text-neutral-300 hover:border-text-muted hover:bg-press hover:text-text-0"
}`}
>
{preset.label}
@@ -355,12 +343,12 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
placeholder={followUp.placeholder}
aria-label={followUp.prompt}
maxLength={500}
className="h-7 min-w-0 flex-1 rounded-md border border-white/10 bg-black/20 px-2 text-[11px] text-neutral-100 outline-hidden transition-colors duration-150 placeholder:text-neutral-500 focus:border-white/25"
className="h-7 min-w-0 flex-1 rounded-md border border-border bg-input px-2 text-[11px] text-neutral-100 outline-hidden transition-colors duration-150 placeholder:text-neutral-500 focus:border-ring"
/>
<button
type="button"
onClick={() => submit()}
className="h-7 shrink-0 rounded-md bg-white/10 px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-white/16 active:scale-[0.97] motion-reduce:transition-none"
className="h-7 shrink-0 rounded-md bg-press px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-on active:scale-[0.97] motion-reduce:transition-none"
>
Send
</button>
@@ -142,7 +142,7 @@ export function AssetPreviewOverlay() {
>
{/* Close button */}
<button
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-300 hover:text-white flex items-center justify-center transition-colors z-10"
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-raised hover:bg-press text-text-2 hover:text-text-0 flex items-center justify-center transition-colors z-10"
onClick={(e) => {
e.stopPropagation();
clearPreviewAsset();
@@ -215,7 +215,7 @@ function useMeterLoop(
function Bar({ maskRef, peakRef }: { maskRef: Ref<HTMLDivElement>; peakRef: Ref<HTMLDivElement> }) {
return (
<div className="relative h-full w-[18px] overflow-hidden rounded-[2px] bg-neutral-900">
<div className="scheme-dark relative h-full w-[18px] overflow-hidden rounded-[2px] bg-neutral-900">
<div
className="absolute inset-x-0 bottom-0 bg-green-500"
style={{ height: `${AMBER_AT * 100}%` }}
@@ -237,7 +237,7 @@ function Bar({ maskRef, peakRef }: { maskRef: Ref<HTMLDivElement>; peakRef: Ref<
<div
ref={peakRef}
data-testid="meter-peak"
className="absolute inset-x-0 bottom-0 h-px bg-white"
className="absolute inset-x-0 bottom-0 h-px bg-text-0"
/>
</div>
);
@@ -152,7 +152,7 @@ export function PreviewOverlays({
<button
type="button"
onClick={enterCaptionMode}
className="pointer-events-auto absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-studio-accent focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
className="pointer-events-auto absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-accent-ink focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
>
Edit captions
</button>
@@ -116,9 +116,9 @@ export function PreviewPane({
<div
ref={containerRef}
// A recessed well inset in its dock card; dropped in fullscreen so the
// preview fills the screen edge-to-edge.
className={`flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
isFullscreen ? "" : "m-3 rounded-md border border-neutral-800/50"
// preview fills the screen edge-to-edge. The player stays dark in both themes.
className={`scheme-dark flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
isFullscreen ? "" : "m-3 rounded-md border border-border"
}`}
data-studio-fullscreen-target=""
>
@@ -85,7 +85,7 @@ export function PreviewZoomOverlay({
</span>
<button
type="button"
className="rounded px-1.5 py-0.5 font-medium text-studio-accent hover:bg-white/10 transition-colors"
className="rounded px-1.5 py-0.5 font-medium text-accent-ink hover:bg-white/10 transition-colors"
onClick={onFit}
aria-label="Fit the whole frame in view"
data-testid="preview-zoom-fit"
@@ -413,7 +413,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
return (
<div
className="flex flex-col h-full overflow-y-auto text-white"
className="flex flex-col h-full overflow-y-auto text-text-0"
onKeyDown={(e) => {
// In-panel undo — scoped so it never fights the app-level file undo.
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z") {
@@ -428,14 +428,14 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
{persistError && (
<div
role="alert"
className="flex items-center justify-between gap-2 px-3 py-2 bg-red-950/40 border-b border-red-500/40"
className="flex items-center justify-between gap-2 px-3 py-2 bg-danger/15 border-b border-red-500/40"
>
<span className="text-[11px] text-red-300">Changes not saved</span>
<span className="text-[11px] text-danger-ink">Changes not saved</span>
<button
type="button"
disabled={retrying}
onClick={handleRetryPersist}
className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white enabled:hover:bg-red-500 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger enabled:hover:brightness-95 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
>
{retrying ? "Retrying…" : "Retry"}
</button>
@@ -72,9 +72,7 @@ export function SlideList({
tabIndex={0}
aria-pressed={isSelected}
className={`flex items-center gap-2 px-3 py-1.5 rounded cursor-pointer text-[11px] transition-colors ${
isSelected
? "bg-studio-accent/20 text-white"
: "hover:bg-neutral-800/60 text-neutral-300"
isSelected ? "bg-on text-text-0" : "hover:bg-neutral-800/60 text-neutral-300"
}`}
onClick={() => onSelect(scene.id)}
onKeyDown={(e) => {
@@ -100,7 +98,7 @@ export function SlideList({
aria-label="Move slide up"
title="Move up"
disabled={slideIndex <= 0}
className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-white enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-text-0 enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
onClick={(e) => {
e.stopPropagation();
onReorder(scene.id, "up");
@@ -113,7 +111,7 @@ export function SlideList({
aria-label="Move slide down"
title="Move down"
disabled={slideIndex === slides.length - 1}
className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-white enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-text-0 enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
onClick={(e) => {
e.stopPropagation();
onReorder(scene.id, "down");
@@ -162,7 +160,7 @@ export function SlideInspector({
<div className="flex flex-col gap-1">
<label className="text-[11px] text-neutral-400">Notes</label>
<textarea
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-white resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-text-0 resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
rows={3}
placeholder="Speaker notes or script..."
value={slide?.notes ?? ""}
@@ -174,7 +172,7 @@ export function SlideInspector({
<span className="text-[11px] text-neutral-400">Fragment hold-points</span>
<button
type="button"
className="text-[10px] px-2 py-0.5 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-neutral-200 transition-colors"
className="text-[10px] px-2 py-0.5 rounded-sm bg-hover hover:bg-press text-neutral-200 transition-colors"
onClick={onMarkFragment}
title={`Mark ${currentTime.toFixed(2)}s as hold-point`}
>
@@ -192,7 +190,7 @@ export function SlideInspector({
<button
type="button"
aria-label={`Remove fragment at ${t.toFixed(2)}s`}
className="text-neutral-400 hover:text-red-400 transition-colors"
className="text-neutral-400 hover:text-danger-ink transition-colors"
onClick={() => onRemoveFragment(t)}
>
×
@@ -249,7 +247,7 @@ export function BranchTree({
<input
id={inputId}
type="text"
className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
placeholder="New branch name..."
value={newLabel}
onChange={(e) => setNewLabel(e.target.value)}
@@ -260,7 +258,7 @@ export function BranchTree({
/>
<button
type="button"
className="px-2 py-1 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-[11px] text-neutral-200 transition-colors shrink-0"
className="px-2 py-1 rounded-sm bg-hover hover:bg-press text-[11px] text-neutral-200 transition-colors shrink-0"
onClick={handleCreate}
>
Add
@@ -326,7 +324,7 @@ function BranchItem({
<div className="flex items-center gap-1">
{editing ? (
<input
className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
value={draft}
autoFocus
onChange={(e) => setDraft(e.target.value)}
@@ -341,7 +339,7 @@ function BranchItem({
<span
role="button"
tabIndex={0}
className="flex-1 text-[11px] text-white font-medium truncate cursor-pointer hover:text-neutral-300"
className="flex-1 text-[11px] text-text-0 font-medium truncate cursor-pointer hover:text-neutral-300"
title="Click to rename"
onClick={() => setEditing(true)}
onKeyDown={(e) => {
@@ -357,15 +355,15 @@ function BranchItem({
<button
type="button"
aria-label={`Delete branch ${seq.label}`}
className="text-[10px] text-neutral-500 hover:text-red-400 active:scale-[0.95] transition-colors px-1"
className="text-[10px] text-neutral-500 hover:text-danger-ink active:scale-[0.95] transition-colors px-1"
onClick={() => setConfirmingDelete(true)}
>
✕
</button>
</div>
{confirmingDelete && (
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
<span className="text-[10px] text-red-400">
<div className="px-2 py-1.5 bg-danger/15 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
<span className="text-[10px] text-danger-ink">
Delete branch &ldquo;{seq.label}&rdquo;
{seq.slides.length > 0
? ` and its ${seq.slides.length} slide${seq.slides.length === 1 ? "" : "s"}`
@@ -379,7 +377,7 @@ function BranchItem({
setConfirmingDelete(false);
onDelete(seq.id);
}}
className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger hover:brightness-95 active:brightness-90 transition-colors"
>
Delete
</button>
@@ -414,7 +412,7 @@ function BranchItem({
type="button"
aria-pressed={isSelected}
className={`flex-1 text-left truncate transition-colors hover:text-neutral-200 ${
isSelected ? "text-white" : "text-neutral-400"
isSelected ? "text-text-0" : "text-neutral-400"
}`}
onClick={() => onSelectBranchSlide(seq.id, scene.id)}
>
@@ -497,7 +495,7 @@ export function HotspotTool({
<label className="text-[11px] text-neutral-400">Hotspot label</label>
<input
type="text"
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
placeholder="Button label..."
value={hotspotLabel}
onChange={(e) => setHotspotLabel(e.target.value)}
@@ -505,7 +503,7 @@ export function HotspotTool({
/>
<label className="text-[11px] text-neutral-400">Target branch</label>
<select
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
value={targetSequenceId}
onChange={(e) => setTargetSequenceId(e.target.value)}
aria-label="Target branch sequence"
@@ -527,7 +525,7 @@ export function HotspotTool({
? "Choose a target branch first"
: undefined
}
className="px-3 py-1.5 rounded-sm bg-studio-accent/80 enabled:hover:bg-studio-accent enabled:active:scale-[0.98] text-white text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
className="px-3 py-1.5 rounded-sm bg-accent enabled:hover:bg-accent-hover enabled:active:scale-[0.98] text-on-accent text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
onClick={handleMakeHotspot}
>
Make hotspot
@@ -550,7 +548,7 @@ export function HotspotTool({
<button
type="button"
aria-label={`Remove hotspot ${h.label}`}
className="text-[10px] text-neutral-500 hover:text-red-400 transition-colors"
className="text-[10px] text-neutral-500 hover:text-danger-ink transition-colors"
onClick={() => onRemoveHotspot(selectedSceneId, h.id)}
>
✕
@@ -215,7 +215,7 @@ export function VariablesBindElement({
className={`${VARIABLES_INPUT_CLASS} font-mono`}
/>
{existingDecl && (
<p className="text-[9px] leading-snug text-amber-400/90">
<p className="text-[9px] leading-snug text-warning-ink">
"{trimmedId}" already exists. This element will use its current value
{existingDecl.default !== undefined && (
<span className="font-mono"> ({String(existingDecl.default)})</span>
@@ -299,7 +299,7 @@ export function DeclarationForm({
className={VARIABLES_INPUT_CLASS}
/>
</Field>
{error && <p className="text-[10px] text-red-400">{error}</p>}
{error && <p className="text-[10px] text-danger-ink">{error}</p>}
<div className="flex items-center justify-end gap-2 pt-1">
<button
type="button"
@@ -56,9 +56,9 @@ function formatIssue(issue: VariableValidationIssue): string {
function ValidationStrip({ issues }: { issues: VariableValidationIssue[] }) {
if (issues.length === 0) return null;
return (
<div className="space-y-1 rounded-lg border border-red-900/60 bg-red-950/30 p-2">
<div className="space-y-1 rounded-lg border border-danger/40 bg-danger/15 p-2">
{issues.map((issue) => (
<p key={`${issue.kind}:${issue.variableId}`} className="text-[10px] text-red-300">
<p key={`${issue.kind}:${issue.variableId}`} className="text-[10px] text-danger-ink">
{formatIssue(issue)}
</p>
))}
@@ -99,7 +99,7 @@ function VariableRow({
</span>
{unused && (
<span
className="rounded-sm bg-amber-900/40 px-1 py-px text-[8px] text-amber-400"
className="rounded-sm bg-amber-500/15 px-1 py-px text-[8px] text-warning-ink"
title="No script reads this variable"
>
unused
@@ -175,9 +175,7 @@ function PreviewModeHeader({
<span className="text-[11px] font-semibold text-neutral-200">Variables</span>
<span
className={`rounded-full px-2 py-0.5 text-[9px] font-medium ${
hasOverrides
? "bg-studio-accent/20 text-studio-accent"
: "bg-neutral-800 text-neutral-500"
hasOverrides ? "bg-studio-accent/20 text-accent-ink" : "bg-neutral-800 text-neutral-500"
}`}
>
{hasOverrides ? `Previewing ${overrideCount} custom` : "Previewing defaults"}
@@ -16,7 +16,7 @@ export function RowAction({
onClick={onClick}
title={title}
className={`h-5 rounded px-1.5 text-[9px] text-neutral-500 hover:bg-neutral-800 ${
danger ? "hover:text-red-400" : "hover:text-neutral-200"
danger ? "hover:text-danger-ink" : "hover:text-neutral-200"
}`}
>
{label}
@@ -67,7 +67,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
className="flex flex-col gap-2 rounded-xl border border-amber-500/30 bg-amber-500/10 p-2.5"
>
<div className="flex flex-col gap-0.5">
<span className="text-[11px] font-semibold text-amber-300">
<span className="text-[11px] font-semibold text-warning-ink">
{status.title ?? "FFmpeg not found"}
</span>
{/* text-2, not the panel's usual text-4 for secondary copy: the amber
@@ -81,7 +81,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
{status.command ? (
<div className="flex items-center gap-1.5">
<code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-black/40 px-2 py-1 text-[10px] text-panel-text-2">
<code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-input px-2 py-1 text-[10px] text-panel-text-2">
{status.command}
</code>
{/* Fixed width so swapping the label to "Copied" cannot shift the
@@ -89,7 +89,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
<button
type="button"
onClick={() => void copy(status.command ?? "")}
className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-amber-200 transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-warning-ink transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
>
{copied ? "Copied" : "Copy"}
</button>
@@ -107,7 +107,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
type="button"
onClick={onRecheck}
disabled={checking}
className={`rounded-xs py-0.5 text-[10px] font-medium text-amber-200 underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
className={`rounded-xs py-0.5 text-[10px] font-medium text-warning-ink underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
>
{checking ? "Checking…" : "Recheck"}
</button>
@@ -467,7 +467,7 @@ export const RenderQueue = memo(function RenderQueue({
role="alert"
className="flex items-start justify-between gap-2 px-3 py-2 border-b border-border bg-danger/10"
>
<span className="text-step-10 text-danger">{actionError}</span>
<span className="text-step-10 text-danger-ink">{actionError}</span>
{onDismissActionError && (
<IconButton
size="sm"
@@ -497,7 +497,7 @@ export const RenderQueue = memo(function RenderQueue({
<div ref={listRef} className="flex-1 overflow-y-auto">
{loadError && jobs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full px-4 gap-2" role="alert">
<p className="text-step-10 text-danger text-center">{loadError}</p>
<p className="text-step-10 text-danger-ink text-center">{loadError}</p>
{onRetryLoad && (
<Button size="sm" variant="secondary" onClick={onRetryLoad}>
Retry
@@ -115,7 +115,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
)}
{job.status === "failed" && (
<div className="w-full h-full flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-danger" />
<div className="w-2 h-2 rounded-full bg-danger-ink" />
</div>
)}
{job.status === "cancelled" && (
@@ -140,7 +140,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
<div className="mt-1">
<div className="flex items-center justify-between mb-0.5">
<span className="text-step-9 text-text-4">{job.stage || "Rendering"}</span>
<span className="text-step-9 font-mono text-accent">{job.progress}%</span>
<span className="text-step-9 font-mono text-accent-ink">{job.progress}%</span>
</div>
<div
className="w-full h-1 bg-border rounded-full overflow-hidden"
@@ -159,7 +159,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
)}
{job.status === "failed" && job.error && (
<span className="text-step-9 text-danger mt-0.5 block">{job.error}</span>
<span className="text-step-9 text-danger-ink mt-0.5 block">{job.error}</span>
)}
{job.status === "cancelled" && (
<span className="text-step-9 text-text-4 mt-0.5 block">Cancelled</span>
@@ -31,7 +31,7 @@ function CopyChip({ feedback, asset }: { feedback: CopyFeedback; asset: string }
<span
role="status"
className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
feedback.ok ? "text-panel-accent bg-panel-accent/10" : "text-red-400 bg-red-500/10"
feedback.ok ? "text-accent-ink bg-panel-accent/10" : "text-danger-ink bg-red-500/10"
}`}
>
{feedback.ok ? "Copied" : "Copy failed"}
@@ -355,7 +355,7 @@ export function FontRow({
}`}
>
<div className="w-[50px] h-[32px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 flex items-center justify-center">
<span className="text-[9px] font-medium text-neutral-700">{extension}</span>
<span className="text-[9px] font-medium text-text-muted">{extension}</span>
</div>
<div className="min-w-0 flex-1">
<span
@@ -366,7 +366,7 @@ export function FontRow({
<div className="flex items-center gap-1.5">
<span className="text-[10px] text-neutral-600 truncate">{extension}</span>
{used && (
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm">
<span className="text-[9px] font-medium text-accent-ink bg-panel-accent/10 px-1.5 py-px rounded-sm">
in use
</span>
)}
@@ -147,7 +147,7 @@ export function ContextMenu({
<button
role="menuitem"
onClick={() => setMode("confirm-delete")}
className={`${itemCls} text-red-400`}
className={`${itemCls} text-danger-ink`}
>
Delete
</button>
@@ -181,9 +181,9 @@ export function ContextMenu({
}
}}
aria-label={`Rename ${filename(asset)}`}
className="w-full bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
className="w-full bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-1 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
/>
{renameError && <span className="text-[10px] text-red-400">{renameError}</span>}
{renameError && <span className="text-[10px] text-danger-ink">{renameError}</span>}
<div className="flex items-center justify-end gap-1">
<button
onClick={() => setMode("menu")}
@@ -193,7 +193,7 @@ export function ContextMenu({
</button>
<button
onClick={commitRename}
className="px-2 py-0.5 text-[10px] rounded-sm bg-studio-accent/80 hover:bg-studio-accent text-white transition-colors"
className="px-2 py-0.5 text-[10px] rounded-sm bg-accent hover:bg-accent-hover text-on-accent transition-colors"
>
Rename
</button>
@@ -215,12 +215,12 @@ function DeleteConfirm({
onCancel: () => void;
}) {
return (
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
<span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
<div className="px-2 py-1.5 bg-danger/15 border-l-2 border-red-500 flex items-center justify-between gap-2">
<span className="text-[10px] text-danger-ink truncate">Delete {name}?</span>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={onConfirm}
className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger hover:brightness-95 active:brightness-90 transition-colors"
>
Delete
</button>
@@ -177,7 +177,7 @@ function EmptyState({
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-neutral-700"
className="text-text-off"
>
<path
d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"
@@ -349,7 +349,7 @@ export const AssetsTab = memo(function AssetsTab({
onClick={() => setViewMode(m)}
className={`flex-1 px-2 py-1 text-[11px] font-medium rounded transition-colors ${
viewMode === m
? "bg-panel-accent/15 text-panel-accent"
? "bg-panel-accent/15 text-accent-ink"
: "text-panel-text-3 hover:text-panel-text-1"
}`}
>
@@ -396,7 +396,7 @@ export const AssetsTab = memo(function AssetsTab({
aria-pressed={activeFilter === "all"}
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors active:scale-[0.98] ${
activeFilter === "all"
? "bg-panel-accent/15 text-panel-accent"
? "bg-panel-accent/15 text-accent-ink"
: "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
}`}
>
@@ -410,7 +410,7 @@ export const AssetsTab = memo(function AssetsTab({
aria-pressed={activeFilter === cat}
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors active:scale-[0.98] ${
activeFilter === cat
? "bg-panel-accent/15 text-panel-accent"
? "bg-panel-accent/15 text-accent-ink"
: "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
}`}
>
@@ -425,7 +425,7 @@ export const AssetsTab = memo(function AssetsTab({
onClick={() => setUsageFilter(usageFilter === "used" ? "all" : "used")}
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors ${
usageFilter === "used"
? "bg-panel-accent/15 text-panel-accent"
? "bg-panel-accent/15 text-accent-ink"
: "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
}`}
>
@@ -435,7 +435,7 @@ export const AssetsTab = memo(function AssetsTab({
onClick={() => setUsageFilter(usageFilter === "unused" ? "all" : "unused")}
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors ${
usageFilter === "unused"
? "bg-panel-accent/15 text-panel-accent"
? "bg-panel-accent/15 text-accent-ink"
: "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
}`}
>
@@ -240,7 +240,7 @@ export function AudioRow({
aria-pressed={playing}
className={`w-7 h-7 rounded-md shrink-0 flex items-center justify-center transition-colors active:scale-[0.95] ${
playing
? "bg-panel-accent/15 text-panel-accent"
? "bg-panel-accent/15 text-accent-ink"
: "text-panel-text-5 group-hover:text-panel-text-3"
}`}
onClick={(e) => {
@@ -273,7 +273,7 @@ export function AudioRow({
</span>
)}
{used && (
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm shrink-0">
<span className="text-[9px] font-medium text-accent-ink bg-panel-accent/10 px-1.5 py-px rounded-sm shrink-0">
in use
</span>
)}
@@ -281,7 +281,9 @@ export function AudioRow({
<span
role="status"
className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
copyFailed ? "text-red-400 bg-red-500/10" : "text-panel-accent bg-panel-accent/10"
copyFailed
? "text-danger-ink bg-red-500/10"
: "text-accent-ink bg-panel-accent/10"
}`}
>
{copyFailed ? "Copy failed" : "Copied"}
@@ -40,7 +40,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
if (error) {
return (
<div className="flex flex-1 items-center justify-center text-red-400 text-xs px-4 text-center">
<div className="flex flex-1 items-center justify-center text-danger-ink text-xs px-4 text-center">
{error}
</div>
);
@@ -77,9 +77,9 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
{/* Block grid */}
<div className="flex-1 overflow-y-auto min-h-0 px-2 pb-2">
{category === "vfx" && (
<div className="mb-2 px-2 py-1.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-[9px] text-purple-300 leading-relaxed">
<div className="mb-2 px-2 py-1.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-[9px] text-text-2 leading-relaxed">
VFX blocks use WebGL via HTML-in-Canvas. Enable{" "}
<span className="font-mono text-purple-200">chrome://flags/#html-in-canvas</span> for
<span className="font-mono text-text-0">chrome://flags/#html-in-canvas</span> for
preview.
</div>
)}
@@ -423,8 +423,8 @@ function BlockCard({
title="Add to composition at current time"
className={`flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-semibold transition-colors active:scale-[0.97] ${
addState === "failed"
? "bg-red-500 text-white"
: "bg-white text-black hover:bg-neutral-200"
? "bg-danger text-on-danger"
: "bg-white text-black hover:bg-white/85"
}`}
>
<svg
@@ -453,7 +453,7 @@ function BlockCard({
className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors active:scale-[0.97] ${
onAdd
? "bg-white/15 text-white/90 hover:bg-white/25 text-[9px]"
: "bg-white text-black hover:bg-neutral-200 text-[10px] font-semibold"
: "bg-text-0 text-bg-0 hover:bg-text-2 text-[10px] font-semibold"
}`}
>
<svg
@@ -373,7 +373,7 @@ function CompCard({
{lintInfo && lintInfo.count > 0 && (
<span
aria-label={`${lintInfo.count} lint finding${lintInfo.count === 1 ? "" : "s"}`}
className="shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400"
className="shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-warning-ink"
>
{lintInfo.count}
</span>
@@ -390,7 +390,7 @@ function CompCard({
event.stopPropagation();
onAddToTimeline();
}}
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-neutral-600 opacity-0 transition-[color,background-color,opacity] hover:bg-neutral-800 hover:text-studio-accent group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-neutral-600 opacity-0 transition-[color,background-color,opacity] hover:bg-neutral-800 hover:text-accent-ink group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
>
<span aria-hidden="true">+</span>
</button>
@@ -411,7 +411,7 @@ function CompCard({
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors ${
isRendering
? "text-neutral-600 cursor-not-allowed"
: "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
: "text-neutral-600 hover:text-accent-ink hover:bg-neutral-800"
}`}
>
<svg
@@ -106,10 +106,10 @@ export function PromptPreviewModal({
<button
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-colors active:scale-[0.97] ${
copyState === "copied"
? "bg-emerald-500 text-white"
? "bg-accent text-on-accent"
: copyState === "failed"
? "bg-red-500 text-white"
: "bg-studio-accent/90 text-neutral-950 hover:bg-studio-accent"
? "bg-danger text-on-danger"
: "bg-accent text-on-accent hover:bg-accent-hover"
}`}
onClick={handleCopy}
>
@@ -17,7 +17,7 @@ export function SidebarLintButton({
type="button"
onClick={onLint}
disabled={linting}
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 enabled:hover:text-amber-300 enabled:hover:bg-neutral-800 enabled:active:scale-[0.98] transition-colors disabled:opacity-40"
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 enabled:hover:text-warning-ink enabled:hover:bg-neutral-800 enabled:active:scale-[0.98] transition-colors disabled:opacity-40"
>
<svg
width="12"
@@ -38,8 +38,8 @@ export function SidebarLintButton({
style={hasError ? { animationIterationCount: ERROR_PULSES_BEFORE_IDLE } : undefined}
className={
hasError
? "ml-1 min-w-[16px] rounded-full bg-panel-danger/25 px-1 text-[9px] font-bold text-panel-danger animate-pulse motion-reduce:animate-none"
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-amber-400"
? "ml-1 min-w-[16px] rounded-full bg-danger px-1 text-[9px] font-bold text-on-danger animate-pulse motion-reduce:animate-none"
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-warning-ink"
}
>
{findingCount}
@@ -1,5 +1,5 @@
// CapCut-flat icon buttons: a transparent 28px hit area with a subtle hover wash.
export const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
export const flatIdle = `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-neutral-200 active:scale-[0.98]`;
export const flatActive = `${flatBtn} bg-white/8 text-neutral-100 active:scale-[0.98]`;
export const flatDisabled = `${flatBtn} text-neutral-700 cursor-not-allowed`;
export const flatIdle = `${flatBtn} text-text-2 hover:bg-hover hover:text-text-0 active:bg-press active:scale-[0.98]`;
export const flatActive = `${flatBtn} bg-on text-text-0 hover:bg-on-hover active:scale-[0.98]`;
export const flatDisabled = `${flatBtn} text-text-off cursor-not-allowed`;
+4 -4
View File
@@ -50,8 +50,8 @@ export const buttonBase = cn(
*/
export const buttonVariants: Record<ButtonVariant, string> = {
primary: cn(
"bg-accent text-bg-0 font-semibold",
"enabled:hover:brightness-110 data-[preview-state=hover]:brightness-110",
"bg-accent text-on-accent font-semibold",
"enabled:hover:bg-accent-hover data-[preview-state=hover]:bg-accent-hover",
"enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
),
secondary: cn(
@@ -60,8 +60,8 @@ export const buttonVariants: Record<ButtonVariant, string> = {
"enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
),
danger: cn(
"bg-danger text-text-0 font-medium",
"enabled:hover:brightness-110 data-[preview-state=hover]:brightness-110",
"bg-danger text-on-danger font-medium",
"enabled:hover:brightness-95 data-[preview-state=hover]:brightness-95",
"enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
),
ghost: cn(
@@ -1,6 +1,6 @@
export function HyperframesLogo() {
// Full logo from logo-dark.svg (263×79): heygen label + gradient mark + hyperframes wordmark.
// All fill="black" paths inverted to white for the dark header.
// The wordmark paths take the text colour, so the logo reads in both themes.
const height = 28;
const width = Math.round(height * (263 / 79));
return (
@@ -9,6 +9,7 @@ export function HyperframesLogo() {
height={height}
viewBox="0 0 263 79"
fill="none"
className="text-text-0"
xmlns="http://www.w3.org/2000/svg"
aria-label="Hyperframes"
>
@@ -39,27 +40,27 @@ export function HyperframesLogo() {
{/* heygen label */}
<path
d="M0 16.6738H4.96V23.3838H11.53V16.6738H16.49V35.4538H11.53V27.6738H4.96V35.4538H0V16.6738Z"
fill="white"
fill="currentColor"
/>
<path
d="M17.8899 28.0137C17.8899 23.4237 21.2399 20.0237 25.7999 20.0237C30.0099 20.0237 32.9399 23.0837 32.9399 27.5637C32.9399 28.2037 32.8599 28.8237 32.8299 29.3337H22.6099C23.0399 31.1337 24.6199 32.1237 27.1399 32.1237C28.7199 32.1237 30.2199 31.7237 31.2999 31.0237V34.6737C30.1699 35.3937 28.2999 35.8537 26.3599 35.8537C21.2599 35.8537 17.8799 32.6037 17.8799 28.0237L17.8899 28.0137ZM28.5699 26.1937C28.4899 24.5337 27.3899 23.5637 25.7499 23.5637C24.1099 23.5637 22.9599 24.5337 22.6099 26.1937H28.5699Z"
fill="white"
fill="currentColor"
/>
<path
d="M37.85 33.8638L32 20.4238H37.29L40.46 28.1538L43.6 20.4238H48.48L40.33 39.5738H35.26L37.86 33.8638H37.85Z"
fill="white"
fill="currentColor"
/>
<path
d="M48.1797 26.1138C48.1797 20.4238 52.7097 16.2438 58.9397 16.2438C61.2997 16.2438 63.5297 16.8038 64.8197 17.7238V21.9338C63.5597 20.9138 61.6797 20.2938 59.7497 20.2938C55.7797 20.2938 53.2597 22.6238 53.2597 26.1438C53.2597 29.6638 55.5697 31.8838 58.8897 31.8838C59.6397 31.8838 60.4997 31.7538 61.1997 31.5038V27.8838H57.4697V24.2338H65.5197V33.7038C63.6697 35.0938 61.1997 35.8738 58.5997 35.8738C52.5397 35.8738 48.1897 31.7438 48.1897 26.1138H48.1797Z"
fill="white"
fill="currentColor"
/>
<path
d="M66.6604 28.0137C66.6604 23.4237 70.0104 20.0237 74.5704 20.0237C78.7804 20.0237 81.7104 23.0837 81.7104 27.5637C81.7104 28.2037 81.6304 28.8237 81.6004 29.3337H71.3804C71.8104 31.1337 73.3904 32.1237 75.9104 32.1237C77.4904 32.1237 78.9904 31.7237 80.0704 31.0237V34.6737C78.9404 35.3937 77.0704 35.8537 75.1304 35.8537C70.0304 35.8537 66.6504 32.6037 66.6504 28.0237L66.6604 28.0137ZM77.3404 26.1937C77.2604 24.5337 76.1604 23.5637 74.5204 23.5637C72.8804 23.5637 71.7304 24.5337 71.3804 26.1937H77.3404Z"
fill="white"
fill="currentColor"
/>
<path
d="M82.9688 20.4238H87.8488V22.4138C88.7588 20.9638 90.3488 20.1038 92.3288 20.1038C95.4988 20.1038 97.3988 22.3338 97.3988 25.8138V35.4438H92.5188V26.8638C92.5188 25.0438 91.8188 24.1838 90.4788 24.1838C88.9488 24.1838 87.8488 25.3638 87.8488 27.2638V35.4438H82.9688V20.4238Z"
fill="white"
fill="currentColor"
/>
{/* gradient icon mark */}
<path
@@ -73,47 +74,47 @@ export function HyperframesLogo() {
{/* hyperframes wordmark */}
<path
d="M0 71.9996V42.7256H7.7367V53.1806H17.9826V42.7256H25.7193V71.9996H17.9826V59.8718H7.7367V71.9996H0Z"
fill="white"
fill="currentColor"
/>
<path
d="M31.9606 78.4405L36.0171 69.5329L26.9004 48.5811H35.1389L40.0737 60.6252L44.9666 48.5811H52.5779L39.8646 78.4405H31.9606Z"
fill="white"
fill="currentColor"
/>
<path
d="M53.3711 78.4404V48.5809H60.9823V52.052C62.446 49.501 65.0807 48.0791 68.3008 48.0791C74.3647 48.0791 78.9649 53.3066 78.9649 60.2487C78.9649 67.2745 74.3229 72.4602 68.259 72.4602C65.0807 72.4602 62.446 71.1637 60.9823 68.78V78.4404H53.3711ZM66.1262 66.1872C69.2627 66.1872 71.3119 63.7616 71.3119 60.2905C71.3119 56.8195 69.2627 54.4357 66.1262 54.4357C62.7388 54.4357 60.9823 57.2795 60.9823 59.7051V60.9178C60.9823 63.3434 62.7388 66.1872 66.1262 66.1872Z"
fill="white"
fill="currentColor"
/>
<path
d="M93.4397 72.6269C85.4939 72.6269 80.2246 67.5667 80.2246 60.4155C80.2246 53.2643 85.4521 47.9531 92.5615 47.9531C99.1273 47.9531 103.686 52.7206 103.686 59.7045C103.686 60.7082 103.56 61.6701 103.518 62.4647H87.5849C88.2541 65.2666 90.7214 66.8139 94.6525 66.8139C97.1199 66.8139 99.4618 66.1866 101.135 65.0993V70.7868C99.3782 71.916 96.4508 72.6269 93.4397 72.6269ZM87.5849 57.5717H96.869C96.7435 54.9789 95.0289 53.4734 92.4779 53.4734C89.9687 53.4734 88.1286 54.9789 87.5849 57.5717Z"
fill="white"
fill="currentColor"
/>
<path
d="M105.645 72V48.5808H113.214V52.9719C114.218 50.0445 116.183 48.2881 118.692 48.2881C119.361 48.2881 119.989 48.4135 120.407 48.6645V55.8993C119.822 55.5648 119.111 55.4393 118.274 55.4393C115.138 55.4393 113.256 57.6558 113.256 61.2941V72H105.645Z"
fill="white"
fill="currentColor"
/>
<path
d="M122.643 71.9996V42.7256H139.371V49.124H130.379V54.477H138.911V60.5827H130.379V71.9996H122.643Z"
fill="white"
fill="currentColor"
/>
<path
d="M141.258 72V48.5808H148.827V52.9719C149.831 50.0445 151.796 48.2881 154.306 48.2881C154.975 48.2881 155.602 48.4135 156.02 48.6645V55.8993C155.435 55.5648 154.724 55.4393 153.887 55.4393C150.751 55.4393 148.869 57.6558 148.869 61.2941V72H141.258Z"
fill="white"
fill="currentColor"
/>
<path
d="M167.26 72.4602C161.154 72.4602 156.596 67.2745 156.596 60.3324C156.596 53.3066 161.196 48.0791 167.302 48.0791C170.438 48.0791 173.031 49.4173 174.536 51.8429V48.5809H182.148V72.0001H174.536V68.4873C173.031 71.0801 170.438 72.4602 167.26 72.4602ZM169.393 66.1872C172.78 66.1872 174.536 63.3434 174.536 60.876V59.6632C174.536 57.2377 172.78 54.4357 169.393 54.4357C166.298 54.4357 164.207 56.7777 164.207 60.2905C164.207 63.7616 166.298 66.1872 169.393 66.1872Z"
fill="white"
fill="currentColor"
/>
<path
d="M184.998 72.0001V48.5809H192.609V51.6756C193.989 49.4173 196.373 48.0791 199.426 48.0791C202.521 48.0791 204.779 49.5428 206.075 52.052C207.497 49.5846 210.132 48.0791 213.352 48.0791C218.245 48.0791 221.214 51.592 221.214 57.0286V72.0001H213.603V58.5341C213.603 55.774 212.557 54.4357 210.634 54.4357C208.459 54.4357 206.912 56.234 206.912 59.0778V72.0001H199.3V58.5341C199.3 55.774 198.255 54.4357 196.373 54.4357C194.157 54.4357 192.609 56.234 192.609 59.0778V72.0001H184.998Z"
fill="white"
fill="currentColor"
/>
<path
d="M236.338 72.6269C228.392 72.6269 223.123 67.5667 223.123 60.4155C223.123 53.2643 228.351 47.9531 235.46 47.9531C242.026 47.9531 246.584 52.7206 246.584 59.7045C246.584 60.7082 246.459 61.6701 246.417 62.4647H230.483C231.152 65.2666 233.62 66.8139 237.551 66.8139C240.018 66.8139 242.36 66.1866 244.033 65.0993V70.7868C242.277 71.916 239.349 72.6269 236.338 72.6269ZM230.483 57.5717H239.767C239.642 54.9789 237.927 53.4734 235.376 53.4734C232.867 53.4734 231.027 54.9789 230.483 57.5717Z"
fill="white"
fill="currentColor"
/>
<path
d="M252.115 72.502C250.192 72.502 248.268 72.0838 246.93 71.3728V65.6853C247.808 66.3126 248.937 66.689 250.066 66.689C251.739 66.689 252.784 65.8526 252.784 64.5562C252.784 63.8452 252.492 63.0925 251.655 62.0888L249.899 59.956C248.728 58.4923 248.226 56.9031 248.226 55.2721C248.226 51.0065 251.864 48.0791 256.883 48.0791C258.723 48.0791 260.438 48.4555 261.65 49.0828V54.7703C260.898 54.2266 259.768 53.8503 258.723 53.8503C257.092 53.8503 256.046 54.6867 256.046 55.8994C256.046 56.6522 256.423 57.405 257.259 58.4086L258.974 60.4996C260.187 62.047 260.73 63.5943 260.73 65.2671C260.73 69.5746 257.05 72.502 252.115 72.502Z"
fill="white"
fill="currentColor"
/>
</svg>
);
+1 -1
View File
@@ -23,7 +23,7 @@ export const fieldBase = cn(
// `aria-invalid` is not one of Tailwind's built-in aria variants, so the
// arbitrary form is the one that compiles. The token gate would have caught
// the bare `aria-invalid:` spelling as a class that resolves to nothing.
"aria-[invalid]:border-danger",
"aria-[invalid]:border-danger-ink",
"has-[:disabled]:opacity-40 has-[:disabled]:cursor-not-allowed",
);
+4 -2
View File
@@ -67,7 +67,9 @@ const itemBase = cn(
"data-[disabled]:pointer-events-none data-[disabled]:opacity-40",
);
const itemDanger = cn("text-danger data-[highlighted]:bg-danger/15 data-[highlighted]:text-danger");
const itemDanger = cn(
"text-danger-ink data-[highlighted]:bg-danger/15 data-[highlighted]:text-danger-ink",
);
export type MenuItemTone = "default" | "danger";
@@ -229,7 +231,7 @@ export function MenuCheckboxItem({
return (
<BaseMenu.CheckboxItem className={cn(itemBase, className)} {...props}>
<span className="truncate">{children}</span>
<span className="flex size-3 shrink-0 items-center justify-center text-accent">
<span className="flex size-3 shrink-0 items-center justify-center text-accent-ink">
<BaseMenu.CheckboxItemIndicator>✓</BaseMenu.CheckboxItemIndicator>
</span>
</BaseMenu.CheckboxItem>
@@ -201,7 +201,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
<button
type="button"
role="menuitem"
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-danger-ink hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
onClick={() => {
onDelete(element);
onClose();
@@ -194,7 +194,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
</div>
)}
{snapshot.status === "loading" && (
<div className="absolute inset-0 animate-pulse bg-white/[0.035]" />
<div className="absolute inset-0 animate-pulse bg-text-0/[0.035]" />
)}
{label && (
<div className="absolute inset-y-0 left-3 z-10 flex items-center">

Some files were not shown because too many files have changed in this diff Show More