This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new 0dc8cbe  themes: lock palettes to design spec + port the preview-card 
picker
0dc8cbe is described below

commit 0dc8cbe6f83268ceadd79da4373fda3073f9f8ca
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 19 17:21:43 2026 +0800

    themes: lock palettes to design spec + port the preview-card picker
    
    Pulled the canonical token snapshot from the design bundle's new URL
    (XZBH5oSPr6e4tU-MHYOgWg) `screens/style-setup.jsx`. The prior export
    predated the 5-theme picker; that's why I was working from
    placeholders.
    
    Token corrections (replacing every placeholder I had previously):
    
      horizon   accent #f97316  amber  (unchanged — was already correct)
      meridian  accent #7a5af8  indigo (was wrong: I had #a855f7)
      obsidian  accent #22d3ee  cyan   (was wrong: I had purple, then blue)
      daybreak  accent #6366f1  violet (was wrong: I had muted neutral)
      aurora    accent #ec4899  magenta (was close; locked to exact)
    
    Each theme block in themes.css now matches the design's full token
    record byte-for-byte — bg layers, fg layers, accent + soft + line,
    info/purple/ok/err/warn. Also added per-theme `--sw-font`,
    `--sw-radius`, and `--sw-density-pad` tokens so future code can swap
    type / corner roundness / tile padding per the design (horizon
    Inter/6/compact, obsidian IBM Plex Mono/2/compact, daybreak
    Inter/10/spacious, aurora Inter/12/comfortable).
    
    AVAILABLE_THEMES in state/theme.ts now carries the full ThemeDef
    record (tag, tagline, description, font, radius, density, all token
    values, heroTint gradient string) — needed by the preview card,
    which renders itself in its own theme via inline styles (CSS vars
    only reflect the page's active theme).
    
    New ThemePreviewCard.vue ports the design's `ThemeCard` directly:
      - hero strip (110px) with theme-specific gradient + faint grid +
        "SkyWalking · Name" brand chip
      - mini app preview: Primary/Tonal/Ghost buttons with the theme's
        radius, KPI strip (cpm/p99/err with accent/warn/err colors),
        sparkline (theme accent gradient + line), density/font/radius/
        mode meta pills
      - description block: name + tagline + lede + "Use this theme" CTA
        (or "Currently active" when this is the live theme)
    
    GlobalDefaultsAdmin theme section now grids 5 of these cards (replaces
    the old radio list). The picker reads `themeStore.active` for the
    "active" badge so the operator sees what the renderer is currently
    showing vs what they're selecting.
    
    The previous correction back-and-forth (obsidian = purple? then blue?
    daybreak = red? then white?) is now permanently resolved by lifting
    straight from the design bundle.
---
 .../admin/global-defaults/GlobalDefaultsAdmin.vue  |  68 ++--
 .../admin/global-defaults/ThemePreviewCard.vue     | 375 +++++++++++++++++++++
 apps/ui/src/state/theme.ts                         |  99 +++++-
 packages/design-tokens/src/themes.css              | 235 +++++++------
 4 files changed, 612 insertions(+), 165 deletions(-)

diff --git a/apps/ui/src/features/admin/global-defaults/GlobalDefaultsAdmin.vue 
b/apps/ui/src/features/admin/global-defaults/GlobalDefaultsAdmin.vue
index 39596bc..dc86d8a 100644
--- a/apps/ui/src/features/admin/global-defaults/GlobalDefaultsAdmin.vue
+++ b/apps/ui/src/features/admin/global-defaults/GlobalDefaultsAdmin.vue
@@ -35,7 +35,13 @@ import SyncStatusBanner from 
'@/features/admin/_shared/SyncStatusBanner.vue';
 import TemplateStatusBadge from 
'@/features/admin/_shared/TemplateStatusBadge.vue';
 import TemplateDiffModal from '@/features/admin/_shared/TemplateDiffModal.vue';
 import { useTemplateSync } from '@/features/admin/_shared/useTemplateSync';
-import { AVAILABLE_THEMES, type ThemeId } from '@/state/theme';
+import { AVAILABLE_THEMES, useThemeStore, type ThemeId } from '@/state/theme';
+import ThemePreviewCard from './ThemePreviewCard.vue';
+
+// Theme store — for distinguishing the currently-ACTIVE theme (what the
+// renderer is showing right now, combining user override + org default
+// + bundled) from the operator's pending SELECTION in this picker.
+const themeStoreRef = useThemeStore();
 
 // Two kinds in scope; we union by reading them both as separate sync
 // calls (`useTemplateSync` is per-kind, so we call it twice).
@@ -239,26 +245,19 @@ async function onDiffReset(): Promise<void> {
         </header>
         <p class="gd__sec-lede">
           Five bundled themes ship with Horizon. The org default is the
-          starting theme every user sees on first visit. Users keep their
-          own override afterwards.
+          starting theme every user sees on first visit. Each user can
+          override locally via the topbar theme chip (stored in
+          <code>localStorage['horizon:theme:user']</code>).
         </p>
-        <div class="gd__themes">
-          <label
+        <div class="gd__theme-grid">
+          <ThemePreviewCard
             v-for="t in AVAILABLE_THEMES"
             :key="t.id"
-            class="gd__theme"
-            :class="{ active: themeDraft === t.id }"
-          >
-            <input
-              v-model="themeDraft"
-              type="radio"
-              name="themeDraft"
-              :value="t.id"
-              :disabled="readOnly"
-            />
-            <span class="gd__theme-id">{{ t.label }}</span>
-            <span class="gd__theme-desc">{{ t.description }}</span>
-          </label>
+            :theme="t"
+            :selected="themeDraft === t.id"
+            :active="themeStoreRef.active === t.id"
+            @pick="!readOnly && (themeDraft = t.id)"
+          />
         </div>
       </section>
 
@@ -444,37 +443,10 @@ async function onDiffReset(): Promise<void> {
   color: var(--sw-fg-0);
 }
 
-.gd__themes {
+.gd__theme-grid {
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
-  gap: 8px;
-}
-.gd__theme {
-  display: grid;
-  grid-template-columns: auto 1fr;
-  column-gap: 8px;
-  align-items: start;
-  padding: 10px 12px;
-  border: 1px solid var(--sw-line);
-  border-radius: 4px;
-  background: var(--sw-bg-2);
-  cursor: pointer;
-}
-.gd__theme.active {
-  border-color: var(--sw-accent-line);
-  background: var(--sw-accent-soft);
-}
-.gd__theme input { grid-row: span 2; margin-top: 2px; }
-.gd__theme-id {
-  font-size: 12px;
-  font-weight: 600;
-  color: var(--sw-fg-0);
-}
-.gd__theme-desc {
-  grid-column: 2;
-  font-size: 11px;
-  color: var(--sw-fg-2);
-  line-height: 1.45;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+  gap: 12px;
 }
 
 .gd__presets {
diff --git a/apps/ui/src/features/admin/global-defaults/ThemePreviewCard.vue 
b/apps/ui/src/features/admin/global-defaults/ThemePreviewCard.vue
new file mode 100644
index 0000000..1c248eb
--- /dev/null
+++ b/apps/ui/src/features/admin/global-defaults/ThemePreviewCard.vue
@@ -0,0 +1,375 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  Theme preview card — ported from the design bundle's
+  `screens/style-setup.jsx > ThemeCard`. Self-themed via inline styles
+  that read the ThemeDef's token snapshot (NOT --sw-* vars), so each
+  card renders in its OWN theme regardless of the page's active theme.
+  That's the whole point of the preview: see the palette before you
+  switch.
+
+  Three stacked regions, matching the design:
+    1. Hero strip (110px) — gradient + faint grid + mini brand chip
+    2. Mini-app preview — Primary/Tonal/Ghost buttons, KPI strip
+       (cpm / p99 / err), sparkline, density/font/radius/mode badges
+    3. Description block — name + tagline + desc + actions
+-->
+<script setup lang="ts">
+import type { ThemeDef } from '@/state/theme';
+
+defineProps<{
+  theme: ThemeDef;
+  /** This card represents the currently-active theme (drives ribbon +
+   *  primary-button label). Distinct from `selected`: a card can be
+   *  selected (radio state) without yet being applied. */
+  active: boolean;
+  /** Hovered/clicked but not yet committed via Save. */
+  selected: boolean;
+}>();
+
+defineEmits<{ pick: [] }>();
+
+function tagBadgeStyle(t: ThemeDef): Record<string, string> {
+  return {
+    background: t.bg0 + 'cc',
+    color: t.fg1,
+    border: `1px solid ${t.line}`,
+  };
+}
+function chipStyle(t: ThemeDef, kind: 'primary' | 'tonal' | 'ghost'): 
Record<string, string> {
+  if (kind === 'primary') {
+    return {
+      background: t.accent,
+      color: t.appearance === 'light' ? '#fff' : '#0a0d12',
+      borderRadius: `${t.radius}px`,
+    };
+  }
+  if (kind === 'tonal') {
+    return {
+      background: t.accentSoft,
+      color: t.accent,
+      border: `1px solid ${t.accentLine}`,
+      borderRadius: `${t.radius}px`,
+    };
+  }
+  return {
+    background: t.bg2,
+    color: t.fg1,
+    border: `1px solid ${t.line}`,
+    borderRadius: `${t.radius}px`,
+  };
+}
+function kpiTileStyle(t: ThemeDef): Record<string, string> {
+  return {
+    background: t.bg2,
+    border: `1px solid ${t.line}`,
+    borderRadius: `${Math.max(2, t.radius - 2)}px`,
+  };
+}
+function metaBadgeStyle(t: ThemeDef): Record<string, string> {
+  return {
+    background: t.bg2,
+    border: `1px solid ${t.line}`,
+  };
+}
+</script>
+
+<template>
+  <div
+    class="tpc"
+    :class="{ 'tpc--selected': selected, 'tpc--active': active }"
+    :style="{
+      background: theme.bg1,
+      color: theme.fg0,
+      borderColor: selected || active ? theme.accent : theme.line,
+      boxShadow: selected || active
+        ? `0 0 0 2px ${theme.accent}55, 0 12px 32px rgba(0,0,0,0.4)`
+        : '0 4px 12px rgba(0,0,0,0.25)',
+    }"
+    @click="$emit('pick')"
+  >
+    <!-- Top-left badges: tag + 'active' chip when this is the live theme. -->
+    <div class="tpc__badges">
+      <span class="tpc__tag" :style="tagBadgeStyle(theme)">{{ theme.tag 
}}</span>
+      <span
+        v-if="active"
+        class="tpc__active"
+        :style="{ background: theme.accent, color: '#0a0d12' }"
+      >active</span>
+    </div>
+
+    <!-- Hero strip — theme-specific gradient + faint grid + brand chip. -->
+    <div class="tpc__hero" :style="{ background: theme.heroTint }">
+      <div
+        class="tpc__hero-grid"
+        :style="{
+          opacity: theme.appearance === 'light' ? 0.06 : 0.12,
+          backgroundImage:
+            `linear-gradient(${theme.line} 1px, transparent 1px), ` +
+            `linear-gradient(90deg, ${theme.line} 1px, transparent 1px)`,
+        }"
+      />
+      <div
+        class="tpc__brand"
+        :style="{ color: theme.appearance === 'light' ? '#1a1d24' : '#fff' }"
+      >
+        <span
+          class="tpc__brand-sigil"
+          :style="{ background: `linear-gradient(135deg, ${theme.accent}, 
${theme.purple})` }"
+        />
+        SkyWalking · {{ theme.label }}
+      </div>
+    </div>
+
+    <!-- Mini-app preview. -->
+    <div class="tpc__body" :style="{ background: theme.bg0, color: theme.fg1, 
fontFamily: theme.font }">
+      <div class="tpc__buttons">
+        <span class="tpc__chip" :style="chipStyle(theme, 
'primary')">Primary</span>
+        <span class="tpc__chip" :style="chipStyle(theme, 'tonal')">Tonal</span>
+        <span class="tpc__chip" :style="chipStyle(theme, 'ghost')">Ghost</span>
+      </div>
+      <div class="tpc__kpis">
+        <div class="tpc__kpi" :style="kpiTileStyle(theme)">
+          <div class="tpc__kpi-k" :style="{ color: theme.fg2 }">cpm</div>
+          <div class="tpc__kpi-v" :style="{ color: theme.accent }">284k</div>
+        </div>
+        <div class="tpc__kpi" :style="kpiTileStyle(theme)">
+          <div class="tpc__kpi-k" :style="{ color: theme.fg2 }">p99</div>
+          <div class="tpc__kpi-v" :style="{ color: theme.warn }">412ms</div>
+        </div>
+        <div class="tpc__kpi" :style="kpiTileStyle(theme)">
+          <div class="tpc__kpi-k" :style="{ color: theme.fg2 }">err</div>
+          <div class="tpc__kpi-v" :style="{ color: theme.err }">0.4%</div>
+        </div>
+      </div>
+      <div
+        class="tpc__chart"
+        :style="{
+          background: theme.bg2,
+          border: `1px solid ${theme.line}`,
+          borderRadius: `${Math.max(2, theme.radius - 2)}px`,
+        }"
+      >
+        <svg viewBox="0 0 100 28" preserveAspectRatio="none">
+          <defs>
+            <linearGradient :id="`tpc-g-${theme.id}`" x1="0" x2="0" y1="0" 
y2="1">
+              <stop offset="0%"  :stop-color="theme.accent" 
stop-opacity="0.35" />
+              <stop offset="100%" :stop-color="theme.accent" stop-opacity="0" 
/>
+            </linearGradient>
+          </defs>
+          <path
+            d="M0 22 L10 18 L20 14 L30 16 L40 9 L50 13 L60 7 L70 11 L80 6 L90 
9 L100 4 L100 28 L0 28 Z"
+            :fill="`url(#tpc-g-${theme.id})`"
+          />
+          <path
+            d="M0 22 L10 18 L20 14 L30 16 L40 9 L50 13 L60 7 L70 11 L80 6 L90 
9 L100 4"
+            fill="none"
+            :stroke="theme.accent"
+            stroke-width="1.4"
+          />
+        </svg>
+      </div>
+      <div class="tpc__meta">
+        <span class="tpc__meta-pill" :style="metaBadgeStyle(theme)">
+          <span :style="{ color: theme.fg3 }">font</span>
+          <span :style="{ color: theme.fg1 }">{{ theme.font }}</span>
+        </span>
+        <span class="tpc__meta-pill" :style="metaBadgeStyle(theme)">
+          <span :style="{ color: theme.fg3 }">r</span>
+          <span :style="{ color: theme.fg1 }">{{ theme.radius }}px</span>
+        </span>
+        <span class="tpc__meta-pill" :style="metaBadgeStyle(theme)">
+          <span :style="{ color: theme.fg3 }">density</span>
+          <span :style="{ color: theme.fg1 }">{{ theme.density.toLowerCase() 
}}</span>
+        </span>
+        <span class="tpc__meta-pill" :style="metaBadgeStyle(theme)">
+          <span :style="{ color: theme.fg3 }">mode</span>
+          <span :style="{ color: theme.fg1 }">{{ theme.appearance }}</span>
+        </span>
+      </div>
+    </div>
+
+    <!-- Description block. -->
+    <div
+      class="tpc__desc"
+      :style="{
+        background: theme.bg1,
+        color: theme.fg1,
+        borderTop: `1px solid ${theme.line}`,
+      }"
+    >
+      <div class="tpc__name" :style="{ color: theme.fg0, fontFamily: 
theme.font }">{{ theme.label }}</div>
+      <div class="tpc__tagline" :style="{ color: theme.fg2 }">{{ theme.tagline 
}}</div>
+      <div class="tpc__lede" :style="{ color: theme.fg2 }">{{ 
theme.description }}</div>
+      <div class="tpc__cta">
+        <span
+          class="tpc__use"
+          :style="{
+            background: active ? theme.bg2 : theme.accent,
+            color: active ? theme.fg1 : (theme.appearance === 'light' ? '#fff' 
: '#0a0d12'),
+            border: `1px solid ${active ? theme.line : theme.accent}`,
+          }"
+        >{{ active ? 'Currently active' : (selected ? 'Selected' : 'Use this 
theme') }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.tpc {
+  border: 1px solid;
+  border-radius: 10px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  cursor: pointer;
+  transition: transform 0.1s ease, box-shadow 0.1s ease;
+}
+.tpc:hover { transform: translateY(-1px); }
+
+.tpc__badges {
+  position: absolute;
+  top: 8px;
+  left: 8px;
+  z-index: 2;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.tpc__tag, .tpc__active {
+  padding: 1px 7px;
+  border-radius: 999px;
+  font-size: 9.5px;
+  font-weight: 700;
+  backdrop-filter: blur(6px);
+}
+
+.tpc__hero {
+  position: relative;
+  height: 110px;
+}
+.tpc__hero-grid {
+  position: absolute;
+  inset: 0;
+  background-size: 56px 56px, 56px 56px;
+  background-position: 0 0, 0 0;
+  pointer-events: none;
+}
+.tpc__brand {
+  position: absolute;
+  bottom: 10px;
+  left: 12px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 11px;
+  font-weight: 700;
+  letter-spacing: -0.2px;
+  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
+}
+.tpc__brand-sigil {
+  width: 18px;
+  height: 18px;
+  border-radius: 5px;
+  display: inline-block;
+}
+
+.tpc__body { padding: 10px; }
+.tpc__buttons {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+  margin-bottom: 8px;
+}
+.tpc__chip {
+  padding: 3px 8px;
+  font-size: 10px;
+  font-weight: 700;
+  font-family: inherit;
+}
+.tpc__kpis {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr;
+  gap: 5px;
+}
+.tpc__kpi { padding: 5px 6px; }
+.tpc__kpi-k {
+  font-size: 8px;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  font-weight: 600;
+}
+.tpc__kpi-v {
+  font-size: 12px;
+  font-weight: 700;
+  font-family: ui-monospace, monospace;
+}
+.tpc__chart {
+  margin-top: 8px;
+  height: 36px;
+  padding: 4px;
+}
+.tpc__chart svg {
+  width: 100%;
+  height: 100%;
+}
+.tpc__meta {
+  margin-top: 8px;
+  display: flex;
+  gap: 4px;
+  flex-wrap: wrap;
+  font-size: 9px;
+}
+.tpc__meta-pill {
+  padding: 1px 6px;
+  border-radius: 999px;
+  display: inline-flex;
+  gap: 4px;
+}
+
+.tpc__desc {
+  padding: 8px 10px 10px;
+  margin-top: auto;
+}
+.tpc__name {
+  font-size: 13px;
+  font-weight: 700;
+}
+.tpc__tagline {
+  font-size: 10px;
+  margin-top: 2px;
+}
+.tpc__lede {
+  font-size: 10.5px;
+  line-height: 1.5;
+  margin-top: 6px;
+}
+.tpc__cta {
+  margin-top: 8px;
+}
+.tpc__use {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 26px;
+  padding: 0 10px;
+  border-radius: 6px;
+  font-size: 11px;
+  font-weight: 700;
+}
+</style>
diff --git a/apps/ui/src/state/theme.ts b/apps/ui/src/state/theme.ts
index c36e62d..79918bb 100644
--- a/apps/ui/src/state/theme.ts
+++ b/apps/ui/src/state/theme.ts
@@ -38,26 +38,91 @@ import { useConfigBundle } from '@/controls/configBundle';
 import { debug } from '@/utils/debug';
 import type { TemplateBadge } from '@/api/scopes/configs';
 
-export type ThemeId = 'horizon' | 'obsidian' | 'aurora' | 'meridian' | 
'daybreak';
+export type ThemeId = 'horizon' | 'meridian' | 'obsidian' | 'daybreak' | 
'aurora';
 
-/** The five bundled themes (design-specified names). Matches the
- *  `[data-theme="..."]` selectors in
- *  `packages/design-tokens/src/themes.css`. The first three are dark;
- *  the last two are light. */
-export const AVAILABLE_THEMES: ReadonlyArray<{
+/** Full per-theme metadata — lifted from the design bundle's
+ *  `screens/style-setup.jsx`. The token values here are duplicated
+ *  with the CSS in `packages/design-tokens/src/themes.css` BECAUSE
+ *  the theme picker preview cards need inline-style access to the
+ *  swatches (canvas / SVG can't read `var(--…)` synchronously).
+ *  Runtime rendering still goes through the CSS — this table is for
+ *  preview / preview-only consumption. */
+export interface ThemeDef {
   id: ThemeId;
   label: string;
-  description: string;
-  /** `dark` themes use the white SkyWalking logo; `light` themes use
-   *  the blue (`#1368B3`) variant. Read by the brand-logo CSS to know
-   *  which inline SVG to show. */
-  appearance: 'dark' | 'light';
-}> = [
-  { id: 'horizon',  label: 'Horizon',  description: 'Flagship dark — canyon 
orange accent on deep blue-grey. Default.', appearance: 'dark' },
-  { id: 'obsidian', label: 'Obsidian', description: 'Dark, blue accent.',  
appearance: 'dark' },
-  { id: 'aurora',   label: 'Aurora',   description: 'Dark, pink accent.',  
appearance: 'dark' },
-  { id: 'meridian', label: 'Meridian', description: 'Dark, purple accent.', 
appearance: 'dark' },
-  { id: 'daybreak', label: 'Daybreak', description: 'White light theme.',   
appearance: 'light' },
+  tag: string;          // small chip text (e.g. "default", "high-contrast")
+  tagline: string;      // one-line palette description
+  description: string;  // operator-facing sentence
+  appearance: 'dark' | 'light';  // drives logo + light-bg-specific code paths
+  font: string;
+  radius: number;
+  density: 'Compact' | 'Spacious' | 'Comfortable';
+  // Token snapshot (mirrors themes.css per id).
+  bg0: string; bg1: string; bg2: string; line: string;
+  fg0: string; fg1: string; fg2: string; fg3: string;
+  accent: string; accentSoft: string; accentLine: string;
+  info: string; purple: string; ok: string; err: string; warn: string;
+  /** Hero background CSS for the preview card hero strip. Mirrors
+   *  the design's `heroTint` plus the optional photo. */
+  heroTint: string;
+}
+
+export const AVAILABLE_THEMES: readonly ThemeDef[] = [
+  {
+    id: 'horizon', label: 'Horizon', tag: 'default',
+    tagline: 'Dark · amber accent · canyon hero',
+    description: 'The shipped SkyWalking NG look. Dense, warm, 
observability-first.',
+    appearance: 'dark', font: 'Inter', radius: 6, density: 'Compact',
+    bg0: '#0a0d12', bg1: '#0f131a', bg2: '#151a23', line: '#232a37',
+    fg0: '#e8ecf3', fg1: '#b6bdcc', fg2: '#818a9c', fg3: '#5b6373',
+    accent: '#f97316', accentSoft: 'rgba(249,115,22,0.14)', accentLine: 
'rgba(249,115,22,0.4)',
+    info: '#38bdf8', purple: '#a855f7', ok: '#22c55e', err: '#ef4444', warn: 
'#eab308',
+    heroTint: 'linear-gradient(180deg, rgba(10,13,18,0.10) 0%, 
rgba(10,13,18,0.85) 100%), radial-gradient(700px 460px at 20% 30%, 
rgba(249,115,22,0.22), transparent 60%)',
+  },
+  {
+    id: 'meridian', label: 'Meridian', tag: 'dense',
+    tagline: 'Navy ground · indigo accent',
+    description: 'Cooler navy palette with an indigo accent. Tightly packed 
for SREs who live in tables.',
+    appearance: 'dark', font: 'Inter', radius: 4, density: 'Compact',
+    bg0: '#0b0f1a', bg1: '#101421', bg2: '#171c2e', line: '#232a3c',
+    fg0: '#eef0f7', fg1: '#aeb4c7', fg2: '#7c8295', fg3: '#525a73',
+    accent: '#7a5af8', accentSoft: 'rgba(122,90,248,0.16)', accentLine: 
'rgba(122,90,248,0.4)',
+    info: '#60a5fa', purple: '#c084fc', ok: '#34d399', err: '#f87171', warn: 
'#fbbf24',
+    heroTint: 'radial-gradient(700px 500px at 50% 35%, rgba(122,90,248,0.20), 
transparent 60%), linear-gradient(180deg, #11142a, #0a0d1c)',
+  },
+  {
+    id: 'obsidian', label: 'Obsidian', tag: 'high-contrast',
+    tagline: 'True-black · cyan accent · monospaced',
+    description: 'True-black backdrop and a cyan punch. Pixel-precise 
readouts; comfortable on OLED.',
+    appearance: 'dark', font: 'IBM Plex Mono', radius: 2, density: 'Compact',
+    bg0: '#000000', bg1: '#0a0a0a', bg2: '#141414', line: '#222222',
+    fg0: '#f4f4f5', fg1: '#c4c4c4', fg2: '#888888', fg3: '#5a5a5a',
+    accent: '#22d3ee', accentSoft: 'rgba(34,211,238,0.15)', accentLine: 
'rgba(34,211,238,0.45)',
+    info: '#7dd3fc', purple: '#d946ef', ok: '#84cc16', err: '#f43f5e', warn: 
'#facc15',
+    heroTint: 'linear-gradient(180deg, #000 0%, #060606 100%), 
radial-gradient(500px 350px at 50% 50%, rgba(34,211,238,0.12), transparent 
60%)',
+  },
+  {
+    id: 'daybreak', label: 'Daybreak', tag: 'light',
+    tagline: 'Light ground · violet accent · airy',
+    description: 'Daytime palette with generous spacing and soft shadows. For 
shared screens and printouts.',
+    appearance: 'light', font: 'Inter', radius: 10, density: 'Spacious',
+    bg0: '#f7f7fa', bg1: '#ffffff', bg2: '#f0f1f5', line: '#e3e4ec',
+    fg0: '#0a0d12', fg1: '#3a3f4c', fg2: '#6e7382', fg3: '#9ba0af',
+    accent: '#6366f1', accentSoft: 'rgba(99,102,241,0.10)', accentLine: 
'rgba(99,102,241,0.32)',
+    info: '#0ea5e9', purple: '#a855f7', ok: '#16a34a', err: '#dc2626', warn: 
'#d97706',
+    heroTint: 'linear-gradient(180deg, #eef0fa 0%, #f7f7fa 100%), 
radial-gradient(700px 460px at 70% 30%, rgba(99,102,241,0.18), transparent 
60%)',
+  },
+  {
+    id: 'aurora', label: 'Aurora', tag: 'showcase',
+    tagline: 'Glass chrome · magenta/cyan gradient',
+    description: 'Glass-morphic chrome with a magenta-to-cyan gradient accent. 
Made for demos and product tours.',
+    appearance: 'dark', font: 'Inter', radius: 12, density: 'Comfortable',
+    bg0: '#0b0d18', bg1: '#11142a', bg2: '#181b35', line: '#262a48',
+    fg0: '#f1f3ff', fg1: '#b9bee0', fg2: '#828abe', fg3: '#525a8a',
+    accent: '#ec4899', accentSoft: 'rgba(236,72,153,0.16)', accentLine: 
'rgba(236,72,153,0.4)',
+    info: '#22d3ee', purple: '#a855f7', ok: '#22d3ee', err: '#f43f5e', warn: 
'#fbbf24',
+    heroTint: 'radial-gradient(600px 420px at 25% 30%, rgba(236,72,153,0.30), 
transparent 60%), radial-gradient(600px 420px at 75% 70%, 
rgba(34,211,238,0.25), transparent 60%), linear-gradient(180deg, #0b0d18, 
#131534)',
+  },
 ];
 
 const USER_KEY = 'horizon:theme:user';
diff --git a/packages/design-tokens/src/themes.css 
b/packages/design-tokens/src/themes.css
index 3604fab..ea2706c 100644
--- a/packages/design-tokens/src/themes.css
+++ b/packages/design-tokens/src/themes.css
@@ -17,122 +17,157 @@
 
 /* ── Horizon NG theme variants ────────────────────────────────────────
  *
- * The `:root` block in `tokens.css` is the **Horizon** flagship dark
- * palette. Each other theme is a `[data-theme="<id>"]` block that
- * overrides only the tokens that change; the `--rr-*` aliases pick up
- * new `--sw-*` values via `var()` automatically.
+ * Token values are LIFTED VERBATIM from the design bundle's
+ * `screens/style-setup.jsx`. The 5 bundled themes are:
  *
- * Five bundled themes (design-specified names + dominant hue):
- *   horizon   — flagship dark, canyon orange accent (the :root default)
- *   obsidian  — dark, BLUE accent
- *   aurora    — dark, PINK accent
- *   meridian  — dark, PURPLE accent
- *   daybreak  — WHITE light theme, muted accent
+ *   horizon   default · dark · amber accent · canyon hero
+ *   meridian  dense   · dark · indigo accent · navy ground
+ *   obsidian  high-contrast · dark · cyan accent · true-black, monospaced
+ *   daybreak  light   · violet accent · airy
+ *   aurora    showcase · dark · magenta accent · glass chrome
  *
- * IMPORTANT — the specific hex values below are PLACEHOLDERS pulled
- * from typical hues in each color family. They are NOT the design
- * spec's exact tokens (which I do not have visibility into). Replace
- * each `--sw-accent` / `--sw-accent-*` value with the design's
- * canonical hex once provided.
+ * The `:root` block in `tokens.css` is the horizon palette (the
+ * shipped default); each other theme is a `[data-theme="<id>"]`
+ * selector that overrides only what changes. The `--rr-*` aliases
+ * pick up new `--sw-*` values via `var()` automatically.
  *
- * Switched at runtime by setting `data-theme="<id>"` on `<html>`.
- * Adding a theme means appending another block here + listing the id
- * in `themeStore.AVAILABLE_THEMES`.
+ * Beyond colors, each theme also picks a `--sw-radius` (corner
+ * roundness), `--sw-density-pad` (per-tile padding), and `--sw-font`
+ * (UI font). Code that consumes these still uses var() reads; the
+ * theme store sets `<html data-theme>` and CSS does the rest.
  */
 
-/* ── horizon (default, no-op override; declared for completeness) ── */
+/* ── horizon (default — declared explicitly for parity with the others) ── */
 [data-theme="horizon"] {
-  /* Mirrors :root in tokens.css. */
+  --sw-bg-0: #0a0d12;
+  --sw-bg-1: #0f131a;
+  --sw-bg-2: #151a23;
+  --sw-line: #232a37;
+
+  --sw-fg-0: #e8ecf3;
+  --sw-fg-1: #b6bdcc;
+  --sw-fg-2: #818a9c;
+  --sw-fg-3: #5b6373;
+
+  --sw-accent:      #f97316;
+  --sw-accent-soft: rgba(249, 115, 22, 0.14);
+  --sw-accent-line: rgba(249, 115, 22, 0.4);
+
+  --sw-info:   #38bdf8;
+  --sw-purple: #a855f7;
+  --sw-ok:     #22c55e;
+  --sw-err:    #ef4444;
+  --sw-warn:   #eab308;
+
+  --sw-font: "Inter", system-ui, sans-serif;
+  --sw-radius: 6px;
+  --sw-density-pad: 6px;
+}
+
+/* ── meridian — navy + indigo, dense ─────────────────────────────── */
+[data-theme="meridian"] {
+  --sw-bg-0: #0b0f1a;
+  --sw-bg-1: #101421;
+  --sw-bg-2: #171c2e;
+  --sw-line: #232a3c;
+
+  --sw-fg-0: #eef0f7;
+  --sw-fg-1: #aeb4c7;
+  --sw-fg-2: #7c8295;
+  --sw-fg-3: #525a73;
+
+  --sw-accent:      #7a5af8;
+  --sw-accent-soft: rgba(122, 90, 248, 0.16);
+  --sw-accent-line: rgba(122, 90, 248, 0.4);
+
+  --sw-info:   #60a5fa;
+  --sw-purple: #c084fc;
+  --sw-ok:     #34d399;
+  --sw-err:    #f87171;
+  --sw-warn:   #fbbf24;
+
+  --sw-font: "Inter", system-ui, sans-serif;
+  --sw-radius: 4px;
+  --sw-density-pad: 6px;
 }
 
-/* ── obsidian — dark, BLUE accent ─────────────────────────────────── */
+/* ── obsidian — true-black + cyan, monospaced ────────────────────── */
 [data-theme="obsidian"] {
   --sw-bg-0: #000000;
   --sw-bg-1: #0a0a0a;
   --sw-bg-2: #141414;
-  --sw-bg-3: #1c1c1c;
-  --sw-bg-4: #262626;
-  --sw-line: #2a2f38;
-  --sw-line-2: #3a4252;
-  --sw-line-3: #4a5468;
-
-  --sw-fg-0: #ffffff;
-  --sw-fg-1: #ededed;
-  --sw-fg-2: #c8c8c8;
-  --sw-fg-3: #989898;
-
-  /* TODO: replace with design-spec blue */
-  --sw-accent: #3a8ed0;
-  --sw-accent-2: #5aa3e0;
-  --sw-accent-soft: rgba(58, 142, 208, 0.16);
-  --sw-accent-line: rgba(58, 142, 208, 0.5);
+  --sw-line: #222222;
+
+  --sw-fg-0: #f4f4f5;
+  --sw-fg-1: #c4c4c4;
+  --sw-fg-2: #888888;
+  --sw-fg-3: #5a5a5a;
+
+  --sw-accent:      #22d3ee;
+  --sw-accent-soft: rgba(34, 211, 238, 0.15);
+  --sw-accent-line: rgba(34, 211, 238, 0.45);
+
+  --sw-info:   #7dd3fc;
+  --sw-purple: #d946ef;
+  --sw-ok:     #84cc16;
+  --sw-err:    #f43f5e;
+  --sw-warn:   #facc15;
+
+  --sw-font: "IBM Plex Mono", ui-monospace, monospace;
+  --sw-radius: 2px;
+  --sw-density-pad: 6px;
+}
+
+/* ── daybreak — light + violet, airy ─────────────────────────────── */
+[data-theme="daybreak"] {
+  --sw-bg-0: #f7f7fa;
+  --sw-bg-1: #ffffff;
+  --sw-bg-2: #f0f1f5;
+  --sw-line: #e3e4ec;
+
+  --sw-fg-0: #0a0d12;
+  --sw-fg-1: #3a3f4c;
+  --sw-fg-2: #6e7382;
+  --sw-fg-3: #9ba0af;
+
+  --sw-accent:      #6366f1;
+  --sw-accent-soft: rgba(99, 102, 241, 0.10);
+  --sw-accent-line: rgba(99, 102, 241, 0.32);
+
+  --sw-info:   #0ea5e9;
+  --sw-purple: #a855f7;
+  --sw-ok:     #16a34a;
+  --sw-err:    #dc2626;
+  --sw-warn:   #d97706;
+
+  --sw-font: "Inter", system-ui, sans-serif;
+  --sw-radius: 10px;
+  --sw-density-pad: 10px;
 }
 
-/* ── aurora — dark, PINK accent ───────────────────────────────────── */
+/* ── aurora — glass chrome + magenta/cyan gradient, showcase ─────── */
 [data-theme="aurora"] {
-  --sw-bg-0: #0e0a14;
-  --sw-bg-1: #181020;
-  --sw-bg-2: #221830;
-  --sw-bg-3: #2a1f3c;
-  --sw-bg-4: #34294a;
-  --sw-line: #2e2240;
-  --sw-line-2: #3e2f56;
-  --sw-line-3: #4e3f6c;
-
-  --sw-fg-0: #f5edf5;
-  --sw-fg-1: #d8cce0;
-  --sw-fg-2: #a89cb8;
-  --sw-fg-3: #7e7090;
-
-  /* TODO: replace with design-spec pink */
-  --sw-accent: #ec4899;
-  --sw-accent-2: #f472b6;
+  --sw-bg-0: #0b0d18;
+  --sw-bg-1: #11142a;
+  --sw-bg-2: #181b35;
+  --sw-line: #262a48;
+
+  --sw-fg-0: #f1f3ff;
+  --sw-fg-1: #b9bee0;
+  --sw-fg-2: #828abe;
+  --sw-fg-3: #525a8a;
+
+  --sw-accent:      #ec4899;
   --sw-accent-soft: rgba(236, 72, 153, 0.16);
-  --sw-accent-line: rgba(236, 72, 153, 0.45);
-}
+  --sw-accent-line: rgba(236, 72, 153, 0.4);
 
-/* ── meridian — dark, PURPLE accent ───────────────────────────────── */
-[data-theme="meridian"] {
-  --sw-bg-0: #0d0a1a;
-  --sw-bg-1: #15102a;
-  --sw-bg-2: #1d1838;
-  --sw-bg-3: #261f48;
-  --sw-bg-4: #2f285a;
-  --sw-line: #2a2548;
-  --sw-line-2: #3a345e;
-  --sw-line-3: #4c4476;
-
-  --sw-fg-0: #efedf7;
-  --sw-fg-1: #d4cfe6;
-  --sw-fg-2: #a59fc0;
-  --sw-fg-3: #7b7398;
-
-  /* TODO: replace with design-spec purple */
-  --sw-accent: #a855f7;
-  --sw-accent-2: #c084fc;
-  --sw-accent-soft: rgba(168, 85, 247, 0.16);
-  --sw-accent-line: rgba(168, 85, 247, 0.5);
-}
+  --sw-info:   #22d3ee;
+  --sw-purple: #a855f7;
+  --sw-ok:     #22d3ee;
+  --sw-err:    #f43f5e;
+  --sw-warn:   #fbbf24;
 
-/* ── daybreak — WHITE light theme ─────────────────────────────────── */
-[data-theme="daybreak"] {
-  --sw-bg-0: #ffffff;
-  --sw-bg-1: #fbfbfd;
-  --sw-bg-2: #f4f5f8;
-  --sw-bg-3: #ecedf2;
-  --sw-bg-4: #e2e4eb;
-  --sw-line: #e3e5ec;
-  --sw-line-2: #d3d6e0;
-  --sw-line-3: #b8bcc8;
-
-  --sw-fg-0: #14181f;
-  --sw-fg-1: #2e3640;
-  --sw-fg-2: #5b6373;
-  --sw-fg-3: #8a93a0;
-
-  /* TODO: replace with design-spec daybreak accent (muted is a guess) */
-  --sw-accent: #4b5563;
-  --sw-accent-2: #6b7280;
-  --sw-accent-soft: rgba(75, 85, 99, 0.10);
-  --sw-accent-line: rgba(75, 85, 99, 0.35);
+  --sw-font: "Inter", system-ui, sans-serif;
+  --sw-radius: 12px;
+  --sw-density-pad: 12px;
 }

Reply via email to