xPulse
🇩🇪 DE

@xpulse/theme – Component Spec

Status: CONCEPT · Erstellt März 2026 Übergeordnet: GLOBAL_concept-ecosystem.md Löst auf: COMP_ui_spec.md, COMP_ui_concept.md (Theme-Schicht) Begleitdokument: GLOBAL_concept-brand.md


Übersicht

CSS Design System für alle xPulse Tools – Struktur, Farben, Effekte und Animationen in einem Package. Kein separates @xpulse/theme – alles lebt in @xpulse/theme.

Ein Theme ist eine CSS-Datei die Custom Properties, Komponenten-Styles, Hover/Focus-Effekte und Animationen definiert. Themes werden per Discovery automatisch gefunden – kein manuelles Registrieren.

Was das Package liefert:


Custom Properties – Naming Convention

Alle Custom Properties folgen dem --xpulse-* Prefix – eindeutig, kein Konflikt mit anderen Libraries, konsistent mit dem BEM-Gedanken.

/* Farben */
--xpulse-bg
--xpulse-surface
--xpulse-surface-strong
--xpulse-border
--xpulse-muted
--xpulse-text
--xpulse-text-dim
--xpulse-accent
--xpulse-accent2
--xpulse-accent-soft
--xpulse-warning
--xpulse-danger
--xpulse-shadow
--xpulse-input-bg
/* Typografie */
--xpulse-font-mono
--xpulse-font-serif
--xpulse-font-size-body
--xpulse-font-size-display
--xpulse-font-size-h2
--xpulse-font-size-h3
--xpulse-font-size-lead
--xpulse-font-size-meta
/* Effekte */
--xpulse-transition
--xpulse-radius
--xpulse-hover-opacity
/* Buttons */
--xpulse-button-text
--xpulse-button-radius
--xpulse-button-font-size
--xpulse-button-min-height
--xpulse-button-padding-y
--xpulse-button-padding-x
/* …sowie Größenvarianten: xs, sm, md, lg */
--xpulse-button-hover-opacity
--xpulse-button-active-scale

Themes

dark.css – Standard xPulse Theme

Datei: public/css/themes/dark.css

CSS-Selektor: html[data-theme="dark"] (plus :root als Default-Fallback).

Auszug der wichtigsten Tokens:

:root,
html[data-theme="dark"] {
--xpulse-bg: #0d0d0d;
--xpulse-surface: #141414;
--xpulse-surface-strong: #1b1b1b;
--xpulse-text: #c8c8c8;
--xpulse-text-dim: #555;
--xpulse-border: #222;
--xpulse-muted: #444;
--xpulse-accent: #8703b0;
--xpulse-accent2: #7eb8a4;
--xpulse-accent-soft: rgba(135, 3, 176, 0.18);
--xpulse-warning: #b89a3e;
--xpulse-danger: #c0606a;
--xpulse-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
--xpulse-input-bg: #141414;
/* …Font-Size-Tokens, Button-Tokens etc. */
}
/* System-Preference-Fallback falls noch kein data-theme gesetzt */
@media (prefers-color-scheme: dark) {
html:not([data-theme]) { ... }
}

light.css – Helles Theme

Analog zu dark.css, Selektor html[data-theme="light"].

App-Theme-Erweiterung

Eine App legt eigene CSS-Overrides unter src/themes/ ab:

/* src/themes/app.css */
.xpulse-shell {
width: min(1100px, calc(100% - 32px));
margin: 32px auto;
}

app.css wird zusätzlich zum Theme-Bundle als separater Link eingebunden – sie ersetzt das Basis-Theme nicht, sondern ergänzt es.


CSS-Bundle und Discovery

Alle CSS-Dateien werden beim init() zu einem einzigen Bundle zusammengefasst und über eine einzelne Route ausgeliefert:

GET /_theme/css/xpulse.css ← minifiziertes All-in-One-Bundle
GET /_theme/theme.js ← optionales Browser-Runtime-Script

Kein separates Laden einzelner Theme-Dateien im Browser – ein Request, ein Bundle.

Aufbaureihenfolge im Bundle

