fix(web): remove marketing backdrop stack for lighter dashboard shell

Drop the CSS lens overlay (blend modes, noise, inversion) and backdrop-blur
from the ops dashboard so compositing no longer competes with xterm on /chat.
Use flat theme backgrounds and direct Nous Blue palette colors instead of
FG-inversion authoring.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Austin Pickett 2026-06-26 14:38:02 -04:00 committed by Teknium
parent b963d3238b
commit 57d98ebed7
21 changed files with 50 additions and 299 deletions

View file

@ -59,7 +59,6 @@ import { SelectionSwitcher } from "@nous-research/ui/ui/components/selection-swi
import { Spinner } from "@nous-research/ui/ui/components/spinner";
import { Typography } from "@nous-research/ui/ui/components/typography/index";
import { cn } from "@/lib/utils";
import { Backdrop } from "@/components/Backdrop";
import { SidebarFooter } from "@/components/SidebarFooter";
import { SidebarStatusStrip, gatewayLine } from "@/components/SidebarStatusStrip";
import { useBelowBreakpoint } from "@nous-research/ui/hooks/use-below-breakpoint";
@ -483,18 +482,16 @@ export default function App() {
<ProfileProvider>
<div
data-layout-variant={layoutVariant}
className="flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden bg-black text-text-primary antialiased"
className="flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden bg-background-base text-text-primary antialiased"
>
<SelectionSwitcher />
<Backdrop />
<PluginSlot name="backdrop" />
<header
className={cn(
"lg:hidden fixed top-0 left-0 right-0 z-40 min-h-14",
"flex items-center gap-2 px-4 py-2",
"border-b border-current/20",
"bg-background-base/90 backdrop-blur-sm",
"bg-background-base",
)}
style={{
background: "var(--component-header-background)",
@ -514,10 +511,7 @@ export default function App() {
<Menu />
</Button>
<Typography
className="font-bold text-[0.95rem] leading-[0.95] tracking-[0.05em] text-midground"
style={{ mixBlendMode: "plus-lighter" }}
>
<Typography className="font-bold text-[0.95rem] leading-[0.95] tracking-[0.05em] text-midground">
{t.app.brand}
</Typography>
</header>
@ -529,7 +523,7 @@ export default function App() {
onClick={closeMobile}
className={cn(
"lg:hidden fixed inset-0 z-40 p-0 block",
"bg-black/60 backdrop-blur-sm",
"bg-black/70",
)}
/>
)}
@ -545,7 +539,7 @@ export default function App() {
className={cn(
"fixed top-0 left-0 z-50 flex h-dvh max-h-dvh w-64 min-h-0 flex-col",
"border-r border-current/20",
"bg-background-base/95 backdrop-blur-sm",
"bg-background-base",
"transition-[transform] duration-200 ease-out",
mobileOpen ? "translate-x-0" : "-translate-x-full",
"lg:sticky lg:top-0 lg:translate-x-0 lg:shrink-0 lg:overflow-hidden",
@ -573,10 +567,7 @@ export default function App() {
>
<PluginSlot name="header-left" />
<Typography
className="font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground uppercase"
style={{ mixBlendMode: "plus-lighter" }}
>
<Typography className="font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground uppercase">
Hermes
<br />
Agent
@ -879,7 +870,6 @@ function SidebarNavLink({
<span
aria-hidden
className="absolute left-0 top-0 bottom-0 w-px bg-midground"
style={{ mixBlendMode: "plus-lighter" }}
/>
)}
</>
@ -1050,7 +1040,6 @@ function SystemActionButton({
<span
aria-hidden
className="absolute left-0 top-0 bottom-0 w-px bg-midground"
style={{ mixBlendMode: "plus-lighter" }}
/>
)}
</button>
@ -1186,7 +1175,7 @@ function SidebarTooltip({ anchor, label, warmRef }: SidebarTooltipProps) {
className={cn(
"fixed z-[100] pointer-events-none",
"px-2 py-1",
"bg-background-base/95 border border-current/20 backdrop-blur-sm shadow-lg",
"bg-background-base border border-current/20 shadow-lg",
"font-mondwest text-display text-xs tracking-[0.1em] text-midground uppercase",
)}
style={{

View file

@ -1,137 +0,0 @@
import { useGpuTier } from "@nous-research/ui/hooks/use-gpu-tier";
import fillerBgUrl from "@nous-research/ui/assets/filler-bg0.webp";
/**
* Replicates the visual layer stack of `<Overlays dark />` from
* `@nous-research/ui` without pulling in its leva / gsap / three peer deps.
*
* See `design-language/src/ui/components/overlays/index.tsx` for the source of
* truth. Defaults match LENS_0 (the Hermes teal dark preset); the deep canvas
* and the warm vignette both read theme-switchable CSS custom properties so
* `ThemeProvider` can repaint the stack without remounting.
*
* z-1 bg = `var(--background-base)`, mix-blend-mode driven by
* `--component-backdrop-bg-blend-mode` (default `difference`).
* Both LENS_0-style dark themes and the LENS_5I-style Nous Blue
* light theme keep `difference` here the canvas is flipped by
* the z-200 FG inversion layer, not by changing this blend mode.
* The CSS var is exposed as a hook so future presets can override
* it (e.g. `multiply` to paint the bg as-is before inversion)
* without touching this component.
* z-2 bundled filler-bg WebP, inverted, opacity 0.033, difference
* z-99 warm top-left vignette (`var(--warm-glow)`), opacity 0.22, lighten
* z-200 FG inversion = `var(--foreground)` (opaque white in LENS_5I,
* alpha-0 in LENS_0), mix-blend-mode: difference. This is the
* layer that flips the dashboard into "light mode" for inverted
* themes; for normal dark themes its alpha is 0 so it's a no-op.
* Deliberately placed above every UI overlay z-index (modals,
* tooltips, and dropUp dropdowns all sit at z-[100]) so portaled
* elements get inverted along with the rest of the page instead
* of painting with pre-inversion colors on top of the lens.
* z-201 noise grain (SVG, ~55% opacity × `--noise-opacity-mul`,
* color-dodge) gated on GPU tier. Sits above the inversion
* layer by design so the grain is not flipped.
*
* `useGpuTier` returns 0 when WebGL is unavailable, the renderer is a
* software rasterizer (SwiftShader/llvmpipe), or the user has
* `prefers-reduced-motion: reduce` set. We skip the animated noise layer
* in that case so low-power / accessibility-conscious sessions stay crisp,
* mirroring the DS `<Noise />` component's own opt-out.
*/
export function Backdrop() {
const gpuTier = useGpuTier();
return (
<>
<div
aria-hidden
className="pointer-events-none fixed inset-0 z-[1]"
style={
{
backgroundColor: "var(--background-base)",
mixBlendMode:
"var(--component-backdrop-bg-blend-mode, difference)",
} as unknown as React.CSSProperties
}
/>
<div
aria-hidden
className="pointer-events-none fixed inset-0 z-[2]"
style={
{
// Themes can override the filler background by setting
// `assets.bg` — the <img> hides itself when a CSS bg is set
// so the two don't double-darken. CSS var fallbacks keep the
// default behaviour unchanged when no theme customises these.
mixBlendMode:
"var(--component-backdrop-filler-blend-mode, difference)",
opacity: "var(--component-backdrop-filler-opacity, 0.033)",
backgroundImage: "var(--theme-asset-bg)",
backgroundSize: "var(--component-backdrop-background-size, cover)",
backgroundPosition:
"var(--component-backdrop-background-position, center)",
} as unknown as React.CSSProperties
}
>
<img
alt=""
className="h-[150dvh] w-auto min-w-[100dvw] object-cover object-top-left invert theme-default-filler"
fetchPriority="low"
src={fillerBgUrl}
/>
</div>
<div
aria-hidden
className="pointer-events-none fixed inset-0 z-[99]"
style={{
background:
"radial-gradient(ellipse at 0% 0%, transparent 60%, var(--warm-glow) 100%)",
mixBlendMode: "lighten",
opacity: 0.22,
}}
/>
{/* Foreground inversion layer. Source-of-truth: LENS_5I.Lens.fgOpacity
+ fgBlend: 'difference' in `design-language/src/ui/components/
overlays/lens.ts`. With `--foreground-alpha: 0` (LENS_0 dark default)
the layer is fully transparent and contributes nothing; with
alpha 1 + opaque white it inverts the entire stack below it,
producing the LENS_5I "light mode" look without altering any
downstream component code.
z-200 (not 100) so it sits above every portaled UI overlay
sidebar tooltips, dropUp dropdowns, and modal dialogs all use
z-[100], which is what the DS Lens picks too; portals append
at the end of <body>, so equal z-index + later DOM order means
they'd paint on top of the inversion and skip the flip. Inlined
z-index for the same reason the DS does it Tailwind's JIT
scan sometimes drops non-default z utilities. */}
<div
aria-hidden
className="pointer-events-none fixed inset-0"
style={{
backgroundColor: "var(--foreground)",
mixBlendMode: "difference",
zIndex: 200,
}}
/>
{gpuTier > 0 && (
<div
aria-hidden
className="pointer-events-none fixed inset-0 z-[201]"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' fill='%23eaeaea' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E\")",
backgroundSize: "512px 512px",
mixBlendMode: "color-dodge",
opacity: "calc(0.55 * var(--noise-opacity-mul, 1))",
}}
/>
)}
</>
);
}

View file

@ -66,7 +66,7 @@ export function ConfirmDialog({
onClick={(e) => {
if (e.target === e.currentTarget) onCancel();
}}
className="fixed inset-0 z-[200] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[200] flex items-center justify-center bg-background/85 p-4"
>
<div
ref={dialogRef}

View file

@ -288,7 +288,7 @@ export function ModelPickerDialog(props: Props) {
// Toast.tsx for the same pattern.
return createPortal(
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && onClose()}
role="dialog"
aria-modal="true"

View file

@ -164,7 +164,7 @@ export function OAuthLoginModal({ provider, onClose, onSuccess }: Props) {
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={handleBackdrop}
role="dialog"
aria-modal="true"

View file

@ -29,7 +29,6 @@ export function SidebarFooter({ status }: SidebarFooterProps) {
"transition-opacity hover:opacity-90",
"focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/40",
)}
style={{ mixBlendMode: "plus-lighter" }}
>
{t.app.footer.org}
</a>

View file

@ -121,7 +121,7 @@ export function ThemeSwitcher({ collapsed = false, dropUp = false }: ThemeSwitch
aria-label={sheetTitle}
className={cn(
"min-w-[240px] max-h-[70dvh] overflow-y-auto",
"border border-current/20 bg-background-base/95 backdrop-blur-sm",
"border border-current/20 bg-background-base/95",
"shadow-[0_12px_32px_-8px_rgba(0,0,0,0.6)]",
dropUp ? "fixed z-[100]" : "absolute z-50 right-0 top-full mt-1",
)}

View file

@ -214,7 +214,7 @@ export function ToolsetConfigDrawer({ toolset, profile, onClose, onChanged }: Pr
return createPortal(
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onMouseDown={(e) => {
if (e.target === e.currentTarget) onClose();
}}

View file

@ -55,7 +55,7 @@ export function PageHeaderProvider({
className={cn(
"z-1 w-full shrink-0",
"box-border border-b border-current/20",
"bg-background-base/40 backdrop-blur-sm",
"bg-background-base",
// Mobile stacks title + toolbar — fixed h-14 clips content; desktop stays one row.
"min-h-0 overflow-x-hidden overflow-y-visible py-3 sm:h-14 sm:min-h-[3.5rem] sm:overflow-hidden sm:py-0",
)}
@ -88,7 +88,6 @@ export function PageHeaderProvider({
? "shrink truncate"
: "truncate",
)}
style={{ mixBlendMode: "plus-lighter" }}
>
{displayTitle}
</h1>

View file

@ -43,10 +43,8 @@
}
/* ------------------------------------------------------------------ */
/* Hermes Agent — Nous DS with the LENS_0 (Hermes teal) lens applied */
/* statically. Mirrors nousnet-web/(hermes-agent)/layout.tsx so the */
/* canonical Hermes palette is the default — teal canvas + cream */
/* accent — without relying on leva/gsap at runtime. */
/* Hermes Agent Nous DS with the LENS_0 (Hermes teal) palette applied
statically as the default dashboard theme. */
/* ------------------------------------------------------------------ */
:root {
@ -63,10 +61,6 @@
--background-base: #041c1c;
--background-alpha: 1;
/* Consumed by <Backdrop />; also theme-switchable. */
--warm-glow: rgba(255, 189, 56, 0.35);
--noise-opacity-mul: 1;
/* Typography tokens rewritten by ThemeProvider. Defaults match the
system stack so themes that don't override look native. */
--theme-font-sans: system-ui, -apple-system, "Segoe UI", Roboto,
@ -228,11 +222,6 @@ code { font-size: 0.875rem; }
display: none;
}
/* Plus-lighter blend used by logos/titles for a subtle glow. */
.blend-lighter {
mix-blend-mode: plus-lighter;
}
/* System UI-monospace stack distinct from `font-courier` (Courier
Prime), used for dense data readouts where the display font would
break the grid. Routes through the theme's mono stack so themes
@ -256,14 +245,3 @@ code { font-size: 0.875rem; }
2px 2px;
}
/* When a theme provides `assets.bg`, the backdrop's <div> renders it as
a CSS background; the default filler <img> is hidden to prevent
double-compositing. Unset initial empty, so the :not() selector
matches and the default image stays visible. */
:root:not([style*="--theme-asset-bg:"]) .theme-default-filler {
display: block;
}
:root[style*="--theme-asset-bg:"] .theme-default-filler {
display: none;
}

View file

@ -326,7 +326,7 @@ export default function ChannelsPage() {
{editing && (
<div
ref={editModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && setEditing(null)}
role="dialog"
aria-modal="true"

View file

@ -932,7 +932,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
onClick={closeMobilePanel}
className={cn(
"fixed inset-0 z-[55] p-0 block",
"bg-black/60 backdrop-blur-sm",
"bg-black/60",
)}
/>
)}
@ -944,7 +944,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
className={cn(
"font-mondwest fixed top-0 right-0 z-[60] flex h-dvh max-h-dvh w-64 min-w-0 flex-col antialiased",
"border-l border-current/20 text-midground",
"bg-background-base/95 backdrop-blur-sm",
"bg-background-base/95",
"transition-transform duration-200 ease-out",
"[background:var(--component-sidebar-background)]",
"[clip-path:var(--component-sidebar-clip-path)]",
@ -962,7 +962,6 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
<Typography
mondwest
className="text-display font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
style={{ mixBlendMode: "plus-lighter" }}
>
{t.app.modelToolsSheetTitle}
<br />
@ -1037,7 +1036,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
Offer an in-place restart so the user never has to refresh the
whole page to get a working chat back. */}
{sessionEnded && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-3 bg-black/60 backdrop-blur-sm">
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-3 bg-black/60">
<div className="text-sm tracking-wide text-white/80">
Session ended.
</div>
@ -1060,7 +1059,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
"absolute z-10",
"normal-case tracking-normal font-normal",
"rounded border border-current/30",
"bg-black/20 backdrop-blur-sm",
"bg-black/20",
"opacity-70 hover:opacity-100 hover:border-current/60",
"transition-opacity duration-150",
"bottom-2 right-2 px-2 py-1 text-xs sm:bottom-3 sm:right-3 sm:px-2.5 sm:py-1.5",

View file

@ -814,7 +814,7 @@ export default function CronPage() {
{createModalOpen && (
<div
ref={createModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && setCreateModalOpen(false)}
role="dialog"
aria-modal="true"
@ -889,7 +889,7 @@ export default function CronPage() {
{editJob && (
<div
ref={editModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && setEditJob(null)}
role="dialog"
aria-modal="true"

View file

@ -334,7 +334,7 @@ export default function McpPage() {
{createModalOpen && (
<div
ref={createModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) =>
e.target === e.currentTarget && setCreateModalOpen(false)
}
@ -455,7 +455,7 @@ export default function McpPage() {
{installEntry && (
<div
ref={installModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) =>
e.target === e.currentTarget && setInstallEntry(null)
}

View file

@ -575,7 +575,7 @@ function AuxiliaryTasksModal({
return (
<div
ref={modalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && onClose()}
role="dialog"
aria-modal="true"
@ -779,7 +779,7 @@ function MoaModelsModal({
if (!preset) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 p-4 backdrop-blur-sm">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 p-4">
<Card className="max-h-[85vh] w-full max-w-2xl overflow-auto">
<CardHeader>
<CardTitle className="text-sm">Configure Mixture of Agents presets</CardTitle>

View file

@ -804,7 +804,7 @@ export default function ProfilesPage() {
{createModalOpen && (
<div
ref={createModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) =>
e.target === e.currentTarget && setCreateModalOpen(false)
}
@ -1231,7 +1231,7 @@ export default function ProfilesPage() {
{editorName && (
<div
ref={editorModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && closeEditor()}
role="dialog"
aria-modal="true"

View file

@ -684,7 +684,7 @@ export default function SystemPage() {
{hookModalOpen && (
<div
ref={hookModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && setHookModalOpen(false)}
role="dialog"
aria-modal="true"

View file

@ -303,7 +303,7 @@ export default function WebhooksPage() {
{createModalOpen && (
<div
ref={createModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"
onClick={(e) => e.target === e.currentTarget && closeCreateModal()}
role="dialog"
aria-modal="true"

View file

@ -19,7 +19,7 @@ import React, { Fragment, useEffect, useState } from "react";
* these in their manifest's `slots` field get wired in automatically.
*
* Shell-wide slots:
* - `backdrop` rendered inside `<Backdrop />`, above the noise layer
* - `backdrop` optional full-viewport background decoration
* - `header-left` injected before the Hermes brand in the top bar
* - `header-right` injected before the theme/language switchers
* - `header-banner` injected below the top nav bar, full-width

View file

@ -184,98 +184,26 @@ export const roseTheme: DashboardTheme = {
},
};
/**
* Nous Blue the inverted "light mode" Hermes look, ported from the
* LENS_5I overlay preset in `@nous-research/ui`.
*
* Unlike the other built-ins (which paint dark color directly on the
* canvas), this theme relies on `<Backdrop />`'s foreground inversion
* layer: an opaque white sheet at z-200 with `mix-blend-mode: difference`
* that flips the entire stack below it. Authoring colors stay dark
* (`#170d02` brown background, `#FFAC02` orange midground), and the
* inversion converts them to their visual complements at paint time
* the orange midground reads as #0053FD Nous-blue on screen, against a
* cream `#E8F2FD` canvas.
*
* Note on bg blend mode: the DS Lens uses `multiply` for LENS_5I because
* nousnet-web's <body> is white; hermes-agent's App root is `bg-black`,
* so we leave the bg layer's blend mode at the `difference` default
* `difference(#170d02, #000)` passes the bg through unchanged, and the
* subsequent FG-difference layer then inverts it to cream. Using
* `multiply` here would collapse the bg to pure black against the
* `bg-black` root and produce a plain-white canvas instead of the
* intended cream-blue.
*
* Source of truth for the palette: `design-language/src/ui/components/
* overlays/lens.ts` (LENS_5I export).
*/
/** Light mode — vivid Nous-blue accents on a cream canvas. */
export const nousBlueTheme: DashboardTheme = {
name: "nous-blue",
label: "Nous Blue",
description: "Light mode — vivid Nous-blue accents on cream canvas",
palette: {
background: { hex: "#170d02", alpha: 1 },
midground: { hex: "#FFAC02", alpha: 1 },
foreground: { hex: "#FFFFFF", alpha: 1 },
// Same warm-amber as nousnet-web's overlay glow; after the FG
// inversion it reads as a cool ultraviolet vignette in the top-left.
warmGlow: "rgba(255, 172, 2, 0.18)",
// Noise sits above the FG inversion and is NOT flipped, so a softer
// multiplier keeps it from speckling over the bright post-inversion
// canvas.
noiseOpacity: 0.4,
background: { hex: "#E8F2FD", alpha: 1 },
midground: { hex: "#0053FD", alpha: 1 },
foreground: { hex: "#170d02", alpha: 0 },
warmGlow: "rgba(0, 83, 253, 0.12)",
noiseOpacity: 0,
},
typography: DEFAULT_TYPOGRAPHY,
layout: DEFAULT_LAYOUT,
// Inverted page: the embedded terminal is below the FG layer too, so
// a `#000000` source paints as visual white — i.e. a proper light-mode
// terminal pane. xterm picks lighter palette colors against the "black"
// canvas, which then read as dark text on screen post-inversion.
terminalBackground: "#000000",
componentStyles: {
backdrop: {
// Lower than LENS_5I.Lens.fillerOpacity (0.06). The filler texture
// gets amplified post-inversion: small variations against the deep
// `#170d02` source bg are barely visible, but those same variations
// against the bright `#E8F2FD` post-inversion canvas read as a
// heavy cloud/marble pattern — especially on near-empty pages
// (loading spinners, blank states). 0.02 keeps subtle grain
// without overwhelming the canvas.
fillerOpacity: "0.02",
},
},
// Pre-invert absolute-hex tokens so they read as their familiar colors
// through the FG difference layer. e.g. source #04D3C9 (cyan) is what
// gets painted, and `255 - channel` flips it to #FB2C36 (red) on screen.
// Without these, the default destructive/success/warning tokens would
// appear as their unintuitive complements.
colorOverrides: {
destructive: "#04d3c9",
destructiveForeground: "#000000",
success: "#b5217f",
warning: "#0042c7",
},
// Pre-inverted data-series accents for the Analytics/Models token
// charts. The defaults (#ffe6cb cream + #34d399 emerald) would render
// through the FG difference layer as dark navy + hot-coral on the
// bright Nous-blue canvas — the coral is the "red" users see for
// Output values without these overrides. Source → on-screen:
// Input: #ffe6cb → #001934 (dark navy) ← unchanged
// Output: #ffac02 → #0053fd (vivid Nous-blue) ← brand accent
// Input keeps the cream source so it stays a neutral, low-contrast
// dark-blue against the cream canvas; output paints as the brand
// Nous-blue so the "primary" series in token-flow charts reads as
// the highlight color, matching the rest of the inverted UI chrome.
terminalBackground: "#f5f8fc",
seriesColors: {
inputTokenAccent: "#ffe6cb",
outputTokenAccent: "#ffac02",
inputTokenAccent: "#001934",
outputTokenAccent: "#0053fd",
},
// Explicit picker swatch — the raw palette hex (`#170d02`, `#FFAC02`,
// amber rgba) doesn't reflect what users see after the FG inversion,
// so we paint the post-inversion visual triplet directly:
// white → vivid Nous-blue → cream/light-blue
// matching the actual on-screen rendering of the theme.
swatchColors: ["#FFFFFF", "#0053FD", "#E8F2FD"],
swatchColors: ["#170d02", "#0053FD", "#E8F2FD"],
};
/**

View file

@ -4,10 +4,9 @@
* Themes customise three orthogonal layers:
*
* 1. `palette` the 3-layer color triplet (background/midground/
* foreground) + warm-glow + noise opacity. The
* design-system cascade in `src/index.css` derives
* every shadcn-compat token (card, muted, border,
* primary, etc.) from this triplet via `color-mix()`.
* foreground). Legacy `warmGlow` / `noiseOpacity`
* fields remain for theme YAML compat but are unused
* by the lightweight shell.
* 2. `typography` font families, base font size, line height,
* letter spacing. An optional `fontUrl` is injected
* as `<link rel="stylesheet">` so self-hosted and
@ -33,10 +32,9 @@ export interface ThemePalette {
/** Top-layer highlight. In LENS_0 this is white @ alpha 0 invisible by
* default but still drives `--color-ring`-style accents. */
foreground: ThemeLayer;
/** Warm vignette color for <Backdrop />, as an rgba() string. */
/** Legacy palette field — kept for theme YAML compat. */
warmGlow: string;
/** Scalar multiplier (01.2) on the noise overlay. Lower for softer themes
* like Mono and Rosé, higher for grittier themes like Cyberpunk. */
/** Legacy palette field — kept for theme YAML compat. */
noiseOpacity: number;
}
@ -79,12 +77,10 @@ export interface ThemeLayout {
export type ThemeLayoutVariant = "standard" | "cockpit" | "tiled";
/** Named hero/background assets a theme can populate. Each value is
* emitted as a CSS var (`--theme-asset-<name>`). The default shell
* consumes `bg` in `<Backdrop />` when present; other slots are
* plugin-facing a cockpit sidebar plugin reads `--theme-asset-hero`
* to render its hero render without coupling to the theme name. */
* emitted as a CSS var (`--theme-asset-<name>`). Plugin slots and
* shell chrome may consume these via CSS. */
export interface ThemeAssets {
/** Full-viewport background image URL, injected under the noise layer. */
/** Full-viewport background image URL. */
bg?: string;
/** Hero render (Gundam, mascot, wallpaper) — for plugin sidebars/overlays. */
hero?: string;
@ -103,7 +99,7 @@ export interface ThemeAssets {
/** Component-style override buckets. Each bucket's entries become CSS
* vars (`--component-<bucket>-<kebab-property>`) that shell components
* (Card, Backdrop, App header/footer, etc.) read. Values are plain CSS
* (Card, App header/footer, etc.) read. Values are plain CSS
* strings we don't parse them, so themes can use `clip-path`,
* `border-image`, `background`, `box-shadow`, and anything else CSS
* accepts. */