From 5d77aa20af83377e8aa9d6c57eb49343d5c48416 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Wed, 30 Sep 2026 23:14:12 -0700 Subject: [PATCH 1/8] 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. --- .../components/CaptionAnimationPanel.tsx | 2 +- .../components/CaptionPropertyPanel.tsx | 4 +- .../studio/src/components/AskAgentModal.tsx | 2 +- packages/studio/src/components/LintModal.tsx | 12 +- .../src/components/StudioErrorBoundary.tsx | 2 +- .../src/components/StudioHeader.dom.test.tsx | 8 +- .../studio/src/components/StudioHeader.tsx | 3 +- .../studio/src/components/TimelineToolbar.tsx | 8 +- packages/studio/src/components/dock/Dock.tsx | 2 +- .../src/components/editor/AnimationCard.tsx | 4 +- .../components/editor/ComputedTweenNotice.tsx | 2 +- .../components/editor/DomEditRotateHandle.tsx | 2 +- .../components/editor/EaseModeControls.tsx | 2 +- .../editor/GestureRecordControl.tsx | 2 +- .../components/editor/InlineTextToolbar.tsx | 2 +- .../editor/InspectorHeaderActions.tsx | 2 +- .../src/components/editor/LayersPanel.tsx | 6 +- .../components/editor/PromotableControl.tsx | 4 +- .../editor/PropertyPanelEmptyState.tsx | 8 +- .../editor/PropertyPanelFlatFooter.tsx | 4 +- .../editor/PropertyPanelFlatHeader.test.tsx | 2 +- .../editor/PropertyPanelFlatHeader.tsx | 4 +- .../src/components/editor/SnapToolbar.tsx | 8 +- .../propertyPanelColorGradingSection.tsx | 2 +- .../propertyPanelColorGradingSlider.tsx | 2 +- .../editor/propertyPanelColorSecondary.tsx | 6 +- .../components/editor/propertyPanelFill.tsx | 2 +- .../propertyPanelFlatColorGradingSection.tsx | 4 +- .../propertyPanelFlatEffectsSection.tsx | 6 +- .../editor/propertyPanelFlatMediaSection.tsx | 2 +- .../propertyPanelFlatOverlaysSection.tsx | 2 +- .../propertyPanelFlatPrimitives.test.tsx | 4 +- .../editor/propertyPanelFxControls.tsx | 4 +- .../editor/propertyPanelFxEqModule.tsx | 4 +- .../editor/propertyPanelValueTier.test.ts | 2 +- .../editor/propertyPanelValueTier.ts | 2 +- .../src/components/nle/PreviewOverlays.tsx | 2 +- .../studio/src/components/nle/PreviewPane.tsx | 4 +- .../src/components/nle/PreviewZoomOverlay.tsx | 2 +- .../components/panels/SlideshowSubPanels.tsx | 2 +- .../src/components/panels/VariablesPanel.tsx | 4 +- .../src/components/renders/RenderQueue.tsx | 4 +- .../components/renders/RenderQueueItem.tsx | 4 +- .../src/components/sidebar/AssetCard.tsx | 4 +- .../components/sidebar/AssetContextMenu.tsx | 2 +- .../src/components/sidebar/AssetsTab.tsx | 10 +- .../src/components/sidebar/AudioRow.tsx | 6 +- .../components/sidebar/CompositionsTab.tsx | 4 +- .../components/sidebar/PromptPreviewModal.tsx | 2 +- .../components/sidebar/SidebarLintButton.tsx | 2 +- .../src/components/timelineToolbarStyles.ts | 6 +- packages/studio/src/components/ui/Button.tsx | 2 +- packages/studio/src/components/ui/Menu.tsx | 6 +- .../src/player/components/EditModal.tsx | 6 +- .../src/player/components/PlayerControls.tsx | 4 +- .../src/player/components/ShortcutsPanel.tsx | 9 +- .../src/player/components/TimelineCanvas.tsx | 2 +- .../src/player/components/TimelineClip.tsx | 4 +- .../player/components/TimelineEmptyState.tsx | 4 +- .../src/player/components/TimelineLanes.tsx | 1 - .../src/player/components/VolumeControl.tsx | 2 +- .../components/timelineMotionStyles.test.ts | 32 +- packages/studio/src/styles/hex-baseline.json | 4 +- packages/studio/src/styles/studio.css | 9 - .../src/styles/tailwind-preset.shared.js | 3 +- packages/studio/src/styles/theme.css | 314 ++++---- packages/studio/src/styles/theme.test.ts | 20 +- scripts/contrast-baseline.json | 14 +- scripts/contrast-pairs.json | 740 +++++++++++++++++- scripts/contrast.ts | 104 ++- 70 files changed, 1141 insertions(+), 334 deletions(-) diff --git a/packages/studio/src/captions/components/CaptionAnimationPanel.tsx b/packages/studio/src/captions/components/CaptionAnimationPanel.tsx index 6983b9ebef..92b170aeb3 100644 --- a/packages/studio/src/captions/components/CaptionAnimationPanel.tsx +++ b/packages/studio/src/captions/components/CaptionAnimationPanel.tsx @@ -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 diff --git a/packages/studio/src/captions/components/CaptionPropertyPanel.tsx b/packages/studio/src/captions/components/CaptionPropertyPanel.tsx index bab6f0001e..f1ea71f41c 100644 --- a/packages/studio/src/captions/components/CaptionPropertyPanel.tsx +++ b/packages/studio/src/captions/components/CaptionPropertyPanel.tsx @@ -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(" ")} > diff --git a/packages/studio/src/components/AskAgentModal.tsx b/packages/studio/src/components/AskAgentModal.tsx index 3236c5b1bc..ed555fbb2f 100644 --- a/packages/studio/src/components/AskAgentModal.tsx +++ b/packages/studio/src/components/AskAgentModal.tsx @@ -138,7 +138,7 @@ export function AskAgentModal({ {navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to copy diff --git a/packages/studio/src/components/StudioHeader.dom.test.tsx b/packages/studio/src/components/StudioHeader.dom.test.tsx index 45b926e910..fe4d01f8b7 100644 --- a/packages/studio/src/components/StudioHeader.dom.test.tsx +++ b/packages/studio/src/components/StudioHeader.dom.test.tsx @@ -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", () => { diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 3b9380fca1..a08acc5a49 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -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={} onClick={() => { diff --git a/packages/studio/src/components/TimelineToolbar.tsx b/packages/studio/src/components/TimelineToolbar.tsx index b4a532a2e2..77f311ee3d 100644 --- a/packages/studio/src/components/TimelineToolbar.tsx +++ b/packages/studio/src/components/TimelineToolbar.tsx @@ -309,9 +309,9 @@ export function TimelineToolbar({ ? flatDisabled : `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${ 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" }` } @@ -479,7 +479,7 @@ 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" + ? "bg-studio-accent/10 text-accent-ink" : "text-neutral-400 hover:bg-white/6 hover:text-neutral-200" }`} > @@ -494,7 +494,7 @@ 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" + ? "bg-studio-accent/10 text-accent-ink" : "text-neutral-400 hover:bg-white/6 hover:text-neutral-200" }`} > diff --git a/packages/studio/src/components/dock/Dock.tsx b/packages/studio/src/components/dock/Dock.tsx index 9ba5ab14b6..979c1784f8 100644 --- a/packages/studio/src/components/dock/Dock.tsx +++ b/packages/studio/src/components/dock/Dock.tsx @@ -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" > - {openPanels.has(id) ? "✓" : ""} + {openPanels.has(id) ? "✓" : ""} {PANEL_DEFINITIONS[id].title} ))} diff --git a/packages/studio/src/components/editor/AnimationCard.tsx b/packages/studio/src/components/editor/AnimationCard.tsx index 90ec786b2f..6a35b65cbc 100644 --- a/packages/studio/src/components/editor/AnimationCard.tsx +++ b/packages/studio/src/components/editor/AnimationCard.tsx @@ -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]" > {methodLabel} @@ -385,7 +385,7 @@ export const AnimationCard = memo(function AnimationCard({ )} {animation.method === "fromTo" && Object.keys(animation.properties).length > 0 && ( -

+