1. public/css/base.css ← Reset, Body, Noise-Overlay, Animationen
2. public/css/components/*.css ← alle Komponenten (via @import in components.css)
3. public/css/themes/*.css ← Built-in Theme-Tokens (dark, light)
4. src/themes/*.css ← App-spezifische Overrides (optional)

Theme-Name-Discovery

Theme-Namen werden aus dem gebündelten CSS per Regex extrahiert – kein Filename-Parsing, kein manuelles Registrieren:

[data-theme="dark"] → 'dark'
[data-theme="light"] → 'light'
[data-theme="app"] → 'app'

Nur Themes die im CSS als Selektor auftauchen, gelten als bekannt.

App-Theme-Overrides

App-CSS unter src/themes/ wird ans Bundle-Ende angehängt. Der app-Theme-Link wird zusätzlich als separater <link> eingebunden (via themeAssets()).


xpulse.json Konfiguration

{
"theme": {
"default": "dark",
"allow": ["dark", "light"],
"cache": {
"enabled": true,
"ttl": 0
}
}
}
Key Beschreibung Default
theme.default Aktives Theme beim Start "dark"
theme.allow Welche Themes der User wählen darf. [] = alle entdeckten Themes erlaubt []
theme.cache.enabled CSS-Bundle cachen (siehe Caching-Abschnitt) true
theme.cache.ttl Cache-Ablauf in Sekunden. 0 = kein Ablauf 0

Wenn allow leer oder nicht gesetzt – alle entdeckten Themes sind erlaubt. Wenn allow genau ["dark", "light"] enthält – Toggle-Button statt Select. Wenn allow mehr als 2 Themes enthält – Select-Dropdown. Wenn allow nur ein Theme enthält – kein Switcher in der UI.

Component-Default (`@xpulse/theme/xpulse.json`)

{
"theme": {
"default": "dark",
"allow": [],
"cache": {
"enabled": true,
"ttl": 0
}
}
}

Server-API (Node.js)

import theme from '@xpulse/theme';
// Initialisieren – Bundle bauen, Routes registrieren
await theme.init();
// Mit Optionen
await theme.init({ root: process.cwd(), force: false });
// root → wo nach src/themes/ gesucht wird (default: cwd)
// force → Cache ignorieren und Bundle neu bauen
// Aktives Default-Theme (aus Config)
theme.current(); // → 'dark'
// Alle im CSS entdeckten Themes
theme.available(); // → ['dark', 'light']
// Erlaubte Themes (gefiltert durch theme.allow)
theme.allowed(); // → ['dark', 'light']
// Existenzprüfung
theme.has('dark'); // → true
// Theme-Wechsel (Server-seitig)
theme.set('light');
// Toggle (nur wenn genau 2 Themes in allowed())
theme.toggle();
// URLs
theme.href(); // → '/_theme/css/xpulse.css'
theme.scriptHref(); // → '/_theme/theme.js'

Wie der Toggle funktioniert

Theme-Wechsel wird durch data-theme-Attribut auf <html> gesteuert – CSS-Selektoren html[data-theme="dark"] greifen ohne JS-seitiges DOM-Styling. Zur Sicherheit wird zusätzlich die Klasse theme-${name} gesetzt.

<html data-theme="dark" class="theme-dark"> ← default
<html data-theme="light" class="theme-light"> ← nach toggle

Anti-FOUC: Das von themeAssets() generierte Inline-Script setzt data-theme und die Klasse bereits im <head>, bevor das erste Paint stattfindet.

User-Präferenz wird im Browser in localStorage gespeichert:

// localStorage Key: xpulse_theme
'dark' | 'light' | ...

Gefeuerte Events

Lifecycle

Event Payload Wann
themes:load:start Bundle-Build beginnt
theme:basics:loaded base.css geladen
theme:components:loaded Alle Component-CSS geladen
theme:loaded { name } Einzelnes Theme-CSS geladen
themes:concatenated Alle Parts zusammengefügt
themes:minified Bundle minifiziert
themes:cached { path } Bundle auf Disk geschrieben
themes:load:end Bundle-Build abgeschlossen
theme:ready { theme, available } init() vollständig

Cache

Event Payload Wann
themes:cache:hit { path } Gültiger Cache-File gefunden
themes:cache:miss Kein Cache, Bundle wird neu gebaut
themes:cache:loaded { serveCount } Bundle-Request bedient

Laufzeit

Event Payload Wann
theme:changed { from, to } nach theme.set() / theme.toggle()
theme:warning { type, … } Ungültiges Theme, Toggle nicht möglich etc.
theme:error { type } Keine Themes im Bundle gefunden

Caching

Das CSS-Bundle wird beim ersten init() auf Disk geschrieben und bei Folge-Starts direkt von dort gelesen – kein erneutes Lesen + Minifizieren.

var/cache/theme/xpulse.css

Verhalten nach Umgebung

Situation Verhalten
NODE_ENV !== 'production' (Dev-Mode) Cache immer übersprungen, Bundle wird neu gebaut
Production, Cache-File vorhanden Cache wird geladen (Event: themes:cache:hit)
Production, kein Cache-File Bundle wird gebaut und gecacht (Event: themes:cache:miss)
init({ force: true }) Cache wird ignoriert, Bundle neu gebaut
theme.cache.ttl > 0 Cache-File zu alt → wird neu gebaut

Konfiguration

{
"theme": {
"cache": {
"enabled": true,
"ttl": 0
}
}
}

ttl: 0 bedeutet kein Ablauf – Cache bleibt bis zum nächsten manuellen Build oder force: true.


CSS Reset + Base

Aus pwa/public/css/base.css extrahiert – vollständig generisch:

/* Reset */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }
/* Base */
body {
background: var(--xpulse-bg);
color: var(--xpulse-text);
font-family: var(--xpulse-font-mono);
font-size: 13px;
line-height: 1.7;
}

