PVR Tech Studio

Design Skins

Thirteen switchable visual designs — eleven token-only color skins plus two full designs (Bento Studio and Console) that also swap fonts, radius and the home dashboard.

8 min read
Updated July 15, 2026

Overview

Skins are a headline selling point of Luminaux: the entire app can be re-colored (and, for two of them, re-typeset and re-laid-out) instantly, with the choice persisted and applied before first paint so there's never a flash of the wrong design.

Because design tokens are the only place colors live, a skin is nothing more than a block of token overrides scoped under [data-skin="…"] on <html>. Switch the attribute and the whole UI re-skins.

Color skins (token-only)

Default, Aqua, Lime, Lilac, Prism, Midnight, Ocean, Graphite, Emerald, Amber, Coral — they redefine the raw color tokens and nothing else, as variants of the standard layout.

Full designs

Bento Studio (bento) and Console (console). In addition to colors they swap fonts and card radius, and render a bespoke home dashboard.

Every skin defines both a light and a dark variant, so skin and light/dark mode are orthogonal.

Architecture & files

FileResponsibility
src/styles/index.cssDefines the default palette, scoped to :root, [data-skin='default'] (+ .dark, [data-skin='default'].dark). Also the @theme inline mapping and skin-aware .font-display/.font-data helpers.
src/styles/_skin-colors.scssAll eleven color skins except default — one [data-skin="x"] + one [data-skin="x"].dark block each (raw --* token overrides only).
src/styles/_skin-bento.scssBento Studio: warm palette, big radius (--radius-card: 1.5rem), tile-tint tokens, flattened shadows.
src/styles/_skin-console.scssConsole: terminal palette, monospace --font-*, tight radius (0.25rem).
src/styles/_skin-prism.scssPrism: neutral base + categorical --c1..--c6, applied to .stat-icon / .chart-bar marker classes.
src/config/skins.tsThe Skin union and the SKINS array — the registry, kept free of React imports so build tooling can read it too.
src/context/LayoutContext.tsxskin/setSkin, <html data-skin> reflection, and localStorage('design_skin') persistence. Re-exports SKINS/Skin, so @/context/LayoutContext stays the import site.
index.htmlPre-paint script with the skin allowlist — applies data-skin before paint (falls back to default for unknown values).
src/layout/overlays/LayoutCustomizer.tsxThe "Design" skin picker; maps over SKINS, labels via customizer:skin<Cap>Label/Hint.
src/layout/DesignSystemMenu.tsxHeader palette-icon quick switcher with live swatches.
src/pages/HomeRoute.tsxThe home branch — renders BentoDashboard for bento, ConsoleDashboard for console, else CrmDashboard.
src/locales/en/customizer.jsonPer-skin skin<Cap>Label / skin<Cap>Hint strings.

Mechanism at a glance: setSkin → React state → an effect writes <html data-skin> and localStorage('design_skin') → the scoped SCSS block wins → the app re-skins. On reload, the pre-paint script re-applies data-skin before paint.

Usage

For non-developers. A skin can be switched from four places, all sharing one source of truth:

  • Layout Customizer (header gear → "Design" section) — cards with a label + hint.
  • Layout Settings page (/layout-settings) — the full-page twin of the Customizer.
  • ⌘K command palette — searchable skin entries.
  • Header DesignSystemMenu (palette icon) — quick switch with live color swatches.

The choice is remembered across reloads and sessions.

For developers. Read and set the skin from the layout context:

import {useLayout, SKINS, type Skin} from '@/context/LayoutContext'
 
function SkinPicker() {
    const {skin, setSkin} = useLayout()
    return (
        <div className="flex gap-2">
            {SKINS.map((id) => (
                <button
                    key={id}
                    onClick={() => setSkin(id)}
                    className={id === skin ? 'ring-2 ring-ring' : ''}
                >
                    {id}
                </button>
            ))}
        </div>
    )
}

The Skin type & SKINS list

From src/context/LayoutContext.tsx:

export type Skin =
    | 'default' | 'bento' | 'aqua' | 'lime' | 'lilac' | 'prism'
    | 'midnight' | 'ocean' | 'graphite' | 'emerald' | 'amber' | 'coral' | 'console'
 
export const SKINS: Skin[] = [
    'default', 'aqua', 'lime', 'lilac', 'prism', 'midnight',
    'ocean', 'graphite', 'emerald', 'amber', 'coral', 'console', 'bento',
]

Skin catalog