To

)} diff --git a/packages/studio/src/components/editor/ComputedTweenNotice.tsx b/packages/studio/src/components/editor/ComputedTweenNotice.tsx index 74e2517504..e2c59054d3 100644 --- a/packages/studio/src/components/editor/ComputedTweenNotice.tsx +++ b/packages/studio/src/components/editor/ComputedTweenNotice.tsx @@ -29,7 +29,7 @@ export function ComputedTweenNotice({ diff --git a/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx b/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx index d0eaa238dd..4890b7deb6 100644 --- a/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx +++ b/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx @@ -24,7 +24,7 @@ function FlatEmptyState() {
- ● + ● Record a gesture @@ -33,7 +33,7 @@ function FlatEmptyState() { - ✦ + ✦ Describe a change to the agent @@ -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 (
- +
diff --git a/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx b/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx index 14149a5b16..cf87656260 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx @@ -40,7 +40,7 @@ export function PropertyPanelFlatFooter({ height="13" viewBox="0 0 16 16" fill="currentColor" - className="text-panel-accent" + className="text-accent-ink" > @@ -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"} > {recording ? ( diff --git a/packages/studio/src/components/editor/PropertyPanelFlatHeader.test.tsx b/packages/studio/src/components/editor/PropertyPanelFlatHeader.test.tsx index 51c43ad266..f5722aa3b2 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlatHeader.test.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlatHeader.test.tsx @@ -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()); }); diff --git a/packages/studio/src/components/editor/PropertyPanelFlatHeader.tsx b/packages/studio/src/components/editor/PropertyPanelFlatHeader.tsx index c0ac2c24db..d19ac73fbc 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlatHeader.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlatHeader.tsx @@ -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} > diff --git a/packages/studio/src/components/editor/SnapToolbar.tsx b/packages/studio/src/components/editor/SnapToolbar.tsx index 267da1171b..16612a5737 100644 --- a/packages/studio/src/components/editor/SnapToolbar.tsx +++ b/packages/studio/src/components/editor/SnapToolbar.tsx @@ -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} diff --git a/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx b/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx index 293dde5ac0..3be0bf7282 100644 --- a/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx @@ -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" diff --git a/packages/studio/src/components/editor/propertyPanelColorGradingSlider.tsx b/packages/studio/src/components/editor/propertyPanelColorGradingSlider.tsx index 9f784b8693..d997569cee 100644 --- a/packages/studio/src/components/editor/propertyPanelColorGradingSlider.tsx +++ b/packages/studio/src/components/editor/propertyPanelColorGradingSlider.tsx @@ -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} > diff --git a/packages/studio/src/components/editor/propertyPanelColorSecondary.tsx b/packages/studio/src/components/editor/propertyPanelColorSecondary.tsx index ff027492cd..78f1d2a65b 100644 --- a/packages/studio/src/components/editor/propertyPanelColorSecondary.tsx +++ b/packages/studio/src/components/editor/propertyPanelColorSecondary.tsx @@ -204,7 +204,7 @@ 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" > {sampling ? "Capturing frame" : "Sample color from frame"} @@ -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 @@ -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 diff --git a/packages/studio/src/components/editor/propertyPanelFill.tsx b/packages/studio/src/components/editor/propertyPanelFill.tsx index 16286e246c..721610d9be 100644 --- a/packages/studio/src/components/editor/propertyPanelFill.tsx +++ b/packages/studio/src/components/editor/propertyPanelFill.tsx @@ -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 diff --git a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx index 58bd3a7a81..cb5c3d219c 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.tsx @@ -214,7 +214,7 @@ export function FlatColorGradingSection({ @@ -519,7 +519,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"} diff --git a/packages/studio/src/components/editor/propertyPanelFlatEffectsSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatEffectsSection.tsx index 5b9261ea9e..157b0737cf 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatEffectsSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatEffectsSection.tsx @@ -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" > Custom palette @@ -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" > Add effect @@ -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" }`} > diff --git a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx index e23b8600b2..fead6963db 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx @@ -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" diff --git a/packages/studio/src/components/editor/propertyPanelFlatOverlaysSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatOverlaysSection.tsx index faeca7aff3..0c9004d540 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatOverlaysSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatOverlaysSection.tsx @@ -95,7 +95,7 @@ export function FlatOverlaysSection({ )} diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx index d092ee317a..ea32841b4e 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx @@ -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('[data-flat-row-reset="true"]'); expect(reset).not.toBeNull(); act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); @@ -1172,7 +1172,7 @@ describe("FlatSelectRow", () => { />, ); const select = host.querySelector("select"); - expect(select?.className).toContain("text-panel-accent"); + expect(select?.className).toContain("text-accent-ink"); const reset = host.querySelector('[data-flat-select-reset="true"]'); act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); expect(onReset).toHaveBeenCalledTimes(1); diff --git a/packages/studio/src/components/editor/propertyPanelFxControls.tsx b/packages/studio/src/components/editor/propertyPanelFxControls.tsx index 5bbc926e68..be1bfbc508 100644 --- a/packages/studio/src/components/editor/propertyPanelFxControls.tsx +++ b/packages/studio/src/components/editor/propertyPanelFxControls.tsx @@ -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. */} {param.label} diff --git a/packages/studio/src/components/editor/propertyPanelFxEqModule.tsx b/packages/studio/src/components/editor/propertyPanelFxEqModule.tsx index cb04c68ddf..d76aa0d26b 100644 --- a/packages/studio/src/components/editor/propertyPanelFxEqModule.tsx +++ b/packages/studio/src/components/editor/propertyPanelFxEqModule.tsx @@ -115,7 +115,7 @@ function Fader({ {moved ? shown(value) : "0"} @@ -160,7 +160,7 @@ export function FxEqModule({ {bands.length}-band diff --git a/packages/studio/src/components/nle/PreviewPane.tsx b/packages/studio/src/components/nle/PreviewPane.tsx index 775a508081..98c628e581 100644 --- a/packages/studio/src/components/nle/PreviewPane.tsx +++ b/packages/studio/src/components/nle/PreviewPane.tsx @@ -116,8 +116,8 @@ export function PreviewPane({
diff --git a/packages/studio/src/components/sidebar/AssetsTab.tsx b/packages/studio/src/components/sidebar/AssetsTab.tsx index a1d91dca09..1c13f3c4da 100644 --- a/packages/studio/src/components/sidebar/AssetsTab.tsx +++ b/packages/studio/src/components/sidebar/AssetsTab.tsx @@ -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" }`} > diff --git a/packages/studio/src/components/sidebar/AudioRow.tsx b/packages/studio/src/components/sidebar/AudioRow.tsx index 40a977a2d8..78cd947c56 100644 --- a/packages/studio/src/components/sidebar/AudioRow.tsx +++ b/packages/studio/src/components/sidebar/AudioRow.tsx @@ -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({ )} {used && ( - + in use )} @@ -281,7 +281,7 @@ export function AudioRow({ {copyFailed ? "Copy failed" : "Copied"} diff --git a/packages/studio/src/components/sidebar/CompositionsTab.tsx b/packages/studio/src/components/sidebar/CompositionsTab.tsx index 2cac1d6aba..cd1464aa3d 100644 --- a/packages/studio/src/components/sidebar/CompositionsTab.tsx +++ b/packages/studio/src/components/sidebar/CompositionsTab.tsx @@ -337,7 +337,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" > @@ -358,7 +358,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" }`} > diff --git a/packages/studio/src/components/sidebar/SidebarLintButton.tsx b/packages/studio/src/components/sidebar/SidebarLintButton.tsx index de34e38486..f1253f7f9c 100644 --- a/packages/studio/src/components/sidebar/SidebarLintButton.tsx +++ b/packages/studio/src/components/sidebar/SidebarLintButton.tsx @@ -38,7 +38,7 @@ 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-panel-danger/25 px-1 text-[9px] font-bold text-danger-ink 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" } > diff --git a/packages/studio/src/components/timelineToolbarStyles.ts b/packages/studio/src/components/timelineToolbarStyles.ts index fc6ed3259a..ec9819027c 100644 --- a/packages/studio/src/components/timelineToolbarStyles.ts +++ b/packages/studio/src/components/timelineToolbarStyles.ts @@ -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`; diff --git a/packages/studio/src/components/ui/Button.tsx b/packages/studio/src/components/ui/Button.tsx index 65ca1c3c88..28ccd1b5c0 100644 --- a/packages/studio/src/components/ui/Button.tsx +++ b/packages/studio/src/components/ui/Button.tsx @@ -50,7 +50,7 @@ export const buttonBase = cn( */ export const buttonVariants: Record = { primary: cn( - "bg-accent text-bg-0 font-semibold", + "bg-accent text-on-accent font-semibold", "enabled:hover:brightness-110 data-[preview-state=hover]:brightness-110", "enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]", ), diff --git a/packages/studio/src/components/ui/Menu.tsx b/packages/studio/src/components/ui/Menu.tsx index b4e22b6007..d145d4e313 100644 --- a/packages/studio/src/components/ui/Menu.tsx +++ b/packages/studio/src/components/ui/Menu.tsx @@ -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 ( {children} - + ✓ diff --git a/packages/studio/src/player/components/EditModal.tsx b/packages/studio/src/player/components/EditModal.tsx index 8b1e72c86b..54025cd75d 100644 --- a/packages/studio/src/player/components/EditModal.tsx +++ b/packages/studio/src/player/components/EditModal.tsx @@ -147,7 +147,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
{elementsInRange.map((el) => (
- #{el.id} + #{el.id} {el.tag}
))} @@ -195,12 +195,12 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }: className={`py-1.5 text-[11px] font-medium rounded-lg transition-all ${ copiedAgentPrompt ? "bg-green-500/20 text-green-400 border border-green-500/30" - : "bg-studio-accent/15 text-studio-accent border border-studio-accent/25 hover:bg-studio-accent/25" + : "bg-studio-accent/15 text-accent-ink border border-studio-accent/25 hover:bg-studio-accent/25" }`} > {copiedAgentPrompt ? "Copied!" : "Copy to Agent"} {!copiedAgentPrompt && ( - Cmd+Enter + Cmd+Enter )}
diff --git a/packages/studio/src/player/components/PlayerControls.tsx b/packages/studio/src/player/components/PlayerControls.tsx index e3ed7dfa08..f361e07504 100644 --- a/packages/studio/src/player/components/PlayerControls.tsx +++ b/packages/studio/src/player/components/PlayerControls.tsx @@ -76,7 +76,7 @@ const LoopButton = memo(function LoopButton({ }} disabled={disabled} className={`flex h-7 w-7 items-center justify-center rounded-md transition-colors disabled:opacity-30 ${ - loopEnabled ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200" + loopEnabled ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200" }`} aria-label={loopEnabled ? "Disable loop playback" : "Enable loop playback"} aria-pressed={loopEnabled} @@ -118,7 +118,7 @@ const FullscreenButton = memo(function FullscreenButton({ onToggleFullscreen(); }} className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${ - isFullscreen ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200" + isFullscreen ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200" }`} aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"} > diff --git a/packages/studio/src/player/components/ShortcutsPanel.tsx b/packages/studio/src/player/components/ShortcutsPanel.tsx index bf6a28840e..fe810a6ba3 100644 --- a/packages/studio/src/player/components/ShortcutsPanel.tsx +++ b/packages/studio/src/player/components/ShortcutsPanel.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useEffect, useId, useRef, memo } from "react"; import { formatTime, frameToSeconds } from "../lib/time"; import { Tooltip } from "../../components/ui"; +import { flatActive, flatIdle } from "../../components/timelineToolbarStyles"; import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss"; import { usePlayerStore } from "../store/playerStore"; import { DEFAULT_SHORTCUT_SECTIONS, type ShortcutSection } from "./studioShortcuts"; @@ -77,9 +78,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({ ref={triggerRef} type="button" onClick={() => setShowShortcuts((v) => !v)} - className={`flex h-7 w-7 items-center justify-center rounded-md transition-colors ${ - showShortcuts ? "text-neutral-200" : "text-neutral-600 hover:text-neutral-300" - }`} + className={showShortcuts ? flatActive : flatIdle} aria-label="Shortcuts and tools" aria-expanded={showShortcuts} aria-controls={shortcutsPanelId} @@ -110,10 +109,8 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({ // Deliberately NOT aria-modal. This is a non-modal disclosure: focus is // not trapped and the rest of the editor stays operable, so claiming // modality would make assistive tech treat the whole app as inert. - className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-hidden" + className="absolute bottom-full right-0 mb-2 z-50 rounded-lg border border-border bg-raised shadow-popover min-w-[220px] overflow-y-auto outline-hidden" style={{ - background: "#161618", - border: "1px solid rgba(255,255,255,0.08)", maxHeight: "min(280px, calc(100vh - 80px))", }} > diff --git a/packages/studio/src/player/components/TimelineCanvas.tsx b/packages/studio/src/player/components/TimelineCanvas.tsx index bc03a0ca1a..a40078df0a 100644 --- a/packages/studio/src/player/components/TimelineCanvas.tsx +++ b/packages/studio/src/player/components/TimelineCanvas.tsx @@ -83,7 +83,7 @@ export const TimelineCanvas = memo(function TimelineCanvas() { data-active={ghostLaneActive || undefined} className={`pointer-events-none absolute flex items-center justify-center rounded-lg border border-dashed text-[11px] transition-colors duration-150 ${ ghostLaneActive - ? "border-studio-accent/60 bg-studio-accent/6 text-studio-accent" + ? "border-studio-accent/60 bg-studio-accent/6 text-accent-ink" : "border-neutral-700/50 text-neutral-500" }`} style={{ diff --git a/packages/studio/src/player/components/TimelineClip.tsx b/packages/studio/src/player/components/TimelineClip.tsx index 04e8bad928..c31206aa2a 100644 --- a/packages/studio/src/player/components/TimelineClip.tsx +++ b/packages/studio/src/player/components/TimelineClip.tsx @@ -165,7 +165,7 @@ export const TimelineClip = memo(function TimelineClip({ width: 2, borderRadius: 1, background: "var(--clip-handle)", - opacity: handleOpacity * 0.6, + opacity: handleOpacity, }} />
@@ -195,7 +195,7 @@ export const TimelineClip = memo(function TimelineClip({ width: 2, borderRadius: 1, background: "var(--clip-handle)", - opacity: handleOpacity * 0.6, + opacity: handleOpacity, }} />
diff --git a/packages/studio/src/player/components/TimelineEmptyState.tsx b/packages/studio/src/player/components/TimelineEmptyState.tsx index 8d99d3efab..d19e704aa6 100644 --- a/packages/studio/src/player/components/TimelineEmptyState.tsx +++ b/packages/studio/src/player/components/TimelineEmptyState.tsx @@ -63,13 +63,13 @@ export function TimelineEmptyState({ strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" - className="text-studio-accent shrink-0" + className="text-accent-ink shrink-0" > - Drop media files to import + Drop media files to import ) : ( <> diff --git a/packages/studio/src/player/components/TimelineLanes.tsx b/packages/studio/src/player/components/TimelineLanes.tsx index 3d63d5b540..6ccedb5856 100644 --- a/packages/studio/src/player/components/TimelineLanes.tsx +++ b/packages/studio/src/player/components/TimelineLanes.tsx @@ -353,7 +353,6 @@ export function TimelineLanes({ fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase", - opacity: 0.5, }} > New track diff --git a/packages/studio/src/player/components/VolumeControl.tsx b/packages/studio/src/player/components/VolumeControl.tsx index 1ea6541559..bcdfe344e5 100644 --- a/packages/studio/src/player/components/VolumeControl.tsx +++ b/packages/studio/src/player/components/VolumeControl.tsx @@ -93,7 +93,7 @@ export const VolumeControl = memo(function VolumeControl({ aria-label={muteLabel} aria-pressed={silent} className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-30 ${ - silent ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200" + silent ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200" }`} > diff --git a/packages/studio/src/player/components/timelineMotionStyles.test.ts b/packages/studio/src/player/components/timelineMotionStyles.test.ts index 07a3aaaead..b5fecbddfc 100644 --- a/packages/studio/src/player/components/timelineMotionStyles.test.ts +++ b/packages/studio/src/player/components/timelineMotionStyles.test.ts @@ -39,14 +39,17 @@ function themeTokenValue(token: string): string { describe("timeline motion styles", () => { it.each([ - ["--timeline-track-label", "rgba(255, 255, 255, 0.5)"], - ["--timeline-tick-text", "rgba(255, 255, 255, 0.34)"], - ["--timeline-border-strong", "rgba(255, 255, 255, 0.2)"], - ["--timeline-group-member-tint", "rgba(255, 255, 255, 0.035)"], - ["--timeline-overlay-text", "rgba(255, 255, 255, 0.8)"], - ["--timeline-clip-shadow-dragging", "rgba(0, 0, 0, 0.4)"], - ["--timeline-playhead-glow", "rgba(60, 230, 172, 0.14)"], - ["--timeline-playhead-shadow", "rgba(0, 0, 0, 0.55)"], + ["--timeline-track-label", "var(--color-text-muted)"], + ["--timeline-tick-text", "var(--color-text-2)"], + ["--timeline-border-strong", "color-mix(in oklab, var(--color-text-0) 20%, transparent)"], + ["--timeline-group-member-tint", "color-mix(in oklab, var(--color-text-0) 3.5%, transparent)"], + ["--timeline-overlay-text", "var(--color-text-2)"], + [ + "--timeline-clip-shadow-dragging", + "light-dark(oklch(0.205 0.003 70 / 0.16), rgba(0, 0, 0, 0.4))", + ], + ["--timeline-playhead-glow", "color-mix(in oklab, var(--color-accent) 14%, transparent)"], + ["--timeline-playhead-shadow", "light-dark(oklch(0.205 0.003 70 / 0.2), rgba(0, 0, 0, 0.55))"], ])("keeps the migrated default for %s", (token, expected) => { expect(themeTokenValue(token)).toBe(expected); }); @@ -93,13 +96,7 @@ describe("timeline motion styles", () => { expect(timelineClipLabelRule).toContain("text-overflow: ellipsis"); expect(timelineClipLabelRule).toContain("max-width: calc(100% - 22px)"); expect(timelineClipTimecodeRule).toContain("color: var(--timeline-tick-text)"); - expect(themeCss).toContain("--timeline-clip-bg: rgba(255, 255, 255, 0.12)"); - expect(themeCss).toContain("--timeline-clip-border: rgba(255, 255, 255, 0.22)"); - expect(themeCss).toContain("--timeline-track-label: rgba(255, 255, 255, 0.5)"); - expect(themeCss).toContain("--timeline-tick-text: rgba(255, 255, 255, 0.34)"); - expect(themeCss).toContain("--timeline-clip-label-active: rgba(232, 255, 247, 0.95)"); expect(themeCss).toContain("--timeline-clip-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.85)"); - expect(themeCss).toContain("--timeline-clip-selection: rgba(255, 255, 255, 0.85)"); expect(themeCss).toContain("--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55)"); expect(themeCss).toContain("--timeline-clip-radius: 8px"); expect(themeCss).toContain("--timeline-clip-audio-radius: 21px"); @@ -108,18 +105,11 @@ describe("timeline motion styles", () => { expect(themeCss).toContain("--timeline-clip-chip-bg: rgba(15, 22, 18, 0.73)"); expect(themeCss).toContain("--timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7)"); expect(themeCss).toContain("--timeline-clip-chip-text: #ffffff"); - expect(themeCss).toContain("--timeline-row-bg: #edf0ed"); expect(themeCss).toContain("--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.12)"); - expect(themeCss).toContain("--timeline-clip-selection: #0f766e"); - expect(themeCss).toContain("--timeline-clip-audio-bg: rgba(167, 139, 250, 0.16)"); - expect(themeCss).toContain("--timeline-clip-audio-border: rgba(167, 139, 250, 0.4)"); - expect(themeCss).toContain("--timeline-clip-audio-bg-hover: rgba(167, 139, 250, 0.24)"); - expect(themeCss).toContain("--timeline-clip-audio-bg-dragging: rgba(60, 52, 84, 0.96)"); expect(audioClipRule).toContain("background-color: var(--timeline-clip-audio-bg)"); expect(audioClipRule).toContain("border-color: var(--timeline-clip-audio-border)"); expect(audioClipHoverRule).toContain("background-color: var(--timeline-clip-audio-bg-hover)"); expect(audioClipDraggingRule).toContain("var(--timeline-clip-audio-bg-dragging)"); - expect(themeCss).toContain("--timeline-clip-selection: rgba(255, 255, 255, 0.85)"); expect(selectedTimelineClipRule).toContain( "outline: 1.5px solid var(--timeline-clip-selection)", ); diff --git a/packages/studio/src/styles/hex-baseline.json b/packages/studio/src/styles/hex-baseline.json index a15bc029ec..e327077a7e 100644 --- a/packages/studio/src/styles/hex-baseline.json +++ b/packages/studio/src/styles/hex-baseline.json @@ -1,5 +1,5 @@ { - "total": 297, + "total": 295, "files": { "packages/studio/src/captions/components/CaptionOverlay.tsx": 5, "packages/studio/src/captions/components/CaptionTimeline.tsx": 11, @@ -54,7 +54,7 @@ "packages/studio/src/player/components/CompositionThumbnail.tsx": 2, "packages/studio/src/player/components/Player.tsx": 2, "packages/studio/src/player/components/PlayerControls.tsx": 1, - "packages/studio/src/player/components/ShortcutsPanel.tsx": 7, + "packages/studio/src/player/components/ShortcutsPanel.tsx": 5, "packages/studio/src/player/components/SpeedMenu.tsx": 2, "packages/studio/src/player/components/trackHeaderLabelRows.tsx": 6, "packages/studio/src/styles/components.css": 48, diff --git a/packages/studio/src/styles/studio.css b/packages/studio/src/styles/studio.css index 26e899e0c4..d6ea40bb99 100644 --- a/packages/studio/src/styles/studio.css +++ b/packages/studio/src/styles/studio.css @@ -16,15 +16,6 @@ } } -/* - * Studio is a dark-only UI — pin the user-agent color scheme to dark so that - * browser-native chrome (scrollbars, form controls, focus rings) picks the - * matching palette instead of defaulting to light against our #0a0a0a body. - */ -:root { - color-scheme: dark; -} - body { margin: 0; padding: 0; diff --git a/packages/studio/src/styles/tailwind-preset.shared.js b/packages/studio/src/styles/tailwind-preset.shared.js index 3705d94d4b..85fbbc0321 100644 --- a/packages/studio/src/styles/tailwind-preset.shared.js +++ b/packages/studio/src/styles/tailwind-preset.shared.js @@ -1,6 +1,7 @@ /** @deprecated Superseded by `src/styles/theme.css` (published as * `@hyperframes/studio/theme.css`); kept one more major for Tailwind v3 - * consumers. Values are byte-identical; theme.test.ts fails if they drift. */ + * consumers, frozen at the colours before Graphite (theme.css is light-dark() and colour-mix + * now, which v3 cannot read). */ const studioPreset = { theme: { extend: { diff --git a/packages/studio/src/styles/theme.css b/packages/studio/src/styles/theme.css index 6c91815b4c..cb98f7c210 100644 --- a/packages/studio/src/styles/theme.css +++ b/packages/studio/src/styles/theme.css @@ -5,72 +5,111 @@ @theme static { /* ---------------------------------------------------------------- colour */ - /* Background levels, darkest (app shell) to lightest (inset panel body). */ - --color-bg-0: #0a0a0a; - --color-bg-1: #0c0c0e; - --color-bg-2: #121214; - - /* Raised surfaces. */ - --color-surface: #18181b; - --color-surface-alt: #141414; - --color-input: #161618; - --color-hover: #27272a; - - /* Separators, weakest to strongest. */ - --color-hairline: #1a1a1c; - --color-border: #1e1e1e; - --color-border-input: #27272a; - --color-border-strong: #262626; - - /* Text, brightest (0) to dimmest (5). */ - --color-text-0: #fafafa; - --color-text-1: #e4e4e7; - --color-text-2: #a1a1aa; - --color-text-3: #71717a; - --color-text-4: #52525b; - --color-text-5: #3f3f46; - --color-text-body: #e5e5e5; - --color-text-muted: #737373; + /* Graphite. Each colour is `light-dark(light, dark)`, resolved where it is painted, so the + * colour-scheme decides: Studio is dark, `data-theme="paper"` makes it light, and the player + * keeps a dark scheme in both. Text clears 4.5:1 and icons 3:1 on every surface below. */ + + /* Surfaces, darkest (app shell) to lightest (raised). */ + --color-bg-0: light-dark(oklch(0.955 0.005 70), oklch(0.235 0.007 70)); + --color-bg-1: var(--color-bg-0); + --color-bg-2: var(--color-surface); + --color-input: light-dark(oklch(1 0 70), oklch(0.25 0.007 70)); + --color-surface: light-dark(oklch(0.987 0.005 70), oklch(0.265 0.007 70)); + --color-surface-alt: var(--color-surface); + /* Menus, popovers and tooltips. */ + --color-raised: light-dark(oklch(1 0 70), oklch(0.295 0.007 70)); + + /* States, washes of the brightest text so they work on any surface. `on` is a toggle that is on. */ + --color-hover: light-dark( + color-mix(in oklab, var(--color-text-0) 5.5%, transparent), + color-mix(in oklab, var(--color-text-0) 7.5%, transparent) + ); + --color-press: light-dark( + color-mix(in oklab, var(--color-text-0) 9.5%, transparent), + color-mix(in oklab, var(--color-text-0) 13%, transparent) + ); + --color-on: light-dark( + color-mix(in oklab, var(--color-accent) 13%, transparent), + color-mix(in oklab, var(--color-accent) 16%, transparent) + ); + --color-on-hover: light-dark( + color-mix(in oklab, var(--color-accent) 22%, transparent), + color-mix(in oklab, var(--color-accent) 27%, transparent) + ); + + /* Separators. */ + --color-border: light-dark( + color-mix(in oklab, var(--color-text-0) 8%, transparent), + color-mix(in oklab, var(--color-text-0) 7.5%, transparent) + ); + --color-hairline: var(--color-border); + --color-border-strong: light-dark( + color-mix(in oklab, var(--color-text-0) 15%, transparent), + color-mix(in oklab, var(--color-text-0) 14%, transparent) + ); + --color-border-input: var(--color-border-strong); + + /* Text: primary, secondary (7:1), muted (4.5:1), and off (3:1, disabled controls only). */ + --color-text-0: light-dark(oklch(0.205 0.003 70), oklch(0.965 0.004 70)); + --color-text-1: var(--color-text-0); + --color-text-body: var(--color-text-0); + --color-text-2: light-dark(oklch(0.403 0.003 70), oklch(0.908 0.004 70)); + --color-text-3: var(--color-text-2); + --color-text-muted: light-dark(oklch(0.503 0.003 70), oklch(0.777 0.004 70)); + --color-text-4: var(--color-text-muted); + --color-text-5: var(--color-text-muted); + --color-text-off: light-dark(oklch(0.597 0.003 70), oklch(0.67 0.004 70)); /* Roles. Selection and playhead are reserved and differ from the accent so a - * selected clip, the playhead and an accented control stay tellable apart. */ - --color-accent: #3ce6ac; - --color-selection: #4ba3d2; - --color-playhead: #ffffff; - --color-danger: #ef4444; - --color-media: #00e3ff; - --color-container: #f5a623; + * selected clip, the playhead and an accented control stay tellable apart. + * `accent-ink` is accent text on a surface; `on-accent` is text on an accent fill. */ + --color-accent: light-dark(oklch(0.536 0.114 164), oklch(0.82 0.159 164)); + --color-accent-hover: light-dark(oklch(0.486 0.103 164), oklch(0.87 0.159 164)); + --color-on-accent: light-dark(oklch(1 0 0), oklch(0.2 0.043 164)); + --color-accent-ink: light-dark(oklch(0.45 0.096 164), oklch(0.827 0.159 164)); + --color-ring: var(--color-accent-ink); + --color-selection: light-dark(oklch(0.5 0.13 240), #4ba3d2); + --color-playhead: var(--color-text-0); + --color-danger: oklch(0.56 0.19 25); + --color-danger-ink: light-dark(oklch(0.5 0.17 25), oklch(0.791 0.12 25)); + --color-on-danger: oklch(1 0 0); + --color-media: light-dark(oklch(0.5 0.1 215), #00e3ff); + --color-container: light-dark(oklch(0.55 0.13 70), #f5a623); /* The two stops of the HeyGen brand gradient, worn by the header logo. Fixed brand values * rather than roles, so they are not reachable as a background or text colour by mistake. */ --color-brand-gradient-from: #06e3fa; --color-brand-gradient-to: #4fdb5e; - /* The timeline's own surfaces: darker and bluer than the shared scale, so they - * get their own names instead of snapping to the nearest --color-bg/-surface - * step (a host themes the timeline by overriding these). */ - --timeline-shell-bg: #0a0a0b; - --timeline-shell-border: rgba(255, 255, 255, 0.05); - --timeline-ruler-border: rgba(255, 255, 255, 0.16); - --timeline-row-bg: #101014; - --timeline-row-border: rgba(255, 255, 255, 0.06); - --timeline-gutter-bg: #0e0f12; - --timeline-gutter-border: rgba(255, 255, 255, 0.1); - --timeline-text-primary: rgba(255, 255, 255, 0.92); - --timeline-text-secondary: rgba(255, 255, 255, 0.62); - --timeline-tick-text: rgba(255, 255, 255, 0.34); - --timeline-tick-major: rgba(255, 255, 255, 0.1); - --timeline-tick-minor: rgba(255, 255, 255, 0.06); - --timeline-clip-bg: rgba(255, 255, 255, 0.12); - --timeline-clip-label-active: rgba(232, 255, 247, 0.95); + /* The timeline's own surfaces and inks, named so a host can theme the timeline alone. */ + --timeline-shell-bg: var(--color-surface); + --timeline-shell-border: var(--color-border); + --timeline-ruler-border: color-mix(in oklab, var(--color-text-0) 16%, transparent); + --timeline-row-bg: light-dark(oklch(0.997 0.002 70), oklch(0.275 0.007 70)); + --timeline-row-border: color-mix(in oklab, var(--color-text-0) 6%, transparent); + --timeline-gutter-bg: light-dark(oklch(0.967 0.005 70), oklch(0.253 0.007 70)); + --timeline-gutter-border: color-mix(in oklab, var(--color-text-0) 10%, transparent); + --timeline-text-primary: var(--color-text-0); + --timeline-text-secondary: var(--color-text-2); + --timeline-tick-text: var(--color-text-2); + --timeline-tick-major: color-mix(in oklab, var(--color-text-0) 10%, transparent); + --timeline-tick-minor: color-mix(in oklab, var(--color-text-0) 6%, transparent); + --timeline-clip-bg: light-dark( + color-mix(in oklab, var(--color-text-0) 12%, transparent), + color-mix(in oklab, var(--color-text-0) 13%, transparent) + ); + --timeline-clip-label-active: light-dark(#0b3d2c, rgba(232, 255, 247, 0.95)); --timeline-clip-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.85); - --timeline-clip-audio-bg: rgba(167, 139, 250, 0.16); - --timeline-clip-audio-border: rgba(167, 139, 250, 0.4); - --timeline-clip-audio-bg-hover: rgba(167, 139, 250, 0.24); - --timeline-clip-audio-bg-dragging: rgba(60, 52, 84, 0.96); + /* Audio and text clips are solid fills that carry white ink in both themes. */ + --timeline-clip-audio-bg: oklch(0.45 0.12 300); + --timeline-clip-audio-border: oklch(0.37 0.12 300); + --timeline-clip-audio-bg-hover: oklch(0.49 0.12 300); + --timeline-clip-audio-bg-dragging: oklch(0.42 0.12 300 / 0.96); --timeline-clip-audio-muted-bg: rgb(72, 74, 78); - --timeline-audio-link: #a0aaa4; - --timeline-clip-selection: rgba(255, 255, 255, 0.85); + --timeline-text-clip-bg: oklch(0.56 0.13 36); + --timeline-clip-ink: oklch(1 0 0); + --timeline-audio-link: light-dark(#626d66, #a0aaa4); + --timeline-clip-selection: light-dark(#0f766e, rgba(255, 255, 255, 0.85)); --timeline-clip-selection-highlight: rgb(255 255 255 / 0.55); --timeline-clip-radius: 8px; --timeline-clip-audio-radius: 21px; @@ -79,7 +118,7 @@ --timeline-clip-chip-bg: rgba(15, 22, 18, 0.73); --timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7); --timeline-clip-chip-text: #ffffff; - --timeline-shortcut-bg: rgba(17, 23, 35, 0.84); + --timeline-shortcut-bg: color-mix(in oklab, var(--color-raised) 94%, transparent); /* Composition and text clips draw their frame as it is, like video filmstrips; a host may dim them. */ --timeline-composition-thumbnail-opacity: 1; --timeline-composition-thumbnail-blend: normal; @@ -105,58 +144,67 @@ transparent 2px 5px ); --timeline-waveform-label-shadow: 0 1px 3px rgba(0, 0, 0, 0.9); - --timeline-clip-bg-active: rgba(60, 230, 172, 0.2); - --timeline-clip-bg-hover: rgba(255, 255, 255, 0.16); - --timeline-clip-bg-dragging: rgba(38, 42, 52, 0.96); - --timeline-clip-border: rgba(255, 255, 255, 0.22); - --timeline-clip-border-hover: rgba(60, 230, 172, 0.75); - --timeline-clip-border-active: rgba(60, 230, 172, 0.55); - --timeline-handle: rgba(255, 255, 255, 0.55); + --timeline-clip-bg-active: color-mix(in oklab, var(--color-accent) 20%, transparent); + --timeline-clip-bg-hover: light-dark( + color-mix(in oklab, var(--color-text-0) 26%, transparent), + color-mix(in oklab, var(--color-text-0) 16%, transparent) + ); + --timeline-clip-bg-dragging: color-mix(in oklab, var(--color-raised) 96%, transparent); + --timeline-clip-border: light-dark( + color-mix(in oklab, var(--color-text-0) 40%, transparent), + color-mix(in oklab, var(--color-text-0) 30%, transparent) + ); + --timeline-clip-border-hover: color-mix(in oklab, var(--color-accent) 75%, transparent); + --timeline-clip-border-active: color-mix(in oklab, var(--color-accent) 55%, transparent); + --timeline-handle: var(--color-text-0); /* Audio clip fades: the shaded wedge over the faded gain, and the corner dots. */ --timeline-fade-shade: rgba(0, 0, 0, 0.38); --timeline-fade-dot-active: rgba(255, 255, 255, 1); --timeline-fade-dot-ring: rgba(0, 0, 0, 0.55); - --timeline-resize-seam: rgba(255, 255, 255, 0.12); - --timeline-resize-active: rgba(255, 255, 255, 0.24); - --timeline-track-clip-fill: rgba(255, 255, 255, 0.055); - --timeline-track-clip-active: rgba(60, 230, 172, 0.16); - --timeline-track-label: rgba(255, 255, 255, 0.5); - --timeline-border-strong: rgba(255, 255, 255, 0.2); - --timeline-group-member-tint: rgba(255, 255, 255, 0.035); - --timeline-overlay-text: rgba(255, 255, 255, 0.8); - --timeline-accent: #3ce6ac; - --timeline-accent-soft: rgba(60, 230, 172, 0.18); - --timeline-accent-faint: rgba(60, 230, 172, 0.055); - --timeline-accent-fill: rgba(60, 230, 172, 0.1); - --timeline-accent-glow: rgba(60, 230, 172, 0.5); - --timeline-accent-border: rgba(60, 230, 172, 0.7); - --timeline-accent-rail: rgba(60, 230, 172, 0.35); - --timeline-accent-hover: rgba(60, 230, 172, 0.25); - --timeline-accent-button-bg: rgba(60, 230, 172, 0.15); - --timeline-danger: rgba(239, 68, 68, 0.7); + --timeline-resize-seam: color-mix(in oklab, var(--color-text-0) 12%, transparent); + --timeline-resize-active: color-mix(in oklab, var(--color-text-0) 24%, transparent); + --timeline-track-clip-fill: color-mix(in oklab, var(--color-text-0) 8%, transparent); + --timeline-track-clip-active: color-mix(in oklab, var(--color-accent) 20%, transparent); + --timeline-track-label: var(--color-text-muted); + --timeline-border-strong: color-mix(in oklab, var(--color-text-0) 20%, transparent); + --timeline-group-member-tint: color-mix(in oklab, var(--color-text-0) 3.5%, transparent); + --timeline-overlay-text: var(--color-text-2); + --timeline-accent: var(--color-accent); + --timeline-accent-soft: color-mix(in oklab, var(--color-accent) 18%, transparent); + --timeline-accent-faint: color-mix(in oklab, var(--color-accent) 5.5%, transparent); + --timeline-accent-fill: color-mix(in oklab, var(--color-accent) 10%, transparent); + --timeline-accent-glow: color-mix(in oklab, var(--color-accent) 50%, transparent); + --timeline-accent-border: color-mix(in oklab, var(--color-accent) 70%, transparent); + --timeline-accent-rail: color-mix(in oklab, var(--color-accent) 35%, transparent); + --timeline-accent-hover: var(--color-on-hover); + --timeline-accent-button-bg: var(--color-on); + --timeline-danger: color-mix(in oklab, var(--color-danger) 70%, transparent); --timeline-info-bg: rgba(59, 130, 246, 0.12); --timeline-info-border: rgba(59, 130, 246, 0.4); - --timeline-dialog-bg: #1b1b1f; - --timeline-diamond-bg: #171717; - --timeline-diamond-border: rgba(255, 255, 255, 0.14); - --timeline-text-solid: #ffffff; - --timeline-text-faint: rgba(255, 255, 255, 0.35); - --timeline-text-dim: rgba(255, 255, 255, 0.4); - --timeline-text-soft: rgba(255, 255, 255, 0.75); - --timeline-text-bright: rgba(255, 255, 255, 0.7); - --timeline-snap-guide: rgba(255, 255, 255, 0.6); - --timeline-overlay-bg: rgba(0, 0, 0, 0.8); - --timeline-overlay-bg-strong: rgba(0, 0, 0, 0.85); - --timeline-input-bg: rgba(0, 0, 0, 0.3); - --timeline-automation-rail: rgba(255, 255, 255, 0.08); - --timeline-automation-point-muted: rgba(0, 0, 0, 0.5); - --timeline-diamond-muted: #a3a3a3; + --timeline-dialog-bg: var(--color-raised); + --timeline-diamond-bg: var(--color-raised); + --timeline-diamond-border: color-mix(in oklab, var(--color-text-0) 14%, transparent); + --timeline-text-solid: var(--color-text-0); + --timeline-text-faint: var(--color-text-off); + --timeline-text-dim: var(--color-text-muted); + --timeline-text-soft: var(--color-text-2); + --timeline-text-bright: var(--color-text-2); + --timeline-snap-guide: color-mix(in oklab, var(--color-text-0) 60%, transparent); + --timeline-overlay-bg: light-dark(oklch(1 0 70 / 0.9), rgba(0, 0, 0, 0.8)); + --timeline-overlay-bg-strong: light-dark(oklch(1 0 70 / 0.95), rgba(0, 0, 0, 0.85)); + --timeline-input-bg: light-dark(oklch(0.205 0.003 70 / 0.05), rgba(0, 0, 0, 0.3)); + --timeline-automation-rail: color-mix(in oklab, var(--color-text-0) 8%, transparent); + --timeline-automation-point-muted: light-dark(oklch(0.205 0.003 70 / 0.3), rgba(0, 0, 0, 0.5)); + --timeline-diamond-muted: var(--color-text-muted); --timeline-beat: rgb(34, 197, 94); - --timeline-clip-shadow-dragging: rgba(0, 0, 0, 0.4); - --timeline-playhead-glow: rgba(60, 230, 172, 0.14); - --timeline-playhead-shadow: rgba(0, 0, 0, 0.55); - --timeline-transition-zone: rgba(255, 255, 255, 0.2); - --timeline-transition-badge: rgba(237, 244, 239, 0.98); + --timeline-clip-shadow-dragging: light-dark(oklch(0.205 0.003 70 / 0.16), rgba(0, 0, 0, 0.4)); + --timeline-playhead-glow: color-mix(in oklab, var(--color-accent) 14%, transparent); + --timeline-playhead-shadow: light-dark(oklch(0.205 0.003 70 / 0.2), rgba(0, 0, 0, 0.55)); + --timeline-transition-zone: light-dark( + color-mix(in oklab, var(--color-text-0) 12%, transparent), + color-mix(in oklab, var(--color-text-0) 20%, transparent) + ); + --timeline-transition-badge: light-dark(#ffffff, rgba(237, 244, 239, 0.98)); --timeline-transition-badge-ink: rgba(36, 56, 45, 0.96); /* -------------------------------------------------------------- geometry */ @@ -291,9 +339,10 @@ /* ------------------------------------------- deprecated: Tailwind palette */ - /* Verbatim Tailwind 4.3.3 defaults for stock colours markup still names, so - * `--color-*: initial` does not blank them. U12 deletes this block; - * `theme.test.ts` fails if a value drifts from upstream. */ + /* Stock colours markup still names, so `--color-*: initial` does not blank them. U12 deletes + * this block. The neutral scale is Graphite's roles, by the job each step does in markup: light + * steps are text, dark steps are surfaces and lines, so it follows the theme. The rest are + * verbatim Tailwind 4.3.3 defaults; `theme.test.ts` fails if one drifts from upstream. */ --color-amber-50: oklch(98.7% 0.022 95.277); --color-amber-100: oklch(96.2% 0.059 95.617); --color-amber-200: oklch(92.4% 0.12 95.746); @@ -315,17 +364,17 @@ --color-green-400: oklch(79.2% 0.209 151.711); --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); - --color-neutral-50: oklch(98.5% 0 none); - --color-neutral-100: oklch(97% 0 none); - --color-neutral-200: oklch(92.2% 0 none); - --color-neutral-300: oklch(87% 0 none); - --color-neutral-400: oklch(70.8% 0 none); - --color-neutral-500: oklch(55.6% 0 none); - --color-neutral-600: oklch(43.9% 0 none); - --color-neutral-700: oklch(37.1% 0 none); - --color-neutral-800: oklch(26.9% 0 none); - --color-neutral-900: oklch(20.5% 0 none); - --color-neutral-950: oklch(14.5% 0 none); + --color-neutral-50: var(--color-text-0); + --color-neutral-100: var(--color-text-0); + --color-neutral-200: var(--color-text-0); + --color-neutral-300: var(--color-text-2); + --color-neutral-400: var(--color-text-2); + --color-neutral-500: var(--color-text-muted); + --color-neutral-600: var(--color-text-muted); + --color-neutral-700: var(--color-border-strong); + --color-neutral-800: var(--color-border); + --color-neutral-900: var(--color-surface); + --color-neutral-950: var(--color-bg-0); --color-orange-300: oklch(83.7% 0.128 66.29); --color-orange-400: oklch(75% 0.183 55.934); --color-pink-400: oklch(71.8% 0.202 349.761); @@ -359,31 +408,20 @@ --color-white: #fff; } -/* One paper block. A host sets data-theme="paper" on the document element. */ +/* The colour-scheme picks each token's light or dark half. A host, or Studio's own theme toggle, + * sets data-theme="paper" on the document element for light. */ +:root { + color-scheme: dark; +} :root[data-theme="paper"] { - --timeline-shell-bg: #f4f6f3; - --timeline-row-bg: #edf0ed; - --timeline-clip-bg: #c9d1cb; + color-scheme: light; + --timeline-clip-label-shadow: none; --timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.12); - --timeline-clip-highlight: rgb(255 255 255 / 0.08); - --timeline-clip-selection: #0f766e; - --timeline-clip-selection-highlight: rgb(255 255 255 / 0.55); - --timeline-clip-chip-bg: rgba(15, 22, 18, 0.73); - --timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7); - --timeline-clip-chip-text: #ffffff; - --timeline-clip-audio-bg: rgb(150, 128, 180); - --timeline-clip-audio-bg-hover: rgb(135, 109, 170); - --timeline-clip-audio-bg-dragging: rgb(232, 224, 242); - --timeline-clip-audio-border: rgba(113, 80, 155, 0.45); - --timeline-clip-audio-muted-bg: rgb(156, 161, 157); - --timeline-audio-link: #626d66; - --timeline-waveform-bar-rgb: 20, 12, 32; - --timeline-waveform-muted-rgb: 24, 28, 26; - --timeline-waveform-baseline-rgb: 76, 62, 88; - --timeline-waveform-error: repeating-linear-gradient( + --timeline-thumbnail-shimmer: linear-gradient( 90deg, - rgba(20, 12, 32, 0.35) 0 2px, - transparent 2px 5px + rgba(24, 24, 27, 0.02) 0%, + rgba(24, 24, 27, 0.05) 50%, + rgba(24, 24, 27, 0.02) 100% ); } diff --git a/packages/studio/src/styles/theme.test.ts b/packages/studio/src/styles/theme.test.ts index 039ceeae1a..c067afebd1 100644 --- a/packages/studio/src/styles/theme.test.ts +++ b/packages/studio/src/styles/theme.test.ts @@ -2,7 +2,7 @@ /** Guards the ways `theme.css` can silently break: a dropped CSS custom * property, a utility that stops compiling, the stock Tailwind palette - * leaking back in, or the JS preset / legacy colours drifting from it. */ + * leaking back in, or the legacy colours drifting from it. */ import { readFileSync } from "node:fs"; import path from "node:path"; @@ -11,7 +11,6 @@ import { createRoot } from "react-dom/client"; import { Gear, IconContext } from "@phosphor-icons/react"; import { compile } from "tailwindcss"; import { afterEach, describe, expect, it } from "vitest"; -import studioPreset from "./tailwind-preset.shared.js"; import { readIconTokens } from "./iconTokens"; import { loadStylesheet, STYLES_DIR, TAILWIND_DIR } from "./styleSources"; @@ -52,7 +51,9 @@ describe("studio theme", () => { it("emits the semantic palette as custom properties and as utilities", async () => { const css = await build("studio.css", ["bg-accent", "text-text-2", "border-border"]); - expect(rootVariables(css).get("--color-accent")).toBe("#3ce6ac"); + expect(rootVariables(css).get("--color-accent")).toBe( + "light-dark(oklch(0.536 0.114 164), oklch(0.82 0.159 164))", + ); expect(css).toContain(".bg-accent {"); expect(css).toContain("background-color: var(--color-accent)"); }); @@ -138,7 +139,9 @@ describe("studio theme", () => { ), ].map(([, name, value]) => [name, value.trim()]), ); - const legacy = [...themeSource.matchAll(/\n\s*(--color-[a-z]+-(?:50|\d00|950)):\s*([^;]+);/g)]; + const legacy = [ + ...themeSource.matchAll(/\n\s*(--color-[a-z]+-(?:50|\d00|950)):\s*([^;]+);/g), + ].filter(([, name]) => !name.startsWith("--color-neutral-")); expect(legacy.length).toBeGreaterThan(0); for (const [, name, value] of legacy) { @@ -147,11 +150,12 @@ describe("studio theme", () => { } }); - it("keeps the deprecated JS preset in step with the CSS it shadows", () => { - const colors = studioPreset.theme.extend.colors; + it("maps every neutral step onto a Graphite role, so the stock scale follows the theme", () => { + const steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; - expect(colors.studio.accent.toLowerCase()).toBe(declaredValue("--color-accent")); - expect(colors.panel.surface.toLowerCase()).toBe(declaredValue("--color-surface")); + for (const step of steps) { + expect(declaredValue(`--color-neutral-${step}`)).toMatch(/^var\(--color-[a-z0-9-]+\)$/); + } }); it("reads the icon defaults off the theme rather than hard-coding them", () => { diff --git a/scripts/contrast-baseline.json b/scripts/contrast-baseline.json index 9451f3d045..0967ef424b 100644 --- a/scripts/contrast-baseline.json +++ b/scripts/contrast-baseline.json @@ -1,13 +1 @@ -{ - "dark/ruler-ticks": 3.0215661841856103, - "dark/pending-track-label": 2.2095660952157443, - "dark/clip-timecode": 2.9640400066983745, - "dark/active-clip-timecode": 2.44310754684879, - "dark/clip-border-exterior": 2.8019036818215706, - "dark/selected-handle": 2.3925236869074467, - "dark/active-selected-handle": 2.0618519389873913, - "dark/hovered-handle": 2.1777784316890663, - "dark/active-hovered-handle": 1.8873389821795008, - "dark/dragging-handle": 2.7718581542466456, - "dark/active-dragging-handle": 2.3310570156777346 -} +{} diff --git a/scripts/contrast-pairs.json b/scripts/contrast-pairs.json index 743b14ec3a..5b1df6f7a9 100644 --- a/scripts/contrast-pairs.json +++ b/scripts/contrast-pairs.json @@ -37,8 +37,7 @@ "packages/studio/src/player/components/TimelineLanes.tsx:270", "packages/studio/src/player/components/TimelineLanes.tsx:353", "packages/studio/src/player/components/TimelineLanes.tsx:357" - ], - "opacity": 0.5 + ] }, { "id": "clip-label", @@ -138,7 +137,7 @@ "packages/studio/src/player/components/timelineTheme.ts:86", "packages/studio/src/styles/components.css:155" ], - "opacity": 0.492 + "opacity": 0.82 }, { "id": "active-selected-handle", @@ -154,7 +153,7 @@ "packages/studio/src/styles/components.css:165", "packages/studio/src/styles/components.css:175" ], - "opacity": 0.492 + "opacity": 0.82 }, { "id": "hovered-handle", @@ -168,7 +167,7 @@ "packages/studio/src/player/components/timelineTheme.ts:87", "packages/studio/src/styles/components.css:195" ], - "opacity": 0.456 + "opacity": 0.76 }, { "id": "active-hovered-handle", @@ -188,7 +187,7 @@ "packages/studio/src/styles/components.css:165", "packages/studio/src/styles/components.css:175" ], - "opacity": 0.456 + "opacity": 0.76 }, { "id": "dragging-handle", @@ -202,7 +201,7 @@ "packages/studio/src/player/components/timelineTheme.ts:85", "packages/studio/src/styles/components.css:213" ], - "opacity": 0.57 + "opacity": 0.95 }, { "id": "active-dragging-handle", @@ -222,7 +221,7 @@ "packages/studio/src/styles/components.css:165", "packages/studio/src/styles/components.css:175" ], - "opacity": 0.57 + "opacity": 0.95 }, { "id": "quiet-waveform", @@ -308,7 +307,8 @@ }, { "id": "paper", - "selector": ":root[data-theme=\"paper\"]", + "selector": ["@theme static", ":root[data-theme=\"paper\"]"], + "scheme": "light", "canvas": "--timeline-shell-bg", "pairs": [ { @@ -341,7 +341,8 @@ }, { "id": "paper", - "selector": ":root[data-theme=\"paper\"]", + "selector": ["@theme static", ":root[data-theme=\"paper\"]"], + "scheme": "light", "canvas": "--timeline-row-bg", "pairs": [ { @@ -367,6 +368,725 @@ ] } ] + }, + { + "id": "chrome-dark", + "selector": "@theme static", + "canvas": "--color-bg-0", + "pairs": [ + { + "id": "secondary-text-on-shell", + "foreground": "--color-text-2", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel", + "foreground": "--color-text-2", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-raised", + "foreground": "--color-text-2", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-field", + "foreground": "--color-text-2", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-hover", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-raised-hover", + "foreground": "--color-text-2", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-pressed", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-on", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-on"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-on-hover", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-on-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-shell", + "foreground": "--color-text-muted", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel", + "foreground": "--color-text-muted", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-raised", + "foreground": "--color-text-muted", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-field", + "foreground": "--color-text-muted", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel-hover", + "foreground": "--color-text-muted", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-raised-hover", + "foreground": "--color-text-muted", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel-pressed", + "foreground": "--color-text-muted", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel-on", + "foreground": "--color-text-muted", + "background": ["--color-surface", "--color-on"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-shell", + "foreground": "--color-accent-ink", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel", + "foreground": "--color-accent-ink", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-raised", + "foreground": "--color-accent-ink", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-field", + "foreground": "--color-accent-ink", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-hover", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-raised-hover", + "foreground": "--color-accent-ink", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-pressed", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-on", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-on"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-on-hover", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-on-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-raised-on-hover", + "foreground": "--color-accent-ink", + "background": ["--color-raised", "--color-on-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-shell", + "foreground": "--color-danger-ink", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-panel", + "foreground": "--color-danger-ink", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-raised", + "foreground": "--color-danger-ink", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-field", + "foreground": "--color-danger-ink", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-panel-hover", + "foreground": "--color-danger-ink", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-raised-hover", + "foreground": "--color-danger-ink", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-panel-pressed", + "foreground": "--color-danger-ink", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-shell", + "foreground": "--color-text-off", + "background": ["--color-bg-0"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-panel", + "foreground": "--color-text-off", + "background": ["--color-surface"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-raised", + "foreground": "--color-text-off", + "background": ["--color-raised"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-field", + "foreground": "--color-text-off", + "background": ["--color-input"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "focus-ring-on-shell", + "foreground": "--color-ring", + "background": ["--color-bg-0"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "focus-ring-on-panel", + "foreground": "--color-ring", + "background": ["--color-surface"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "focus-ring-on-raised", + "foreground": "--color-ring", + "background": ["--color-raised"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "on-accent", + "foreground": "--color-on-accent", + "background": ["--color-accent"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "on-accent-hover", + "foreground": "--color-on-accent", + "background": ["--color-accent-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "on-danger", + "foreground": "--color-on-danger", + "background": ["--color-danger"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + } + ] + }, + { + "id": "chrome-light", + "selector": ["@theme static", ":root[data-theme=\"paper\"]"], + "scheme": "light", + "canvas": "--color-bg-0", + "pairs": [ + { + "id": "secondary-text-on-shell", + "foreground": "--color-text-2", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel", + "foreground": "--color-text-2", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-raised", + "foreground": "--color-text-2", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-field", + "foreground": "--color-text-2", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-hover", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-raised-hover", + "foreground": "--color-text-2", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-pressed", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-on", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-on"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "secondary-text-on-panel-on-hover", + "foreground": "--color-text-2", + "background": ["--color-surface", "--color-on-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-shell", + "foreground": "--color-text-muted", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel", + "foreground": "--color-text-muted", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-raised", + "foreground": "--color-text-muted", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-field", + "foreground": "--color-text-muted", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel-hover", + "foreground": "--color-text-muted", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-raised-hover", + "foreground": "--color-text-muted", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel-pressed", + "foreground": "--color-text-muted", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "muted-text-on-panel-on", + "foreground": "--color-text-muted", + "background": ["--color-surface", "--color-on"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-shell", + "foreground": "--color-accent-ink", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel", + "foreground": "--color-accent-ink", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-raised", + "foreground": "--color-accent-ink", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-field", + "foreground": "--color-accent-ink", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-hover", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-raised-hover", + "foreground": "--color-accent-ink", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-pressed", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-on", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-on"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-panel-on-hover", + "foreground": "--color-accent-ink", + "background": ["--color-surface", "--color-on-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "accent-text-on-raised-on-hover", + "foreground": "--color-accent-ink", + "background": ["--color-raised", "--color-on-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-shell", + "foreground": "--color-danger-ink", + "background": ["--color-bg-0"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-panel", + "foreground": "--color-danger-ink", + "background": ["--color-surface"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-raised", + "foreground": "--color-danger-ink", + "background": ["--color-raised"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-field", + "foreground": "--color-danger-ink", + "background": ["--color-input"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-panel-hover", + "foreground": "--color-danger-ink", + "background": ["--color-surface", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-raised-hover", + "foreground": "--color-danger-ink", + "background": ["--color-raised", "--color-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "danger-text-on-panel-pressed", + "foreground": "--color-danger-ink", + "background": ["--color-surface", "--color-press"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-shell", + "foreground": "--color-text-off", + "background": ["--color-bg-0"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-panel", + "foreground": "--color-text-off", + "background": ["--color-surface"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-raised", + "foreground": "--color-text-off", + "background": ["--color-raised"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "disabled-text-on-field", + "foreground": "--color-text-off", + "background": ["--color-input"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "focus-ring-on-shell", + "foreground": "--color-ring", + "background": ["--color-bg-0"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "focus-ring-on-panel", + "foreground": "--color-ring", + "background": ["--color-surface"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "focus-ring-on-raised", + "foreground": "--color-ring", + "background": ["--color-raised"], + "role": "graphic", + "minimum": 3, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "on-accent", + "foreground": "--color-on-accent", + "background": ["--color-accent"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "on-accent-hover", + "foreground": "--color-on-accent", + "background": ["--color-accent-hover"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + }, + { + "id": "on-danger", + "foreground": "--color-on-danger", + "background": ["--color-danger"], + "role": "text", + "minimum": 4.5, + "sources": ["packages/studio/src/styles/theme.css"] + } + ] } ] } diff --git a/scripts/contrast.ts b/scripts/contrast.ts index 77393aae3a..d14211f515 100644 --- a/scripts/contrast.ts +++ b/scripts/contrast.ts @@ -11,9 +11,13 @@ export interface ContrastPair { minimum: number; sources: string[]; } +type Scheme = "light" | "dark"; export interface ContrastTheme { id: string; - selector: string; + /** Blocks read in order, a later one overriding an earlier one. */ + selectors: string[]; + /** Picks each `light-dark()` half, as the page's colour-scheme does. */ + scheme: Scheme; canvas: string; pairs: ContrastPair[]; } @@ -57,6 +61,11 @@ function roleMinimum(row: Record) { if (minimum !== (kind === "text" ? 4.5 : 3)) throw new Error("Minimum must match WCAG AA role"); return { role: kind, minimum }; } +function scheme(value: unknown): Scheme { + if (value === undefined || value === "dark") return "dark"; + if (value === "light") return value; + throw new Error("Unknown colour scheme"); +} function format(value: unknown): ContrastPair["format"] { if (value === undefined) return "color"; if (value === "color" || value === "rgb-channels") return value; @@ -85,7 +94,8 @@ export function parseManifest(text: string): ContrastTheme[] { const theme = record(input); return { id: string(theme.id), - selector: string(theme.selector), + selectors: Array.isArray(theme.selector) ? strings(theme.selector) : [string(theme.selector)], + scheme: scheme(theme.scheme), canvas: string(theme.canvas), pairs: array(theme.pairs).map(parsePair), }; @@ -123,7 +133,66 @@ function rgbColor(value: string): Color { channel(parts[3] ?? "1", 1), ]; } -export function parseColor(value: string): Color { +/** The comma-separated arguments of the function call `value` opens with, split at depth 0. */ +function callArgs(value: string, name: string): string[] | null { + if (!value.startsWith(`${name}(`) || !value.endsWith(")")) return null; + const args: string[] = []; + let depth = 0; + let start = name.length + 1; + for (let at = start; at < value.length - 1; at++) { + if (value[at] === "(") depth++; + else if (value[at] === ")") depth--; + else if (value[at] === "," && depth === 0) { + args.push(value.slice(start, at).trim()); + start = at + 1; + } + } + if (depth !== 0) throw new Error(`Unbalanced color: ${value}`); + return [...args, value.slice(start, -1).trim()]; +} +function oklchColor(value: string): Color { + const parts = value.trim().split(/[\s/]+/); + if (![3, 4].includes(parts.length)) throw new Error(`Invalid oklch color: ${value}`); + const lightness = parts[0]!.endsWith("%") + ? Number(parts[0]!.slice(0, -1)) / 100 + : Number(parts[0]); + const [chroma, hue] = [Number(parts[1]), parts[2] === "none" ? 0 : Number(parts[2])]; + const alpha = channel(parts[3] ?? "1", 1); + if (![lightness, chroma, hue].every(Number.isFinite)) + throw new Error(`Invalid oklch color: ${value}`); + const a = chroma * Math.cos((hue * Math.PI) / 180); + const b = chroma * Math.sin((hue * Math.PI) / 180); + const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3; + const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3; + const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3; + const gamma = (x: number) => { + const c = Math.min(1, Math.max(0, x)); + return c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055; + }; + return [ + gamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), + gamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), + gamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s), + alpha, + ]; +} +/** `light-dark()`, `color-mix(in oklab, N%, transparent)`, `oklch()`, hex and rgb. */ +export function parseColor(value: string, scheme: Scheme = "dark"): Color { + const pair = callArgs(value, "light-dark"); + if (pair) { + if (pair.length !== 2) throw new Error(`Invalid light-dark: ${value}`); + return parseColor(pair[scheme === "light" ? 0 : 1]!, scheme); + } + const mix = callArgs(value, "color-mix"); + if (mix) { + const share = mix[1]?.match(/^(.+)\s+([\d.]+)%$/); + if (mix.length !== 3 || mix[0] !== "in oklab" || mix[2] !== "transparent" || !share) + throw new Error(`Only a mix with transparent is supported: ${value}`); + const color = parseColor(share[1]!, scheme); + return [color[0], color[1], color[2], (color[3] * Number(share[2])) / 100]; + } + const oklch = value.match(/^oklch\(([^()]+)\)$/); + if (oklch) return oklchColor(oklch[1]!); const hex = value.match(/^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i); if (hex) return hexColor(hex[1]!); const rgb = value.match(/^rgba?\(([^)]+)\)$/); @@ -147,7 +216,10 @@ export function contrast(a: Color, b: Color): number { const y = luminance(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); } -function tokensFor(css: string, selector: string): Map { +function tokensFor(css: string, selectors: string[]): Map { + return new Map(selectors.flatMap((selector) => [...tokensIn(css, selector)])); +} +function tokensIn(css: string, selector: string): Map { const clean = css.replace(/\/\*[\s\S]*?\*\//g, ""); const blocks = [...clean.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter( (match) => match[1]!.trim() === selector, @@ -164,12 +236,18 @@ function tokenValue(name: string, tokens: Map, seen: string[] = if (seen.includes(name)) throw new Error(`Token cycle: ${name}`); const value = tokens.get(name); if (value === undefined) throw new Error(`Missing token: ${name}`); - const alias = value.match(/^var\((--[\w-]+)\)$/); - return alias ? tokenValue(alias[1]!, tokens, [...seen, name]) : value; + return value.replace(/var\((--[\w-]+)\)/g, (_, alias: string) => + tokenValue(alias, tokens, [...seen, name]), + ); } -function surface(layers: string[], canvas: Color, tokens: Map): Color { +function surface( + layers: string[], + canvas: Color, + tokens: Map, + theme: ContrastTheme, +): Color { return layers.reduce( - (backing, name) => composite(parseColor(tokenValue(name, tokens)), backing), + (backing, name) => composite(parseColor(tokenValue(name, tokens), theme.scheme), backing), canvas, ); } @@ -178,13 +256,13 @@ function measurePair( theme: ContrastTheme, tokens: Map, ): Measurement { - const canvas = parseColor(tokenValue(theme.canvas, tokens)); + const canvas = parseColor(tokenValue(theme.canvas, tokens), theme.scheme); if (canvas[3] !== 1) throw new Error("Theme canvas must be opaque"); const value = tokenValue(pair.foreground, tokens); - const fg = pair.format === "rgb-channels" ? rgbColor(value) : parseColor(value); + const fg = pair.format === "rgb-channels" ? rgbColor(value) : parseColor(value, theme.scheme); const paint: Color = [fg[0], fg[1], fg[2], fg[3] * pair.opacity]; - const backing = surface(pair.paintBacking, canvas, tokens); - const background = surface(pair.background, canvas, tokens); + const backing = surface(pair.paintBacking, canvas, tokens, theme); + const background = surface(pair.background, canvas, tokens, theme); return { id: `${theme.id}/${pair.id}`, minimum: pair.minimum, @@ -193,7 +271,7 @@ function measurePair( } export function measure(css: string, themes: ContrastTheme[]): Measurement[] { return themes.flatMap((theme) => { - const tokens = tokensFor(css, theme.selector); + const tokens = tokensFor(css, theme.selectors); return theme.pairs.map((pair) => measurePair(pair, theme, tokens)); }); } From d380d102ec6562df673fc4bc5a12153904f60acd Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 1 Oct 2026 00:28:27 -0700 Subject: [PATCH 2/8] 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). --- .../studio/src/components/AskAgentModal.tsx | 2 +- packages/studio/src/components/LintModal.tsx | 64 ++-- packages/studio/src/components/dock/dock.css | 6 + .../src/components/editor/AnimationCard.tsx | 2 +- .../src/components/editor/FileTreeNodes.tsx | 2 +- .../src/components/editor/KeyframeDiamond.tsx | 10 +- .../components/editor/KeyframeNavigation.tsx | 8 +- .../src/components/editor/LayersPanel.tsx | 2 +- .../components/editor/PromotableControl.tsx | 2 +- .../src/components/editor/SnapToolbar.tsx | 2 +- .../components/editor/propertyPanelColor.tsx | 8 +- .../components/editor/propertyPanelFill.tsx | 2 +- .../components/editor/propertyPanelFont.tsx | 4 +- .../components/nle/AssetPreviewOverlay.tsx | 2 +- .../studio/src/components/nle/PreviewPane.tsx | 2 +- .../components/panels/SlideshowSubPanels.tsx | 6 +- .../src/components/sidebar/AssetCard.tsx | 2 +- .../components/sidebar/AssetContextMenu.tsx | 2 +- .../src/components/sidebar/AssetsTab.tsx | 2 +- .../src/components/sidebar/BlocksTab.tsx | 4 +- .../components/sidebar/PromptPreviewModal.tsx | 2 +- .../components/sidebar/SidebarLintButton.tsx | 2 +- packages/studio/src/components/ui/Button.tsx | 2 +- .../src/components/ui/HyperframesLogo.tsx | 37 +- .../src/player/components/EditModal.tsx | 2 +- .../src/player/components/PlayerControls.tsx | 2 +- .../src/player/components/TrackClipCount.tsx | 2 +- .../components/trackHeaderLabelRows.tsx | 22 +- packages/studio/src/styles/hex-baseline.json | 5 +- packages/studio/src/styles/theme.css | 39 +- packages/studio/src/styles/theme.test.ts | 4 +- scripts/contrast-pairs.json | 346 ++---------------- scripts/contrast.ts | 158 ++++---- 33 files changed, 255 insertions(+), 502 deletions(-) diff --git a/packages/studio/src/components/AskAgentModal.tsx b/packages/studio/src/components/AskAgentModal.tsx index ed555fbb2f..f29a11c455 100644 --- a/packages/studio/src/components/AskAgentModal.tsx +++ b/packages/studio/src/components/AskAgentModal.tsx @@ -138,7 +138,7 @@ export function AskAgentModal({ {navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to copy
)} {errors.map((f, i) => ( -
-
- -
-

{f.message}

- {f.file &&

{f.file}

} - {f.fixHint && ( -
- -

{f.fixHint}

-
- )} -
-
-
+ ))} {warnings.map((f, i) => ( -
-
- -
-

{f.message}

- {f.file &&

{f.file}

} - {f.fixHint && ( -
- -

{f.fixHint}

-
- )} -
-
-
+ ))}
); } + +function LintFindingRow({ finding, tone }: { finding: LintFinding; tone: "error" | "warning" }) { + return ( +
+
+ +
+

+ {finding.message} +

+ {finding.file && ( +

{finding.file}

+ )} + {finding.fixHint && ( +
+ +

{finding.fixHint}

+
+ )} +
+
+
+ ); +} diff --git a/packages/studio/src/components/dock/dock.css b/packages/studio/src/components/dock/dock.css index 23a7f940b2..a9bd52e5ab 100644 --- a/packages/studio/src/components/dock/dock.css +++ b/packages/studio/src/components/dock/dock.css @@ -1,5 +1,11 @@ @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); diff --git a/packages/studio/src/components/editor/AnimationCard.tsx b/packages/studio/src/components/editor/AnimationCard.tsx index 6a35b65cbc..6f8f036475 100644 --- a/packages/studio/src/components/editor/AnimationCard.tsx +++ b/packages/studio/src/components/editor/AnimationCard.tsx @@ -385,7 +385,7 @@ export const AnimationCard = memo(function AnimationCard({ )} {animation.method === "fromTo" && Object.keys(animation.properties).length > 0 && ( -

+

To

)} diff --git a/packages/studio/src/components/editor/FileTreeNodes.tsx b/packages/studio/src/components/editor/FileTreeNodes.tsx index 5b8b7af6f4..3f0764be63 100644 --- a/packages/studio/src/components/editor/FileTreeNodes.tsx +++ b/packages/studio/src/components/editor/FileTreeNodes.tsx @@ -344,7 +344,7 @@ export function DeleteConfirm({
diff --git a/packages/studio/src/components/editor/KeyframeDiamond.tsx b/packages/studio/src/components/editor/KeyframeDiamond.tsx index 992464786a..6dd2c6d7c0 100644 --- a/packages/studio/src/components/editor/KeyframeDiamond.tsx +++ b/packages/studio/src/components/editor/KeyframeDiamond.tsx @@ -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-muted hover:text-text-0", + ghost: "text-text-off hover:text-text-0", + }[state]; return ( diff --git a/packages/studio/src/components/editor/SnapToolbar.tsx b/packages/studio/src/components/editor/SnapToolbar.tsx index 16612a5737..e86eed1ff2 100644 --- a/packages/studio/src/components/editor/SnapToolbar.tsx +++ b/packages/studio/src/components/editor/SnapToolbar.tsx @@ -164,7 +164,7 @@ export const SnapToolbar = memo(function SnapToolbar() { {gridPopoverOpen && (