Skip to main content

Theme system

The Qomicex Theme system (theme system)uses a three-tier primitive → semantic → component semantic token structure, carried by .qtheme theme packages, so the launcher and plugin UI reskin instantly and completely, with no plugin-ui dist rebuild.

The three-tier semantic model

  • v1 scope: .qtheme expresses the semantic/component tiers directly (the flat --* variables); primitive derivation and the theme.mjs computation tier are deferred to v2
  • plugin-ui components consume only var(--*), with zero inline color values → switching themes reskins every component instantly, eliminating the stale “change a component, rebuild dist” problem

Semantic token naming

Dot-separated naming, where . normalizes to - (background.emphasis → --background-emphasis).

Color tokens (the standard component-level set = everything plugin-ui consumes)

Extended semantics (optional in v1, emitted as --foreground-accent and so on, for forward compatibility): foreground.accent, foreground.muted, foreground.destructive, background.elevated, background.emphasis, background.sunken, border.strong, border.accent, accent.hover, accent.active, status.success, status.warning, status.error.

Non-color tokens

space.*, font.family/size/weight, motion.*, and shadow.* belong to the v2 non-color extensions.

.qtheme package format

theme.json schema v1

An invalid theme.json is rejected by src/theme/schema.ts with a friendly error.

Theme manager

The theme manager (src/theme/manager.ts)provides:
  • applyTheme(json): validate, then inject CSS variables into :root → every component reskins instantly; persists the theme id to localStorage
  • clearTheme(): remove the custom theme and fall back to the settings layer light/dark/preset
  • restoreSavedTheme(): restore the registered theme at startup
  • useTheme(): React Hook that subscribes to change events and drives re-rendering
  • registerTheme(): register the bundled built-in .qtheme packages (including the four Catppuccin presets)

Theme contributions (contributes)

Plugins and theme packages can declare three kinds of contributions through manifests; v1 supports color themes only:

Icon theme (icon-theme.json)

The Icon theme (icon theme)is a new file in the .qtheme package, alongside theme.json, that provides override mappings for icons such as plugin sidebar entries and menu items.

Format

Fields

Icon definition object

Render priority

The PluginIcon component renders in this order:
  • Look up the src key in the current theme’s icon-theme.json → if it matches, render SVG/char/URL
  • No match and src is a URL → <img>
  • No match and src is a FontAwesome class name → <FontAwesomeIcon>
  • No match at all → null

Registration

Icon themes are registered through the theme manager and bound to color themes:
  • registerTheme(colorJson, iconJson?) — register a color and icon theme together
  • registerIconTheme(themeId, iconJson) — bind icons separately to an already registered color theme
Get the active theme’s icon mappings with getActiveIconTheme() or useIconTheme() (a React Hook). The mappings update automatically when the theme switches.

Font and ligature contribution

manifest declaration

Plugin manifests declare font resources through the contributes field:
  • fontLinks (string[]): list of font CSS/CDN URLs. When the plugin activates, the launcher automatically injects <link rel="stylesheet"> into <head>; it removes them when the plugin deactivates.
  • You can use the font-feature-settings scoping convention to enable ligatures in specific areas (see below).

Ligature scoping convention

The launcher predefines the .font-ligatures class (the plugin-internal equivalent is .p-font-ligatures); apply it to areas that need font ligatures (such as console logs or version diffs):
The corresponding CSS:
Inside plugins, use .p-font-ligatures for the same effect.

Relationship with plugin-ui

Hard rule: plugin-ui components may consume tokens only through var(--*); inline color values are forbidden. An audit confirms packages/plugin-ui/src/ has zero inline color values in total → switching themes reskins instantly with no dist rebuild.

Preset theme

The launcher bundles four Catppuccin Preset theme (preset theme)packages (latte / frappe / macchiato / mocha), already migrated to the new token naming. When you switch presets or apply a custom .qtheme, every UI component that consumes var(--*) responds immediately.

Plugin-driven Theme override

Plugins can dynamically override the launcher UI’s CSS variables through __PLUGIN_API__, reskinning at runtime without shipping a .qtheme package.

Priority levels

  • The <style> injected by applyThemeOverride has higher specificity than .qtheme and @layer base
  • If the user has set a theme color (the inline style from themeColor.ts), inline styling takes over --primary/--ring/--primary-foreground, so overrides of those three do not take effect

API

Usage example

Security limits

  • Token whitelist: only 30 predefined semantic tokens are allowed (background, foreground, primary, accent, border, status-*, and so on)
  • HSL format validation: values must strictly match H S% L% (such as 142 71% 48%), rejecting arbitrary CSS injection
  • Tokens outside the whitelist and invalid formats are silently ignored

Lifecycle

  • applyThemeOverride can be called repeatedly; the latest call overrides earlier ones
  • Overrides ARE cleared automatically when the plugin is disabled: the disable path unconditionally calls clearThemeOverride, so the theme falls back to the active .qtheme or the built-in light/dark theme. You do not need to clean up in a disable hook; call clearThemeOverride explicitly only if you want to drop the override while the plugin stays enabled
  • clearThemeOverride removes only the override styles; it does not affect .qtheme or the built-in light/dark themes