Auch verfügbar in: DE Deutsch

Theming System

Inhalt

ITSWEBER Play features a 6-layer token architecture that enables live theme editing without page reloads. Every visual property — colors, radii, shadows, fonts — flows through a structured chain from raw values to rendered pixels.


The 6 Layers

Layer 1: Primitive Tokens

Location: packages/theme/tokens.json

Raw values with no semantic meaning. Colors, font families, border radii, shadow definitions. These are the foundation — they are never used directly in components. Components always reference semantic tokens (Layer 2).

{
  "color": {
    "neutral": { "50": "#f6f8fa", "…": "…", "900": "#0e1116" },
    "teal": { "400": "#3ba7a7", "500": "#3ba7a7", "600": "#2d8a8a", "700": "#236e6e" },
    "red": { "500": "#d9534f", "600": "#b43935" },
    "amber": { "500": "#f0b429", "600": "#cb8e14" },
    "green": { "500": "#3fb950", "600": "#2ea043" }
  },
  "radius": {
    "sm": "6px",
    "md": "10px",
    "lg": "16px",
    "full": "9999px"
  },
  "shadow": {
    "card": "0 4px 20px rgba(0,0,0,.35)",
    "lg": "0 12px 40px rgba(0,0,0,.45)"
  },
  "font": {
    "sans": "'Geist', 'Geist Variable', system-ui, -apple-system, 'Segoe UI', sans-serif",
    "mono": "'Geist Mono', 'Geist Mono Variable', ui-monospace, 'SF Mono', monospace"
  }
}

Layer 2: Semantic Tokens

Location: packages/theme/styles/semantic.css (mirrored for JavaScript in packages/theme/src/semantic.ts)

Semantic tokens map named roles — bg, fg, brand, danger, warning — to primitive values. Tailwind classes and component styles reference only these tokens, never raw hex values.

Semantic Token Role Maps to (dark default)
--color-background Page background --color-neutral-900
--color-surface Card / panel background --color-neutral-800
--color-foreground Primary text --color-neutral-100
--color-muted Secondary text --color-neutral-300
--color-brand Accent / interactive --color-teal-500
--color-brand-hover Hover state --color-teal-400
--color-danger Errors, destructive actions --color-red-500
--color-warning Warnings --color-amber-500
--shadow-card Card elevation fixed value in semantic.css
// packages/theme/src/semantic.ts (excerpt)
export const semantic = {
  bg: { base: "var(--color-neutral-900)", surface: "var(--color-neutral-800)" },
  fg: { primary: "var(--color-neutral-50)", muted: "var(--color-neutral-400)" },
  brand: { default: "var(--color-teal-500)", hover: "var(--color-teal-600)" },
  intent: { danger: "var(--color-red-500)", warning: "var(--color-amber-500)" },
} as const;

Layer 3: Admin Live-Editor

Path: /admin/theme

A colorpicker and slider UI lets admins adjust semantic tokens at runtime. Changes are:

  1. Persisted in the Prisma model ThemeSettings, field tokensOverride
  2. Broadcast to all open tabs via Redis pub/sub and Server-Sent Events (/api/theme/events)

No deployment or restart is required. See Live Update Mechanism below.


Layer 4: Preset Themes

Built-in named presets from packages/theme/presets/. Switchable in the admin panel with a single click. Custom presets are not stored in the database, but the current state can be exported and imported as JSON.

Preset Description Best for
itsweber-dark Default dark theme with teal accent General use
itsweber-light Light counterpart to itsweber-dark Bright environments
itsweber-brand Dark navy with green accent after itsweber.de Official instance
high-contrast Maximum contrast for low vision Accessibility
retro 80s synthwave with magenta and cyan on violet Personal/niche instances

Layer 5: Custom CSS