Noise-Overlay

Der charakteristische Brand-Effekt – gehört zu jedem xPulse Tool:

body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 9999;
opacity: 0.4;
}

Im light Theme deaktiviert:

html.theme-light body::before { display: none; }

Animationen

@keyframes xpulse-fadeIn /* opacity 01 + translateY 8px0 */
@keyframes xpulse-msgIn /* opacity 01 + translateY 4px0 (subtiler) */
@keyframes xpulse-pulse-dim /* opacity 0.41, wartende States */
@keyframes xpulse-dots /* '''.''..''...' Lade-Indikator */
@keyframes xpulse-shake /* horizontal shake für Fehler-Feedback */
@keyframes xpulse-spin /* 360° rotate für Spinner */
@keyframes xpulse-typingBounce /* Typing-Indikator Punkte */

Utility-Klassen:

.xpulse-fade-in { animation: xpulse-fadeIn 0.3s ease both; }
.xpulse-spin { animation: xpulse-spin 1s linear infinite; }

Komponenten-Klassen

Alle Klassen folgen dem .xpulse-* Prefix – konsistent mit den Custom Properties.

Typografie

.xpulse-h1 /* Heading 1 – Fraunces, groß, accent */
.xpulse-h2 /* Heading 2 */
.xpulse-h3 /* Heading 3 */
.xpulse-label /* Uppercase Label – Mono, klein, letter-spacing */
.xpulse-text /* Fließtext – Mono, 13px, line-height 1.6 */
.xpulse-text-sm /* Kleiner Text – 11px */
.xpulse-code /* Code-Block – Mono, Surface-BG, Border */
.xpulse-code-inline /* Inline Code */

Layout

.xpulse-container /* max-width, padding, centered */
.xpulse-stack /* flex-column mit gap */
.xpulse-row /* flex-row mit gap */
.xpulse-divider /* <hr> Equivalent */

Buttons

.xpulse-btn-primary /* Accent-BG, uppercase, volle Breite */
.xpulse-btn-secondary /* Border-only, hover → accent */
.xpulse-btn-danger /* Border danger, hover → filled */
.xpulse-btn-icon /* Icon-only, kein Border */

Inputs & Fields

.xpulse-input /* Text/Password Input – styled, focus → accent border */
.xpulse-field /* Container mit gap */
.xpulse-field-label /* uppercase, klein */
.xpulse-field-hint /* dim, klein */

Cards & Elemente

.xpulse-card /* Surface-Box mit Border, Padding, Border-Radius */
.xpulse-badge /* Kleines Label-Element */
.xpulse-tag /* Tag/Pill */
.xpulse-table /* Tabelle mit korrektem Spacing */
.xpulse-menu-btn /* Menü-Auslöser Button */
.xpulse-dropdown /* Dropdown-Container */

Sonstige

.xpulse-spinner /* inline Lade-Spinner */