data-skinLabel / hint (i18n)KindCharacterNotes
defaultDefault · Indigo · cleanColorClean blue SaaSBaseline palette in index.css.
aquaAqua · Teal · cyanColorTeal / cyan
limeLime · Sky · limeColorSky + vivid limeUses dark --primary-foreground (#0b0f0a) for contrast on lime.
lilacLilac · LavenderColorLavender / mauve
prismPrism · MulticolorColorViolet chrome + multicolor dataCategorical --c1..--c6; see below.
midnightMidnight · Slate · indigoColorSlate-indigo
oceanOcean · Azure · blueColorAzure / blue
graphiteGraphite · Mono · minimalColorMono near-black / light-grayinfo stays blue for signal.
emeraldEmerald · greenColorGreen
amberAmber · goldColorGoldUses dark --primary-foreground (#1f1500) for contrast on amber.
coralCoral · warmColorSalmon / warm
consoleConsole · Dev · terminalDesignTerminal green, JetBrains Mono, tight radius 0.25remRenders ConsoleDashboard.
bentoBento Studio · Warm · bentoDesignWarm tangerine, bento-grid, big radius 1.5remKeeps default font; renders BentoDashboard.

Each color skin overrides the standard token set (--background, --surface, --surface-muted, --foreground, --muted-foreground, --border, --primary, --primary-hover, --primary-foreground, --ring, and typically --info) under [data-skin="x"] and again under [data-skin="x"].dark.

Layout context skin API

From useLayout():

MemberTypeDescription
skinSkinThe active skin.
setSkin(s)(Skin) => voidSwitch skin; an effect reflects <html data-skin> and persists localStorage('design_skin').

Persistence key: localStorage('design_skin'). Invalid/unknown values fall back to 'default' (both at load time in loadSkin() and in the pre-paint allowlist).

Prism categorical palette

Prism keeps chrome on a single violet --primary but colors data elements with a six-hue categorical palette (src/styles/_skin-prism.scss):

VarLightDark
--c1#7c3aed (violet)#a78bfa
--c2#ec4899 (pink)#f472b6
--c3#f97316 (orange)#fb923c
--c4#14b8a6 (teal)#2dd4bf
--c5#3b82f6 (blue)#60a5fa
--c6#f59e0b (amber)#fbbf24

Applied via marker classes: .stat-grid > * .stat-icon (each card a different hue, cycled 4n) and .chart-bar (cycles the full palette 6n). These marker classes are harmless no-ops under every other skin.

Adding a skin = 5 spots

To ship a new color skin (say sunset), touch exactly these five places:

Add the SCSS block

In src/styles/_skin-colors.scss add a [data-skin="sunset"] block and a [data-skin="sunset"].dark block, each redefining the 11 raw --* tokens (use raw --*, never --color-*).

Extend the type & list

In src/config/skins.ts add 'sunset' to the Skin union and to the SKINS array (its position sets the display order).

Confirm the picker

src/layout/overlays/LayoutCustomizer.tsx maps over SKINS, so it picks the new skin up automatically; just make sure its label keys exist (step 5). (Historically this was a separate skinIds list; the current customizer iterates SKINS directly.)

Add to the pre-paint allowlist

In index.html add 'sunset' to the pre-paint skins allowlist array so it applies before paint (otherwise it falls back to default on reload).

Add the labels

In src/locales/en/customizer.json add skinSunsetLabel and skinSunsetHint strings (the Customizer/Settings/⌘K read customizer:skin<Cap>Label/Hint).

Layout Settings, the ⌘K palette and the header DesignSystemMenu then pick it up automatically from SKINS.

Adding a full "design"

A full design (like Bento or Console) does everything a color skin does plus:

  • Its own _skin-*.scss partial with font/radius overrides (e.g. --font-sans/-display/-mono, --radius-card, extra tokens like Bento's tile tints). Bento also softens .shadow-sm and maps .rounded-xl/.rounded-lg to its larger radius.
  • A branch in HomeRoute() (src/pages/HomeRoute.tsx) to render its bespoke dashboard:
export function HomeRoute() {
    const {skin} = useLayout()
    if (skin === 'bento') return <BentoDashboard/>
    if (skin === 'console') return <ConsoleDashboard/>
    return <CrmDashboard/>
}

Bento tiles live in src/components/bento/; the Console dashboard is src/pages/dashboards/ConsoleDashboard.tsx.

Examples

A skin's SCSS block (raw tokens, light + dark) — the shape you copy for a new color skin:

/* src/styles/_skin-colors.scss */
[data-skin='ocean'] {
  --background: #f0f6fc;
  --surface: #ffffff;
  --surface-muted: #e6f0f9;
  --foreground: #0c2030;
  --muted-foreground: #51687a;
  --border: #d7e6f1;
  --primary: #0a84ff;
  --primary-hover: #0a6fd6;
  --primary-foreground: #ffffff;
  --ring: #0a84ff;
  --info: #22a7f0;
}
 
[data-skin='ocean'].dark {
  --background: #04121f;
  --surface: #0a1f30;
  /* …the dark counterparts… */
  --primary: #3aa0ff;
  --primary-foreground: #04121f;
}

A full-design partial adds fonts + radius (Console):

/* src/styles/_skin-console.scss */
[data-skin='console'] {
  --primary: #15803d;
  --font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-display: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --radius-card: 0.25rem;
}

Rendering a per-skin swatch preview (scope a nested data-skin so it shows that skin's colors regardless of the active one):

<div data-skin="emerald" className="bg-surface border border-border p-3 rounded-lg">
    <span className="inline-block h-4 w-4 rounded-full bg-primary"/>
</div>

Best practices

  • Skin = token overrides only. Keep color skins purely to --* values; don't add layout/behavior. Reserve font/radius/dashboard changes for genuine full "designs".
  • Always use raw --* tokens in the SCSS, never --color-* (those are Tailwind aliases that resolve transparent in plain CSS). See Design Tokens & CSS.
  • Define both light and dark for every skin — they're orthogonal to the theme mode.
  • Watch --primary-foreground contrast on light/vivid primaries. Lime and Amber deliberately use a dark --primary-foreground so text on the accent stays legible.
  • Keep the allowlist in sync. Every skin id must appear in the index.html pre-paint array, or it won't survive a reload (it falls back to default).
  • Register persisted keys. design_skin is already handled by the "Reset to defaults" flow (src/lib/appStorage.ts); any new skin-related key must be added there too.

Troubleshooting

SymptomLikely cause & fix
New skin reverts to default after reload.Its id isn't in the index.html pre-paint skins allowlist. Add it.
New skin doesn't appear in the picker.Missing from SKINS in LayoutContext, or its customizer:skin<Cap>Label key is absent.
Skin colors look transparent / broken in SCSS.You used var(--color-*). Use raw var(--surface) etc.
Text on the accent is unreadable (light primary).Set a dark --primary-foreground for that skin (as Lime/Amber do).
Prism multicolor isn't showing.The data element is missing its marker class (.stat-icon inside .stat-grid, or .chart-bar).
Full design's dashboard doesn't change.Add the skin === 'x' branch in HomeRoute() (src/pages/HomeRoute.tsx).
Nested "default" swatch inherits the active skin.Ensure the default palette stays scoped to :root, [data-skin='default'] (it is, in index.css); wrap the swatch in data-skin="default".

FAQ

How many skins ship? Thirteen: eleven color skins (including Default) plus two full designs (Bento Studio, Console).

Is a skin the same as dark mode? No. Skin (color identity) and mode (light/dark) are independent; every skin defines both light and dark. See Design Tokens & CSS.

Do color skins change the layout? No — they're pure token overrides. Only the two full designs also swap fonts, radius and the home dashboard.

Why do Lime and Amber use dark text on the accent? Their --primary is a bright yellow/gold; white text would fail contrast, so --primary-foreground is a near-black.

Where does the skin persist? localStorage('design_skin'), reflected on <html data-skin> and re-applied pre-paint from index.html.

Can OLED combine with a skin? Yes — OLED deepens the dark neutrals while the skin's --primary accent is preserved.

Notes for designers & content editors

  • To design a new skin, you only pick eleven colors × two modes (light + dark). Match the token roles: canvas (--background), cards (--surface), recessed (--surface-muted), text (--foreground / --muted-foreground), lines (--border), accent (--primary + hover + foreground), focus (--ring), and --info.
  • Verify the accent contrast: whatever sits on a --primary fill uses --primary-foreground. If the accent is light, make that foreground dark.
  • Bento and Console are more than palettes — they change type and layout. Treat them as distinct product looks, not just color themes.
  • Labels and one-line hints for each skin are editable in src/locales/en/customizer.json (skin<Name>Label / skin<Name>Hint) and translated per language.

Was this page helpful?