refactor(web): make identity tints theme-aware and restore colorful default preset

Derive --color-identity-* tokens from theme palette variables (chart and
status hues) instead of fixed oklch hues, so group names, channel ids and
model badges re-tint automatically when a color preset overrides the
palette. The monochrome simple-large preset collapses identity tints to
the foreground via --identity-color-mix: 0%.

Also port main's blue default palette (light/dark), preview the default
preset as a rainbow swatch, and show the token group as a tinted
GroupBadge on the usage-logs page.
This commit is contained in:
t0ng7u
2026-07-12 15:30:25 +08:00
parent fdcd86c851
commit 31b6832314
5 changed files with 93 additions and 104 deletions
+3 -1
View File
@@ -323,10 +323,12 @@ function PresetConfig() {
aria-hidden='true' aria-hidden='true'
className='absolute inset-0 rounded-md' className='absolute inset-0 rounded-md'
style={ style={
// The default preset is the full-color theme; two swatch
// stops cannot convey that, so it previews as a rainbow.
preset.value === 'default' preset.value === 'default'
? { ? {
background: background:
'linear-gradient(135deg, var(--background) 0%, var(--muted) 50%, var(--foreground) 100%)', 'linear-gradient(135deg, oklch(0.68 0.2 25) 0%, oklch(0.8 0.17 85) 25%, oklch(0.72 0.18 155) 50%, oklch(0.66 0.19 245) 75%, oklch(0.68 0.2 315) 100%)',
} }
: { : {
background: `linear-gradient(135deg, ${preset.swatches[0]} 0%, ${preset.swatches[1] ?? preset.swatches[0]} 100%)`, background: `linear-gradient(135deg, ${preset.swatches[0]} 0%, ${preset.swatches[1] ?? preset.swatches[0]} 100%)`,
@@ -21,6 +21,7 @@ import { GitBranch, Sparkles } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { GroupBadge } from '@/components/group-badge'
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge' import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { import {
@@ -567,31 +568,33 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
let group = log.group let group = log.group
if (!group) group = other?.group || '' if (!group) group = other?.group || ''
const metaParts: string[] = []
const groupRatioText = getGroupRatioText( const groupRatioText = getGroupRatioText(
other, other,
group ? groupRatios[group] : undefined group ? groupRatios[group] : undefined
) )
if (group) { const tokenBadgeClassName =
metaParts.push(sensitiveVisible ? group : '••••') 'max-w-full min-w-0 overflow-hidden [&>[data-slot=status-badge-label]]:max-w-full [&>[data-slot=status-badge-label]]:min-w-0 [&>[data-slot=status-badge-label]]:overflow-hidden [&>[data-slot=status-badge-label]]:text-ellipsis'
}
if (groupRatioText) metaParts.push(groupRatioText)
return ( return (
<div className='flex w-max flex-col gap-0.5'> <div className='flex max-w-[200px] flex-col gap-0.5'>
<TooltipProvider delay={300}> <TooltipProvider delay={300}>
<Tooltip> <Tooltip>
<TooltipTrigger render={<div className='shrink-0' />}> <TooltipTrigger render={<div className='max-w-full' />}>
{sensitiveVisible ? ( {sensitiveVisible ? (
<CopyableStatusBadge <CopyableStatusBadge
value={tokenName} value={tokenName}
variant='neutral' variant='neutral'
size='sm' size='sm'
className={tokenBadgeClassName}
> >
{displayName} {displayName}
</CopyableStatusBadge> </CopyableStatusBadge>
) : ( ) : (
<StatusBadge variant='neutral' size='sm'> <StatusBadge
variant='neutral'
size='sm'
className={tokenBadgeClassName}
>
{displayName} {displayName}
</StatusBadge> </StatusBadge>
)} )}
@@ -603,12 +606,21 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
)} )}
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
{metaParts.length > 0 && ( {(group || groupRatioText) && (
<span <span className='flex max-w-full min-w-0 items-baseline gap-1 text-xs leading-none'>
className='text-subtle-foreground max-w-[150px] truncate text-xs' {group && (
title={sensitiveVisible ? metaParts.join(' · ') : undefined} <GroupBadge
> group={group}
{metaParts.join(' · ')} label={sensitiveVisible ? undefined : '••••'}
size='sm'
className='min-w-0 truncate'
/>
)}
{groupRatioText && (
<span className='text-subtle-foreground shrink-0 tabular-nums'>
{groupRatioText}
</span>
)}
</span> </span>
)} )}
</div> </div>
+1 -1
View File
@@ -27,7 +27,7 @@ export const THEME_PRESETS = [
{ {
value: 'default', value: 'default',
name: 'Default', name: 'Default',
swatches: ['oklch(0.13 0 0)', 'oklch(0.95 0 0)'], swatches: ['oklch(0.692 0.141 243.716)', 'oklch(0.7 0.12 280)'],
}, },
{ {
// Inspired by Anthropic's official brand language: warm cream canvas // Inspired by Anthropic's official brand language: warm cream canvas
+6
View File
@@ -342,6 +342,12 @@ For commercial licensing, please contact support@quantumnous.com
--skeleton-base: oklch(0.9 0 0); --skeleton-base: oklch(0.9 0 0);
--skeleton-highlight: oklch(0.97 0 0); --skeleton-highlight: oklch(0.97 0 0);
/* Identity tints (group names, channel ids, model badges) are decorative,
* so this monochrome preset collapses them to the foreground by mixing 0%
* of the palette hue. Status colors above stay colored on purpose: they
* carry meaning (error/success), which accessibility asks us to keep. */
--identity-color-mix: 0%;
--radius: 0.5rem; --radius: 0.5rem;
--text-xs: 0.9rem; --text-xs: 0.9rem;
--text-sm: 1rem; --text-sm: 1rem;
+57 -88
View File
@@ -105,94 +105,66 @@ For commercial licensing, please contact support@quantumnous.com
/* Metric identity accent; intentionally distinct from status semantics. */ /* Metric identity accent; intentionally distinct from status semantics. */
--color-metric-rpm: color-mix( --color-metric-rpm: color-mix(
in oklch, in oklch,
oklch(0.68 calc(0.2 * var(--identity-chroma-scale)) 350) oklch(0.68 0.2 350) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
/* Stable entity hues used by deterministic string-to-color identity tints. /* Stable entity tints used by deterministic string-to-color identity
* Mixing toward the achromatic foreground dilutes chroma along with * coloring. Each tint derives from a theme palette token (chart and status
* lightness, so each hue pre-scales its chroma by the per-mode * hues) instead of a fixed hue, so identity colors re-tint automatically
* `--identity-chroma-scale` (see :root / .dark). */ * when a theme preset overrides the palette. Mixing toward the foreground
* (via the per-mode `--identity-color-mix`) keeps compact text legible in
* both light and dark modes. */
--color-identity-amber: color-mix( --color-identity-amber: color-mix(
in oklch, in oklch,
oklch(0.72 calc(0.17 * var(--identity-chroma-scale)) 75) var(--warning) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-blue: color-mix( --color-identity-blue: color-mix(
in oklch, in oklch,
oklch(0.62 calc(0.18 * var(--identity-chroma-scale)) 250) var(--chart-1) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-cyan: color-mix( --color-identity-cyan: color-mix(
in oklch, in oklch,
oklch(0.7 calc(0.14 * var(--identity-chroma-scale)) 205) var(--chart-2) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-green: color-mix( --color-identity-green: color-mix(
in oklch, in oklch,
oklch(0.64 calc(0.16 * var(--identity-chroma-scale)) 145) var(--success) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-grey: var(--muted-foreground); --color-identity-grey: var(--muted-foreground);
--color-identity-indigo: color-mix( --color-identity-indigo: var(--color-identity-blue);
in oklch,
oklch(0.6 calc(0.2 * var(--identity-chroma-scale)) 275)
var(--identity-color-mix),
var(--foreground)
);
--color-identity-light-blue: color-mix( --color-identity-light-blue: color-mix(
in oklch, in oklch,
oklch(0.74 calc(0.12 * var(--identity-chroma-scale)) 225) var(--info) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-lime: color-mix( --color-identity-lime: color-mix(
in oklch, in oklch,
oklch(0.75 calc(0.17 * var(--identity-chroma-scale)) 125) var(--chart-3) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-orange: color-mix( --color-identity-orange: var(--color-identity-amber);
--color-identity-pink: color-mix(
in oklch, in oklch,
oklch(0.7 calc(0.19 * var(--identity-chroma-scale)) 50) var(--chart-5) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-pink: var(--color-metric-rpm);
--color-identity-purple: color-mix( --color-identity-purple: color-mix(
in oklch, in oklch,
oklch(0.62 calc(0.2 * var(--identity-chroma-scale)) 305) var(--chart-4) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-red: color-mix( --color-identity-red: color-mix(
in oklch, in oklch,
oklch(0.64 calc(0.21 * var(--identity-chroma-scale)) 25) var(--destructive) var(--identity-color-mix),
var(--identity-color-mix),
var(--foreground)
);
--color-identity-teal: color-mix(
in oklch,
oklch(0.66 calc(0.14 * var(--identity-chroma-scale)) 175)
var(--identity-color-mix),
var(--foreground)
);
--color-identity-violet: color-mix(
in oklch,
oklch(0.65 calc(0.2 * var(--identity-chroma-scale)) 325)
var(--identity-color-mix),
var(--foreground)
);
--color-identity-yellow: color-mix(
in oklch,
oklch(0.78 calc(0.16 * var(--identity-chroma-scale)) 95)
var(--identity-color-mix),
var(--foreground) var(--foreground)
); );
--color-identity-teal: var(--color-identity-cyan);
--color-identity-violet: var(--color-identity-purple);
--color-identity-yellow: var(--color-identity-amber);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
@@ -233,19 +205,19 @@ For commercial licensing, please contact support@quantumnous.com
* `[data-theme-font='...']` blocks in theme-presets.css. Default mirrors * `[data-theme-font='...']` blocks in theme-presets.css. Default mirrors
* `--font-sans` so behavior is identical when no font preference is set. */ * `--font-sans` so behavior is identical when no font preference is set. */
--font-body: var(--font-sans); --font-body: var(--font-sans);
/* Light mode uses a bright pure-white canvas (matching the original /* Light mode uses a bright pure-white canvas with the blue brand palette:
* palette) with soft light-grey borders and accents, so the UI reads * blue primary/ring/charts and primary-tinted accents, over soft
* crisp instead of collapsing into muddy greys. */ * light-grey borders, so the default theme reads colorful but calm. */
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.13 0 0); --primary: oklch(0.692 0.141 243.716);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0 0); --secondary: oklch(0.95 0 0);
--secondary-foreground: oklch(0.145 0 0); --secondary-foreground: oklch(0.42 0.16 250);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.49 0 0); --muted-foreground: oklch(0.49 0 0);
/* Low-emphasis text tiers below `muted-foreground`, used for secondary /* Low-emphasis text tiers below `muted-foreground`, used for secondary
@@ -263,7 +235,7 @@ For commercial licensing, please contact support@quantumnous.com
var(--muted-foreground) 40%, var(--muted-foreground) 40%,
transparent transparent
); );
--accent: oklch(0.95 0 0); --accent: color-mix(in oklch, var(--primary) 12%, var(--background));
--accent-foreground: oklch(0.145 0 0); --accent-foreground: oklch(0.145 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0); --destructive-foreground: oklch(0.985 0 0);
@@ -277,20 +249,20 @@ For commercial licensing, please contact support@quantumnous.com
--neutral-foreground: oklch(0.145 0 0); --neutral-foreground: oklch(0.145 0 0);
--border: oklch(0.93 0 0); --border: oklch(0.93 0 0);
--input: oklch(0.93 0 0); --input: oklch(0.93 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0.16 249.003);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.72 0.18 250);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.65 0.15 200);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.7 0.12 280);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.68 0.19 325);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.68 0.16 155);
--sidebar: oklch(0.975 0 0); --sidebar: color-mix(in oklch, var(--foreground) 0.5%, var(--background));
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.13 0 0); --sidebar-primary: oklch(0.64 0.197 253.892);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.92 0 0); --sidebar-accent: color-mix(in oklch, var(--primary) 12%, var(--background));
--sidebar-accent-foreground: oklch(0.145 0 0); --sidebar-accent-foreground: oklch(0.579 0.196 255.909);
--sidebar-border: oklch(0.93 0 0); --sidebar-border: oklch(0.93 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.58 0.2 250);
--skeleton-base: oklch(0.97 0 0); --skeleton-base: oklch(0.97 0 0);
--skeleton-highlight: oklch(0.985 0 0); --skeleton-highlight: oklch(0.985 0 0);
/* 100% keeps the original vivid semantic hues (matching the old palette /* 100% keeps the original vivid semantic hues (matching the old palette
@@ -298,7 +270,6 @@ For commercial licensing, please contact support@quantumnous.com
* of darkening them toward the near-black foreground. */ * of darkening them toward the near-black foreground. */
--status-color-mix: 100%; --status-color-mix: 100%;
--identity-color-mix: 100%; --identity-color-mix: 100%;
--identity-chroma-scale: 1;
--identity-surface-mix: 13%; --identity-surface-mix: 13%;
--identity-border-mix: 28%; --identity-border-mix: 28%;
--table-row: var(--card); --table-row: var(--card);
@@ -333,13 +304,13 @@ For commercial licensing, please contact support@quantumnous.com
--card-foreground: oklch(0.965 0 0); --card-foreground: oklch(0.965 0 0);
--popover: oklch(0.305 0 0); --popover: oklch(0.305 0 0);
--popover-foreground: oklch(0.965 0 0); --popover-foreground: oklch(0.965 0 0);
--primary: oklch(0.965 0 0); --primary: oklch(0.54 0.142 248.516);
--primary-foreground: oklch(0.155 0 0); --primary-foreground: oklch(1 0 0);
--secondary: oklch(0.335 0 0); --secondary: oklch(0.335 0 0);
--secondary-foreground: oklch(0.965 0 0); --secondary-foreground: oklch(0.9 0.05 250);
--muted: oklch(0.305 0 0); --muted: oklch(0.305 0 0);
--muted-foreground: oklch(0.78 0 0); --muted-foreground: oklch(0.78 0 0);
--accent: oklch(0.365 0 0); --accent: color-mix(in oklch, var(--primary) 20%, var(--background));
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0); --destructive-foreground: oklch(0.985 0 0);
@@ -347,32 +318,30 @@ For commercial licensing, please contact support@quantumnous.com
--success-foreground: oklch(0.145 0 0); --success-foreground: oklch(0.145 0 0);
--warning: oklch(0.769 0.188 70.08); --warning: oklch(0.769 0.188 70.08);
--warning-foreground: oklch(0.145 0 0); --warning-foreground: oklch(0.145 0 0);
--info: oklch(0.68 0.17 237.323); --info: oklch(0.613 0.14 239.919);
--info-foreground: oklch(0.145 0 0); --info-foreground: oklch(0.145 0 0);
--neutral: oklch(0.76 0 0); --neutral: oklch(0.76 0 0);
--neutral-foreground: oklch(0.155 0 0); --neutral-foreground: oklch(0.155 0 0);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 17%); --input: oklch(1 0 0 / 17%);
--ring: oklch(0.68 0 0); --ring: oklch(0.554 0.148 250.726);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.76 0.14 250);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.76 0.13 200);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.76 0.13 285);
--chart-4: oklch(0.627 0.265 303.9); --chart-4: oklch(0.75 0.16 325);
--chart-5: oklch(0.645 0.246 16.439); --chart-5: oklch(0.76 0.14 155);
--sidebar: oklch(0.225 0 0); --sidebar: oklch(0.225 0 0);
--sidebar-foreground: oklch(0.95 0 0); --sidebar-foreground: oklch(0.95 0 0);
--sidebar-primary: oklch(0.965 0 0); --sidebar-primary: oklch(0.588 0.123 246.488);
--sidebar-primary-foreground: oklch(0.155 0 0); --sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.355 0 0); --sidebar-accent: color-mix(in oklch, var(--primary) 20%, var(--background));
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.9 0.05 250);
--sidebar-border: oklch(1 0 0 / 11%); --sidebar-border: oklch(1 0 0 / 11%);
--sidebar-ring: oklch(0.68 0 0); --sidebar-ring: oklch(0.7 0.17 250);
--skeleton-base: oklch(0.335 0 0); --skeleton-base: oklch(0.335 0 0);
--skeleton-highlight: oklch(0.44 0 0); --skeleton-highlight: oklch(0.44 0 0);
--status-color-mix: 55%; --status-color-mix: 55%;
--identity-color-mix: 55%; --identity-color-mix: 55%;
/* Dark mode keeps the soft macaron pastels unchanged. */
--identity-chroma-scale: 1;
--identity-surface-mix: 10%; --identity-surface-mix: 10%;
--identity-border-mix: 20%; --identity-border-mix: 20%;
--table-row: var(--background); --table-row: var(--background);