Freeform CSS input in the admin sandbox. Applied after all token layers, giving full override power. Features:

  • Versioned: last 20 revisions retained, each restorable
  • Checked: before saving, the CSS is checked for forbidden constructs (@import, url(javascript:…), expression(), </style>, <script, behavior:) and limited to 100 KB. There is no syntax check
  • Scoped: applied inside a single <style> tag, no framework classes required

See Custom CSS Tips for common patterns.


Layer 6: Layout Blocks

The homepage is composed from reorderable blocks, managed at /admin/page-blocks. Available block types:

Block Description
Hero Full-width banner with title, subtitle, CTA button
Category-Chips Scrollable category filter row
Video-Grid Configurable grid of recent/featured videos
Shorts-Row Row of short videos
Channel-Row Row of channel cards
CTA-Banner Call-to-action strip with custom text and link
Community-Row Row of community posts

Drag-and-drop reordering is supported. Changes take effect immediately without a page reload.


Live Update Mechanism

When an admin saves a theme change, the update propagates to all open browser sessions instantly:

Admin change → tRPC theme.update → DB save + Redis publish → SSE /api/theme/events
Frontend: replaces <style id="theme-vars"> with new CSS variables
Result: instant update, no React rerender required

The frontend keeps an EventSource connection to /api/theme/events. On receiving a theme:updated event, it re-fetches the current theme via tRPC and swaps the contents of <style id="theme-vars"> and <style id="theme-custom">. Because CSS variables cascade, every element using those variables updates in place — no component state change, no navigation.


Token Mapping Example

// tokens.json (primitives)
{
  "color": {
    "teal": { "500": "#3ba7a7" }
  }
}
/* styles/semantic.css (semantic mapping) */
--color-brand: var(--color-teal-500);
/* tokens.json becomes: --color-teal-500: #3ba7a7 */
/* component usage */
.btn-primary {
  background-color: var(--color-brand);
}
.btn-primary:hover {
  background-color: var(--color-brand-hover);
}

Overrides address the primitives in dot notation. An admin setting color.teal.500 to #3b82f6 (blue) in the live editor writes { "color.teal.500": "#3b82f6" } to ThemeSettings.tokensOverride. This becomes --color-teal-500: #3b82f6 in <style id="theme-vars">, and --color-brand picks it up through the cascade.


Logo Filter Presets

The instance logo supports 10 CSS filter presets, applied via var(--logo-filter). This allows the logo to adapt to light/dark backgrounds without requiring separate image assets.

Preset CSS filter value Use case
none none No modification
glow drop-shadow(0 0 12px var(--color-teal-500)) Glow in accent color
softGlow drop-shadow(0 0 6px rgba(59,167,167,.6)) Subtle glow
duotone grayscale(1) brightness(1.1) sepia(.3) hue-rotate(140deg) saturate(3) Single-tone tint
shadow drop-shadow(0 4px 8px rgba(0,0,0,.5)) Shadow for depth
brightness0 brightness(0) Solid black
invert invert(1) Invert colors
invertGlow invert(1) drop-shadow(0 0 12px var(--color-teal-500)) Inverted with glow
mono grayscale(1) contrast(1.2) brightness(1.05) Grayscale
onlyWhite brightness(0) invert(1) Solid white

Custom CSS Tips

Background gradient on the page

body {
  background: linear-gradient(135deg, var(--color-background) 0%, #1a0a00 100%);
}

Font override

:root {
  --font-sans: 'Nunito', system-ui, sans-serif;
}

Radius step adjustment

:root {
  --radius-md: 1.5rem;
}

Full-bleed hero background

/* adjust the selector to the real markup, the code sets no .hero-block class */
.hero-block {
  background-image: url('/s3/play-assets/hero-bg.jpg');
  background-size: cover;
  background-position: center;
}

Safety: Custom CSS containing @import (to any target), url(javascript:…), url(data:text/html…), expression(), behavior:, <script or </style> is rejected with an error on save, not silently stripped. There is no syntax check, so broken CSS is saved and simply has no effect.