Chat-spezifisch (bleibt in xpulse-chat)

Folgende Klassen und Properties gehören nicht ins Package – sie sind spezifisch für die Chat-App und bleiben in deren lokalem CSS:

/* Properties */
--xpulse-sidebar-width
--xpulse-topbar-height
/* Klassen */
.xpulse-status-dot /* Chat Peer-Status */
.xpulse-status-badge /* Chat Peer-Status Badge */
.xpulse-waiting-dots /* Chat Typing-Indikator */

Verwendung

<!-- Empfohlen: Template-Method nutzen -->
{% themeAssets() %}
{% themeSwitcher() %}

themeAssets() rendert:

  1. <link> auf das CSS-Bundle /_theme/css/xpulse.css
  2. Optional: <link> auf /_theme/app.css falls ein App-Theme vorhanden
  3. Inline Anti-FOUC-Script (setzt data-theme + Klasse sofort im <head>)
  4. <script defer src="/_theme/theme.js"> für den Browser-Toggle
// Server: Theme-System initialisieren
import theme from '@xpulse/theme';
await theme.init();

Paket-Struktur

@xpulse/theme/
src/
index.js ← Server-API: init, set, toggle, current, available, has, href, scriptHref
template-methods/
theme-assets.jsthemeAssets() Template-Method
theme-switcher.jsthemeSwitcher() Template-Method
theme-token-editor.jsthemeTokenEditor() Template-Method
public/
css/
base.css ← Reset, Body-Styles, Noise-Overlay, Animationen
components.css@import-Liste aller Komponenten
components/
boxes.css
brand.css
buttons.css
cards.css
forms.css
navigation.css
themes/
dark.css ← Built-in Dark Theme
light.css ← Built-in Light Theme
js/
theme.js ← Browser-Runtime: Toggle, Token-Editor
docs/
de/
index.md
guide.md
api.md
styleguide/
en/
README.md
package.json
xpulse.json

Dependencies

@xpulse/theme
└── @xpulse/event ← theme:ready, theme:changed feuern

Migration von bisherigen CSS Custom Properties

Bestehende xPulse-Projekte nutzen noch die alten unprefix-ten Properties. Migration bei v1.4.0 (Component Based):

Alt Neu
--bg --xpulse-bg
--surface --xpulse-surface
--border --xpulse-border
--muted --xpulse-muted
--text --xpulse-text
--text-dim --xpulse-text-dim
--accent --xpulse-accent
--accent2 --xpulse-accent2
--danger --xpulse-danger
--mono --xpulse-font-mono
--serif --xpulse-font-serif
.btn-primary .xpulse-btn-primary
.btn-secondary .xpulse-btn-secondary
.btn-danger .xpulse-btn-danger
.btn-icon .xpulse-btn-icon
.field .xpulse-field
.field-label .xpulse-field-label
.field-hint .xpulse-field-hint
@keyframes fadeIn @keyframes xpulse-fadeIn
@keyframes spin @keyframes xpulse-spin

Chat-spezifische Properties (--sidebar-width, --topbar-height) und Klassen (.status-dot, .status-badge, .waiting-dots) bleiben im Chat-CSS ohne Umbenennung.


Dokumentation

Die Komponentendokumentation wird als eigenständige HTML-Seite gerendert – analog zu Bootstrap/Tailwind: live Vorschau + Code-Snippet nebeneinander.

xpulse.one/doc/component/theme/

Aufbau:

Das Docs-HTML lebt in docs/de/components.html + docs/en/components.html als eigenständige HTML-Docs – rendered direkt im Docs-Bereich von xpulse.one via <iframe> oder inline eingebettet (siehe GLOBAL_concept-docs.md).


Consumer

Projekt Status Anmerkung
chat.xpulse.one ✅ Quelle Migration bei v1.4.0
xpulse.one 🔜 Erster Smoke Test Entscheidet ob Package wirklich generisch ist
board.xpulse.one 💡 Geplant Startet nach Smoke Test

Zukunft: ui.xpulse.one

Visueller Theme Generator – eigenes xPulse Tool, eigenständige Subdomain:

xpulse.one/doc/component/theme/ ← Dokumentation + Live Preview
ui.xpulse.one ← Theme Generator + Export

Status: Idee – nach board.xpulse.one denkbar.

de/spec.md 2026-04-13