Browse / github.com/storytold/artcraft-services / patch
webapp: colors as theme variables (part 1 of 2, no visible change)
webapp: colors as theme variables (part 1 of 2, no visible change) Part 1 of 2 for #165. I split this work in two to make it easier to review. This part only changes how the web app's colors are written, not how anything looks. Part 2 (the Soul Mark theme) builds on it, and I'll open it once this one is in, or right away if you'd rather see both together. ## What it does The web app's colors were written directly into the code. They now read CSS variables whose defaults are exactly the old values, so **every page looks the same, pixel for pixel**. Nothing in this part sets the variables; it's the groundwork a theme needs. - `styles.css` declares the varia
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Luminous-Enigma (imported from pull request #166, open)
- Base
e57ee3d63ac478a24149384bf1473ef97aa067f8- Size
- +301 −140 in 19 files
- Review
- pending
- Shared
- 2026-10-28
- Id
62a08236d11acd7b37e2a933
How it went
No other operator has reported using it yet.
Diff
raw · frontend/apps/artcraft-webapp/src/components/sidebar/app-sidebar.tsx frontend/apps/artcraft-webapp/src/components/topbar/TopBarActions.tsx frontend/apps/artcraft-webapp/src/components/topbar/task-queue.tsx frontend/apps/artcraft-webapp/src/components/topbar/topbar.tsx frontend/apps/artcraft-webapp/src/components/ui/sheet.tsx frontend/apps/artcraft-webapp/src/components/ui/tooltip.tsx frontend/apps/artcraft-webapp/src/pages/checkout/cancel.tsx frontend/apps/artcraft-webapp/src/pages/checkout/success.tsx frontend/apps/artcraft-webapp/src/pages/onboarding/onboarding.tsx frontend/apps/artcraft-webapp/src/pages/referrals/referrals.tsx frontend/apps/artcraft-webapp/src/pages/support/support.tsx frontend/apps/artcraft-webapp/src/pages/welcome/welcome.tsx frontend/apps/artcraft-webapp/src/styles.css frontend/apps/artcraft-webapp/tailwind.config.js frontend/libs/components/pagedraw/src/lib/PageDraw.tsx frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx frontend/libs/components/pricing-table/src/lib/pricing-table.tsx frontend/libs/components/pricing-table/src/lib/website-pricing-table.tsx frontend/libs/login/src/lib/desktop-login-approval.tsx
@@ -267,7 +267,7 @@ export function AppSidebar() { <img src="/images/artcraft-logo-2.png" alt="ArtCraft"- className="h-5 w-auto shrink-0"+ className="st-wordmark h-5 w-auto shrink-0" /> </Link> )}@@ -161,7 +161,7 @@ export function TopBarActions({ className }: { className?: string }) { > <MenuItems modal={false}- className="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-[3px] bg-[#101014] border border-white/15 focus:outline-none overflow-hidden"+ className="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-[3px] bg-ui-panel border border-white/15 focus:outline-none overflow-hidden" > <div className="px-4 py-3 border-b border-white/15"> <p className="text-sm font-medium text-white truncate">@@ -303,7 +303,7 @@ function CreditsChip({ </span> } buttonClassName="h-8 px-3 ps-2.5 bg-transparent hover:bg-white/10 border border-white/15 hover:border-white/30 shadow-none text-white/80 rounded-[3px] gap-1.5"- panelClassName="mt-2 bg-[#101014] border border-white/15 text-white rounded-[3px]"+ panelClassName="mt-2 bg-ui-panel border border-white/15 text-white rounded-[3px]" > {(close) => ( <div className="w-72 max-w-[calc(100vw-24px)] p-3 text-white">@@ -1137,7 +1137,7 @@ export const TaskQueue = () => { <PopoverMenu mode="default" buttonClassName="h-8 w-8 !p-0 relative bg-transparent hover:bg-white/10 border border-white/15 hover:border-white/30 text-white/80 rounded-[3px] shadow-none"- panelClassName="w-[calc(100vw-5rem)] sm:w-[400px] p-2 bg-[#101014] border border-white/15 mt-2 rounded-[3px]"+ panelClassName="w-[calc(100vw-5rem)] sm:w-[400px] p-2 bg-ui-panel border border-white/15 mt-2 rounded-[3px]" position="bottom" align="end" triggerIcon={@@ -39,7 +39,7 @@ export function TopBar() { : "/images/artcraft-logo-2.png" } alt="ArtCraft"- className={isMobile ? "h-5 w-auto" : "h-5 w-auto"}+ className={isMobile ? "h-5 w-auto" : "st-wordmark h-5 w-auto"} /> </Link> )}@@ -53,7 +53,7 @@ const SheetContent = React.forwardRef< <DialogPrimitive.Content ref={ref} className={cn(- "fixed z-50 flex flex-col bg-[#101014] transition ease-in-out",+ "fixed z-50 flex flex-col bg-ui-panel transition ease-in-out", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300", side === "left" && "inset-y-0 left-0 h-full border-r border-white/15 data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",@@ -15,7 +15,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn(- "z-[60] overflow-hidden rounded-[3px] bg-[#101014] border border-white/15 px-2.5 py-1.5 text-xs text-white",+ "z-[60] overflow-hidden rounded-[3px] bg-ui-panel border border-white/15 px-2.5 py-1.5 text-xs text-white", "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className, )}@@ -16,7 +16,7 @@ const CheckoutCancel = () => { <main className="relative z-10 pt-20 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]"> {/* Cancel Card */} <div className="max-w-lg w-full">- <div className="bg-[#101014] border border-white/15 p-8 md:p-12 text-center">+ <div className="bg-ui-panel border border-white/15 p-8 md:p-12 text-center"> {/* Cancel Icon */} <div className="mb-6"> <div className="w-20 h-20 mx-auto border border-white/15 bg-white/10 flex items-center justify-center">@@ -85,7 +85,7 @@ const CheckoutSuccess = () => { <main className="relative z-10 pt-12 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]"> {/* Success Card */} <div className="max-w-4xl w-full">- <div className="bg-[#101014] border border-white/15 p-8 md:p-12 text-center">+ <div className="bg-ui-panel border border-white/15 p-8 md:p-12 text-center"> {/* Success Icon */} <div className="mb-6"> <div className="w-20 h-20 mx-auto border border-white/15 bg-green-500/10 flex items-center justify-center">@@ -338,7 +338,7 @@ const Onboarding = () => { </div> )} - <div className="bg-[#101014] border border-white/15 p-6 py-8">+ <div className="bg-ui-panel border border-white/15 p-6 py-8"> <div className="text-center mb-8"> <h1 className="text-2xl font-semibold mb-2"> {currentStep === "password" &&@@ -163,7 +163,7 @@ export default function Referrals() { <div className="h-px w-10 bg-white/20" /> </div> - <section className="bg-[#101014] border border-white/15 p-6 sm:p-8">+ <section className="bg-ui-panel border border-white/15 p-6 sm:p-8"> <div className="flex items-start justify-between gap-4 mb-5"> <div> <div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 mb-2">@@ -266,7 +266,7 @@ export default function Referrals() { function ProfileLinkPanel({ profileLink }: { profileLink: string }) { return (- <section className="bg-[#101014] border border-white/15 p-6 sm:p-8">+ <section className="bg-ui-panel border border-white/15 p-6 sm:p-8"> <div className="mb-4"> <div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 mb-2"> Option 1@@ -58,7 +58,7 @@ const Support = () => { </p> </header> - <ul className="divide-y divide-white/15 border border-white/15 overflow-hidden bg-[#101014]">+ <ul className="divide-y divide-white/15 border border-white/15 overflow-hidden bg-ui-panel"> {ROWS.map((row) => ( <li key={row.title}> <SupportLink row={row} />@@ -57,7 +57,7 @@ const Welcome = () => { {/* Create-in-browser CTA — desktop download is optional */} <div className="max-w-3xl mx-auto mb-8">- <div className="bg-[#101014] border border-white/15 p-8 md:p-10">+ <div className="bg-ui-panel border border-white/15 p-8 md:p-10"> <div className="flex items-center gap-3 mb-2"> <div className="w-10 h-10 bg-white/10 flex items-center justify-center"> <WandSparklesIcon className="text-white/70 text-lg" />@@ -101,7 +101,7 @@ const Welcome = () => { {/* Desktop app alternative */} <div className="max-w-3xl mx-auto">- <div className="bg-[#101014] border border-white/15 p-8 md:p-10">+ <div className="bg-ui-panel border border-white/15 p-8 md:p-10"> <div className="flex items-center gap-3 mb-2"> <div className="w-10 h-10 bg-white/10 flex items-center justify-center"> <MonitorIcon className="text-white/70 text-lg" />@@ -4,16 +4,44 @@ @layer base { :root {+ /* Theme tokens, as "R G B" so Tailwind can add alpha. These defaults are+ the app's own colours; a theme overrides them on <html>.+ "white" is the ink that hairlines and most text are drawn in, "black"+ the deepest surface; a theme swaps them as a pair.+ The desktop app's own names (--st-bg, --st-fg, --st-fg-rgb,+ --st-divider, --st-controls-rgb) are read by shared libs with+ fallbacks of their own, so they are left unset here: setting them+ would change those libs' look. A theme sets them as well. */+ --st-white-rgb: 255 255 255;+ --st-black-rgb: 0 0 0;+ --st-text-rgb: 242 241 238;+ --st-bg-rgb: 18 19 22;+ --st-panel-rgb: 16 16 20;+ --st-control-rgb: 30 31 34;+ --st-sunken-rgb: 6 6 7;+ --st-scroll-track-rgb: 11 11 12;+ --st-scroll-thumb-rgb: 44 44 47;+ --st-accent-rgb: 45 129 255;+ --st-accent-300-rgb: 142 199 255;+ --st-accent-400-rgb: 89 167 255;+ --st-accent-600-rgb: 27 99 245;+ --st-accent-700-rgb: 20 78 225;+ --st-accent-ink-rgb: 116 170 255;+ --st-accent-text-rgb: 45 129 255;+ --st-accent-line-rgb: 45 129 255;+ --st-text-fade: 1;+ --st-icon-filter: invert(1);+ /* Sidebar tokens (consumed by tailwind colors in tailwind.config.js). Brutalist system: near-black surfaces, white/15 hairlines, white accent for selection/focus. */- color-scheme: dark;- --sidebar-bg: #1e1f22;- --sidebar-fg: rgba(255, 255, 255, 0.72);- --sidebar-border: rgba(255, 255, 255, 0.15);- --sidebar-accent: rgba(255, 255, 255, 0.08);- --sidebar-accent-fg: #ffffff;- --sidebar-ring: #74aaff;+ color-scheme: var(--st-color-scheme, dark);+ --sidebar-bg: rgb(var(--st-control-rgb));+ --sidebar-fg: rgb(var(--st-white-rgb) / 0.72);+ --sidebar-border: rgb(var(--st-white-rgb) / 0.15);+ --sidebar-accent: rgb(var(--st-white-rgb) / 0.08);+ --sidebar-accent-fg: rgb(var(--st-white-rgb));+ --sidebar-ring: rgb(var(--st-accent-ink-rgb)); /* shadcn design tokens consumed by @storyteller/ui-video-editor. Dark-theme palette (the editor only runs in dark mode today).@@ -91,7 +119,7 @@ * { scrollbar-width: thin;- scrollbar-color: #2c2c2f #0b0b0c;+ scrollbar-color: rgb(var(--st-scroll-thumb-rgb)) rgb(var(--st-scroll-track-rgb)); } *::-webkit-scrollbar {@@ -99,12 +127,12 @@ } *::-webkit-scrollbar-track {- background: #0b0b0c;+ background: rgb(var(--st-scroll-track-rgb)); } *::-webkit-scrollbar-thumb {- background-color: #2c2c2f;- border: 2px solid #0b0b0c;+ background-color: rgb(var(--st-scroll-thumb-rgb));+ border: 2px solid rgb(var(--st-scroll-track-rgb)); } .scrollbar-hidden {@@ -158,19 +186,19 @@ "Helvetica Neue", Arial, sans-serif;- color: #f2f1ee;- background-color: #121316;+ color: rgb(var(--st-text-rgb));+ background-color: rgb(var(--st-bg-rgb)); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ::selection {- background: #2d81ff;- color: #ffffff;+ background: rgb(var(--st-accent-rgb));+ color: rgb(var(--st-white-rgb)); } :focus-visible {- outline: 2px solid #74aaff;+ outline: 2px solid rgb(var(--st-accent-ink-rgb)); outline-offset: 3px; } @@ -203,18 +231,23 @@ /* Shared component utilities (matching desktop app theme) */ .bg-ui-badge {- background-color: rgba(0, 0, 0, 0.4);+ background-color: rgb(var(--st-black-rgb) / 0.4);+}++/* The ArtCraft wordmark image, which is white; a light theme flips it. */+.st-wordmark {+ filter: var(--st-wordmark-filter, none); } .icon-auto-contrast {- filter: invert(1);+ filter: var(--st-icon-filter); } /* Panel surface used by pagescene + lib components. Brutalist system: flat near-black, no gradient (class name kept for lib compatibility). */ .bg-ui-panel-gradient {- background: #101014;- background-color: #101014;+ background: rgb(var(--st-panel-rgb));+ background-color: rgb(var(--st-panel-rgb)); } .prevent-select {@@ -231,57 +264,57 @@ width: 10px; height: 10px; border-radius: 5px;- background-color: #ffffff30;- color: #ffffff30;+ background-color: rgb(var(--st-white-rgb) / 0.188);+ color: rgb(var(--st-white-rgb) / 0.188); box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); animation: dot-typing 1.5s infinite linear; } @keyframes dot-typing { 0% { box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); } 16.667% { box-shadow:- 9984px -10px 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px -10px 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); } 33.333% { box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); } 50% { box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px -10px 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px -10px 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); } 66.667% { box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); } 83.333% { box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px -10px 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px -10px 0 0 rgb(var(--st-white-rgb) / 0.188); } 100% { box-shadow:- 9984px 0 0 0 #ffffff30,- 9999px 0 0 0 #ffffff30,- 10014px 0 0 0 #ffffff30;+ 9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+ 10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188); } } @@ -290,19 +323,19 @@ @layer components { .glass { @apply relative border border-white/15 transition-colors duration-300 rounded-[3px] backdrop-blur-sm;- @apply bg-[#1E1F22]/95 !important;+ @apply bg-ui-controls/95 !important; } } .liquid-glass {- background: rgba(11, 11, 12, 0.92);- border: 1px solid rgba(255, 255, 255, 0.15);+ background: rgb(var(--st-scroll-track-rgb) / 0.92);+ border: 1px solid rgb(var(--st-white-rgb) / 0.15); } /* Minimal thin scrollbar for prompt textareas */ .promptbox-scrollbar { scrollbar-width: thin;- scrollbar-color: rgba(255, 255, 255, 0.2) transparent;+ scrollbar-color: rgb(var(--st-white-rgb) / 0.2) transparent; } .promptbox-scrollbar::-webkit-scrollbar {@@ -314,11 +347,11 @@ } .promptbox-scrollbar::-webkit-scrollbar-thumb {- background-color: rgba(255, 255, 255, 0.2);+ background-color: rgb(var(--st-white-rgb) / 0.2); } .promptbox-scrollbar::-webkit-scrollbar-thumb:hover {- background-color: rgba(255, 255, 255, 0.35);+ background-color: rgb(var(--st-white-rgb) / 0.35); } /* Hide the native resize grip (Chromium draws different grips for textareas@@ -344,16 +377,16 @@ background: linear-gradient( 135deg, transparent 40%,- rgba(255, 255, 255, 0.5) 40%,- rgba(255, 255, 255, 0.5) 45%,+ rgb(var(--st-white-rgb) / 0.5) 40%,+ rgb(var(--st-white-rgb) / 0.5) 45%, transparent 45%, transparent 55%,- rgba(255, 255, 255, 0.5) 55%,- rgba(255, 255, 255, 0.5) 60%,+ rgb(var(--st-white-rgb) / 0.5) 55%,+ rgb(var(--st-white-rgb) / 0.5) 60%, transparent 60%, transparent 70%,- rgba(255, 255, 255, 0.5) 70%,- rgba(255, 255, 255, 0.5) 75%,+ rgb(var(--st-white-rgb) / 0.5) 70%,+ rgb(var(--st-white-rgb) / 0.5) 75%, transparent 75% ); }@@ -448,7 +481,7 @@ background: linear-gradient( 90deg, transparent 0%,- rgba(255, 255, 255, 0.08) 50%,+ rgb(var(--st-white-rgb) / 0.08) 50%, transparent 100% ); background-size: 200% 100%;@@ -456,8 +489,8 @@ } .bg-dots {- background-image: radial-gradient(#ffffff20 1px, transparent 0);- background-color: #101014;+ background-image: radial-gradient(rgb(var(--st-white-rgb) / 0.125) 1px, transparent 0);+ background-color: rgb(var(--st-panel-rgb)); background-size: 33px 33px; @apply bg-center; }@@ -535,15 +568,15 @@ margin: -5px; pointer-events: none; background:- linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 1px 11px no-repeat,- linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 11px 1px no-repeat;+ linear-gradient(rgb(var(--st-text-rgb) / 0.4), rgb(var(--st-text-rgb) / 0.4)) center / 1px 11px no-repeat,+ linear-gradient(rgb(var(--st-text-rgb) / 0.4), rgb(var(--st-text-rgb) / 0.4)) center / 11px 1px no-repeat; } /* Contained section borders match the landing without decorative extensions. */ .create-empty-section { margin-inline: 12px;- border: 1px solid rgba(242, 241, 238, 0.2);- background: rgba(242, 241, 238, 0.015);+ border: 1px solid rgb(var(--st-text-rgb) / 0.2);+ background: rgb(var(--st-text-rgb) / 0.015); } @media (min-width: 640px) {@@ -2,6 +2,129 @@ const { createGlobPatternsForDependencies } = require("@nx/react/tailwind"); const { join } = require("path"); import colors from "tailwindcss/colors"; +// The brand blue. Themeable: each shade a theme may change reads its own+// --st-accent-* variable (defaults in src/styles.css are the values below).+// Fills use DEFAULT; text, rings and lines get their own variables further+// down, so a theme can give each the shade that reads in its place.+const primaryScale = {+ DEFAULT: "rgb(var(--st-accent-rgb) / <alpha-value>)",+ 50: "#eef6ff",+ 100: "#d9ebff",+ 200: "#bcdcff",+ 300: "rgb(var(--st-accent-300-rgb) / <alpha-value>)",+ 400: "rgb(var(--st-accent-400-rgb) / <alpha-value>)",+ 500: "rgb(var(--st-accent-rgb) / <alpha-value>)",+ 600: "rgb(var(--st-accent-600-rgb) / <alpha-value>)",+ 700: "rgb(var(--st-accent-700-rgb) / <alpha-value>)",+ 800: "#173fb6",+ 900: "#19398f",+ 950: "#142457",+};++// Faded text (text-white/40 ...) draws at 1 - (1 - alpha) * --st-text-fade.+// The fade is 1 by default, which is exactly the declared alpha; a light+// theme lowers it so its captions stay readable.+const fadedText = (rgbVar) =>+ `rgb(var(${rgbVar}) / calc(1 - (1 - <alpha-value>) * var(--st-text-fade, 1)))`;+// Faded elements (opacity-40 ... opacity-90, mostly captions) follow the same+// fade. 0, 100 and the decorative 5-30 steps stay exact: they show, hide and+// animate things.+const fadedOpacity = Object.fromEntries(+ [40, 50, 60, 70, 75, 80, 90].map((n) => [+ n,+ `calc(1 - (1 - ${n / 100}) * var(--st-text-fade, 1))`,+ ]),+);+// Text in the palette's status colours (text-red-400 errors, amber BETA+// badges ...) reads --st-<family>-<shade>-rgb, which only a theme sets; until+// then the fallback is Tailwind's own colour.+const MEANING_FAMILIES = [+ "rose",+ "indigo",+ "red",+ "blue",+ "orange",+ "amber",+ "green",+ "emerald",+ "teal",+ "cyan",+ "yellow",+ "pink",+ "purple",+];+const MEANING_SHADES = [200, 300, 400, 500, 600];+const COLOR_DEFAULTS = { red: "#D33242", blue: "#2E70FF", green: "#308752" };+const triplet = (hex) =>+ [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16)).join(" ");+const meaningText = Object.fromEntries(+ MEANING_FAMILIES.map((family) => [+ family,+ {+ ...colors[family],+ ...(COLOR_DEFAULTS[family] ? { DEFAULT: COLOR_DEFAULTS[family] } : {}),+ ...Object.fromEntries(+ MEANING_SHADES.map((shade) => [+ shade,+ `rgb(var(--st-${family}-${shade}-rgb, ${triplet(colors[family][shade])}) / <alpha-value>)`,+ ]),+ ),+ },+ ]),+);+// .st-keep-dark: a branded dark surface (the pricing cards) that keeps+// ArtCraft's own colours whatever the theme. Inside it every theme variable+// is reset to its default; when a theme is on it also gets the default page+// colour beneath it, since its see-through gradients were drawn for a dark page.+const KEEP_DARK = {+ "--st-white-rgb": "255 255 255",+ "--st-black-rgb": "0 0 0",+ "--st-text-rgb": "242 241 238",+ "--st-bg-rgb": "18 19 22",+ "--st-panel-rgb": "16 16 20",+ "--st-control-rgb": "30 31 34",+ "--st-sunken-rgb": "6 6 7",+ "--st-accent-rgb": "45 129 255",+ "--st-accent-300-rgb": "142 199 255",+ "--st-accent-400-rgb": "89 167 255",+ "--st-accent-600-rgb": "27 99 245",+ "--st-accent-700-rgb": "20 78 225",+ "--st-accent-ink-rgb": "116 170 255",+ "--st-accent-text-rgb": "45 129 255",+ "--st-accent-line-rgb": "45 129 255",+ "--st-text-fade": "1",+ // The desktop app's names, which shared libs read with their own fallbacks.+ "--st-bg": "initial",+ "--st-fg": "initial",+ "--st-fg-rgb": "initial",+ "--st-divider": "initial",+ "--st-controls-rgb": "initial",+ "--st-icon-filter": "invert(1)",+ "--st-wordmark-filter": "none",+ ...Object.fromEntries(+ MEANING_FAMILIES.flatMap((family) =>+ MEANING_SHADES.map((shade) => [`--st-${family}-${shade}-rgb`, "initial"]),+ ),+ ),+};+const keepDark = ({ addComponents }) =>+ addComponents({+ ".st-keep-dark": KEEP_DARK,+ // Text that sets no colour of its own inherits the page's, which was+ // worked out before the reset above, so the island sets it again.+ // :where() keeps this at zero specificity, so a card's own background+ // and text colour (bg-[#1b1b20], .website-plan-card) still win.+ ':where(:root[data-theme]:not([data-theme="default"]) .st-keep-dark)': {+ backgroundColor: "rgb(18 19 22)",+ color: "rgb(242 241 238)",+ colorScheme: "dark",+ },+ });+const accentFor = (rgbVar) => ({+ ...primaryScale,+ DEFAULT: `rgb(var(${rgbVar}) / <alpha-value>)`,+});+ /** @type {import('tailwindcss').Config} */ module.exports = { content: [@@ -18,6 +141,34 @@ module.exports = { ], theme: { extend: {+ opacity: fadedOpacity,+ textColor: {+ ...meaningText,+ white: fadedText("--st-white-rgb"),+ "base-fg": fadedText("--st-text-rgb"),+ primary: accentFor("--st-accent-text-rgb"),+ "brand-primary": accentFor("--st-accent-text-rgb"),+ },+ ringColor: {+ primary: accentFor("--st-accent-line-rgb"),+ "brand-primary": accentFor("--st-accent-line-rgb"),+ },+ borderColor: {+ primary: accentFor("--st-accent-line-rgb"),+ "brand-primary": accentFor("--st-accent-line-rgb"),+ },+ outlineColor: {+ primary: accentFor("--st-accent-line-rgb"),+ "brand-primary": accentFor("--st-accent-line-rgb"),+ },+ fill: {+ primary: accentFor("--st-accent-line-rgb"),+ "brand-primary": accentFor("--st-accent-line-rgb"),+ },+ stroke: {+ primary: accentFor("--st-accent-line-rgb"),+ "brand-primary": accentFor("--st-accent-line-rgb"),+ }, fontFamily: { mono: [ "Geist Mono",@@ -53,9 +204,12 @@ module.exports = { colors: { transparent: "transparent", current: "currentColor",- white: colors.white,+ // Themeable: "white" is the ink every surface is drawn+ // against and "black" the deepest surface, so a theme swaps them as a+ // pair. The default values are exactly Tailwind's white and black.+ white: "rgb(var(--st-white-rgb) / <alpha-value>)", gray: colors.gray,- black: colors.black,+ black: "rgb(var(--st-black-rgb) / <alpha-value>)", rose: colors.rose, indigo: colors.indigo, red: { ...colors.red, DEFAULT: "#D33242" },@@ -69,20 +223,7 @@ module.exports = { yellow: colors.yellow, pink: colors.pink, purple: colors.purple,- primary: {- DEFAULT: "#2d81ff",- 50: "#eef6ff",- 100: "#d9ebff",- 200: "#bcdcff",- 300: "#8ec7ff",- 400: "#59a7ff",- 500: "#2d81ff",- 600: "#1b63f5",- 700: "#144ee1",- 800: "#173fb6",- 900: "#19398f",- 950: "#142457",- },+ primary: primaryScale, danger: { // copper rust — aligned with artcraft app's tailwind.config.ts DEFAULT: "#8f4951",@@ -98,20 +239,7 @@ module.exports = { 900: "#64373f", 950: "#361b1e", },- "brand-primary": {- DEFAULT: "#2d81ff",- 50: "#eef6ff",- 100: "#d9ebff",- 200: "#bcdcff",- 300: "#8ec7ff",- 400: "#59a7ff",- 500: "#2d81ff",- 600: "#1b63f5",- 700: "#144ee1",- 800: "#173fb6",- 900: "#19398f",- 950: "#142457",- },+ "brand-primary": primaryScale, "brand-secondary": { DEFAULT: "#3E3E41", 50: "#f6f6f6",@@ -168,7 +296,7 @@ module.exports = { 900: "#3d3d3d", 950: "#3E3E41", },- "base-fg": "rgb(242 241 238 / <alpha-value>)",+ "base-fg": "rgb(var(--st-text-rgb) / <alpha-value>)", sidebar: { DEFAULT: "var(--sidebar-bg)", foreground: "var(--sidebar-fg)",@@ -180,19 +308,19 @@ module.exports = { ui: { // Brutalist system: near-black surfaces with white/15 hairline // borders, aligned with artcraft-website + artcraft-website-next.- background: "#121316",- panel: "#101014",- "panel-border": "rgb(255 255 255 / 0.15)",- border: "rgb(255 255 255 / 0.15)",+ background: "rgb(var(--st-bg-rgb) / <alpha-value>)",+ panel: "rgb(var(--st-panel-rgb) / <alpha-value>)",+ "panel-border": "rgb(var(--st-white-rgb) / 0.15)",+ border: "rgb(var(--st-white-rgb) / 0.15)", // Raised controls stay distinct from the website-matched panel surface.- controls: "#1e1f22",- "controls-button": "#1e1f22",- "controls-border": "rgb(255 255 255 / 0.15)",- divider: "rgb(255 255 255 / 0.15)",- modal: "#101014",- ink: "#f2f1ee",- "accent-ink": "#74aaff",- sunken: "#060607",+ controls: "rgb(var(--st-control-rgb) / <alpha-value>)",+ "controls-button": "rgb(var(--st-control-rgb) / <alpha-value>)",+ "controls-border": "rgb(var(--st-white-rgb) / 0.15)",+ divider: "rgb(var(--st-white-rgb) / 0.15)",+ modal: "rgb(var(--st-panel-rgb) / <alpha-value>)",+ ink: "rgb(var(--st-text-rgb) / <alpha-value>)",+ "accent-ink": "rgb(var(--st-accent-ink-rgb) / <alpha-value>)",+ sunken: "rgb(var(--st-sunken-rgb) / <alpha-value>)", }, "axis-x": "#D33242", "axis-y": "#308752",@@ -227,5 +355,5 @@ module.exports = { ring: "hsl(var(--ring))", }, },- plugins: [],+ plugins: [keepDark], };@@ -268,7 +268,7 @@ interface PageDrawProps { * Tailwind class for the full-viewport canvas backdrop (and the base-image * selector surface). Defaults to the app's `bg-ui-background`; hosts that * embed PageDraw under their own page chrome can pass a matching color (e.g.- * the webapp's `bg-[#101014]`).+ * the webapp's `bg-ui-panel`). */ backgroundClassName?: string; /**@@ -194,7 +194,7 @@ export function PricingContent({ title, subtitle }: PricingContentProps) { className={twMerge( getColorSchemeClasses(plan.colorScheme), frame- ? `relative z-10 h-full w-full border-2 bg-[#101014] shadow-2xl ${frame.border}`+ ? `relative z-10 h-full w-full border-2 bg-ui-panel shadow-2xl ${frame.border}` : "", isCurrent ? "ring-2 ring-white/50" : "", )}@@ -323,7 +323,7 @@ const getColorSchemeClasses = ( colorScheme: SubscriptionPlanDetails["colorScheme"], ) => { const base =- "relative flex flex-col rounded-3xl border p-6 transition-all duration-300 backdrop-blur-md md:p-8";+ "st-keep-dark relative flex flex-col rounded-3xl border p-6 transition-all duration-300 backdrop-blur-md md:p-8"; switch (colorScheme) { case "green":@@ -430,7 +430,7 @@ export function PricingModal({}: PricingModalProps = {}) { <Modal isOpen={isOpen} onClose={closeModal}- className="flex max-h-[90vh] max-w-screen-2xl flex-col overflow-y-auto rounded-2xl border border-white/5 bg-[#101014]"+ className="flex max-h-[90vh] max-w-screen-2xl flex-col overflow-y-auto rounded-2xl border border-white/5 bg-ui-panel" allowBackgroundInteraction={false} showClose={true} closeOnOutsideClick={true}@@ -141,8 +141,8 @@ const PricingTable = ({ colorScheme: SubscriptionPlanDetails["colorScheme"], ) => { const baseClasses = unifiedTheme- ? "relative p-5 md:p-6 border flex flex-col transition-all duration-300"- : "relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md";+ ? "st-keep-dark relative p-5 md:p-6 border flex flex-col transition-all duration-300"+ : "st-keep-dark relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md"; switch (colorScheme) { case "dark":@@ -397,7 +397,7 @@ const PricingTable = ({ className={twMerge( getColorSchemeClasses(plan.colorScheme), frame- ? `relative z-10 border-2 h-full w-full shadow-2xl bg-[#101014] ${frame.border}`+ ? `relative z-10 border-2 h-full w-full shadow-2xl bg-ui-panel ${frame.border}` : "", isCurrent ? "ring-2 ring-white/50" : "", )}@@ -528,8 +528,8 @@ const PricingTable = ({ <div className={ unifiedTheme- ? "relative p-5 md:p-6 border flex flex-col transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"- : "relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+ ? "st-keep-dark relative p-5 md:p-6 border flex flex-col transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+ : "st-keep-dark relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]" } > <h3 className="text-xl md:text-2xl font-semibold mb-2 text-white">@@ -564,8 +564,8 @@ const PricingTable = ({ <div className={ unifiedTheme- ? "mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"- : "mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+ ? "st-keep-dark mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+ : "st-keep-dark mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]" } > <div className="flex-shrink-0">@@ -718,7 +718,7 @@ const ContactUsButton = ({ {open && ( <div className={twMerge(- "absolute top-full z-20 mt-2 overflow-hidden border border-white/15 bg-[#1b1b20] shadow-[0_20px_60px_-15px_rgba(0,0,0,0.7)]",+ "st-keep-dark absolute top-full z-20 mt-2 overflow-hidden border border-white/15 bg-[#1b1b20] shadow-[0_20px_60px_-15px_rgba(0,0,0,0.7)]", menuClassName, )} >@@ -193,7 +193,7 @@ function PlanCell({ data-highlight={ PLAN_STYLE[plan.slug]?.highlight || isCurrent ? "" : undefined }- className={`website-plan-card plan-${plan.colorScheme} flex flex-col`}+ className={`st-keep-dark website-plan-card plan-${plan.colorScheme} flex flex-col`} > <CellIndexRow index={index}> {isCurrent ? (@@ -270,7 +270,7 @@ function EnterpriseCell({ index }: { index: string }) { return ( <article id="enterprise"- className="website-plan-card plan-enterprise flex flex-col"+ className="st-keep-dark website-plan-card plan-enterprise flex flex-col" > <CellIndexRow index={index}>For teams</CellIndexRow> @@ -85,7 +85,7 @@ export function DesktopLoginApproval({ loggedIn, authChecked, signIn }: DesktopL if (!token) return <Status>Open a new login link or scan the QR code from ArtCraft.</Status>; if (!loggedIn) return <>{signIn}</>; - return <main className="flex min-h-svh items-center justify-center bg-[#101014] p-6 text-white">+ return <main className="flex min-h-svh items-center justify-center bg-ui-panel p-6 text-white"> <section className="w-full max-w-md border border-white/15 bg-white/5 p-6 sm:p-8"> <Link to="/" className="text-sm text-white/60">ArtCraft</Link> <h1 className="mb-4 mt-6 text-3xl font-semibold">Log in to ArtCraft Desktop?</h1>@@ -108,7 +108,7 @@ export function DesktopLoginApproval({ loggedIn, authChecked, signIn }: DesktopL } function Status({ children }: { children: ReactNode }) {- return <main className="flex min-h-svh items-center justify-center bg-[#101014] p-8 text-white" role="status">{children}</main>;+ return <main className="flex min-h-svh items-center justify-center bg-ui-panel p-8 text-white" role="status">{children}</main>; } function completionMessage(result: LoginChallengeResult): string | null {