refactor(design-tokens): V2 engine + verifier

Migrates component styling to the element/tone/emphasis/state token system
and adds a standalone verifier (`npm run verify:theme`) plus vitest coverage
that exercises every mode × theme × contrast-grid variant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Douglas 2026-04-20 20:11:30 +01:00
parent 9adaded472
commit 7edfbb11dc
94 changed files with 3820 additions and 2311 deletions

View file

@ -413,13 +413,8 @@ protocol.registerSchemesAsPrivileged([
process.env.APP_ROOT = MAIN_DIST;
process.env.VITE_PUBLIC = VITE_PUBLIC;
// Respect system theme on Windows, keep light theme on macOS for consistency
const isWindows = process.platform === 'win32';
if (isWindows) {
nativeTheme.themeSource = 'system'; // Respect Windows dark/light mode
} else {
nativeTheme.themeSource = 'light'; // Keep existing behavior for macOS
}
// Always follow OS appearance so renderer `prefers-color-scheme` stays accurate.
nativeTheme.themeSource = 'system';
// Set log level
log.transports.console.level = 'info';
@ -2737,6 +2732,7 @@ let installationLock: Promise<PromiseReturnType> = Promise.resolve({
// ==================== window create ====================
async function createWindow() {
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
// Ensure .eigent directories exist before anything else
ensureEigentDirectories();

View file

@ -41,6 +41,7 @@
"test:e2e": "vitest run --config vitest.config.ts",
"test:coverage": "vitest run --coverage",
"check:i18n": "node scripts/check-i18n-locale-parity.js",
"verify:theme": "vite-node scripts/verify-theme-tokens.ts",
"type-check": "tsc -p tsconfig.build.json --noEmit",
"lint": "eslint . --no-warn-ignored",
"lint:fix": "eslint . --fix --no-warn-ignored",

View file

@ -0,0 +1,195 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Standalone V2 design-token verification CLI.
//
// Runs the verifier over every registered theme/mode/contrast variant and
// prints a human-readable report. Exits with a non-zero code if any `error`
// findings are produced. Auxiliary contrast warnings do not fail the run
// unless `--strict` is passed.
//
// Usage:
// npm run verify:theme
// npm run verify:theme -- --strict # auxiliary warnings fail too
// npm run verify:theme -- --json # machine-readable output
// npm run verify:theme -- --contrast 0,50,100
import {
getDefaultContrastGrid,
listRegisteredThemes,
verifyThemeEngine,
type VerifyFinding,
} from '../src/lib/themeTokens/verifier';
type CliFlags = {
strict: boolean;
json: boolean;
contrastGrid: number[];
};
function parseArgs(argv: string[]): CliFlags {
const flags: CliFlags = {
strict: false,
json: false,
contrastGrid: getDefaultContrastGrid(),
};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--strict') flags.strict = true;
else if (arg === '--json') flags.json = true;
else if (arg === '--contrast') {
const raw = argv[++i];
if (raw) {
flags.contrastGrid = raw
.split(',')
.map((s) => Number(s.trim()))
.filter((n) => Number.isFinite(n));
}
} else if (arg === '--help' || arg === '-h') {
process.stdout.write(
[
'Usage: verify-theme-tokens [options]',
'',
'Options:',
' --strict Treat auxiliary contrast warnings as errors',
' --json Emit JSON report on stdout',
' --contrast a,b,c Override contrast grid (default: 0,25,43,75,100)',
' -h, --help Show this help',
'',
].join('\n')
);
process.exit(0);
}
}
return flags;
}
const COLORS = {
reset: '\x1b[0m',
red: '\x1b[31m',
yellow: '\x1b[33m',
green: '\x1b[32m',
dim: '\x1b[2m',
bold: '\x1b[1m',
cyan: '\x1b[36m',
};
function colorize(text: string, code: string): string {
if (!process.stdout.isTTY) return text;
return `${code}${text}${COLORS.reset}`;
}
function groupFindings(
findings: VerifyFinding[]
): Map<string, VerifyFinding[]> {
const groups = new Map<string, VerifyFinding[]>();
for (const f of findings) {
const key = `${f.mode} / ${f.themeId} / contrast=${f.contrast}`;
const bucket = groups.get(key);
if (bucket) bucket.push(f);
else groups.set(key, [f]);
}
return groups;
}
function printHumanReport(
themes: Array<{ mode: string; id: string }>,
flags: CliFlags,
report: ReturnType<typeof verifyThemeEngine>
): void {
const { summary, findings } = report;
process.stdout.write(
`\n${colorize('Design Token Engine Verification (V2)', COLORS.bold)}\n`
);
process.stdout.write(
colorize(
` Registered themes: ${themes.map((t) => `${t.mode}/${t.id}`).join(', ')}\n`,
COLORS.dim
)
);
process.stdout.write(
colorize(
` Contrast grid: ${flags.contrastGrid.join(', ')}\n`,
COLORS.dim
)
);
process.stdout.write(
colorize(` Variants checked: ${summary.variantsChecked}\n\n`, COLORS.dim)
);
if (findings.length === 0) {
process.stdout.write(
`${colorize('PASS', COLORS.green)} No findings — engine is clean.\n\n`
);
return;
}
const groups = groupFindings(findings);
for (const [variant, bucket] of groups) {
process.stdout.write(`${colorize(variant, COLORS.cyan)}\n`);
for (const f of bucket) {
const badge =
f.severity === 'error'
? colorize('ERROR', COLORS.red)
: colorize('WARN ', COLORS.yellow);
const ratioSuffix =
f.ratio !== undefined && f.threshold !== undefined
? colorize(
` (ratio ${f.ratio.toFixed(2)} / threshold ${f.threshold})`,
COLORS.dim
)
: '';
process.stdout.write(
` ${badge} [${f.code}] ${f.message}${ratioSuffix}\n`
);
if (f.tokenKey && f.value) {
process.stdout.write(
colorize(`${f.tokenKey} = ${f.value}\n`, COLORS.dim)
);
}
}
process.stdout.write('\n');
}
const errBadge =
summary.errors === 0
? colorize(`${summary.errors} errors`, COLORS.green)
: colorize(`${summary.errors} errors`, COLORS.red);
const warnBadge =
summary.warnings === 0
? colorize(`${summary.warnings} warnings`, COLORS.green)
: colorize(`${summary.warnings} warnings`, COLORS.yellow);
process.stdout.write(`Summary: ${errBadge}, ${warnBadge}\n\n`);
}
function main() {
const flags = parseArgs(process.argv.slice(2));
const themes = listRegisteredThemes();
const report = verifyThemeEngine({
contrastGrid: flags.contrastGrid,
strictAuxContrast: flags.strict,
});
if (flags.json) {
process.stdout.write(JSON.stringify({ themes, ...report }, null, 2) + '\n');
} else {
printHumanReport(themes, flags, report);
}
const failed = report.summary.errors > 0;
process.exit(failed ? 1 : 0);
}
main();

View file

@ -758,12 +758,12 @@ const ToolSelect = forwardRef<
{(initialSelectedTools || []).map((item: any) => (
<Badge
key={item.id + item.key + (item.isLocal + '')}
className="h-5 gap-1 bg-button-tertiery-fill-default px-xs flex w-auto flex-shrink-0 items-center"
className="h-5 gap-1 bg-ds-bg-neutral-subtle-default px-xs flex w-auto flex-shrink-0 items-center"
>
{item.name || item.mcp_name || item.key || `tool_${item.id}`}
<div className="rounded-sm bg-button-secondary-fill-disabled flex items-center justify-center">
<div className="rounded-sm bg-ds-bg-neutral-muted-disabled flex items-center justify-center">
<X
className="h-4 w-4 text-button-secondary-icon-disabled cursor-pointer"
className="h-4 w-4 text-ds-text-neutral-muted-disabled hover:text-ds-text-neutral-default-default cursor-pointer"
onClick={() => removeOption(item)}
/>
</div>
@ -852,7 +852,7 @@ const ToolSelect = forwardRef<
>
<div className="gap-1 flex items-center">
{/* {getCategoryIcon(item.category?.name)} */}
<div className="text-sm font-bold leading-17 text-ds-text-brand-default-default line-clamp-1 overflow-hidden break-words text-ellipsis">
<div className="text-body-md font-bold text-ds-text-brand-default-default line-clamp-1 overflow-hidden break-words text-ellipsis">
{item.mcp_name}
</div>
<TooltipSimple content={item.mcp_desc}>
@ -863,10 +863,7 @@ const ToolSelect = forwardRef<
</TooltipSimple>
</div>
<div className="gap-1 flex items-center">
<Button
className="h-6 rounded-md bg-button-secondary-fill-default px-sm py-xs text-xs font-bold leading-17 text-button-secondary-text-default shadow-sm hover:bg-button-tertiery-text-default"
disabled={true}
>
<Button variant="secondary" size="sm" textWeight="bold" disabled>
{t('layout.installed')}
</Button>
</div>
@ -889,7 +886,7 @@ const ToolSelect = forwardRef<
inputRef.current?.focus();
setIsOpen(true);
}}
className="gap-1 rounded-lg border-input-border-default bg-input-bg-default py-1 flex max-h-[120px] min-h-[60px] w-full flex-wrap justify-start overflow-y-auto border border-solid px-[6px]"
className="gap-1 rounded-lg border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default py-1 flex max-h-[120px] min-h-[60px] w-full flex-wrap justify-start overflow-y-auto border border-solid px-[6px]"
>
{renderSelectedItems()}
<Textarea
@ -905,7 +902,7 @@ const ToolSelect = forwardRef<
{/* floating dropdown */}
{isOpen && (
<div className="left-0 right-0 mt-1 rounded-lg border-input-border-default bg-dropdown-bg absolute top-full z-50 overflow-y-auto border border-solid">
<div className="left-0 right-0 mt-1 rounded-lg border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default absolute top-full z-50 overflow-y-auto border border-solid">
<div className="max-h-[192px] overflow-y-auto">
<IntegrationList
variant="select"

View file

@ -522,12 +522,12 @@ export function AddWorker({
secretVisible[key] ? (
<EyeOff
size={16}
className="text-button-transparent-icon-disabled"
className="text-ds-text-neutral-muted-disabled"
/>
) : (
<Eye
size={16}
className="text-button-transparent-icon-disabled"
className="text-ds-text-neutral-muted-disabled"
/>
)
) : undefined
@ -544,7 +544,7 @@ export function AddWorker({
</div>
</DialogContentSection>
<DialogFooter
className="!rounded-b-xl bg-white-100% p-md"
className="!rounded-b-xl bg-ds-bg-neutral-inverse-default p-md"
showCancelButton={true}
showConfirmButton={true}
cancelButtonText={t('workforce.cancel')}
@ -569,7 +569,7 @@ export function AddWorker({
) : (
// default add interface
<>
<DialogContentSection className="gap-3 bg-white-100% p-md flex flex-col">
<DialogContentSection className="gap-3 bg-ds-bg-neutral-inverse-default p-md flex flex-col">
<div className="gap-4 flex flex-col">
<div className="gap-sm flex items-center">
<div className="h-16 w-16 flex items-center justify-center">
@ -688,7 +688,7 @@ export function AddWorker({
</div>
</DialogContentSection>
<DialogFooter
className="!rounded-b-xl bg-white-100% p-md"
className="!rounded-b-xl bg-ds-bg-neutral-inverse-default p-md"
showCancelButton={true}
showConfirmButton={true}
cancelButtonText={t('workforce.cancel')}

View file

@ -45,9 +45,9 @@ export default function BrowserAgentWorkspace() {
<CodeXml size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-emerald-700',
bgColor: 'bg-bg-fill-coding-active',
shapeColor: 'bg-bg-fill-coding-default',
borderColor: 'border-bg-fill-coding-active',
bgColor: 'bg-ds-bg-terminal-default-default',
shapeColor: 'bg-ds-bg-terminal-subtle-default',
borderColor: 'border-ds-border-terminal-default-default',
bgColorLight: 'bg-emerald-200',
},
browser_agent: {
@ -56,9 +56,9 @@ export default function BrowserAgentWorkspace() {
<Globe size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-blue-700',
bgColor: 'bg-bg-fill-browser-active',
shapeColor: 'bg-bg-fill-browser-default',
borderColor: 'border-bg-fill-browser-active',
bgColor: 'bg-ds-bg-browser-default-default',
shapeColor: 'bg-ds-bg-browser-subtle-default',
borderColor: 'border-ds-border-browser-default-default',
bgColorLight: 'bg-blue-200',
},
document_agent: {
@ -67,9 +67,9 @@ export default function BrowserAgentWorkspace() {
<FileText size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-yellow-700',
bgColor: 'bg-bg-fill-writing-active',
shapeColor: 'bg-bg-fill-writing-default',
borderColor: 'border-bg-fill-writing-active',
bgColor: 'bg-ds-bg-document-default-default',
shapeColor: 'bg-ds-bg-document-subtle-default',
borderColor: 'border-ds-border-document-default-default',
bgColorLight: 'bg-yellow-200',
},
multi_modal_agent: {
@ -78,9 +78,9 @@ export default function BrowserAgentWorkspace() {
<Image size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-fuchsia-700',
bgColor: 'bg-bg-fill-multimodal-active',
shapeColor: 'bg-bg-fill-multimodal-default',
borderColor: 'border-bg-fill-multimodal-active',
bgColor: 'bg-ds-bg-neutral-default-default',
shapeColor: 'bg-ds-bg-neutral-subtle-default',
borderColor: 'border-ds-border-neutral-default-default',
bgColorLight: 'bg-fuchsia-200',
},
social_media_agent: {
@ -202,7 +202,7 @@ export default function BrowserAgentWorkspace() {
<div
className={`ease-in-out flex h-full w-full flex-1 items-center justify-center transition-all duration-300`}
>
<div className="blur-bg rounded-xl bg-ds-bg-neutral-default-default relative flex h-full w-full flex-col overflow-hidden">
<div className="backdrop-blur-sm rounded-xl bg-ds-bg-neutral-default-default relative flex h-full w-full flex-col overflow-hidden">
<div className="rounded-t-2xl px-2 pb-2 pt-3 flex flex-shrink-0 items-center justify-between">
<div className="gap-sm flex items-center justify-start">
<Button

View file

@ -54,7 +54,7 @@ export const FloatingAction = ({
className
)}
>
<div className="gap-2 p-1 backdrop-blur-md pointer-events-auto flex items-center rounded-full border border-[color:var(--ds-border-neutral-default-default)] bg-[var(--ds-bg-neutral-subtle-default)] shadow-[0px_4px_16px_rgba(0,0,0,0.12)]">
<div className="gap-2 p-1 backdrop-blur-md pointer-events-auto flex items-center rounded-full border border-[color:var(--ds-border-neutral-default-default)] bg-[var(--ds-bg-neutral-subtle-default)] shadow-[0px_4px_16px_color-mix(in_srgb,var(--ds-bg-neutral-inverse-default)_14%,transparent)]">
{/* Always show Stop Task button when running (removed pause/resume logic) */}
<Button
variant="outline"
@ -70,7 +70,7 @@ export const FloatingAction = ({
{status === "running" ? (
// State 1: Running - Show Pause button
<Button
variant="cuation"
variant="caution"
size="sm"
onClick={onPause}
disabled={loading}

View file

@ -136,7 +136,7 @@ export function TaskItem({
<Button
onClick={() => onDelete()}
className="rounded-full"
variant="cuation"
variant="caution"
size="xs"
buttonContent="icon-only"
>

View file

@ -245,7 +245,7 @@ export default function ProjectGroup({
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
>
<Tag variant="info" size="xs">
<Tag variant="primary" tone="information" size="xs">
<Activity className="w-3.5 h-3.5" />
{t("layout.ongoing")}
</Tag>
@ -258,7 +258,7 @@ export default function ProjectGroup({
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
>
<Tag variant="warning" size="xs">
<Tag variant="primary" tone="warning" size="xs">
{t("layout.issue") || "Issue"}
</Tag>
</motion.div>
@ -292,7 +292,7 @@ export default function ProjectGroup({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="border-ds-border-neutral-default-default bg-dropdown-bg z-50"
className="border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default z-50"
>
{onProjectEdit && (
<DropdownMenuItem
@ -417,7 +417,7 @@ export default function ProjectGroup({
{/* Middle: Project, Trigger, Agent tags - Aligned to right */}
<div className="gap-4 flex w-fit flex-1 items-center justify-end">
<Tag variant="info" size="sm">
<Tag variant="primary" tone="information" size="sm">
<Hash />
<span>
{project.total_tokens
@ -427,14 +427,24 @@ export default function ProjectGroup({
</Tag>
<TooltipSimple content={t('layout.tasks')}>
<Tag variant="default" size="sm" className="min-w-10">
<Tag
variant="primary"
tone="neutral"
size="sm"
className="min-w-10"
>
<Pin />
<span>{project.task_count}</span>
</Tag>
</TooltipSimple>
<TooltipSimple content="Triggers">
<Tag variant="warning" size="sm" className="min-w-10">
<Tag
variant="primary"
tone="warning"
size="sm"
className="min-w-10"
>
<Zap />
<span>{project.total_triggers || 0}</span>
</Tag>
@ -445,14 +455,14 @@ export default function ProjectGroup({
<div className="ml-4 min-w-32 gap-2 border-ds-border-neutral-muted-disabled pl-4 flex w-fit items-center justify-end border border-y-0 border-r-0 border-solid">
{/* Status tag */}
{/* {isOngoing && (
<Tag variant="info" size="sm">
<Tag variant="primary" tone="information" size="sm">
<Activity />
{t("layout.ongoing")}
</Tag>
)} */}
{/* {!isOngoing && hasIssue && (
<Tag variant="warning" size="sm">
<Tag variant="primary" tone="warning" size="sm">
{t("layout.issue") || "Issue"}
</Tag>
)} */}
@ -471,7 +481,7 @@ export default function ProjectGroup({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="border-ds-border-neutral-default-default bg-dropdown-bg z-50"
className="border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default z-50"
>
{onProjectEdit && (
<DropdownMenuItem

View file

@ -72,21 +72,21 @@ export default function TaskItem({
switch (status) {
case 1: // ChatStatus.ongoing
return (
<Tag variant="info" size="sm">
<Tag variant="primary" tone="information" size="sm">
<Clock />
<span>{t('layout.running')}</span>
</Tag>
);
case 2: // ChatStatus.done
return (
<Tag variant="success" size="sm">
<Tag variant="primary" tone="success" size="sm">
<CheckCircle />
<span>{t('layout.completed')}</span>
</Tag>
);
default: // Unknown status
return (
<Tag variant="default" size="sm">
<Tag variant="primary" tone="neutral" size="sm">
<Clock />
<span>{t('layout.unknown')}</span>
</Tag>
@ -102,7 +102,7 @@ export default function TaskItem({
return (
<div
onClick={onSelect}
className={` ${isActive ? '!bg-white-100%' : ''} h-14 gap-md rounded-xl border-ds-border-neutral-muted-disabled bg-white-30% p-3 shadow-history-item hover:bg-white-100% relative flex w-full cursor-pointer items-center justify-between border border-solid transition-all duration-300 ${!isLast ? 'mb-2' : ''} `}
className={` ${isActive ? '!bg-ds-bg-neutral-inverse-default' : ''} h-14 gap-md rounded-xl border-ds-border-neutral-muted-disabled bg-ds-bg-neutral-inverse-default/30 p-3 shadow-history-item hover:bg-ds-bg-neutral-inverse-default relative flex w-full cursor-pointer items-center justify-between border border-solid transition-all duration-300 ${!isLast ? 'mb-2' : ''} `}
>
<div className="min-w-0 gap-2 flex flex-1 items-center">
<TooltipSimple content={t('layout.tasks')}>
@ -138,14 +138,15 @@ export default function TaskItem({
<div className="gap-2 flex flex-shrink-0 items-center">
{!isOngoing && getStatusTag(task.status)}
<Tag variant="info" size="sm">
<Tag variant="primary" tone="information" size="sm">
<Hash />
<span>{task.tokens ? task.tokens.toLocaleString() : '0'}</span>
</Tag>
{isOngoing && (onPause || onResume) && (
<Tag
variant={isPaused ? 'info' : 'success'}
variant="primary"
tone={isPaused ? 'information' : 'success'}
size="sm"
onClick={(e) => {
e.stopPropagation();
@ -176,7 +177,7 @@ export default function TaskItem({
</PopoverTrigger>
<PopoverContent
align="end"
className="border-ds-border-neutral-default-default bg-dropdown-bg p-sm w-[98px] rounded-[12px] border border-solid"
className="border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default p-sm w-[98px] rounded-[12px] border border-solid"
>
<div className="space-y-1">
{!isOngoing && (

View file

@ -397,23 +397,20 @@ export default function GroupedHistoryView({
{/* Summary */}
<div className="pb-4 flex items-center justify-between">
<div className="gap-2 flex items-center">
<Tag variant="default" size="sm" className="gap-2">
<Tag variant="secondary" tone="neutral" size="sm" className="gap-2">
<Sparkle />
<span className="text-body-sm"> {t('layout.projects')}</span>
<span className="h-5 w-5 bg-tag-fill-default-foreground text-label-xs font-bold text-ds-text-neutral-default-default flex items-center justify-center rounded-full">
{allProjects.length}
</span>
{allProjects.length}
</Tag>
<Tag variant="default" size="sm" className="gap-2">
<Tag variant="secondary" tone="neutral" size="sm" className="gap-2">
<Pin />
<span className="text-body-sm"> {t('layout.total-tasks')}</span>
<span className="h-5 w-5 bg-tag-fill-default-foreground text-label-xs font-bold text-ds-text-neutral-default-default flex items-center justify-center rounded-full">
{allProjects.reduce(
(total, project) => total + project.task_count,
0
)}
</span>
{t('layout.total-tasks')}
{allProjects.reduce(
(total, project) => total + project.task_count,
0
)}
</Tag>
</div>
<div className="gap-md flex items-center">

View file

@ -74,9 +74,9 @@ export function VerticalNavigation({
className={cn(
'gap-2 rounded-lg px-5 py-1.5 text-body-sm w-full justify-start',
'bg-transparent data-[state=inactive]:bg-transparent',
'data-[state=inactive]:text-menubutton-text-default data-[state=inactive]:opacity-70',
'data-[state=inactive]:hover:bg-menubutton-fill-hover data-[state=inactive]:hover:opacity-100',
'data-[state=active]:bg-menubutton-fill-active data-[state=active]:text-menutabs-text-active',
'data-[state=inactive]:text-ds-text-neutral-muted-default data-[state=inactive]:opacity-70',
'data-[state=inactive]:hover:bg-ds-bg-neutral-default-hover data-[state=inactive]:hover:opacity-100',
'data-[state=active]:bg-ds-bg-neutral-default-default data-[state=active]:text-ds-text-neutral-default-default',
triggerClassName
)}
>

View file

@ -81,7 +81,7 @@ export default function SearchInput({
<motion.div
className={cn(
'rounded-lg py-0.5 flex items-center justify-center overflow-hidden border border-solid border-transparent bg-transparent',
'focus-within:border-input-border-focus focus-within:bg-input-bg-input',
'focus-within:border-ds-border-brand-default-focus focus-within:bg-ds-bg-neutral-strong-default',
'hover:bg-ds-bg-neutral-strong-hover hover:border-transparent'
)}
initial={false}

View file

@ -50,7 +50,7 @@ export default function CloseNoticeDialog({
<div className="gap-md bg-ds-bg-neutral-strong-default p-md flex flex-col">
{t('layout.a-task-is-currently-running')}
</div>
<DialogFooter className="!rounded-b-xl bg-white-100% p-md">
<DialogFooter className="!rounded-b-xl bg-ds-bg-neutral-inverse-default p-md">
<DialogClose asChild>
<Button variant="ghost" size="md">
{t('layout.cancel')}

View file

@ -55,7 +55,7 @@ export default function EndNoticeDialog({
<div className="gap-md bg-ds-bg-neutral-strong-default p-md flex flex-col">
{t('layout.ending-this-project-will-stop')}
</div>
<DialogFooter className="!rounded-b-xl bg-white-100% p-md">
<DialogFooter className="!rounded-b-xl bg-ds-bg-neutral-inverse-default p-md">
<DialogClose asChild>
<Button variant="ghost" size="md" disabled={loading}>
{t('layout.cancel')}
@ -64,7 +64,7 @@ export default function EndNoticeDialog({
<Button
size="md"
onClick={onSubmit}
variant="cuation"
variant="caution"
disabled={loading}
>
{t('layout.yes-end-project')}

View file

@ -96,11 +96,11 @@ export function SearchHistoryDialog() {
<>
<Button
variant="ghost"
className="border-menutabs-border-default bg-ds-bg-neutral-strong-default h-[32px] border border-solid"
className="border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default h-[32px] border border-solid"
size="sm"
onClick={() => setOpen(true)}
>
<Search className="text-menutabs-icon-active" size={16} />
<Search className="text-ds-icon-neutral-default-default" size={16} />
<span>{t('dashboard.search')}</span>
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>

View file

@ -38,7 +38,7 @@ export const ZoomControls = ({
variant="ghost"
onClick={onZoomOut}
title="Zoom Out"
className="h-7 w-7 text-ds-text-neutral-muted-default hover:bg-fill-fill-transparent-hover hover:text-ds-text-neutral-default-default"
className="h-7 w-7 text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-subtle-hover hover:text-ds-text-neutral-default-default"
>
<ZoomOut className="h-3.5 w-3.5" />
</Button>
@ -51,18 +51,18 @@ export const ZoomControls = ({
variant="ghost"
onClick={onZoomIn}
title="Zoom In"
className="h-7 w-7 text-ds-text-neutral-muted-default hover:bg-fill-fill-transparent-hover hover:text-ds-text-neutral-default-default"
className="h-7 w-7 text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-subtle-hover hover:text-ds-text-neutral-default-default"
>
<ZoomIn className="h-3.5 w-3.5" />
</Button>
<div className="mx-0.5 h-4 bg-border-secondary w-px" />
<div className="mx-0.5 h-4 bg-ds-border-neutral-default-default w-px" />
<Button
size="xs"
buttonContent="icon-only"
variant="ghost"
onClick={onZoomReset}
title="Reset Zoom"
className="h-7 w-7 text-ds-text-neutral-muted-default hover:bg-fill-fill-transparent-hover hover:text-ds-text-neutral-default-default"
className="h-7 w-7 text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-subtle-hover hover:text-ds-text-neutral-default-default"
>
<RotateCcw className="h-3.5 w-3.5" />
</Button>

View file

@ -957,7 +957,7 @@ export default function Folder({ data: _data }: { data?: Agent }) {
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="border-ds-border-neutral-default-default bg-dropdown-bg z-50"
className="border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default z-50"
>
<DropdownMenuItem
onClick={() => handleOpenInIDE('system')}
@ -1018,7 +1018,7 @@ export default function Folder({ data: _data }: { data?: Agent }) {
<DropdownMenuContent
side="bottom"
align="start"
className="border-ds-border-neutral-default-default bg-dropdown-bg z-50 min-w-[10rem]"
className="border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default z-50 min-w-[10rem]"
>
<DropdownMenuRadioGroup
value={fileTreeScope}

View file

@ -443,7 +443,7 @@ export default function HistorySidebar() {
<div className="gap-2 flex flex-shrink-0 items-center">
<TooltipSimple content={t('chat.token')}>
<Tag variant="info" size="sm">
<Tag variant="primary" tone="information" size="sm">
<Hash className="h-3.5 w-3.5" />
<span className="text-xs">
{(project.total_tokens || 0).toLocaleString()}
@ -452,7 +452,7 @@ export default function HistorySidebar() {
</TooltipSimple>
<TooltipSimple content="Tasks">
<Tag variant="default" size="sm">
<Tag variant="primary" tone="neutral" size="sm">
<Pin className="h-3.5 w-3.5" />
<span className="text-xs">
{project.task_count}
@ -565,7 +565,7 @@ export default function HistorySidebar() {
<div className="gap-2 flex flex-shrink-0 items-center">
<TooltipSimple content={t('chat.token')}>
<Tag variant="info" size="sm">
<Tag variant="primary" tone="information" size="sm">
<Hash className="h-3.5 w-3.5" />
<span className="text-xs">
{(project.total_tokens || 0).toLocaleString()}
@ -574,7 +574,7 @@ export default function HistorySidebar() {
</TooltipSimple>
<TooltipSimple content="Tasks">
<Tag variant="default" size="sm">
<Tag variant="primary" tone="neutral" size="sm">
<Pin className="h-3.5 w-3.5" />
<span className="text-xs">
{project.task_count}

View file

@ -220,8 +220,8 @@ export const CarouselStep: React.FC = () => {
onMouseEnter={() => handleIndicatorHover(index)}
className={`h-1 w-32 cursor-pointer rounded-full transition-all duration-300 ${
index === currentSlide
? 'bg-fill-fill-secondary'
: 'bg-fill-fill-tertiary hover:bg-fill-fill-secondary'
? 'bg-ds-bg-neutral-default-default'
: 'bg-ds-bg-neutral-subtle-default hover:bg-ds-bg-neutral-default-default'
}`}
></div>
))}

View file

@ -0,0 +1,101 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { act, cleanup, render, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/store/authStore';
import { ThemeProvider } from './ThemeProvider';
describe('ThemeProvider', () => {
let mediaQuery: {
matches: boolean;
media: string;
onchange: null;
addListener: ReturnType<typeof vi.fn>;
removeListener: ReturnType<typeof vi.fn>;
dispatchEvent: ReturnType<typeof vi.fn>;
addEventListener?: undefined;
removeEventListener?: undefined;
};
let changeListener: (() => void) | null;
beforeEach(() => {
changeListener = null;
mediaQuery = {
matches: true,
media: '(prefers-color-scheme: dark)',
onchange: null,
addListener: vi.fn((listener: () => void) => {
changeListener = listener;
}),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
addEventListener: undefined,
removeEventListener: undefined,
};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(() => mediaQuery),
});
useAuthStore.setState({
appearance: 'light',
appearanceMode: 'system',
lightColorThemeId: 'eigent',
darkColorThemeId: 'eigent',
customThemeCatalog: { light: {}, dark: {} },
themeContrast: 43,
});
});
afterEach(() => {
cleanup();
document.documentElement.removeAttribute('data-theme');
document.documentElement.removeAttribute('data-theme-mode');
document.documentElement.removeAttribute('data-color-theme');
document.documentElement.style.removeProperty('color-scheme');
document.documentElement.style.removeProperty('--ds-theme-contrast');
});
it('uses addListener fallback and follows system preference changes', async () => {
const { unmount } = render(
<ThemeProvider>
<div data-testid="child" />
</ThemeProvider>
);
await waitFor(() => {
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
});
expect(mediaQuery.addListener).toHaveBeenCalledTimes(1);
expect(changeListener).toBeTruthy();
mediaQuery.matches = false;
act(() => {
changeListener?.();
});
await waitFor(() => {
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
});
expect(useAuthStore.getState().appearance).toBe('light');
unmount();
expect(mediaQuery.removeListener).toHaveBeenCalledTimes(1);
});
});

View file

@ -13,8 +13,8 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import {
applyThemeContractV1,
createDefaultThemeContract,
applyThemeContractV2,
createDefaultThemeContractV2,
} from '@/lib/themeTokens';
import { DEFAULT_THEME_CATALOG } from '@/lib/themeTokens/catalog';
import type { Mode } from '@/lib/themeTokens/types';
@ -46,9 +46,17 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
};
update();
media.addEventListener('change', update);
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', update);
} else {
media.addListener(update);
}
return () => {
media.removeEventListener('change', update);
if (typeof media.removeEventListener === 'function') {
media.removeEventListener('change', update);
} else {
media.removeListener(update);
}
};
}, []);
@ -82,13 +90,12 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
root.setAttribute('data-theme', resolvedMode);
root.setAttribute('data-theme-mode', appearanceMode);
root.setAttribute('data-color-theme', colorThemeId);
root.style.setProperty('color-scheme', resolvedMode);
root.style.setProperty('--ds-theme-contrast', String(themeContrast));
// V2 semantic tokens are generated in parallel to legacy tokens.
// Existing components continue to use legacy variables until migration.
applyThemeContractV1(
createDefaultThemeContract(resolvedMode, {
colorThemeId,
applyThemeContractV2(
createDefaultThemeContractV2(resolvedMode, {
themeId: colorThemeId,
contrast: themeContrast,
}),
root,
@ -136,9 +143,9 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
(
window as Window & {
__eigentThemeV1?: typeof api;
__eigentThemeV2?: typeof api;
}
).__eigentThemeV1 = api;
).__eigentThemeV2 = api;
}, [mergedCatalog]);
return <>{children}</>;

View file

@ -29,12 +29,6 @@ export function mergeLayoutAliasStyles(
// Shared layout-level aliases for TopBar, HistorySidebar, and ProjectPageSidebar.
export const productLayoutTokenAliases = asCssVarMap({
'--surface-primary': 'var(--ds-bg-neutral-subtle-default)',
'--surface-secondary': 'var(--ds-bg-neutral-default-default)',
'--surface-tertiary': 'var(--ds-bg-neutral-strong-default)',
'--surface-information': 'var(--ds-bg-status-splitting-subtle-default)',
'--surface-hover-subtle': 'var(--ds-bg-neutral-default-hover)',
'--border-secondary': 'var(--ds-border-neutral-default-default)',
'--border-disabled': 'var(--ds-border-neutral-subtle-default)',
@ -51,7 +45,7 @@ export const productLayoutTokenAliases = asCssVarMap({
'--icon-information': 'var(--ds-icon-status-splitting-default-default)',
'--icon-success': 'var(--ds-icon-status-completed-default-default)',
'--icon-warning': 'var(--ds-icon-status-pending-default-default)',
'--icon-cuation': 'var(--ds-icon-status-error-default-default)',
'--icon-caution': 'var(--ds-icon-status-error-default-default)',
'--project-surface': 'var(--ds-bg-neutral-default-default)',
'--project-surface-hover': 'var(--ds-bg-neutral-default-hover)',

View file

@ -192,7 +192,7 @@ export function HeaderAction() {
type="button"
className={cn(
'no-drag h-8 px-3 rounded-lg text-ds-icon-neutral-muted-default ease-in-out flex shrink-0 items-center justify-center transition-colors duration-200',
'hover:bg-ds-bg-neutral-subtle-hover focus-visible:ring-ds-border-neutral-subtle-default hover:text-ds-icon-neutral-muted-hover focus-visible:ring-2 focus-visible:outline-none'
'hover:bg-ds-bg-neutral-subtle-hover focus-visible:ring-ds-ring-neutral-subtle-default hover:text-ds-icon-neutral-muted-hover focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={foldTooltip}
onClick={() => toggleProjectSidebarFolded()}

View file

@ -123,7 +123,7 @@ export function NavList({
onClick={() => setExpanded((v) => !v)}
className={cn(
'no-drag gap-1 rounded-xl py-1 px-2 text-body-xs font-bold text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-subtle-default inline-flex w-fit max-w-[calc(100%-5rem)] shrink items-center text-left outline-none',
'focus-visible:ring-ds-border-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
)}
aria-expanded={expanded}
aria-label={expanded ? collapseAria : expandAria}
@ -149,7 +149,7 @@ export function NavList({
className={cn(
'no-drag gap-1 rounded-xl py-1 px-2 text-body-xs font-semibold !text-ds-text-neutral-muted-default inline-flex w-fit shrink-0 items-center text-left outline-none',
showAllActive ? 'underline underline-offset-2' : 'hover:underline',
'focus-visible:ring-ds-border-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
)}
>
{showAllLabel}
@ -183,7 +183,7 @@ export function NavList({
onClick={() => onSessionClick?.(session.id)}
className={cn(
'no-drag min-h-0 min-w-0 gap-3 py-1 relative z-0 flex flex-1 items-center overflow-hidden text-left outline-none',
'focus-visible:ring-ds-border-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
)}
>
<MessageCircle
@ -226,7 +226,7 @@ export function NavList({
'md:group-focus-within/session-item:opacity-100',
'data-[state=open]:opacity-100',
],
'focus-visible:ring-ds-border-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={sessionMenuAria}
onClick={(e) => e.stopPropagation()}

View file

@ -32,7 +32,7 @@ export function workspaceTabButtonClass(active: boolean): string {
return cn(
'no-drag h-8 min-h-8 w-full min-w-0 shrink-0 rounded-xl cursor-pointer ease-in-out flex items-center justify-start gap-3 px-3 text-left outline-none overflow-hidden transition-colors duration-200',
'text-ds-text-neutral-muted-default',
'hover:bg-ds-bg-neutral-subtle-hover focus-visible:ring-ds-border-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none',
'hover:bg-ds-bg-neutral-subtle-hover focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none',
active && 'bg-ds-bg-neutral-subtle-default'
);
}
@ -41,7 +41,7 @@ export const WORKSPACE_TAB_LABEL_CLASS =
'min-w-0 flex-1 truncate text-ds-text-neutral-muted-default text-body-sm font-medium';
const SPLIT_MAIN_BUTTON_CLASS =
'no-drag min-h-8 min-w-0 gap-3 rounded-xl py-0 px-3 relative flex flex-1 items-center text-left outline-none text-ds-text-neutral-muted-default focus-visible:ring-ds-border-neutral-subtle-default hover:bg-transparent focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none';
'no-drag min-h-8 min-w-0 gap-3 rounded-xl py-0 px-3 relative flex flex-1 items-center text-left outline-none text-ds-text-neutral-muted-default focus-visible:ring-ds-ring-neutral-subtle-default hover:bg-transparent focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none';
const SPLIT_OUTER_EXTRA_CLASS =
'min-w-0 gap-0 !p-0 relative flex items-stretch overflow-visible';
@ -83,7 +83,7 @@ export function NavTabReconnectSuffix({
type="button"
className={cn(
'no-drag h-8 w-8 rounded-xl text-ds-icon-neutral-muted-default hover:bg-ds-bg-neutral-subtle-default flex shrink-0 items-center justify-center transition-colors outline-none',
'focus-visible:ring-ds-border-neutral-subtle-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={reconnectHint}
>

View file

@ -354,11 +354,11 @@ export default function TerminalComponent({
return (
<div
ref={terminalContainerRef}
className="rounded-2xl border-ds-border-neutral-strong-default relative flex h-full w-full flex-col overflow-hidden border border-solid"
className="rounded-2xl relative flex h-full w-full flex-col overflow-hidden"
style={{ fontFamily: '"Courier New", Courier, monospace' }}
>
{/* background blur effect */}
<div className="blur-bg inset-0 rounded-xl bg-black-100% pointer-events-none absolute"></div>
<div className="blur-bg inset-0 rounded-xl bg-terminal-viewport-surface pointer-events-none absolute"></div>
{/* terminal container */}
<div

View file

@ -64,9 +64,9 @@ export default function TerminalAgentWorkspace() {
<CodeXml size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-emerald-700',
bgColor: 'bg-bg-fill-coding-active',
shapeColor: 'bg-bg-fill-coding-default',
borderColor: 'border-bg-fill-coding-active',
bgColor: 'bg-ds-bg-terminal-default-default',
shapeColor: 'bg-ds-bg-terminal-subtle-default',
borderColor: 'border-ds-border-terminal-default-default',
bgColorLight: 'bg-emerald-200',
},
browser_agent: {
@ -75,9 +75,9 @@ export default function TerminalAgentWorkspace() {
<Globe size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-blue-700',
bgColor: 'bg-bg-fill-browser-active',
shapeColor: 'bg-bg-fill-browser-default',
borderColor: 'border-bg-fill-browser-active',
bgColor: 'bg-ds-bg-browser-default-default',
shapeColor: 'bg-ds-bg-browser-subtle-default',
borderColor: 'border-ds-border-browser-default-default',
bgColorLight: 'bg-blue-200',
},
document_agent: {
@ -86,9 +86,9 @@ export default function TerminalAgentWorkspace() {
<FileText size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-yellow-700',
bgColor: 'bg-bg-fill-writing-active',
shapeColor: 'bg-bg-fill-writing-default',
borderColor: 'border-bg-fill-writing-active',
bgColor: 'bg-ds-bg-document-default-default',
shapeColor: 'bg-ds-bg-document-subtle-default',
borderColor: 'border-ds-border-document-default-default',
bgColorLight: 'bg-yellow-200',
},
multi_modal_agent: {
@ -97,9 +97,9 @@ export default function TerminalAgentWorkspace() {
<Image size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-fuchsia-700',
bgColor: 'bg-bg-fill-multimodal-active',
shapeColor: 'bg-bg-fill-multimodal-default',
borderColor: 'border-bg-fill-multimodal-active',
bgColor: 'bg-ds-bg-neutral-default-default',
shapeColor: 'bg-ds-bg-neutral-subtle-default',
borderColor: 'border-ds-border-neutral-default-default',
bgColorLight: 'bg-fuchsia-200',
},
social_media_agent: {
@ -152,7 +152,7 @@ export default function TerminalAgentWorkspace() {
<div
className={`ease-in-out flex h-full w-full flex-1 items-center justify-center transition-all duration-300`}
>
<div className="blur-bg rounded-xl bg-ds-bg-neutral-default-default relative flex h-full w-full flex-col overflow-hidden">
<div className="backdrop-blur-sm rounded-xl bg-ds-bg-neutral-default-default relative flex h-full w-full flex-col overflow-hidden">
<div className="rounded-t-2xl px-2 pb-2 pt-3 flex flex-shrink-0 items-center justify-between">
<div className="gap-sm flex items-center justify-start">
<Button
@ -222,7 +222,7 @@ export default function TerminalAgentWorkspace() {
}
/>
{/* <div className=" flex justify-center items-center opacity-0 transition-all group-hover:opacity-100 rounded-b-lg absolute inset-0 w-full h-full bg-black/20 pointer-events-none">
<Button className="cursor-pointer px-md py-sm h-auto flex gap-sm rounded-full bg-bg-fill-primary">
<Button className="cursor-pointer px-md py-sm h-auto flex gap-sm rounded-full bg-ds-bg-brand-default-default">
<Hand size={24} className="text-ds-icon-neutral-inverse-default" />
<span className="text-base leading-9 font-medium text-ds-text-neutral-inverse-default">
Take Control
@ -259,7 +259,7 @@ export default function TerminalAgentWorkspace() {
onClick={() => handleTakeControl(task.id)}
className="flex justify-center items-center opacity-0 transition-all group-hover:opacity-100 rounded-lg absolute inset-0 w-full h-full bg-black/20 pointer-events-none"
>
<Button className="cursor-pointer px-md py-sm h-auto flex gap-sm rounded-full bg-bg-fill-primary">
<Button className="cursor-pointer px-md py-sm h-auto flex gap-sm rounded-full bg-ds-bg-brand-default-default">
<Hand
size={24}
className="text-ds-icon-neutral-inverse-default"

View file

@ -325,7 +325,7 @@ function HeaderWin() {
<button
id="active-task-title-btn"
type="button"
className="no-drag min-w-0 px-2 text-label-sm font-bold focus-visible:ring-ring/50 !text-ds-text-neutral-default-default hover:bg-ds-bg-neutral-default-hover active:bg-ds-bg-neutral-default-active flex min-h-[28px] max-w-[300px] flex-1 items-center text-left outline-none focus-visible:ring-[3px]"
className="no-drag min-w-0 px-2 text-label-sm font-bold focus-visible:ring-ds-ring-brand-default-focus/50 !text-ds-text-neutral-default-default hover:bg-ds-bg-neutral-default-hover active:bg-ds-bg-neutral-default-active flex min-h-[28px] max-w-[300px] flex-1 items-center text-left outline-none focus-visible:ring-[3px]"
onClick={toggleHistorySidebar}
aria-expanded={historySidebarOpen}
aria-haspopup="dialog"
@ -342,7 +342,7 @@ function HeaderWin() {
>
<button
type="button"
className="no-drag w-8 focus-visible:ring-ring/50 !text-ds-text-neutral-default-default hover:bg-ds-bg-neutral-default-hover active:bg-ds-bg-neutral-default-active box-border flex min-h-[28px] shrink-0 items-center justify-center outline-none focus-visible:ring-[3px]"
className="no-drag w-8 focus-visible:ring-ds-ring-brand-default-focus/50 !text-ds-text-neutral-default-default hover:bg-ds-bg-neutral-default-hover active:bg-ds-bg-neutral-default-active box-border flex min-h-[28px] shrink-0 items-center justify-center outline-none focus-visible:ring-[3px]"
onClick={createNewProject}
aria-label={t('layout.new-project')}
>

View file

@ -472,7 +472,7 @@ const MultiSelectField: React.FC<FieldProps> = ({
<Badge
key={val}
variant="secondary"
className="hover:bg-destructive/20 text-xs cursor-pointer"
className="text-xs hover:bg-ds-bg-status-error-subtle-default/50 cursor-pointer"
onClick={() => handleToggle(val)}
>
{opt?.label || val}
@ -602,7 +602,7 @@ const MultiTextInputField: React.FC<FieldProps> = ({
<Badge
key={val}
variant="secondary"
className="hover:bg-destructive/20 text-xs cursor-pointer"
className="text-xs hover:bg-ds-bg-status-error-subtle-default/50 cursor-pointer"
onClick={() => handleRemove(val)}
>
{val}

View file

@ -402,7 +402,7 @@ export default function Overview({
<Button
size="md"
onClick={handleConfirmDelete}
variant="cuation"
variant="caution"
disabled={isDeleting}
>
{isDeleting

View file

@ -165,7 +165,7 @@ export const MarkDown = ({
className="mb-4 min-w-0 !table w-full"
style={{
borderCollapse: 'collapse',
border: '1px solid #d1d5db',
border: '1px solid var(--ds-border-neutral-default-default)',
borderSpacing: 0,
}}
>
@ -186,7 +186,7 @@ export const MarkDown = ({
<th
className="text-ds-text-neutral-default-default font-semibold !table-cell text-left text-[10px]"
style={{
border: '1px solid #d1d5db',
border: '1px solid var(--ds-border-neutral-default-default)',
padding: '2px 5px',
borderCollapse: 'collapse',
}}
@ -198,7 +198,7 @@ export const MarkDown = ({
<td
className="text-ds-text-neutral-default-default !table-cell text-[10px]"
style={{
border: '1px solid #d1d5db',
border: '1px solid var(--ds-border-neutral-default-default)',
padding: '2px 5px',
borderCollapse: 'collapse',
}}

View file

@ -39,18 +39,18 @@ export const agentMap: Record<WorkflowAgentType, AgentDisplayInfo> = {
<CodeXml size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-ds-text-terminal-default-default',
bgColor: 'bg-bg-fill-coding-active',
shapeColor: 'bg-bg-fill-coding-default',
borderColor: 'border-bg-fill-coding-active',
bgColor: 'bg-ds-bg-terminal-default-default',
shapeColor: 'bg-ds-bg-terminal-subtle-default',
borderColor: 'border-ds-border-terminal-default-default',
bgColorLight: 'bg-emerald-200',
},
browser_agent: {
name: 'Browser Agent',
icon: <Globe size={16} className="text-ds-text-neutral-default-default" />,
textColor: 'text-blue-700',
bgColor: 'bg-bg-fill-browser-active',
shapeColor: 'bg-bg-fill-browser-default',
borderColor: 'border-bg-fill-browser-active',
bgColor: 'bg-ds-bg-browser-default-default',
shapeColor: 'bg-ds-bg-browser-subtle-default',
borderColor: 'border-ds-border-browser-default-default',
bgColorLight: 'bg-blue-200',
},
document_agent: {
@ -59,18 +59,18 @@ export const agentMap: Record<WorkflowAgentType, AgentDisplayInfo> = {
<FileText size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-yellow-700',
bgColor: 'bg-bg-fill-writing-active',
shapeColor: 'bg-bg-fill-writing-default',
borderColor: 'border-bg-fill-writing-active',
bgColor: 'bg-ds-bg-document-default-default',
shapeColor: 'bg-ds-bg-document-subtle-default',
borderColor: 'border-ds-border-document-default-default',
bgColorLight: 'bg-yellow-200',
},
multi_modal_agent: {
name: 'Multi Modal Agent',
icon: <Image size={16} className="text-ds-text-neutral-default-default" />,
textColor: 'text-fuchsia-700',
bgColor: 'bg-bg-fill-multimodal-active',
shapeColor: 'bg-bg-fill-multimodal-default',
borderColor: 'border-bg-fill-multimodal-active',
bgColor: 'bg-ds-bg-neutral-default-default',
shapeColor: 'bg-ds-bg-neutral-subtle-default',
borderColor: 'border-ds-border-neutral-default-default',
bgColorLight: 'bg-fuchsia-200',
},
social_media_agent: {

View file

@ -85,7 +85,7 @@ function WorkforceOverlayCanvas() {
{activeWorkSpace === 'documentWorkSpace' && (
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
<div className="blur-bg inset-0 rounded-xl bg-ds-bg-neutral-default-default pointer-events-none absolute"></div>
<div className="backdrop-blur-sm inset-0 rounded-xl bg-ds-bg-neutral-default-default pointer-events-none absolute"></div>
<div className="relative z-10 h-full w-full">
<Folder />
</div>
@ -97,7 +97,7 @@ function WorkforceOverlayCanvas() {
)?.type === 'document_agent' && (
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
<div className="blur-bg inset-0 rounded-xl bg-ds-bg-neutral-default-default pointer-events-none absolute"></div>
<div className="backdrop-blur-sm inset-0 rounded-xl bg-ds-bg-neutral-default-default pointer-events-none absolute"></div>
<div className="relative z-10 h-full w-full">
<Folder
data={activeTask.taskAssigning?.find(
@ -111,7 +111,7 @@ function WorkforceOverlayCanvas() {
{activeWorkSpace === 'inbox' && (
<div className="flex h-full w-full flex-1 items-center justify-center">
<div className="relative flex h-full w-full flex-col">
<div className="blur-bg inset-0 rounded-xl bg-ds-bg-neutral-default-default pointer-events-none absolute"></div>
<div className="backdrop-blur-sm inset-0 rounded-xl bg-ds-bg-neutral-default-default pointer-events-none absolute"></div>
<div className="relative z-10 h-full w-full">
<Folder />
</div>

View file

@ -255,9 +255,9 @@ export default function WorkforceMenu({
<CodeXml size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-ds-text-terminal-default-default',
bgColor: 'bg-bg-fill-coding-active',
shapeColor: 'bg-bg-fill-coding-default',
borderColor: 'border-bg-fill-coding-active',
bgColor: 'bg-ds-bg-terminal-default-default',
shapeColor: 'bg-ds-bg-terminal-subtle-default',
borderColor: 'border-ds-border-terminal-default-default',
bgColorLight: 'bg-emerald-200',
},
browser_agent: {
@ -266,9 +266,9 @@ export default function WorkforceMenu({
<Globe size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-blue-700',
bgColor: 'bg-bg-fill-browser-active',
shapeColor: 'bg-bg-fill-browser-default',
borderColor: 'border-bg-fill-browser-active',
bgColor: 'bg-ds-bg-browser-default-default',
shapeColor: 'bg-ds-bg-browser-subtle-default',
borderColor: 'border-ds-border-browser-default-default',
bgColorLight: 'bg-blue-200',
},
document_agent: {
@ -277,9 +277,9 @@ export default function WorkforceMenu({
<FileText size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-yellow-700',
bgColor: 'bg-bg-fill-writing-active',
shapeColor: 'bg-bg-fill-writing-default',
borderColor: 'border-bg-fill-writing-active',
bgColor: 'bg-ds-bg-document-default-default',
shapeColor: 'bg-ds-bg-document-subtle-default',
borderColor: 'border-ds-border-document-default-default',
bgColorLight: 'bg-yellow-200',
},
multi_modal_agent: {
@ -288,9 +288,9 @@ export default function WorkforceMenu({
<Image size={16} className="text-ds-text-neutral-default-default" />
),
textColor: 'text-fuchsia-700',
bgColor: 'bg-bg-fill-multimodal-active',
shapeColor: 'bg-bg-fill-multimodal-default',
borderColor: 'border-bg-fill-multimodal-active',
bgColor: 'bg-ds-bg-neutral-default-default',
shapeColor: 'bg-ds-bg-neutral-subtle-default',
borderColor: 'border-ds-border-neutral-default-default',
bgColorLight: 'bg-fuchsia-200',
},
social_media_agent: {

View file

@ -90,7 +90,7 @@ export function WorkforceAgentList({
'p-2 inline-flex items-center justify-center',
'text-ds-text-neutral-muted-default transition-all duration-200',
'hover:text-ds-text-neutral-default-default opacity-80 hover:opacity-100',
'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none'
'focus-visible:ring-ds-ring-brand-default-focus focus-visible:ring-2 focus-visible:outline-none'
)}
onClick={onAddWorker}
aria-label={t('triggers.add')}

View file

@ -40,13 +40,13 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
'py-4 text-sm font-medium flex flex-1 items-center justify-between text-left transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200" />
<ChevronDown className="h-4 w-4 text-ds-text-neutral-muted-default shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
@ -58,7 +58,7 @@ const AccordionContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down text-sm overflow-hidden"
{...props}
>
<div className={cn('p-0', className)}>{children}</div>

View file

@ -18,13 +18,14 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-ds-text-neutral-default-default [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-background text-foreground',
default:
'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default',
destructive:
'border-destructive/50 text-ds-text-status-error-strong-default dark:border-destructive [&>svg]:text-destructive',
'border-ds-border-status-error-default-default/50 text-ds-text-status-error-strong-default [&>svg]:text-ds-text-status-error-strong-default',
},
},
defaultVariants: {

View file

@ -12,18 +12,14 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Button } from '@/components/ui/button';
import {
Button,
type ButtonLegacyVariant,
type ButtonVariant,
} from '@/components/ui/button';
import { AnimatePresence, motion } from 'framer-motion';
type ButtonVariant =
| 'primary'
| 'secondary'
| 'outline'
| 'ghost'
| 'success'
| 'cuation'
| 'information'
| 'warning';
type ConfirmVariant = ButtonVariant | ButtonLegacyVariant;
interface ConfirmModalProps {
isOpen: boolean;
@ -33,7 +29,7 @@ interface ConfirmModalProps {
message?: string;
confirmText?: string;
cancelText?: string;
confirmVariant?: ButtonVariant;
confirmVariant?: ConfirmVariant;
}
export default function ConfirmModal({
@ -44,7 +40,7 @@ export default function ConfirmModal({
message = 'Confirm content?',
confirmText = 'Confirm',
cancelText = 'Cancel',
confirmVariant = 'cuation',
confirmVariant = 'caution',
}: ConfirmModalProps) {
return (
<AnimatePresence>

View file

@ -26,7 +26,7 @@ import {
} from './semanticProps';
const badgeBase = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2'
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ds-ring-brand-default-focus focus:ring-offset-2 focus:ring-offset-ds-bg-neutral-subtle-default'
);
type BadgeLegacyVariant = 'default' | 'secondary' | 'destructive' | 'outline';

View file

@ -45,7 +45,7 @@ type ButtonStyleVariant = UiVariant | 'inverse';
* @deprecated Map to `variant` + `tone` (+ optional `emphasis`) instead:
* - success variant="primary" tone="success"
* - warning variant="primary" tone="warning"
* - cuation variant="primary" tone="error"
* - caution variant="primary" tone="error"
* - information variant="primary" tone="information"
* - inverse variant="primary" emphasis="inverse"
*/
@ -53,7 +53,7 @@ export type ButtonLegacyVariant =
| 'inverse'
| 'success'
| 'warning'
| 'cuation'
| 'caution'
| 'information';
const LEGACY_VARIANT_TO_TONE: Record<
@ -62,7 +62,7 @@ const LEGACY_VARIANT_TO_TONE: Record<
> = {
success: { variant: 'primary', tone: 'success' },
warning: { variant: 'primary', tone: 'warning' },
cuation: { variant: 'primary', tone: 'error' },
caution: { variant: 'primary', tone: 'error' },
information: { variant: 'primary', tone: 'information' },
};
@ -100,7 +100,7 @@ function resolveVariantToneAndEmphasis(
if (
v === 'success' ||
v === 'warning' ||
v === 'cuation' ||
v === 'caution' ||
v === 'information'
) {
const mapped = LEGACY_VARIANT_TO_TONE[v];
@ -194,28 +194,28 @@ const TONE_PRIMARY: Record<ButtonToneForStyles, string> = {
`focus:bg-ds-bg-brand-default-hover focus:border-ds-bg-brand-default-hover ${FOCUS_RING}`,
].join(' '),
success: [
'bg-ds-bg-success-default-default border-ds-bg-success-default-default !text-ds-text-success-strong-default',
'bg-ds-bg-success-default-default border-ds-bg-success-default-default !text-ds-text-success-inverse-default',
'shadow-button-shadow',
'hover:bg-ds-bg-success-default-hover hover:border-ds-bg-success-default-hover',
'active:bg-ds-bg-success-default-active active:border-ds-bg-success-default-active',
`focus:bg-ds-bg-success-default-hover focus:border-ds-bg-success-default-hover ${FOCUS_RING}`,
].join(' '),
error: [
'bg-ds-bg-error-default-default border-ds-bg-error-default-default !text-ds-text-error-strong-default',
'bg-ds-bg-error-default-default border-ds-bg-error-default-default !text-ds-text-error-inverse-default',
'shadow-button-shadow',
'hover:bg-ds-bg-error-default-hover hover:border-ds-bg-error-default-hover',
'active:bg-ds-bg-error-default-active active:border-ds-bg-error-default-active',
`focus:bg-ds-bg-error-default-hover focus:border-ds-bg-error-default-hover ${FOCUS_RING}`,
].join(' '),
information: [
'bg-ds-bg-information-default-default border-ds-bg-information-default-default !text-ds-text-information-strong-default',
'bg-ds-bg-information-default-default border-ds-bg-information-default-default !text-ds-text-information-inverse-default',
'shadow-button-shadow',
'hover:bg-ds-bg-information-default-hover hover:border-ds-bg-information-default-hover',
'active:bg-ds-bg-information-default-active active:border-ds-bg-information-default-active',
`focus:bg-ds-bg-information-default-hover focus:border-ds-bg-information-default-hover ${FOCUS_RING}`,
].join(' '),
warning: [
'bg-ds-bg-warning-default-default border-ds-bg-warning-default-default !text-ds-text-warning-strong-default',
'bg-ds-bg-warning-default-default border-ds-bg-warning-default-default !text-ds-text-warning-inverse-default',
'shadow-button-shadow',
'hover:bg-ds-bg-warning-default-hover hover:border-ds-bg-warning-default-hover',
'active:bg-ds-bg-warning-default-active active:border-ds-bg-warning-default-active',
@ -233,7 +233,7 @@ const TONE_SECONDARY: Record<ButtonToneForStyles, string> = {
`focus:bg-ds-bg-neutral-subtle-hover focus:border-ds-bg-neutral-subtle-hover ${FOCUS_RING}`,
].join(' '),
success: [
'bg-ds-bg-status-completed-subtle-default border-ds-bg-status-completed-subtle-default !text-ds-text-status-completed-strong-default',
'bg-ds-bg-success-subtle-default border-ds-bg-success-subtle-default !text-ds-text-neutral-inverse-default',
'shadow-button-shadow',
'hover:bg-ds-bg-status-completed-subtle-hover hover:border-ds-bg-status-completed-subtle-hover',
'active:bg-ds-bg-status-completed-subtle-active active:border-ds-bg-status-completed-subtle-active',
@ -343,7 +343,7 @@ const INVERSE = [
].join(' ');
const buttonVariants = cva(
'inline-flex items-center whitespace-nowrap border border-solid transition-all duration-200 ease-in-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive shrink-0 cursor-pointer',
'inline-flex items-center whitespace-nowrap border border-solid transition-all duration-200 ease-in-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 outline-none focus-visible:border-ds-border-brand-default-focus focus-visible:ring-ds-ring-brand-default-focus/50 focus-visible:ring-[3px] aria-invalid:ring-ds-ring-error-default-default/20 aria-invalid:border-ds-border-status-error-default-default shrink-0 cursor-pointer',
{
variants: {
variant: {

View file

@ -34,7 +34,7 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
className={cn('space-y-1.5 p-6 flex flex-col', className)}
{...props}
/>
));
@ -46,7 +46,7 @@ const CardTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
className={cn('font-semibold tracking-tight leading-none', className)}
{...props}
/>
));
@ -58,7 +58,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-muted-foreground text-sm', className)}
className={cn('text-ds-text-neutral-muted-default text-sm', className)}
{...props}
/>
));
@ -78,7 +78,7 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
className={cn('p-6 pt-0 flex items-center', className)}
{...props}
/>
));

View file

@ -26,7 +26,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitives.Root
ref={ref}
className={cn(
'focus-visible:ring-ring peer h-4 w-4 rounded border-input-border-default bg-input-bg-default hover:border-input-border-hover data-[state=checked]:border-switch-on-fill-track-fill data-[state=checked]:bg-switch-on-fill-track-fill data-[state=checked]:text-switch-on-fill-thumb-fill shrink-0 border border-solid transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
'focus-visible:ring-ds-ring-brand-default-focus peer h-4 w-4 rounded border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default hover:border-ds-border-neutral-strong-default data-[state=checked]:border-ds-border-status-completed-default-default data-[state=checked]:bg-ds-bg-status-completed-default-default data-[state=checked]:text-ds-text-brand-inverse-default shrink-0 border border-solid transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
style={mergeAliasStyles(checkboxTokenAliases, style)}

View file

@ -27,7 +27,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
'text-popover-foreground rounded-md bg-white-100% flex h-full w-full flex-col overflow-hidden',
'text-ds-text-neutral-default-default rounded-md bg-ds-bg-neutral-inverse-default flex h-full w-full flex-col overflow-hidden',
className
)}
{...props}
@ -51,12 +51,15 @@ const CommandDialog = ({
return (
<Dialog {...props}>
<DialogContent
className={cn('bg-white-100% p-0 overflow-hidden', contentClassName)}
className={cn(
'bg-ds-bg-neutral-inverse-default p-0 overflow-hidden',
contentClassName
)}
overlayClassName={overlayClassName}
>
<Command
className={cn(
'[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5',
'[&_[cmdk-group-heading]]:text-ds-text-neutral-muted-default [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5',
commandClassName
)}
>
@ -72,14 +75,14 @@ const CommandInput = React.forwardRef<
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="px-3 flex items-center border-b"
className="border-ds-border-neutral-default-default px-3 flex items-center border-b"
{...({ 'cmdk-input-wrapper': '' } as any)}
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'placeholder:text-muted-foreground h-10 rounded-md py-3 text-sm flex w-full bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50',
'placeholder:text-ds-text-neutral-muted-default h-10 rounded-md py-3 text-sm flex w-full bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
@ -122,7 +125,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium overflow-hidden',
'text-ds-text-neutral-default-default [&_[cmdk-group-heading]]:text-ds-text-neutral-muted-default p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium overflow-hidden',
className
)}
{...props}
@ -137,7 +140,7 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('bg-border -mx-1 h-px', className)}
className={cn('-mx-1 bg-ds-border-neutral-default-default h-px', className)}
{...props}
/>
));
@ -150,7 +153,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
'data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm hover:border-green-600 [&_svg]:size-4 relative flex cursor-default items-center border border-solid border-transparent transition-all duration-300 outline-none select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
'data-[selected=true]:bg-ds-bg-neutral-default-hover data-[selected=true]:text-ds-text-neutral-default-default gap-2 rounded-sm px-2 py-1.5 text-sm hover:border-ds-border-status-completed-default-focus [&_svg]:size-4 relative flex cursor-default items-center border border-solid border-transparent transition-all duration-300 outline-none select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
className
)}
{...props}
@ -166,7 +169,7 @@ const CommandShortcut = ({
return (
<span
className={cn(
'text-muted-foreground text-xs tracking-widest ml-auto',
'text-ds-text-neutral-muted-default text-xs tracking-widest ml-auto',
className
)}
{...props}

View file

@ -242,7 +242,7 @@ interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {
| 'outline'
| 'ghost'
| 'success'
| 'cuation'
| 'caution'
| 'information'
| 'warning';
cancelButtonVariant?:
@ -251,7 +251,7 @@ interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {
| 'outline'
| 'ghost'
| 'success'
| 'cuation'
| 'caution'
| 'information'
| 'warning';
confirmButtonDisabled?: boolean;

View file

@ -59,7 +59,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'text-popover-foreground rounded-xl border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden border border-solid',
'text-ds-text-neutral-default-default rounded-xl border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden border border-solid',
className
)}
{...props}
@ -77,7 +77,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'text-popover-foreground rounded-xl border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-xs shadow-md z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto border border-solid',
'text-ds-text-neutral-default-default rounded-xl border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-xs shadow-md z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto border border-solid',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
className
)}
@ -97,7 +97,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground gap-2 rounded-xl px-2 py-1.5 text-sm hover:bg-menutabs-fill-hover [&>svg]:size-4 relative flex cursor-pointer items-center transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:shrink-0',
'focus:bg-ds-bg-neutral-default-hover focus:text-ds-text-neutral-default-default gap-2 rounded-xl px-2 py-1.5 text-sm hover:bg-ds-bg-neutral-default-hover [&>svg]:size-4 relative flex cursor-pointer items-center transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:shrink-0',
inset && 'pl-8',
className
)}
@ -113,7 +113,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground rounded-sm py-1.5 pl-8 pr-2 text-sm relative flex cursor-default items-center transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-ds-bg-neutral-default-hover focus:text-ds-text-neutral-default-default rounded-sm py-1.5 pl-8 pr-2 text-sm relative flex cursor-default items-center transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
@ -137,7 +137,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground rounded-sm py-1.5 pl-8 pr-2 text-sm relative flex cursor-default items-center transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-ds-bg-neutral-default-hover focus:text-ds-text-neutral-default-default rounded-sm py-1.5 pl-8 pr-2 text-sm relative flex cursor-default items-center transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@ -176,7 +176,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('bg-muted -mx-1 my-1 h-px', className)}
className={cn('bg-ds-bg-neutral-muted-default -mx-1 my-1 h-px', className)}
{...props}
/>
));

View file

@ -73,27 +73,29 @@ function resolveStateClasses(
if (disabled) {
return {
wrapper: 'opacity-50 cursor-not-allowed',
container: 'border-transparent bg-input-bg-default',
container: 'border-transparent bg-ds-bg-neutral-default-default',
note: 'text-ds-text-neutral-muted-default',
};
}
if (state === 'error') {
return {
wrapper: '',
container: 'border-input-border-cuation bg-input-bg-default',
container:
'border-ds-border-status-error-default-default bg-ds-bg-neutral-default-default',
note: 'text-ds-text-status-error-strong-default',
};
}
if (state === 'success') {
return {
wrapper: '',
container: 'border-input-border-success bg-input-bg-confirm',
container:
'border-ds-border-status-completed-default-default bg-ds-bg-status-completed-subtle-default',
note: 'text-ds-text-status-completed-strong-default',
};
}
return {
wrapper: '',
container: 'border-transparent bg-input-bg-default',
container: 'border-transparent bg-ds-bg-neutral-default-default',
note: 'text-ds-text-neutral-muted-default',
};
}
@ -358,9 +360,9 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
!disabled &&
state !== 'error' &&
state !== 'success' && [
'hover:bg-input-bg-hover hover:ring-input-border-hover hover:ring-1 hover:ring-offset-0',
'hover:bg-ds-bg-neutral-default-hover hover:ring-ds-ring-neutral-strong-default hover:ring-1 hover:ring-offset-0',
isOpen &&
'bg-input-bg-input ring-input-border-focus ring-1 ring-offset-0',
'bg-ds-bg-neutral-strong-default ring-ds-ring-brand-default-focus ring-1 ring-offset-0',
]
)}
>
@ -393,7 +395,7 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
{isOpen && (
<div
ref={dropdownRef}
className="left-0 right-0 mt-1 bg-input-bg-default rounded-lg shadow-md absolute top-full z-50 overflow-hidden border border-solid border-transparent"
className="left-0 right-0 mt-1 bg-ds-bg-neutral-default-default rounded-lg shadow-md absolute top-full z-50 overflow-hidden border border-solid border-transparent"
>
<div
className="p-1 overflow-x-hidden overflow-y-auto overscroll-contain"
@ -405,9 +407,9 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
key={option.value}
onClick={() => handleOptionClick(option)}
className={cn(
'rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center transition-colors outline-none select-none',
'rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-ds-bg-neutral-default-hover relative flex w-full cursor-pointer items-center transition-colors outline-none select-none',
selectedOption?.value === option.value &&
'bg-menutabs-fill-hover'
'bg-ds-bg-neutral-default-hover'
)}
>
{option.label}

View file

@ -53,7 +53,8 @@ function resolveStateClasses(state: InputState | undefined) {
if (state === 'disabled') {
return {
container: 'opacity-50 cursor-not-allowed',
field: 'border-input-border-default bg-input-bg-default',
field:
'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default',
input: 'text-ds-text-neutral-default-default',
placeholder: 'placeholder-input-label-default',
};
@ -61,7 +62,8 @@ function resolveStateClasses(state: InputState | undefined) {
if (state === 'hover') {
return {
container: '',
field: 'border-input-border-hover bg-input-bg-default',
field:
'border-ds-border-neutral-strong-default bg-ds-bg-neutral-default-default',
input: 'text-ds-text-neutral-default-default',
placeholder: 'placeholder-input-label-default',
};
@ -69,7 +71,8 @@ function resolveStateClasses(state: InputState | undefined) {
if (state === 'input') {
return {
container: '',
field: 'border-input-border-focus bg-input-bg-input',
field:
'border-ds-border-brand-default-focus bg-ds-bg-neutral-strong-default',
input: 'text-ds-text-neutral-default-default',
placeholder: 'placeholder-input-label-default',
};
@ -77,7 +80,8 @@ function resolveStateClasses(state: InputState | undefined) {
if (state === 'error') {
return {
container: '',
field: 'border-input-border-cuation bg-input-bg-default',
field:
'border-ds-border-status-error-default-default bg-ds-bg-neutral-default-default',
input: 'text-ds-text-neutral-default-default',
placeholder: 'placeholder-input-label-default',
};
@ -85,14 +89,16 @@ function resolveStateClasses(state: InputState | undefined) {
if (state === 'success') {
return {
container: '',
field: 'border-input-border-success bg-input-bg-confirm',
field:
'border-ds-border-status-completed-default-default bg-ds-bg-status-completed-subtle-default',
input: 'text-ds-text-neutral-default-default',
placeholder: 'placeholder-input-label-default',
};
}
return {
container: '',
field: 'border-input-border-default bg-input-bg-default',
field:
'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default',
input: 'text-ds-text-neutral-default-default',
placeholder: 'placeholder-input-label-default/10',
};
@ -159,7 +165,7 @@ const Input = React.forwardRef<HTMLInputElement, BaseInputProps>(
// Only apply hover/focus visuals when not in error state
state !== 'error' &&
state !== 'success' &&
'focus-within:bg-input-bg-input focus-within:ring-input-border-focus hover:bg-input-bg-hover hover:ring-input-border-hover focus-within:ring-1 focus-within:ring-offset-0 hover:ring-1 hover:ring-offset-0',
'focus-within:bg-ds-bg-neutral-strong-default focus-within:ring-ds-ring-brand-default-focus hover:bg-ds-bg-neutral-default-hover hover:ring-ds-ring-neutral-strong-default focus-within:ring-1 focus-within:ring-offset-0 hover:ring-1 hover:ring-offset-0',
stateCls.field,
sizeClasses[size]
)}

View file

@ -19,15 +19,15 @@ import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
const menuButtonVariants = cva(
'relative inline-flex items-center justify-center select-none transition-colors duration-200 ease-in-out outline-none disabled:opacity-30 disabled:pointer-events-none bg-menubutton-fill-default hover:bg-menubutton-fill-hover hover:text-ds-text-neutral-default-default focus:text-ds-text-neutral-default-default data-[state=on]:bg-menubutton-fill-active data-[state=on]:text-ds-text-neutral-default-default text-ds-text-neutral-muted-default disabled:text-ds-text-neutral-muted-disabled cursor-pointer data-[state=on]:shadow-button-shadow rounded-lg',
'relative inline-flex items-center justify-center select-none transition-colors duration-200 ease-in-out outline-none disabled:opacity-30 disabled:pointer-events-none bg-ds-bg-neutral-subtle-default hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default focus:text-ds-text-neutral-default-default data-[state=on]:bg-ds-bg-neutral-default-default data-[state=on]:text-ds-text-neutral-default-default text-ds-text-neutral-muted-default disabled:text-ds-text-neutral-muted-disabled cursor-pointer data-[state=on]:shadow-button-shadow rounded-lg',
{
variants: {
variant: {
default:
'border border-solid text-ds-text-neutral-default-default border-menubutton-border-default hover:border-menubutton-border-hover focus:bg-menubutton-fill-active focus:border-menubutton-border-active data-[state=on]:border-menubutton-border-active data-[state=on]:shadow-button-shadow',
'border border-solid text-ds-text-neutral-default-default border-ds-border-neutral-default-default hover:border-ds-border-neutral-strong-default focus:bg-ds-bg-neutral-default-default focus:border-ds-border-brand-default-focus data-[state=on]:border-ds-border-brand-default-focus data-[state=on]:shadow-button-shadow',
clear:
'border border-solid text-ds-text-neutral-default-default border-menubutton-border-default hover:border-menubutton-border-hover focus:bg-menubutton-fill-active focus:border-menubutton-border-default data-[state=on]:shadow-button-shadow',
info: 'text-ds-text-neutral-default-default !font-medium hover:bg-menubutton-fill-active focus:bg-menubutton-fill-active data-[state=on]:text-ds-text-neutral-default-default data-[state=on]:!font-bold',
'border border-solid text-ds-text-neutral-default-default border-ds-border-neutral-default-default hover:border-ds-border-neutral-strong-default focus:bg-ds-bg-neutral-default-default focus:border-ds-border-neutral-default-default data-[state=on]:shadow-button-shadow',
info: 'text-ds-text-neutral-default-default !font-medium hover:bg-ds-bg-neutral-default-default focus:bg-ds-bg-neutral-default-default data-[state=on]:text-ds-text-neutral-default-default data-[state=on]:!font-bold',
},
size: {
xs: 'px-2 py-1 text-label-sm font-bold [&_svg]:size-[16px] rounded-lg',

View file

@ -41,14 +41,16 @@ function resolveStateClasses(
if (state === 'error') {
return {
wrapper: '',
trigger: 'border-input-border-cuation bg-input-bg-default',
trigger:
'border-ds-border-status-error-default-default bg-ds-bg-neutral-default-default',
note: 'text-ds-text-status-error-strong-default',
};
}
if (state === 'success') {
return {
wrapper: '',
trigger: 'border-input-border-success bg-input-bg-confirm',
trigger:
'border-ds-border-status-completed-default-default bg-ds-bg-status-completed-subtle-default',
note: 'text-ds-text-status-completed-strong-default',
};
}
@ -152,17 +154,17 @@ const PopoverTrigger = React.forwardRef<
sizeClasses[size],
'whitespace-nowrap [&>span]:line-clamp-1',
// Default state (when no error/success)
!state && 'bg-input-bg-default',
!state && 'bg-ds-bg-neutral-default-default',
// Interactive states (only when no error/success state)
state !== 'error' &&
state !== 'success' && [
'hover:bg-input-bg-hover hover:ring-input-border-hover hover:ring-1 hover:ring-offset-0',
'focus-visible:ring-input-border-focus data-[state=open]:bg-input-bg-input data-[state=open]:ring-input-border-focus focus-visible:ring-1 focus-visible:ring-offset-0 data-[state=open]:ring-1 data-[state=open]:ring-offset-0',
'hover:bg-ds-bg-neutral-default-hover hover:ring-ds-ring-neutral-strong-default hover:ring-1 hover:ring-offset-0',
'focus-visible:ring-ds-ring-brand-default-focus data-[state=open]:bg-ds-bg-neutral-strong-default data-[state=open]:ring-ds-ring-brand-default-focus focus-visible:ring-1 focus-visible:ring-offset-0 data-[state=open]:ring-1 data-[state=open]:ring-offset-0',
],
// Validation states (override defaults)
stateCls.trigger,
// Placeholder styling
'data-[placeholder]:text-input-label-default/50',
'data-[placeholder]:text-ds-text-neutral-muted-default/50',
className
)}
{...props}
@ -272,7 +274,7 @@ const PopoverContent = React.forwardRef<
onOpenAutoFocus={handleOpenAutoFocus}
onInteractOutside={handleInteractOutside}
className={cn(
'text-popover-foreground rounded-lg bg-input-bg-default shadow-md relative z-50 min-w-[8rem] overflow-hidden border border-solid border-transparent',
'text-ds-text-neutral-default-default rounded-lg bg-ds-bg-neutral-default-default shadow-md relative z-50 min-w-[8rem] overflow-hidden border border-solid border-transparent',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]',
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
'w-[var(--radix-popover-trigger-width)]',
@ -296,9 +298,9 @@ const PopoverItem = React.forwardRef<HTMLDivElement, PopoverItemProps>(
<div
ref={ref}
className={cn(
'rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center outline-none select-none',
'rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-ds-bg-neutral-default-hover relative flex w-full cursor-pointer items-center outline-none select-none',
disabled && 'pointer-events-none opacity-50',
selected && 'bg-menutabs-fill-hover',
selected && 'bg-ds-bg-neutral-default-hover',
className
)}
{...props}

View file

@ -62,13 +62,13 @@ function ResizableHandle({
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
'bg-border focus-visible:ring-ring after:inset-y-0 after:w-1 data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:translate-x-0 relative flex w-px items-center justify-center after:absolute after:left-1/2 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
'bg-ds-border-neutral-default-default after:inset-y-0 after:w-1 focus-visible:ring-ds-ring-brand-default-focus focus-visible:ring-offset-ds-bg-neutral-subtle-default data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:translate-x-0 relative flex w-px items-center justify-center after:absolute after:left-1/2 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className
)}
{...props}
>
{withHandle && (
<div className="bg-border rounded-xs h-4 w-3 z-10 flex items-center justify-center border">
<div className="h-4 w-3 rounded-xs border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default z-10 flex items-center justify-center border">
<GripVerticalIcon className="size-2.5" />
</div>
)}

View file

@ -51,14 +51,16 @@ function resolveStateClasses(
if (state === 'error') {
return {
wrapper: '',
trigger: 'border-input-border-cuation bg-input-bg-default',
trigger:
'border-ds-border-status-error-default-default bg-ds-bg-neutral-default-default',
note: 'text-ds-text-status-error-strong-default',
};
}
if (state === 'success') {
return {
wrapper: '',
trigger: 'border-input-border-success bg-input-bg-confirm',
trigger:
'border-ds-border-status-completed-default-default bg-ds-bg-status-completed-subtle-default',
note: 'text-ds-text-status-completed-strong-default',
};
}
@ -127,17 +129,17 @@ const SelectTrigger = React.forwardRef<
sizeClasses[size],
'whitespace-nowrap [&>span]:line-clamp-1',
// Default state (when no error/success)
!state && 'bg-input-bg-default',
!state && 'bg-ds-bg-neutral-default-default',
// Interactive states (only when no error/success state)
state !== 'error' &&
state !== 'success' && [
'hover:bg-input-bg-hover hover:ring-input-border-hover hover:ring-1 hover:ring-offset-0',
'focus-visible:ring-input-border-focus data-[state=open]:bg-input-bg-input data-[state=open]:ring-input-border-focus focus-visible:ring-1 focus-visible:ring-offset-0 data-[state=open]:ring-1 data-[state=open]:ring-offset-0',
'hover:bg-ds-bg-neutral-default-hover hover:ring-ds-ring-neutral-strong-default hover:ring-1 hover:ring-offset-0',
'focus-visible:ring-ds-ring-brand-default-focus data-[state=open]:bg-ds-bg-neutral-strong-default data-[state=open]:ring-ds-ring-brand-default-focus focus-visible:ring-1 focus-visible:ring-offset-0 data-[state=open]:ring-1 data-[state=open]:ring-offset-0',
],
// Validation states (override defaults)
stateCls.trigger,
// Placeholder styling
'data-[placeholder]:text-input-label-default/50',
'data-[placeholder]:text-ds-text-neutral-muted-default/50',
className
)}
style={mergeAliasStyles(formControlTokenAliases, style)}
@ -200,7 +202,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'text-popover-foreground rounded-xl bg-input-bg-default shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] origin-[--radix-select-content-transform-origin] overflow-x-hidden overflow-y-auto border border-solid border-transparent',
'text-ds-text-neutral-default-default rounded-xl bg-ds-bg-neutral-default-default shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] origin-[--radix-select-content-transform-origin] overflow-x-hidden overflow-y-auto border border-solid border-transparent',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
@ -244,7 +246,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-ds-bg-neutral-default-hover focus:text-ds-text-neutral-default-default rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-ds-bg-neutral-default-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@ -265,7 +267,7 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('bg-muted -mx-1 my-1 h-px', className)}
className={cn('bg-ds-bg-neutral-muted-default -mx-1 my-1 h-px', className)}
{...props}
/>
));
@ -301,7 +303,7 @@ const SelectItemWithButton = React.forwardRef<
value={value}
disabled={!enabled}
className={cn(
'focus:bg-accent focus:text-accent-foreground group rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-ds-bg-neutral-default-hover focus:text-ds-text-neutral-default-default group rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-ds-bg-neutral-default-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}

View file

@ -30,7 +30,7 @@ const Separator = React.forwardRef<
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0',
'bg-ds-border-neutral-default-default shrink-0',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}

View file

@ -35,7 +35,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'bg-black/80 fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'inset-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed z-50 bg-[color:var(--ds-bg-neutral-inverse-default)]/80',
className
)}
{...props}
@ -45,7 +45,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
'fixed z-50 gap-4 bg-ds-bg-neutral-subtle-default p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
{
variants: {
side: {
@ -79,7 +79,7 @@ const SheetContent = React.forwardRef<
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<SheetPrimitive.Close className="right-4 top-4 rounded-sm ring-offset-ds-bg-neutral-subtle-default focus:ring-ds-ring-brand-default-focus data-[state=open]:bg-ds-bg-neutral-strong-default absolute opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
@ -95,7 +95,7 @@ const SheetHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
'space-y-2 sm:text-left flex flex-col text-center',
className
)}
{...props}
@ -109,7 +109,7 @@ const SheetFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
'sm:flex-row sm:justify-end sm:space-x-2 flex flex-col-reverse',
className
)}
{...props}
@ -123,7 +123,10 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-foreground text-lg font-semibold', className)}
className={cn(
'text-lg font-semibold text-ds-text-neutral-default-default',
className
)}
{...props}
/>
));
@ -135,7 +138,7 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-muted-foreground text-sm', className)}
className={cn('text-sm text-ds-text-neutral-muted-default', className)}
{...props}
/>
));

View file

@ -159,7 +159,7 @@ const SidebarProvider = React.forwardRef<
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full',
'group/sidebar-wrapper has-[[data-variant=inset]]:bg-ds-bg-neutral-strong-default flex min-h-svh w-full',
className
)}
ref={ref}
@ -199,7 +199,7 @@ const Sidebar = React.forwardRef<
return (
<div
className={cn(
'bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col',
'bg-ds-bg-neutral-strong-default text-ds-text-neutral-default-default flex h-full w-[--sidebar-width] flex-col',
className
)}
ref={ref}
@ -216,7 +216,7 @@ const Sidebar = React.forwardRef<
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground p-0 w-[--sidebar-width] [&>button]:hidden"
className="bg-ds-bg-neutral-strong-default text-ds-text-neutral-default-default p-0 w-[--sidebar-width] [&>button]:hidden"
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
@ -237,7 +237,7 @@ const Sidebar = React.forwardRef<
return (
<div
ref={ref}
className="text-sidebar-foreground group peer md:block hidden"
className="text-ds-text-neutral-default-default group peer md:block hidden"
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
@ -270,7 +270,7 @@ const Sidebar = React.forwardRef<
>
<div
data-sidebar="sidebar"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow flex h-full w-full flex-col group-data-[variant=floating]:border"
className="bg-ds-bg-neutral-strong-default group-data-[variant=floating]:border-ds-border-neutral-default-default group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow flex h-full w-full flex-col group-data-[variant=floating]:border"
>
{children}
</div>
@ -323,10 +323,10 @@ const SidebarRail = React.forwardRef<
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'hover:after:bg-sidebar-border inset-y-0 w-4 after:inset-y-0 group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex absolute z-20 hidden -translate-x-1/2 transition-all ease-linear after:absolute after:left-1/2 after:w-[2px]',
'hover:after:bg-ds-border-neutral-default-default inset-y-0 w-4 after:inset-y-0 group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex absolute z-20 hidden -translate-x-1/2 transition-all ease-linear after:absolute after:left-1/2 after:w-[2px]',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
'group-data-[collapsible=offcanvas]:hover:bg-ds-bg-neutral-strong-default group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className
@ -345,7 +345,7 @@ const SidebarInset = React.forwardRef<
<main
ref={ref}
className={cn(
'bg-background relative flex w-full flex-1 flex-col',
'bg-ds-bg-neutral-subtle-default relative flex w-full flex-1 flex-col',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
className
)}
@ -364,7 +364,7 @@ const SidebarInput = React.forwardRef<
ref={ref}
data-sidebar="input"
className={cn(
'bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2',
'bg-ds-bg-neutral-subtle-default focus-visible:ring-ds-ring-brand-default-focus h-8 w-full shadow-none focus-visible:ring-2',
className
)}
{...props}
@ -411,7 +411,10 @@ const SidebarSeparator = React.forwardRef<
<Separator
ref={ref}
data-sidebar="separator"
className={cn('bg-sidebar-border mx-2 w-auto', className)}
className={cn(
'bg-ds-border-neutral-default-default mx-2 w-auto',
className
)}
{...props}
/>
);
@ -462,7 +465,7 @@ const SidebarGroupLabel = React.forwardRef<
ref={ref}
data-sidebar="group-label"
className={cn(
'text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium [&>svg]:size-4 flex shrink-0 items-center transition-[margin,opacity] duration-200 ease-linear outline-none focus-visible:ring-2 [&>svg]:shrink-0',
'text-ds-text-neutral-muted-default ring-ds-ring-brand-default-focus h-8 rounded-md px-2 text-xs font-medium [&>svg]:size-4 flex shrink-0 items-center transition-[margin,opacity] duration-200 ease-linear outline-none focus-visible:ring-2 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className
)}
@ -483,7 +486,7 @@ const SidebarGroupAction = React.forwardRef<
ref={ref}
data-sidebar="group-action"
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground right-3 top-3.5 w-5 rounded-md p-0 [&>svg]:size-4 absolute flex aspect-square items-center justify-center transition-transform outline-none focus-visible:ring-2 [&>svg]:shrink-0',
'text-ds-text-neutral-default-default ring-ds-ring-brand-default-focus hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default right-3 top-3.5 w-5 rounded-md p-0 [&>svg]:size-4 absolute flex aspect-square items-center justify-center transition-transform outline-none focus-visible:ring-2 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:-inset-2 after:md:hidden after:absolute',
'group-data-[collapsible=icon]:hidden',
@ -535,13 +538,14 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-ds-ring-brand-default-focus transition-[width,height,padding] hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default focus-visible:ring-2 active:bg-ds-bg-neutral-default-hover active:text-ds-text-neutral-default-default disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-ds-bg-neutral-default-hover data-[active=true]:font-medium data-[active=true]:text-ds-text-neutral-default-default data-[state=open]:hover:bg-ds-bg-neutral-default-hover data-[state=open]:hover:text-ds-text-neutral-default-default group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
default:
'hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
'bg-ds-bg-neutral-subtle-default shadow-[0_0_0_1px_var(--ds-border-neutral-default-default)] hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default hover:shadow-[0_0_0_1px_var(--ds-bg-neutral-default-hover)]',
},
size: {
default: 'h-8 text-sm',
@ -629,7 +633,7 @@ const SidebarMenuAction = React.forwardRef<
ref={ref}
data-sidebar="menu-action"
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground right-1 top-1.5 w-5 rounded-md p-0 [&>svg]:size-4 absolute flex aspect-square items-center justify-center transition-transform outline-none focus-visible:ring-2 [&>svg]:shrink-0',
'text-ds-text-neutral-default-default ring-ds-ring-brand-default-focus hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default peer-hover/menu-button:text-ds-text-neutral-default-default right-1 top-1.5 w-5 rounded-md p-0 [&>svg]:size-4 absolute flex aspect-square items-center justify-center transition-transform outline-none focus-visible:ring-2 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:-inset-2 after:md:hidden after:absolute',
'peer-data-[size=sm]/menu-button:top-1',
@ -637,7 +641,7 @@ const SidebarMenuAction = React.forwardRef<
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0 group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100',
'peer-data-[active=true]/menu-button:text-ds-text-neutral-default-default md:opacity-0 group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100',
className
)}
{...props}
@ -654,8 +658,8 @@ const SidebarMenuBadge = React.forwardRef<
ref={ref}
data-sidebar="menu-badge"
className={cn(
'text-sidebar-foreground right-1 h-5 min-w-5 rounded-md px-1 text-xs font-medium pointer-events-none absolute flex items-center justify-center tabular-nums select-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'text-ds-text-neutral-default-default right-1 h-5 min-w-5 rounded-md px-1 text-xs font-medium pointer-events-none absolute flex items-center justify-center tabular-nums select-none',
'peer-hover/menu-button:text-ds-text-neutral-default-default peer-data-[active=true]/menu-button:text-ds-text-neutral-default-default',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
@ -713,7 +717,7 @@ const SidebarMenuSub = React.forwardRef<
ref={ref}
data-sidebar="menu-sub"
className={cn(
'border-sidebar-border mx-3.5 min-w-0 gap-1 px-2.5 py-0.5 flex translate-x-px flex-col border-l',
'border-ds-border-neutral-default-default mx-3.5 min-w-0 gap-1 px-2.5 py-0.5 flex translate-x-px flex-col border-l',
'group-data-[collapsible=icon]:hidden',
className
)}
@ -745,8 +749,8 @@ const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground h-7 min-w-0 gap-2 rounded-md px-2 [&>svg]:size-4 flex -translate-x-px items-center overflow-hidden outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
'text-ds-text-neutral-default-default ring-ds-ring-brand-default-focus hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-default-default active:bg-ds-bg-neutral-default-hover active:text-ds-text-neutral-default-default [&>svg]:text-ds-text-neutral-default-default h-7 min-w-0 gap-2 rounded-md px-2 [&>svg]:size-4 flex -translate-x-px items-center overflow-hidden outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
'data-[active=true]:bg-ds-bg-neutral-default-hover data-[active=true]:text-ds-text-neutral-default-default',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',

View file

@ -27,12 +27,12 @@ const Toaster = ({ ...props }: ToasterProps) => {
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
'group toast group-[.toaster]:bg-ds-bg-neutral-subtle-default group-[.toaster]:text-ds-text-neutral-default-default group-[.toaster]:border-ds-border-neutral-default-default group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-ds-text-neutral-muted-default',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
'group-[.toast]:bg-ds-bg-brand-default-default group-[.toast]:text-ds-text-brand-inverse-default',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
'group-[.toast]:bg-ds-bg-neutral-muted-default group-[.toast]:text-ds-text-neutral-muted-default',
},
}}
{...props}

View file

@ -43,7 +43,7 @@ const Switch = React.forwardRef<
>(({ className, size = 'default', style, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'focus-visible:ring-ring focus-visible:ring-offset-background peer data-[state=checked]:bg-switch-on-fill-track-fill data-[state=unchecked]:bg-switch-off-fill-track-fill inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
'peer focus-visible:ring-ds-ring-brand-default-focus focus-visible:ring-offset-ds-bg-neutral-subtle-default data-[state=checked]:bg-ds-bg-status-completed-default-default data-[state=unchecked]:bg-ds-bg-neutral-default-default inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
sizeClasses[size].root,
className
)}
@ -53,7 +53,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'bg-switch-on-fill-thumb-fill data-[state=unchecked]:translate-x-0 pointer-events-none block rounded-full shadow-none ring-0 transition-transform',
'bg-ds-text-brand-inverse-default data-[state=unchecked]:translate-x-0 pointer-events-none block rounded-full shadow-none ring-0 transition-transform',
sizeClasses[size].thumb
)}
/>

View file

@ -1,6 +1,20 @@
import * as React from "react"
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { cn } from "@/lib/utils"
import * as React from 'react';
import { cn } from '@/lib/utils';
const Table = React.forwardRef<
HTMLTableElement,
@ -9,20 +23,20 @@ const Table = React.forwardRef<
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn('text-sm w-full caption-bottom', className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
@ -30,11 +44,11 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@ -43,13 +57,13 @@ const TableFooter = React.forwardRef<
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
'bg-ds-bg-neutral-muted-default/50 font-medium border-t [&>tr]:last:border-b-0',
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
@ -58,13 +72,13 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
'hover:bg-ds-bg-neutral-muted-default/50 data-[state=selected]:bg-ds-bg-neutral-muted-default border-b transition-colors',
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
@ -73,13 +87,13 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
'h-10 px-2 font-medium text-ds-text-neutral-muted-default [&:has([role=checkbox])]:pr-0 text-left align-middle [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
@ -88,13 +102,13 @@ const TableCell = React.forwardRef<
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
'p-2 [&:has([role=checkbox])]:pr-0 align-middle [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
@ -102,19 +116,19 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
className={cn('mt-4 text-sm text-ds-text-neutral-muted-default', className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
TableCell,
TableCaption,
}
};

View file

@ -111,7 +111,7 @@ const TabsList = React.forwardRef<
className={cn(
variant === 'outline'
? 'gap-0 bg-ds-bg-neutral-muted-disabled p-0 relative inline-flex items-center justify-center'
: 'rounded-xl border-menutabs-border-default bg-ds-bg-neutral-strong-default p-0.5 inline-flex items-center justify-center border border-solid',
: 'rounded-xl border-ds-border-neutral-default-default bg-ds-bg-neutral-strong-default p-0.5 inline-flex items-center justify-center border border-solid',
'data-[orientation=vertical]:flex data-[orientation=vertical]:h-full data-[orientation=vertical]:w-full data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[orientation=vertical]:justify-start',
className
)}
@ -158,7 +158,7 @@ const TabsTrigger = React.forwardRef<
className={cn(
variant === 'outline'
? 'gap-2 px-4 py-3 !text-body-sm !font-semibold text-ds-text-neutral-default-default data-[state=active]:bg-ds-bg-neutral-muted-disabled data-[state=active]:!font-bold [&_svg]:text-ds-icon-neutral-default-default relative flex cursor-pointer flex-row items-center justify-center bg-transparent transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50'
: 'ring-offset-background focus-visible:ring-ring gap-1 rounded-xl bg-menutabs-fill-default px-2 py-1 text-body-sm font-semibold data-[state=active]:bg-menutabs-fill-active data-[state=active]:text-menutabs-text-active data-[state=active]:shadow-sm inline-flex items-center justify-center whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
: 'ring-offset-ds-bg-neutral-subtle-default focus-visible:ring-ds-ring-brand-default-focus gap-1 rounded-xl bg-ds-bg-neutral-strong-default px-2 py-1 text-body-sm font-semibold data-[state=active]:bg-ds-bg-neutral-default-default data-[state=active]:text-ds-text-neutral-default-default data-[state=active]:shadow-sm inline-flex items-center justify-center whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
className
)}
data-variant={variant}
@ -177,7 +177,7 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
'ring-offset-ds-bg-neutral-subtle-default focus-visible:ring-ds-ring-brand-default-focus mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
className
)}
{...props}

View file

@ -18,7 +18,7 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
import {
normalizeUiTone,
DEFAULT_EMPHASIS_BY_VARIANT,
type UiEmphasis,
type UiTone,
type UiToneInput,
@ -26,120 +26,316 @@ import {
} from './semanticProps';
import { mergeAliasStyles, tagTokenAliases } from './tokenAliases';
/** User-friendly tone aliases map to {@link UiTone}. */
export type TagToneInput = UiToneInput | 'info' | 'caution';
export type TagVariant = UiVariant;
export type TagTone = UiTone;
export type TagEmphasis = UiEmphasis;
type TagStyleTone = 'default' | 'success' | 'error' | 'information' | 'warning';
type TagEmphasisMatrix = 'subtle' | 'muted' | 'default' | 'strong';
const TAG_INVERSE =
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-neutral-default-default';
export function normalizeTagTone(tone?: TagToneInput): UiTone {
if (!tone || tone === 'default') return 'neutral';
if (tone === 'info') return 'information';
if (tone === 'caution') return 'error';
return tone;
}
function toStyleTone(tone: UiTone): TagStyleTone {
return tone === 'neutral' ? 'default' : tone;
}
function resolveTagAxes(
variant: TagVariant | undefined,
tone: TagToneInput | undefined,
emphasis: TagEmphasis | undefined
): {
variant: UiVariant;
tone: UiTone;
emphasis: TagEmphasis;
} {
const base = variant ?? 'primary';
return {
variant: base,
tone: normalizeTagTone(tone),
emphasis: emphasis ?? DEFAULT_EMPHASIS_BY_VARIANT[base],
};
}
/** Filled chips — emphasis ramps from quiet surface to solid semantic fill. */
const TAG_PRIMARY: Record<TagStyleTone, Record<TagEmphasisMatrix, string>> = {
default: {
subtle:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-neutral-default-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-neutral-muted-default',
default:
'border-transparent bg-ds-bg-neutral-default-default !text-ds-text-neutral-default-default',
strong:
'border-transparent bg-ds-bg-brand-default-default !text-ds-text-brand-inverse-default',
},
success: {
subtle:
'border-transparent bg-ds-bg-status-completed-subtle-default !text-ds-text-status-completed-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-success-strong-default',
default:
'border-transparent bg-ds-bg-success-subtle-default !text-ds-text-success-strong-default',
strong:
'border-transparent bg-ds-bg-success-default-default !text-ds-text-success-inverse-default',
},
error: {
subtle:
'border-transparent bg-ds-bg-status-error-subtle-default !text-ds-text-status-error-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-error-strong-default',
default:
'border-transparent bg-ds-bg-error-subtle-default !text-ds-text-error-strong-default',
strong:
'border-transparent bg-ds-bg-error-default-default !text-ds-text-error-inverse-default',
},
information: {
subtle:
'border-transparent bg-ds-bg-status-splitting-subtle-default !text-ds-text-status-splitting-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-information-strong-default',
default:
'border-transparent bg-ds-bg-information-subtle-default !text-ds-text-information-strong-default',
strong:
'border-transparent bg-ds-bg-information-default-default !text-ds-text-information-inverse-default',
},
warning: {
subtle:
'border-transparent bg-ds-bg-status-pending-subtle-default !text-ds-text-status-pending-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-warning-strong-default',
default:
'border-transparent bg-ds-bg-warning-subtle-default !text-ds-text-warning-strong-default',
strong:
'border-transparent bg-ds-bg-warning-default-default !text-ds-text-warning-inverse-default',
},
};
/** Softer bordered / filled secondary surface. */
const TAG_SECONDARY: Record<TagStyleTone, Record<TagEmphasisMatrix, string>> = {
default: {
subtle:
'border-ds-border-neutral-muted-default bg-ds-bg-neutral-subtle-default !text-ds-text-neutral-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-default-default !text-ds-text-neutral-muted-default',
default:
'border-ds-border-neutral-default-default bg-ds-bg-neutral-subtle-default !text-ds-text-neutral-default-default',
strong:
'border-ds-border-neutral-strong-default bg-ds-bg-neutral-default-default !text-ds-text-neutral-default-default',
},
success: {
subtle:
'border-ds-border-success-muted-default bg-ds-bg-success-subtle-default !text-ds-text-status-completed-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-success-strong-default',
default:
'border-ds-border-success-default-default bg-ds-bg-success-subtle-default !text-ds-text-success-strong-default',
strong:
'border-ds-border-success-default-default bg-ds-bg-success-default-default !text-ds-text-success-inverse-default',
},
error: {
subtle:
'border-ds-border-error-muted-default bg-ds-bg-error-subtle-default !text-ds-text-status-error-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-error-strong-default',
default:
'border-ds-border-error-default-default bg-ds-bg-error-subtle-default !text-ds-text-error-strong-default',
strong:
'border-ds-border-error-default-default bg-ds-bg-error-default-default !text-ds-text-error-inverse-default',
},
information: {
subtle:
'border-ds-border-information-muted-default bg-ds-bg-information-subtle-default !text-ds-text-status-splitting-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-information-strong-default',
default:
'border-ds-border-information-default-default bg-ds-bg-information-subtle-default !text-ds-text-information-strong-default',
strong:
'border-ds-border-information-default-default bg-ds-bg-information-default-default !text-ds-text-information-inverse-default',
},
warning: {
subtle:
'border-ds-border-warning-muted-default bg-ds-bg-warning-subtle-default !text-ds-text-status-pending-muted-default',
muted:
'border-transparent bg-ds-bg-neutral-subtle-default !text-ds-text-warning-strong-default',
default:
'border-ds-border-warning-default-default bg-ds-bg-warning-subtle-default !text-ds-text-warning-strong-default',
strong:
'border-ds-border-warning-default-default bg-ds-bg-warning-default-default !text-ds-text-warning-inverse-default',
},
};
/** Transparent fill; semantic border. */
const TAG_OUTLINE: Record<TagStyleTone, Record<TagEmphasisMatrix, string>> = {
default: {
subtle:
'border-ds-border-neutral-muted-default bg-transparent !text-ds-text-neutral-muted-default',
muted:
'border-ds-border-neutral-muted-default bg-transparent !text-ds-text-neutral-muted-default',
default:
'border-ds-border-neutral-strong-default bg-transparent !text-ds-text-neutral-default-default',
strong:
'border-ds-border-neutral-strong-default bg-transparent !text-ds-text-neutral-default-default font-semibold',
},
success: {
subtle:
'border-ds-border-success-muted-default bg-transparent !text-ds-text-status-completed-muted-default',
muted:
'border-ds-border-neutral-default-default bg-transparent !text-ds-text-success-strong-default',
default:
'border-ds-border-success-default-default bg-transparent !text-ds-text-success-strong-default',
strong:
'border-ds-border-success-strong-default bg-transparent !text-ds-text-success-strong-default font-semibold',
},
error: {
subtle:
'border-ds-border-error-muted-default bg-transparent !text-ds-text-status-error-muted-default',
muted:
'border-ds-border-neutral-default-default bg-transparent !text-ds-text-error-strong-default',
default:
'border-ds-border-error-default-default bg-transparent !text-ds-text-error-strong-default',
strong:
'border-ds-border-error-strong-default bg-transparent !text-ds-text-error-strong-default font-semibold',
},
information: {
subtle:
'border-ds-border-information-muted-default bg-transparent !text-ds-text-status-splitting-muted-default',
muted:
'border-ds-border-neutral-default-default bg-transparent !text-ds-text-information-strong-default',
default:
'border-ds-border-information-default-default bg-transparent !text-ds-text-information-strong-default',
strong:
'border-ds-border-information-strong-default bg-transparent !text-ds-text-information-strong-default font-semibold',
},
warning: {
subtle:
'border-ds-border-warning-muted-default bg-transparent !text-ds-text-status-pending-muted-default',
muted:
'border-ds-border-neutral-default-default bg-transparent !text-ds-text-warning-strong-default',
default:
'border-ds-border-warning-default-default bg-transparent !text-ds-text-warning-strong-default',
strong:
'border-ds-border-warning-strong-default bg-transparent !text-ds-text-warning-strong-default font-semibold',
},
};
/** No border; text-first. */
const TAG_GHOST: Record<TagStyleTone, Record<TagEmphasisMatrix, string>> = {
default: {
subtle:
'border-transparent bg-transparent !text-ds-text-neutral-muted-default',
muted:
'border-transparent bg-transparent !text-ds-text-neutral-muted-default',
default:
'border-transparent bg-transparent !text-ds-text-neutral-default-default',
strong:
'border-transparent bg-transparent !text-ds-text-neutral-default-default font-semibold',
},
success: {
subtle:
'border-transparent bg-transparent !text-ds-text-status-completed-muted-default',
muted:
'border-transparent bg-transparent opacity-80 !text-ds-text-success-strong-default',
default:
'border-transparent bg-transparent !text-ds-text-success-strong-default',
strong:
'border-transparent bg-transparent !text-ds-text-success-strong-default font-semibold',
},
error: {
subtle:
'border-transparent bg-transparent !text-ds-text-status-error-muted-default',
muted:
'border-transparent bg-transparent opacity-80 !text-ds-text-error-strong-default',
default:
'border-transparent bg-transparent !text-ds-text-error-strong-default',
strong:
'border-transparent bg-transparent !text-ds-text-error-strong-default font-semibold',
},
information: {
subtle:
'border-transparent bg-transparent !text-ds-text-status-splitting-muted-default',
muted:
'border-transparent bg-transparent opacity-80 !text-ds-text-information-strong-default',
default:
'border-transparent bg-transparent !text-ds-text-information-strong-default',
strong:
'border-transparent bg-transparent !text-ds-text-information-strong-default font-semibold',
},
warning: {
subtle:
'border-transparent bg-transparent !text-ds-text-status-pending-muted-default',
muted:
'border-transparent bg-transparent opacity-80 !text-ds-text-warning-strong-default',
default:
'border-transparent bg-transparent !text-ds-text-warning-strong-default',
strong:
'border-transparent bg-transparent !text-ds-text-warning-strong-default font-semibold',
},
};
const TAG_BY_VARIANT: Record<
UiVariant,
Record<TagStyleTone, Record<TagEmphasisMatrix, string>>
> = {
primary: TAG_PRIMARY,
secondary: TAG_SECONDARY,
outline: TAG_OUTLINE,
ghost: TAG_GHOST,
};
function tagChromeClasses(
variant: UiVariant,
styleTone: TagStyleTone,
emphasis: TagEmphasis
): string {
if (emphasis === 'inverse') {
return TAG_INVERSE;
}
const em = emphasis as TagEmphasisMatrix;
return TAG_BY_VARIANT[variant][styleTone][em];
}
const tagVariants = cva(
'inline-flex justify-start items-center leading-relaxed',
'inline-flex justify-start items-center border border-solid leading-relaxed transition-colors duration-150',
{
variants: {
variant: {
primary: 'bg-tag-fill-info text-[var(--tag-foreground-info)]',
info: 'bg-tag-fill-info !text-[var(--tag-foreground-info)]',
success: 'bg-tag-fill-success !text-[var(--tag-foreground-success)]',
cuation: 'bg-tag-fill-cuation !text-[var(--tag-foreground-cuation)]',
warning: 'bg-tag-fill-warning !text-[var(--tag-foreground-warning)]',
default: 'bg-tag-fill-default !text-[var(--tag-foreground-default)]',
ghost: 'bg-transparent !text-[var(--tag-foreground-default)]',
},
size: {
xs: 'px-2 py-0.5 gap-1 text-body-xs font-bold leading-tight [&_svg]:size-[10px] rounded-full',
sm: 'px-2 py-1.5 gap-1 text-body-xs font-bold leading-tight [&_svg]:size-[16px] rounded-full',
md: 'px-3 py-1.5 gap-2 text-body-md font-semibold leading-relaxed [&_svg]:size-[20px] rounded-xl',
xxs: 'gap-0.5 rounded-full px-1.5 py-px text-label-xs font-medium [&_svg]:size-[12px]',
xs: 'gap-1 rounded-full px-2 py-0.5 text-label-xs font-medium [&_svg]:size-[14px]',
sm: 'gap-1 rounded-full px-2 py-1 text-label-sm font-medium [&_svg]:size-[16px]',
md: 'gap-1.5 rounded-full px-2.5 py-1 text-label-md font-medium [&_svg]:size-[18px]',
lg: 'gap-2 rounded-full px-3 py-1.5 text-label-md font-semibold [&_svg]:size-[20px]',
},
},
defaultVariants: {
variant: 'primary',
size: 'sm',
},
}
);
type TagVisualVariant = NonNullable<
VariantProps<typeof tagVariants>['variant']
>;
type TagSize = NonNullable<VariantProps<typeof tagVariants>['size']>;
export type TagVariant = UiVariant;
export type TagEmphasis = UiEmphasis;
export type TagTone = UiTone;
export type TagLegacyVariant = TagVisualVariant;
const TONE_TO_TAG_VARIANT: Record<TagTone, TagVisualVariant> = {
neutral: 'default',
success: 'success',
error: 'cuation',
information: 'info',
warning: 'warning',
};
const OUTLINE_BORDER_BY_TONE: Record<TagTone, string> = {
neutral: 'border-ds-border-neutral-default-default',
success: 'border-ds-border-success-default-default',
error: 'border-ds-border-error-default-default',
information: 'border-ds-border-information-default-default',
warning: 'border-ds-border-warning-default-default',
};
function resolveTagVisual(
variant: TagVariant | TagLegacyVariant | undefined,
tone: UiToneInput | undefined,
emphasis: TagEmphasis | undefined
): {
visualVariant: TagVisualVariant;
tone: TagTone;
emphasis: TagEmphasis;
outlineBorderClass: string | null;
} {
const normalizedTone = normalizeUiTone(tone);
const resolvedEmphasis = emphasis ?? 'default';
const v = variant ?? 'primary';
// Legacy dedicated semantic variants remain valid.
if (
v === 'info' ||
v === 'success' ||
v === 'cuation' ||
v === 'warning' ||
v === 'default'
) {
return {
visualVariant: v,
tone: normalizedTone,
emphasis: resolvedEmphasis,
outlineBorderClass: null,
};
}
if (resolvedEmphasis === 'inverse' || v === 'ghost') {
return {
visualVariant: 'ghost',
tone: normalizedTone,
emphasis: resolvedEmphasis,
outlineBorderClass: null,
};
}
const visualVariant = TONE_TO_TAG_VARIANT[normalizedTone];
if (v === 'outline') {
return {
visualVariant,
tone: normalizedTone,
emphasis: resolvedEmphasis,
outlineBorderClass: OUTLINE_BORDER_BY_TONE[normalizedTone],
};
}
return {
visualVariant,
tone: normalizedTone,
emphasis: resolvedEmphasis,
outlineBorderClass: null,
};
}
interface TagProps extends React.ComponentProps<'div'> {
asChild?: boolean;
variant?: TagVariant | TagLegacyVariant;
/** Chrome pattern: filled, softer fill, outline, or text-only. */
variant?: TagVariant;
/** Visual weight within the chosen `variant`. */
emphasis?: TagEmphasis;
tone?: UiToneInput;
/**
* Semantic palette. Shorthands: `info` information, `caution` error; `default` neutral.
* Omitted tone reads as neutral.
*/
tone?: TagToneInput;
size?: TagSize;
text?: string;
icon?: React.ReactNode;
@ -149,9 +345,9 @@ const Tag = React.forwardRef<HTMLDivElement, TagProps>(
(
{
className,
variant,
emphasis,
tone,
variant: variantProp,
emphasis: emphasisProp,
tone: toneProp,
size,
asChild = false,
text,
@ -163,27 +359,22 @@ const Tag = React.forwardRef<HTMLDivElement, TagProps>(
ref
) => {
const Comp = asChild ? Slot : 'div';
const {
visualVariant,
tone: resolvedTone,
emphasis: resolvedEmphasis,
outlineBorderClass,
} = resolveTagVisual(variant, tone, emphasis);
const { variant, tone, emphasis } = resolveTagAxes(
variantProp,
toneProp,
emphasisProp
);
const styleTone = toStyleTone(tone);
const chrome = tagChromeClasses(variant, styleTone, emphasis);
// When asChild is true, just pass through the child without wrapping
if (asChild) {
return (
<Comp
ref={ref}
data-variant={variant ?? 'primary'}
data-tone={resolvedTone}
data-emphasis={resolvedEmphasis}
className={cn(
tagVariants({ variant: visualVariant, size, className }),
outlineBorderClass
? ['border', 'border-solid', outlineBorderClass]
: null
)}
data-variant={variant}
data-tone={tone}
data-emphasis={emphasis}
className={cn(tagVariants({ size }), chrome, className)}
style={mergeAliasStyles(tagTokenAliases, style)}
{...props}
>
@ -192,19 +383,13 @@ const Tag = React.forwardRef<HTMLDivElement, TagProps>(
);
}
// Normal rendering when asChild is false
return (
<Comp
ref={ref}
data-variant={variant ?? 'primary'}
data-tone={resolvedTone}
data-emphasis={resolvedEmphasis}
className={cn(
tagVariants({ variant: visualVariant, size, className }),
outlineBorderClass
? ['border', 'border-solid', outlineBorderClass]
: null
)}
data-variant={variant}
data-tone={tone}
data-emphasis={emphasis}
className={cn(tagVariants({ size }), chrome, className)}
style={mergeAliasStyles(tagTokenAliases, style)}
{...props}
>

View file

@ -54,44 +54,48 @@ function resolveStateClasses(state: TextareaState | undefined) {
if (state === 'disabled') {
return {
container: 'opacity-50 cursor-not-allowed',
field: 'border-transparent bg-input-bg-default text-input-text-default',
placeholder: 'text-input-label-default',
field:
'border-transparent bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default',
placeholder: 'text-ds-text-neutral-muted-default',
};
}
if (state === 'hover') {
return {
container: '',
field: 'border-transparent bg-input-bg-default text-input-text-default',
placeholder: 'text-input-label-default',
field:
'border-transparent bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default',
placeholder: 'text-ds-text-neutral-muted-default',
};
}
if (state === 'input') {
return {
container: '',
field: 'border-transparent bg-input-bg-input text-input-text-focus',
placeholder: 'text-input-label-default',
field:
'border-transparent bg-ds-bg-neutral-strong-default text-ds-text-neutral-default-default',
placeholder: 'text-ds-text-neutral-muted-default',
};
}
if (state === 'error') {
return {
container: '',
field:
'border-input-border-cuation bg-input-bg-default text-ds-text-neutral-default-default',
placeholder: 'text-input-label-default',
'border-ds-border-status-error-default-default bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default',
placeholder: 'text-ds-text-neutral-muted-default',
};
}
if (state === 'success') {
return {
container: '',
field:
'border-input-border-success bg-input-bg-confirm text-ds-text-neutral-default-default',
placeholder: 'text-input-label-default',
'border-ds-border-status-completed-default-default bg-ds-bg-status-completed-subtle-default text-ds-text-neutral-default-default',
placeholder: 'text-ds-text-neutral-muted-default',
};
}
return {
container: '',
field: 'border-transparent bg-input-bg-default text-input-text-default',
placeholder: 'text-input-label-default/10',
field:
'border-transparent bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default',
placeholder: 'text-ds-text-neutral-muted-default/10',
};
}
@ -126,7 +130,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, BaseTextareaProps>(
<textarea
data-scrollbar="ui-textarea"
className={cn(
'border-input placeholder:text-ds-text-neutral-muted-default/20 focus-visible:ring-ring rounded-lg py-2 pl-3 pr-3 text-body-sm shadow-sm flex min-h-[60px] w-full border bg-transparent [scrollbar-gutter:stable] focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
'border-ds-border-neutral-default-default placeholder:text-ds-text-neutral-muted-default/20 focus-visible:ring-ds-ring-brand-default-focus rounded-lg py-2 pl-3 pr-3 text-body-sm shadow-sm flex min-h-[60px] w-full border bg-transparent [scrollbar-gutter:stable] focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
style={mergeAliasStyles(formControlTokenAliases, {
@ -195,7 +199,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, BaseTextareaProps>(
// Only apply hover/focus visuals when not in error or success state
state !== 'error' &&
state !== 'success' &&
'focus-within:bg-input-bg-input focus-within:ring-input-border-focus hover:bg-input-bg-hover hover:ring-input-border-hover focus-within:ring-1 focus-within:ring-offset-0 hover:ring-1 hover:ring-offset-0',
'focus-within:bg-ds-bg-neutral-strong-default focus-within:ring-ds-ring-brand-default-focus hover:bg-ds-bg-neutral-default-hover hover:ring-ds-ring-neutral-strong-default focus-within:ring-1 focus-within:ring-offset-0 hover:ring-1 hover:ring-offset-0',
stateCls.field,
sizeClasses[size]
)}

View file

@ -21,13 +21,13 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const toggleVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors border border-transparent border-solid hover:bg-menubutton-fill-active focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:border-menubutton-border-active data-[state=on]:bg-menubutton-fill-active data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
'inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors border border-transparent border-solid hover:bg-ds-bg-neutral-default-default focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ds-ring-brand-default-focus disabled:pointer-events-none disabled:opacity-50 data-[state=on]:border-ds-border-brand-default-focus data-[state=on]:bg-ds-bg-neutral-default-default data-[state=on]:text-ds-text-neutral-default-default [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent shadow-sm hover:bg-white-100% hover:text-accent-foreground',
'border border-ds-border-neutral-default-default bg-transparent shadow-sm hover:bg-ds-bg-neutral-inverse-default hover:text-ds-text-neutral-default-default',
},
size: {
default: 'h-9 px-2 min-w-9',

View file

@ -36,16 +36,15 @@ export const formControlTokenAliases = asCssVarMap({
'--input-border-hover': 'var(--ds-border-neutral-strong-default)',
'--input-border-focus': 'var(--ds-border-brand-default-focus)',
'--input-border-success': 'var(--ds-border-status-completed-default-default)',
'--input-border-cuation': 'var(--ds-border-status-error-default-default)',
'--input-border-caution': 'var(--ds-border-status-error-default-default)',
'--input-text-default': 'var(--ds-text-neutral-default-default)',
'--input-text-focus': 'var(--ds-text-neutral-default-default)',
'--input-label-default': 'var(--ds-text-neutral-muted-default)',
'--surface-disabled': 'var(--ds-bg-neutral-muted-disabled)',
'--text-heading': 'var(--ds-text-neutral-default-default)',
'--text-body': 'var(--ds-text-neutral-default-default)',
'--text-label': 'var(--ds-text-neutral-muted-default)',
'--text-success': 'var(--ds-text-status-completed-strong-default)',
'--text-cuation': 'var(--ds-text-status-error-strong-default)',
'--text-caution': 'var(--ds-text-status-error-strong-default)',
'--text-information': 'var(--ds-text-status-splitting-strong-default)',
'--icon-primary': 'var(--ds-icon-neutral-default-default)',
'--menutabs-fill-hover': 'var(--ds-bg-neutral-default-hover)',
@ -70,14 +69,14 @@ export const buttonTokenAliases = asCssVarMap({
'--button-secondary-text-active': 'var(--ds-text-neutral-default-default)',
'--button-secondary-text-disabled': 'var(--ds-text-neutral-muted-disabled)',
'--button-tertiery-fill-default': 'var(--ds-bg-neutral-subtle-default)',
'--button-tertiery-fill-hover': 'var(--ds-bg-neutral-default-hover)',
'--button-tertiery-fill-active': 'var(--ds-bg-neutral-default-active)',
'--button-tertiery-fill-disabled': 'var(--ds-bg-neutral-muted-disabled)',
'--button-tertiery-text-default': 'var(--ds-text-neutral-default-default)',
'--button-tertiery-text-hover': 'var(--ds-text-neutral-default-default)',
'--button-tertiery-text-active': 'var(--ds-text-neutral-default-default)',
'--button-tertiery-text-disabled': 'var(--ds-text-neutral-muted-disabled)',
'--button-tertiary-fill-default': 'var(--ds-bg-neutral-subtle-default)',
'--button-tertiary-fill-hover': 'var(--ds-bg-neutral-default-hover)',
'--button-tertiary-fill-active': 'var(--ds-bg-neutral-default-active)',
'--button-tertiary-fill-disabled': 'var(--ds-bg-neutral-muted-disabled)',
'--button-tertiary-text-default': 'var(--ds-text-neutral-default-default)',
'--button-tertiary-text-hover': 'var(--ds-text-neutral-default-default)',
'--button-tertiary-text-active': 'var(--ds-text-neutral-default-default)',
'--button-tertiary-text-disabled': 'var(--ds-text-neutral-muted-disabled)',
'--button-transparent-fill-default': 'transparent',
'--button-transparent-fill-hover': 'var(--ds-bg-neutral-default-hover)',
@ -94,8 +93,8 @@ export const buttonTokenAliases = asCssVarMap({
'--fill-fill-success-hover': 'var(--ds-bg-status-completed-subtle-hover)',
'--fill-fill-success-active': 'var(--ds-bg-status-completed-default-default)',
'--button-fill-cuation': 'var(--ds-bg-status-error-default-default)',
'--button-fill-cuation-foreground':
'--button-fill-caution': 'var(--ds-bg-status-error-default-default)',
'--button-fill-caution-foreground':
'var(--ds-text-status-error-strong-default)',
'--button-fill-information': 'var(--ds-bg-status-splitting-default-default)',
@ -114,8 +113,8 @@ export const tagTokenAliases = asCssVarMap({
'--tag-foreground-success': 'var(--ds-text-status-completed-strong-default)',
'--tag-fill-warning': 'var(--ds-bg-status-pending-subtle-default)',
'--tag-foreground-warning': 'var(--ds-text-status-pending-strong-default)',
'--tag-fill-cuation': 'var(--ds-bg-status-error-subtle-default)',
'--tag-foreground-cuation': 'var(--ds-text-status-error-strong-default)',
'--tag-fill-caution': 'var(--ds-bg-status-error-subtle-default)',
'--tag-foreground-caution': 'var(--ds-text-status-error-strong-default)',
'--tag-fill-default': 'var(--ds-bg-neutral-default-default)',
'--tag-foreground-default': 'var(--ds-text-neutral-default-default)',
});
@ -138,6 +137,5 @@ export const switchTokenAliases = asCssVarMap({
export const tooltipTokenAliases = asCssVarMap({
'--border-secondary': 'var(--ds-border-neutral-default-default)',
'--surface-tertiary': 'var(--ds-bg-neutral-strong-default)',
'--text-primary': 'var(--ds-text-neutral-default-default)',
});

View file

@ -52,7 +52,7 @@ const Update = () => {
if (isDownloading) {
toast.custom(
(_toastId) => (
<div className="rounded-lg bg-white-100% p-4 shadow-lg w-[300px]">
<div className="rounded-lg bg-ds-bg-neutral-inverse-default p-4 shadow-lg w-[300px]">
<div className="mb-2 text-sm font-medium">
{t('update.downloading-update')}
</div>

View file

@ -1,71 +0,0 @@
# Legacy To V2 Token Mapping
This document maps existing CSS variables to the new semantic token model:
`element.tone.emphasis.state`
## Task Lifecycle Mapping
| Legacy Token | New Token | Notes |
| --- | --- | --- |
| `--badge-running-surface` | `bg.status-running.subtle.default` | Running badge/card background |
| `--badge-running-surface-foreground` | `text.status-running.strong.default` | Running text/icon foreground |
| `--badge-splitting-surface` | `bg.status-splitting.subtle.default` | Splitting status |
| `--badge-splitting-surface-foreground` | `text.status-splitting.strong.default` | Splitting text/icon |
| `--badge-paused-surface` | `bg.status-paused.subtle.default` | Pause status |
| `--badge-paused-surface-foreground` | `text.status-paused.strong.default` | Pause text/icon |
| `--badge-error-surface` | `bg.status-error.subtle.default` | Error status |
| `--badge-error-surface-foreground` | `text.status-error.strong.default` | Error text/icon |
| `--badge-complete-surface` | `bg.status-completed.subtle.default` | Completed status |
| `--badge-complete-surface-foreground` | `text.status-completed.strong.default` | Completed text/icon |
| `--task-fill-running` | `bg.status-running.subtle.default` | Task row/card running |
| `--task-fill-success` | `bg.status-completed.subtle.default` | Task row/card completed |
| `--task-fill-warning` | `bg.status-blocked.subtle.default` | Used by blocked/reassigning flows |
| `--task-fill-error` | `bg.status-error.subtle.default` | Task row/card failed |
| `--task-border-focus-success` | `border.status-completed.default.focus` | Focus ring/border for success |
| `--task-border-focus-warning` | `border.status-blocked.default.focus` | Focus ring/border for warning |
| `--task-border-focus-error` | `border.status-error.default.focus` | Focus ring/border for error |
## Generic Semantic Mapping
| Legacy Token | New Token | Notes |
| --- | --- | --- |
| `--surface-success` | `bg.status-completed.subtle.default` | |
| `--surface-information` | `bg.status-splitting.subtle.default` | Info is used for splitting state in chat/task UIs |
| `--surface-warning` | `bg.status-pending.subtle.default` | |
| `--surface-cuation` | `bg.status-error.subtle.default` | Legacy spelling retained; migrate naming |
| `--text-success` | `text.status-completed.strong.default` | |
| `--text-information` | `text.status-splitting.strong.default` | |
| `--text-warning` | `text.status-pending.strong.default` | |
| `--text-cuation` | `text.status-error.strong.default` | |
| `--border-success` | `border.status-completed.default.default` | |
| `--border-information` | `border.status-splitting.default.default` | |
| `--border-warning` | `border.status-pending.default.default` | |
| `--border-cuation` | `border.status-error.default.default` | |
| `--icon-success` | `icon.status-completed.default.default` | |
| `--icon-information` | `icon.status-splitting.default.default` | |
| `--icon-warning` | `icon.status-pending.default.default` | |
| `--icon-cuation` | `icon.status-error.default.default` | |
## Recommended Status Selection
When mapping runtime status to tokens:
- `running` -> `status-running`
- `splitting` -> `status-splitting`
- `pending`/`waiting` -> `status-pending`
- `reassigning` -> `status-reassigning`
- `failed`/`error` -> `status-error`
- `completed` -> `status-completed`
- `blocked` -> `status-blocked`
- `paused` -> `status-paused`
- `skipped` -> `status-skipped`
- `cancelled` -> `status-cancelled`
## Migration Order
1. Introduce `--ds-*` semantic tokens.
2. Add/maintain aliases for legacy tokens.
3. Migrate component aliases to `--ds-*`.
4. Replace legacy token usage in components.
5. Remove legacy aliases after migration completion.

View file

@ -1,190 +1,68 @@
# Theme Tokens V2
This module introduces a new semantic token system based on:
Theme Tokens V2 is a full cutover to a DTCG-driven, OKLCH-based token engine.
`element.tone.emphasis.state`
## Architecture
Example: `bg.status-running.subtle.default`
1. **Canonical token sources** in `/tokens`:
- `base.color.json` (theme seeds + fixed role anchors)
- `semantic.color.json` (axes + transforms + contrast policy)
- `component.color.json` (component/global alias vars)
- `contracts/*.json` (contract presets with `$extends`)
2. **Compiler pipeline** in `engine.ts`:
- DTCG parse
- `$extends` resolution
- Semantic generation (`tone × emphasis × state × element`)
- WCAG contrast enforcement
- APCA diagnostics emission
- CSS variable emission (`--ds-*` + component aliases)
3. **Runtime application** in `ThemeProvider` via `applyThemeContractV2`.
## Goals
## Contract
- Keep user-facing controls small: mode + theme id + single contrast scalar.
- Derive most UI color decisions from seeds (`accent`, `background`, `ink`) and `contrast`.
- Make task lifecycle states first-class semantics (running, splitting, pending, error, reassigning).
- Keep components token-driven (no hardcoded color values).
## Token Layers
1. Theme contract (`ThemeContractV1`)
2. Theme seed (`accent`, `background`, `ink`)
3. Derived tokens (`buildThemeV1`)
4. Semantic tokens (`element.tone.emphasis.state`)
5. Component aliases (component-local CSS variables referencing semantic tokens)
## Naming Contract
- `element`: `bg`, `text`, `border`, `icon`, `ring`
- `tone`:
- global: `neutral`, `brand`
- task states: `status-running`, `status-splitting`, `status-pending`, `status-error`, `status-reassigning`, `status-completed`, `status-blocked`, `status-paused`, `status-skipped`, `status-cancelled`
- fixed tones: `single-agent`, `workforce`, `browser`, `terminal`, `document`, `success`, `caution`, `warning`, `information`
- `emphasis`: `subtle`, `muted`, `default`, `strong`, `inverse`
- `state`: `default`, `hover`, `active`, `selected`, `focus`, `disabled`
## Surface Level Mapping
Legacy surface levels should map to V2 semantic naming:
- `surface-primary` -> `bg.neutral.subtle.default`
- `surface-secondary` -> `bg.neutral.default.default`
- `surface-tertiary` -> `bg.neutral.strong.default`
All surface tokens are generated as solid fills (not opacity overlays).
## Fixed Tone Tokens
Fixed tones are generated in the same `--ds-*` graph but are not derived from user seed colors.
They currently use the original light/dark values from `token.css` as a migration baseline.
Fixed-tone palette source (developer-owned, not user-editable):
- `src/lib/themeTokens/fixedToneSchema.ts`
- Edit `DEFAULT_FIXED_TONE_SCHEMA` to set the base tone colors.
- The engine derives hover/active/focus/selected state tokens from these base colors.
Examples:
- `--ds-text-success-default-default`
- `--ds-border-warning-default-default`
- `--ds-icon-browser-default-default`
- `--ds-bg-single-agent-subtle-selected`
## Direct Tailwind Usage
`tailwind.config.js` registers semantic `--ds-*` color entries so you can use token classes directly:
- `text-ds-text-success-default-default`
- `hover:text-ds-text-success-default-hover`
- `bg-ds-bg-warning-subtle-selected`
- `border-ds-border-browser-default-focus`
## How To Search Token Usage
Use ripgrep to track usage in code and styles:
```bash
rg "var\\(--ds-" src
rg "status-running|status-splitting|status-pending|status-error" src
rg "--task-fill-|--badge-|--surface-(information|success|warning|cuation)" src/style tailwind.config.js
```ts
type Adjustment = { dL?: number; dC?: number; dH?: number; alpha?: number };
type ThemeContractV2 = {
version: 2;
mode: "light" | "dark";
themeId: string;
contrast: number; // 0..100
overrides?: {
tone?: Record<Tone, Adjustment>;
emphasis?: Record<Emphasis, Adjustment>;
state?: Record<State, Adjustment>;
cell?: Record<`${Tone}.${Emphasis}.${State}`, Adjustment>;
};
};
```
Search strategy:
Override precedence is deterministic:
1. Search semantic tokens first (`--ds-*`).
2. Search legacy tokens from `legacyMapping.ts`.
3. Migrate component aliases before touching component internals.
1. tone defaults
2. emphasis transform
3. state transform
4. axis overrides (`tone`, `emphasis`, `state`)
5. cell override (`tone.emphasis.state`)
## How To Select Tokens For Components
## Developer API
For each component area, pick tokens in this order:
1. Base layer:
- background: `bg.neutral.*`
- text/icon: `text.neutral.*`, `icon.neutral.*`
- border/ring: `border.neutral.*`, `ring.*`
2. Interactive state:
- hover/active/focus from same tone/emphasis family.
3. Domain state (task lifecycle):
- use `status-*` tones for task chips, cards, rows, progress markers.
Rules:
- Do not mix unrelated tones in one control unless intentional.
- Prefer `subtle` backgrounds and `strong` text for status badges.
- Use `default` borders and `focus` rings for keyboard state.
- Avoid direct palette tokens (`--colors-*`) in components.
## Semantic Token vs Component Alias
Create semantic tokens when the token expresses stable product meaning:
1. The meaning is reused across multiple components/features.
2. The value should respond consistently to theme mode/contrast.
3. The name is domain-driven, not component-driven (`status-running`, `status-error`).
4. It is part of design language, not local layout implementation.
Create component aliases when the token is local implementation detail:
1. The value is specific to one components structure.
2. You need to map several semantic tokens into a simpler component API.
3. You are preserving backwards compatibility during migration.
4. You are tuning only one component without changing global semantics.
Promotion rule:
1. Start with a component alias if unsure.
2. If the same alias pattern appears in 2 or more components, promote to semantic token.
Anti-patterns:
1. Semantic tokens named after a single component (`button-primary-bg`).
2. Component aliases pointing to raw hex values.
3. Creating semantic tokens for one-off visual exceptions.
## How To Create New Component Tokens
Create component-scoped aliases that reference semantic tokens. Example:
```css
.task-row {
--task-row-bg-default: var(--ds-bg-neutral-default-default);
--task-row-bg-hover: var(--ds-bg-neutral-default-hover);
--task-row-border-focus: var(--ds-border-neutral-default-focus);
}
.task-row[data-status='running'] {
--task-row-bg-default: var(--ds-bg-status-running-subtle-default);
--task-row-border-focus: var(--ds-border-status-running-default-focus);
}
```
Guidelines:
1. Always define at least `default`, `hover`, and `focus` aliases.
2. Keep aliases local to the component namespace (`--task-row-*`, `--badge-*`).
3. Point aliases to semantic tokens, not to raw color constants.
4. If a status is new, add a new semantic tone in `types.ts` first.
## Runtime Usage
- Build tokens: `buildThemeV1(contract)`
- Apply to root: `applyThemeContractV1(contract, document.documentElement)`
Current integration applies `--ds-*` variables at runtime without replacing old tokens yet.
In development builds, `ThemeProvider` exposes `window.__eigentThemeV1`:
In development, `ThemeProvider` exposes:
```js
window.__eigentThemeV1.listThemes(); // { light: [...], dark: [...] }
window.__eigentThemeV1.setTheme('light', 'vivid');
window.__eigentThemeV1.setContrast(65);
window.__eigentThemeV1.getState();
window.__eigentThemeV2.listThemes(); // { light: [...], dark: [...] }
window.__eigentThemeV2.setTheme("light", "codex");
window.__eigentThemeV2.setContrast(65);
window.__eigentThemeV2.getState();
```
## Refactor Workflow (Required)
## Validation
1. Define/adjust base theme seeds first in `catalog.ts` (`accent`, `background`, `ink`).
2. Apply themes and validate semantics across multiple color themes (same component states, different seeds).
3. Fix semantic token mapping issues discovered during theme switching.
4. Only then migrate feature/component token usage.
Why:
- Seed-first validation exposes wrong token selection early (for example neutral vs status tone misuse).
- Switching theme IDs should change appearance without requiring component code changes.
## Migration Notes
- See `MAPPING.md` for old-to-new token migration.
- Keep old token names only as aliases during migration.
- Correct naming drift as part of migration (`spliting` -> `splitting`, `cuation` -> `error/caution`, `tertiery` -> `tertiary`).
- WCAG AA is enforced by default on required semantic pairs.
- APCA scores are emitted as diagnostics (non-blocking).
- Engine tests live in `engine.v2.test.ts` and cover:
- determinism
- override precedence
- gamut-safe outputs
- contrast enforcement
- randomized seed stability
- runtime reapplication behavior

View file

@ -12,128 +12,99 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import baseColorTokens from '../../../tokens/base.color.json';
import contractBase from '../../../tokens/contracts/default.base.json';
import darkContractRaw from '../../../tokens/contracts/default.dark.json';
import lightContractRaw from '../../../tokens/contracts/default.light.json';
import { clamp } from './colorMath';
import { resolveExtends } from './dtcg';
import {
THEME_CONTRACT_VERSION,
type ColorThemeDefinitionV1,
type ColorThemeDefinitionV2,
type Mode,
type ThemeCatalog,
type ThemeContractV1,
type ThemeCatalogV2,
type ThemeContractV2,
type ThemeSeedV2,
} from './types';
export const DEFAULT_CONTRAST = 43;
export const DEFAULT_COLOR_THEME_ID = 'eigent';
export const DEFAULT_THEME_ID = 'eigent';
export const DEFAULT_COLOR_THEME_ID = DEFAULT_THEME_ID;
export const DEFAULT_THEME_CATALOG: ThemeCatalog = {
light: {
eigent: {
id: 'eigent',
mode: 'light',
seed: {
accent: '#000000',
background: '#faf7f6',
ink: '#1d1d1d',
},
},
claude: {
id: 'claude',
mode: 'light',
seed: {
accent: '#cc7d5e',
background: '#f9f9f7',
ink: '#2d2d2b',
},
},
codex: {
id: 'codex',
mode: 'light',
seed: {
accent: '#0169cc',
background: '#ffffff',
ink: '#0d0d0d',
},
},
camel: {
id: 'camel',
mode: 'light',
seed: {
accent: '#4c19e8',
background: '#ffffff',
ink: '#1d1d1d',
},
},
},
dark: {
eigent: {
id: 'eigent',
mode: 'dark',
seed: {
accent: '#ede1db',
background: '#1f1f1f',
ink: '#ffffff',
},
},
claude: {
id: 'claude',
mode: 'dark',
seed: {
accent: '#cc7d5e',
background: '#2d2d2b',
ink: '#f9f9f7',
},
},
codex: {
id: 'codex',
mode: 'dark',
seed: {
accent: '#0169cc',
background: '#111111',
ink: '#fcfcfc',
},
},
camel: {
id: 'camel',
mode: 'dark',
seed: {
accent: '#b5afff',
background: '#1f1f1f',
ink: '#fafafa',
},
},
},
type BaseColorTokenShape = {
themes: Record<Mode, Record<string, ThemeSeedV2>>;
};
export function getColorThemeDefinition(
mode: Mode,
colorThemeId: string,
catalog: ThemeCatalog = DEFAULT_THEME_CATALOG
): ColorThemeDefinitionV1 {
const modeThemes = catalog[mode] ?? {};
const selected = modeThemes[colorThemeId];
if (selected) {
return selected;
}
const BASE = baseColorTokens as BaseColorTokenShape;
const fallback = modeThemes[DEFAULT_COLOR_THEME_ID];
if (fallback) {
return fallback;
}
function toCatalog(themes: BaseColorTokenShape['themes']): ThemeCatalogV2 {
return {
light: Object.fromEntries(
Object.entries(themes.light).map(([id, seed]) => [
id,
{ id, mode: 'light', seed },
])
),
dark: Object.fromEntries(
Object.entries(themes.dark).map(([id, seed]) => [
id,
{ id, mode: 'dark', seed },
])
),
};
}
export const DEFAULT_THEME_CATALOG: ThemeCatalogV2 = toCatalog(BASE.themes);
export function getColorThemeDefinitionV2(
mode: Mode,
themeId: string,
catalog: ThemeCatalogV2 = DEFAULT_THEME_CATALOG
): ColorThemeDefinitionV2 {
const modeThemes = catalog[mode] ?? {};
const selected = modeThemes[themeId];
if (selected) return selected;
const fallback = modeThemes[DEFAULT_THEME_ID];
if (fallback) return fallback;
const firstTheme = Object.values(modeThemes)[0];
if (firstTheme) {
return firstTheme;
}
if (firstTheme) return firstTheme;
throw new Error(`No color themes configured for mode "${mode}"`);
}
export function createDefaultThemeContract(
mode: Mode,
overrides?: Partial<Omit<ThemeContractV1, 'version' | 'mode'>>
): ThemeContractV1 {
function resolveContractPreset(raw: unknown): ThemeContractV2 {
const tree = resolveExtends({
...(contractBase as Record<string, unknown>),
contract: raw as Record<string, unknown>,
});
const contract = (tree as { contract: ThemeContractV2 }).contract;
return {
...contract,
version: THEME_CONTRACT_VERSION,
mode,
colorThemeId: overrides?.colorThemeId ?? DEFAULT_COLOR_THEME_ID,
contrast: overrides?.contrast ?? DEFAULT_CONTRAST,
contrast: clamp(Math.round(contract.contrast), 0, 100),
};
}
const LIGHT_CONTRACT_PRESET = resolveContractPreset(lightContractRaw);
const DARK_CONTRACT_PRESET = resolveContractPreset(darkContractRaw);
export function createDefaultThemeContractV2(
mode: Mode,
overrides?: Partial<Omit<ThemeContractV2, 'version' | 'mode'>>
): ThemeContractV2 {
const preset = mode === 'dark' ? DARK_CONTRACT_PRESET : LIGHT_CONTRACT_PRESET;
return {
...preset,
version: THEME_CONTRACT_VERSION,
mode,
themeId: overrides?.themeId ?? preset.themeId ?? DEFAULT_THEME_ID,
contrast: clamp(
Math.round(overrides?.contrast ?? preset.contrast ?? DEFAULT_CONTRAST),
0,
100
),
overrides: overrides?.overrides ?? preset.overrides,
};
}

View file

@ -16,6 +16,12 @@ export function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
export function normalizeHue(degrees: number): number {
let h = degrees % 360;
if (h < 0) h += 360;
return h;
}
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
const normalized = hex.replace('#', '').trim();
const full =
@ -58,13 +64,21 @@ export function alpha(hex: string, opacity: number): string {
return `rgba(${r}, ${g}, ${b}, ${clamp(opacity, 0, 1).toFixed(3)})`;
}
function srgbToLinear(channel: number): number {
const value = channel / 255;
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
}
function linearToSrgb(linear: number): number {
const clamped = clamp(linear, 0, 1);
return clamped <= 0.0031308
? clamped * 12.92
: 1.055 * clamped ** (1 / 2.4) - 0.055;
}
function luminance(hex: string): number {
const { r, g, b } = hexToRgb(hex);
const toLinear = (channel: number) => {
const value = channel / 255;
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
};
const [lr, lg, lb] = [toLinear(r), toLinear(g), toLinear(b)];
const [lr, lg, lb] = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
}
@ -89,3 +103,164 @@ export function chooseReadableText(
? black
: white;
}
export type Oklch = {
l: number; // 0..1
c: number; // >= 0
h: number; // 0..360
};
export type Oklab = {
l: number;
a: number;
b: number;
};
function srgbHexToLinearRgb(hex: string): { r: number; g: number; b: number } {
const { r, g, b } = hexToRgb(hex);
return {
r: srgbToLinear(r),
g: srgbToLinear(g),
b: srgbToLinear(b),
};
}
function linearRgbToHex(rgb: {
r: number;
g: number;
b: number;
}): `#${string}` {
return rgbToHex(
linearToSrgb(rgb.r) * 255,
linearToSrgb(rgb.g) * 255,
linearToSrgb(rgb.b) * 255
);
}
function linearRgbToOklab(rgb: { r: number; g: number; b: number }): Oklab {
const l = 0.4122214708 * rgb.r + 0.5363325363 * rgb.g + 0.0514459929 * rgb.b;
const m = 0.2119034982 * rgb.r + 0.6806995451 * rgb.g + 0.1073969566 * rgb.b;
const s = 0.0883024619 * rgb.r + 0.2817188376 * rgb.g + 0.6299787005 * rgb.b;
const l_ = Math.cbrt(l);
const m_ = Math.cbrt(m);
const s_ = Math.cbrt(s);
return {
l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
};
}
function oklabToLinearRgb(oklab: Oklab): { r: number; g: number; b: number } {
const l_ = oklab.l + 0.3963377774 * oklab.a + 0.2158037573 * oklab.b;
const m_ = oklab.l - 0.1055613458 * oklab.a - 0.0638541728 * oklab.b;
const s_ = oklab.l - 0.0894841775 * oklab.a - 1.291485548 * oklab.b;
const l = l_ * l_ * l_;
const m = m_ * m_ * m_;
const s = s_ * s_ * s_;
return {
r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
};
}
export function hexToOklch(hex: string): Oklch {
const oklab = linearRgbToOklab(srgbHexToLinearRgb(hex));
const c = Math.sqrt(oklab.a * oklab.a + oklab.b * oklab.b);
const h = normalizeHue((Math.atan2(oklab.b, oklab.a) * 180) / Math.PI);
return {
l: clamp(oklab.l, 0, 1),
c: Math.max(0, c),
h,
};
}
function oklchToOklab(color: Oklch): Oklab {
const hRad = (normalizeHue(color.h) * Math.PI) / 180;
return {
l: clamp(color.l, 0, 1),
a: Math.max(0, color.c) * Math.cos(hRad),
b: Math.max(0, color.c) * Math.sin(hRad),
};
}
function isLinearRgbInSrgbGamut(rgb: {
r: number;
g: number;
b: number;
}): boolean {
return (
rgb.r >= 0 &&
rgb.r <= 1 &&
rgb.g >= 0 &&
rgb.g <= 1 &&
rgb.b >= 0 &&
rgb.b <= 1
);
}
export function deltaEOK(a: Oklch, b: Oklch): number {
const la = oklchToOklab(a);
const lb = oklchToOklab(b);
const dl = la.l - lb.l;
const da = la.a - lb.a;
const db = la.b - lb.b;
return Math.sqrt(dl * dl + da * da + db * db);
}
export function oklchToHex(input: Oklch): `#${string}` {
const normalized: Oklch = {
l: clamp(input.l, 0, 1),
c: Math.max(0, input.c),
h: normalizeHue(input.h),
};
const candidateRgb = oklabToLinearRgb(oklchToOklab(normalized));
if (isLinearRgbInSrgbGamut(candidateRgb)) {
return linearRgbToHex(candidateRgb);
}
// Gamut mapping by chroma reduction (binary search, local minimum deltaEOK).
const start = normalized;
let low = 0;
let high = normalized.c;
let best = { ...normalized, c: 0 };
for (let i = 0; i < 24; i += 1) {
const mid = (low + high) / 2;
const probe = { ...normalized, c: mid };
const probeRgb = oklabToLinearRgb(oklchToOklab(probe));
if (isLinearRgbInSrgbGamut(probeRgb)) {
best = probe;
low = mid;
} else {
high = mid;
}
}
// Keep the in-gamut candidate with smallest deltaEOK to requested color.
const edgeA = { ...normalized, c: low };
const edgeB = { ...normalized, c: Math.max(0, low - 0.0005) };
const bestFinal =
deltaEOK(start, edgeA) <= deltaEOK(start, edgeB) ? edgeA : edgeB;
return linearRgbToHex(oklabToLinearRgb(oklchToOklab(bestFinal)));
}
export function wcagMinimumContrast(largeText?: boolean): number {
return largeText ? 3 : 4.5;
}
export function apcaContrastApprox(textHex: string, bgHex: string): number {
const yText = luminance(textHex);
const yBg = luminance(bgHex);
const polarity = yBg >= yText ? 1 : -1;
// Lightweight APCA-like diagnostic curve (non-gating for this release).
const bgExp = yBg >= yText ? 0.56 : 0.65;
const textExp = yBg >= yText ? 0.57 : 0.62;
const lc = (yBg ** bgExp - yText ** textExp) * 1.14 * 100;
return polarity * lc;
}

150
src/lib/themeTokens/dtcg.ts Normal file
View file

@ -0,0 +1,150 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
type JsonObject = Record<string, unknown>;
export type DtcgLeafToken = {
path: string;
value: unknown;
type?: string;
extensions?: Record<string, unknown>;
};
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function deepClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function deepMerge(base: unknown, override: unknown): unknown {
if (!isObject(base) || !isObject(override)) {
return deepClone(override);
}
const out: JsonObject = deepClone(base);
for (const [key, value] of Object.entries(override)) {
if (key === '$extends') continue;
const existing = out[key];
out[key] =
isObject(existing) && isObject(value)
? (deepMerge(existing, value) as JsonObject)
: deepClone(value);
}
return out;
}
function getByPath(root: JsonObject, path: string): unknown {
const parts = path.split('.').filter(Boolean);
let current: unknown = root;
for (const part of parts) {
if (!isObject(current) || !(part in current)) {
return undefined;
}
current = current[part];
}
return current;
}
function resolveExtendsRec(
root: JsonObject,
node: unknown,
path: string,
stack: Set<string>
): unknown {
if (!isObject(node)) return node;
const extendsPath =
typeof node.$extends === 'string'
? node.$extends.trim().replace(/^\{|\}$/g, '')
: null;
if (extendsPath) {
const stackKey = `${path} -> ${extendsPath}`;
if (stack.has(stackKey)) {
throw new Error(`Circular $extends detected at "${path}"`);
}
stack.add(stackKey);
const base = getByPath(root, extendsPath);
if (!isObject(base)) {
throw new Error(
`$extends target "${extendsPath}" not found for "${path}"`
);
}
const resolvedBase = resolveExtendsRec(root, base, extendsPath, stack);
const merged = deepMerge(resolvedBase, node);
stack.delete(stackKey);
return resolveExtendsRec(root, merged, path, stack);
}
const out: JsonObject = {};
for (const [key, value] of Object.entries(node)) {
out[key] = resolveExtendsRec(
root,
value,
path ? `${path}.${key}` : key,
stack
);
}
return out;
}
export function resolveExtends<T extends JsonObject>(root: T): T {
const cloned = deepClone(root);
return resolveExtendsRec(cloned, cloned, '', new Set<string>()) as T;
}
function flattenRec(
node: unknown,
path: string[],
output: DtcgLeafToken[]
): void {
if (!isObject(node)) return;
if ('$value' in node) {
output.push({
path: path.join('.'),
value: node.$value,
type: typeof node.$type === 'string' ? node.$type : undefined,
extensions: isObject(node.$extensions)
? (node.$extensions as Record<string, unknown>)
: undefined,
});
return;
}
for (const [key, value] of Object.entries(node)) {
if (key.startsWith('$')) continue;
flattenRec(value, [...path, key], output);
}
}
export function flattenDtcgTokens(tree: JsonObject): DtcgLeafToken[] {
const output: DtcgLeafToken[] = [];
flattenRec(tree, [], output);
return output;
}
export function resolveAliasReferences<T>(
value: T,
lookup: (path: string) => unknown
): T {
if (typeof value === 'string') {
return value.replace(/\{([^}]+)\}/g, (_m, tokenPath: string) => {
const resolved = lookup(tokenPath.trim());
return resolved == null ? '' : String(resolved);
}) as T;
}
return value;
}

View file

@ -12,60 +12,122 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { DEFAULT_THEME_CATALOG, getColorThemeDefinition } from './catalog';
import { alpha, chooseReadableText, clamp, mix } from './colorMath';
import baseColorTokens from '../../../tokens/base.color.json';
import componentTokens from '../../../tokens/component.color.json';
import semanticTokens from '../../../tokens/semantic.color.json';
import { DEFAULT_THEME_CATALOG, getColorThemeDefinitionV2 } from './catalog';
import {
DEFAULT_FIXED_TONE_SCHEMA,
type FixedToneSchema,
type FixedToneSeed,
} from './fixedToneSchema';
alpha,
apcaContrastApprox,
chooseReadableText,
clamp,
contrastRatio,
hexToOklch,
mix,
normalizeHue,
oklchToHex,
rgbToHex,
wcagMinimumContrast,
type Oklch,
} from './colorMath';
import {
flattenDtcgTokens,
resolveAliasReferences,
resolveExtends,
} from './dtcg';
import { tokenKeyToCssVarName } from './naming';
import type {
FixedTone,
Adjustment,
ContrastDiagnostic,
Element,
Emphasis,
Mode,
ResolvedThemeV1,
StatusTone,
ThemeCatalog,
ThemeContractV1,
ResolvedThemeV2,
State,
ThemeCatalogV2,
ThemeContractV2,
ThemeTokens,
TokenKey,
Tone,
} from './types';
const STATUS_TONES: StatusTone[] = [
'status-running',
'status-splitting',
'status-pending',
'status-error',
'status-reassigning',
'status-completed',
'status-blocked',
'status-paused',
'status-skipped',
'status-cancelled',
];
type SemanticShape = {
axes: {
elements: Element[];
tones: Tone[];
emphasis: Emphasis[];
states: State[];
};
transforms: {
emphasis: Record<Emphasis, Adjustment>;
state: Record<State, Adjustment>;
element: Record<Element, Adjustment>;
};
toneSource: Record<
Tone,
{
source: 'accent' | 'background' | 'ink' | 'fixed';
sourceByElement?: Partial<
Record<Element, 'accent' | 'background' | 'ink' | 'fixed'>
>;
dL?: number;
dC?: number;
dH?: number;
dLLight?: number;
dLDark?: number;
}
>;
contrastPairs: Array<{
fg: TokenKey;
bg: TokenKey;
minContrast: number;
largeText?: boolean;
}>;
};
type RuntimeStatusTone = Record<StatusTone, `#${string}`>;
type NeutralTokenElement = 'bg' | 'text' | 'border' | 'icon' | 'ring';
type BaseShape = {
fixedAnchors: Record<Mode, Partial<Record<Tone, `#${string}`>>>;
};
const NEUTRAL_EMPHASIS = [
const BASE = baseColorTokens as BaseShape;
const SEMANTIC = semanticTokens as SemanticShape;
const LEGACY_NEUTRAL_EMPHASIS: Emphasis[] = [
'subtle',
'muted',
'default',
'strong',
'inverse',
] as const;
const UI_STATES = [
];
const LEGACY_UI_STATES: State[] = [
'default',
'hover',
'active',
'selected',
'focus',
'disabled',
] as const;
];
type NeutralEmphasis = (typeof NEUTRAL_EMPHASIS)[number];
type UiState = (typeof UI_STATES)[number];
type NeutralStateMatrix = Record<NeutralEmphasis, Record<UiState, string>>;
type NeutralStateMatrix = Record<Emphasis, Record<State, string>>;
function mergeAdjustment(...values: Array<Adjustment | undefined>): Adjustment {
const out: Adjustment = {};
for (const value of values) {
if (!value) continue;
if (typeof value.dL === 'number') out.dL = (out.dL ?? 0) + value.dL;
if (typeof value.dC === 'number') out.dC = (out.dC ?? 0) + value.dC;
if (typeof value.dH === 'number') out.dH = (out.dH ?? 0) + value.dH;
if (typeof value.alpha === 'number') out.alpha = value.alpha;
}
return out;
}
function applyAdjustment(base: Oklch, adjustment: Adjustment): Oklch {
return {
l: clamp(base.l + (adjustment.dL ?? 0), 0, 1),
c: Math.max(0, base.c + (adjustment.dC ?? 0)),
h: normalizeHue(base.h + (adjustment.dH ?? 0)),
};
}
function setTokenIfMissing(
tokens: ThemeTokens,
@ -79,11 +141,11 @@ function setTokenIfMissing(
function ensureNeutralMatrix(
tokens: ThemeTokens,
element: NeutralTokenElement,
element: Element,
matrix: NeutralStateMatrix
): void {
for (const emphasis of NEUTRAL_EMPHASIS) {
for (const state of UI_STATES) {
for (const emphasis of LEGACY_NEUTRAL_EMPHASIS) {
for (const state of LEGACY_UI_STATES) {
setTokenIfMissing(
tokens,
`${element}.neutral.${emphasis}.${state}` as TokenKey,
@ -93,40 +155,18 @@ function ensureNeutralMatrix(
}
}
function resolveStatusToneBase(
accent: `#${string}`,
ink: `#${string}`,
background: `#${string}`
): RuntimeStatusTone {
return {
'status-running': accent,
'status-splitting': mix(accent, '#2563eb', 0.46),
'status-pending': mix(accent, '#d97706', 0.65),
'status-error': mix(accent, '#dc2626', 0.76),
'status-reassigning': mix(accent, '#a16207', 0.62),
'status-completed': mix(accent, '#16a34a', 0.62),
'status-blocked': mix(accent, '#ca8a04', 0.62),
'status-paused': mix(accent, '#a16207', 0.48),
'status-skipped': mix(ink, background, 0.46),
'status-cancelled': mix(ink, background, 0.56),
};
}
function buildCoreTokens(
accent: `#${string}`,
ink: `#${string}`,
background: `#${string}`,
contrastT: number
function buildLegacyNeutralContrastTokens(
seed: { accent: `#${string}`; background: `#${string}`; ink: `#${string}` },
contrast: number
): ThemeTokens {
const tokens: ThemeTokens = {};
const contrastT = clamp(contrast, 0, 100) / 100;
const { background, ink } = seed;
// Required core formulas (contract-controlled by contrast)
const panel = mix(background, ink, 0.02 + contrastT * 0.06);
const border = alpha(ink, 0.08 + contrastT * 0.16);
const textSecondary = mix(ink, background, 0.28 - contrastT * 0.08);
const hover = mix(background, ink, 0.03 + contrastT * 0.05);
// Extended derived values for consistency across interactive states.
const active = mix(background, ink, 0.05 + contrastT * 0.08);
const panelStrong = mix(background, ink, 0.04 + contrastT * 0.08);
const panelSelected = mix(background, ink, 0.1 + contrastT * 0.1);
@ -159,7 +199,6 @@ function buildCoreTokens(
const textInverse = chooseReadableText(inverseBgDefault, ink);
tokens['bg.neutral.subtle.default'] = background;
// Hover between canvas and selected (used by nav tabs, outline buttons, etc.)
tokens['bg.neutral.subtle.hover'] = subtleHover;
tokens['bg.neutral.subtle.selected'] = subtleSelected;
tokens['bg.neutral.default.default'] = panel;
@ -168,7 +207,6 @@ function buildCoreTokens(
tokens['bg.neutral.default.selected'] = panelSelected;
tokens['bg.neutral.strong.default'] = panelStrong;
tokens['bg.neutral.strong.selected'] = panelSelectedStrong;
// Muted surfaces remain solid fills (no alpha overlays).
tokens['bg.neutral.muted.default'] = mutedDefault;
tokens['bg.neutral.muted.hover'] = mutedHover;
tokens['bg.neutral.muted.active'] = mutedActive;
@ -180,28 +218,13 @@ function buildCoreTokens(
tokens['text.neutral.subtle.default'] = textMuted;
tokens['text.neutral.muted.disabled'] = textDisabled;
const accentHover = mix(accent, ink, 0.08 + contrastT * 0.08);
const accentActive = mix(accent, ink, 0.14 + contrastT * 0.1);
// Brand buttons should prefer white text when contrast is acceptable for
// UI button label sizing; fall back to darker text for very light accents.
const accentForeground = chooseReadableText(accent, '#ffffff', 3);
tokens['bg.brand.default.default'] = accent;
tokens['bg.brand.default.hover'] = accentHover;
tokens['bg.brand.default.active'] = accentActive;
tokens['text.brand.inverse.default'] = accentForeground;
tokens['icon.brand.default.default'] = accent;
tokens['border.neutral.subtle.default'] = alpha(ink, 0.05 + contrastT * 0.1);
tokens['border.neutral.muted.default'] = alpha(ink, 0.07 + contrastT * 0.12);
tokens['border.neutral.muted.hover'] = alpha(ink, 0.1 + contrastT * 0.14);
tokens['border.neutral.muted.disabled'] = alpha(ink, 0.05 + contrastT * 0.08);
tokens['border.neutral.default.default'] = border;
tokens['border.neutral.strong.default'] = alpha(ink, 0.14 + contrastT * 0.2);
tokens['border.brand.default.focus'] = alpha(accent, 0.55 + contrastT * 0.25);
tokens['ring.neutral.subtle.focus'] = alpha(ink, 0.2 + contrastT * 0.2);
tokens['ring.brand.default.focus'] = alpha(accent, 0.45 + contrastT * 0.3);
tokens['icon.neutral.default.default'] = textSecondary;
tokens['icon.neutral.muted.default'] = textMuted;
@ -423,110 +446,253 @@ function buildCoreTokens(
return tokens;
}
function buildStatusTokens(
accent: `#${string}`,
ink: `#${string}`,
background: `#${string}`,
contrastT: number
): ThemeTokens {
const tokens: ThemeTokens = {};
const statusBase = resolveStatusToneBase(accent, ink, background);
function parseCssColor(
color: string | undefined
): { oklch: Oklch; alpha?: number } | null {
const hex = parseHexOnly(color);
if (hex) return { oklch: hexToOklch(hex) };
for (const tone of STATUS_TONES) {
const base = statusBase[tone];
const bgSubtleDefault = mix(background, base, 0.1 + contrastT * 0.1);
const bgSubtleHover = mix(background, base, 0.14 + contrastT * 0.12);
const bgDefault = mix(background, base, 0.2 + contrastT * 0.14);
tokens[`bg.${tone}.subtle.default`] = bgSubtleDefault;
tokens[`bg.${tone}.subtle.hover`] = bgSubtleHover;
tokens[`bg.${tone}.default.default`] = bgDefault;
tokens[`border.${tone}.default.default`] = alpha(
base,
0.32 + contrastT * 0.28
if (!color) return null;
const rgbaMatch = color
.trim()
.match(
/^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|0?\.\d+|1(?:\.0+)?)\s*\)$/i
);
tokens[`border.${tone}.default.focus`] = alpha(
base,
0.45 + contrastT * 0.3
);
tokens[`ring.${tone}.default.focus`] = alpha(base, 0.4 + contrastT * 0.35);
if (!rgbaMatch) return null;
tokens[`icon.${tone}.default.default`] = base;
tokens[`text.${tone}.strong.default`] = chooseReadableText(
bgSubtleDefault,
mix(base, ink, 0.2)
);
tokens[`text.${tone}.muted.default`] = mix(base, ink, 0.35);
const r = clamp(Number(rgbaMatch[1]), 0, 255);
const g = clamp(Number(rgbaMatch[2]), 0, 255);
const b = clamp(Number(rgbaMatch[3]), 0, 255);
const a = clamp(Number(rgbaMatch[4]), 0, 1);
const parsedHex = rgbToHex(r, g, b);
return { oklch: hexToOklch(parsedHex), alpha: a };
}
function contrastBias(
element: Element,
mode: Mode,
contrast: number
): Adjustment {
const t = clamp(contrast, 0, 100) / 100;
const direction = mode === 'dark' ? 1 : -1;
if (element === 'bg') {
return { dL: direction * (0.01 + t * 0.07) };
}
if (element === 'text' || element === 'icon') {
return { dL: direction * (0.02 + t * 0.08) };
}
if (element === 'border' || element === 'ring') {
return { dL: direction * (0.01 + t * 0.05) };
}
return {};
}
function toneBaseColor(
tone: Tone,
mode: Mode,
seed: { accent: `#${string}`; background: `#${string}`; ink: `#${string}` },
element: Element
): Oklch {
const spec = SEMANTIC.toneSource[tone];
const source = spec.sourceByElement?.[element] ?? spec.source;
const sourceHex =
source === 'fixed'
? (BASE.fixedAnchors[mode][tone] ?? seed.accent)
: seed[source as 'accent' | 'background' | 'ink'];
const base = hexToOklch(sourceHex);
return applyAdjustment(base, {
dL:
(spec.dL ?? 0) +
(mode === 'light' ? (spec.dLLight ?? 0) : (spec.dLDark ?? 0)),
dC: spec.dC ?? 0,
dH: spec.dH ?? 0,
});
}
function parseHexOnly(color: string | undefined): `#${string}` | null {
if (!color) return null;
const trimmed = color.trim().toLowerCase();
if (!/^#[0-9a-f]{6}$/.test(trimmed)) return null;
return trimmed as `#${string}`;
}
function solveForegroundContrast(
fgHex: `#${string}`,
bgHex: `#${string}`,
minContrast: number
): `#${string}` {
const current = contrastRatio(fgHex, bgHex);
if (current >= minContrast) return fgHex;
const base = hexToOklch(fgHex);
let best: { hex: `#${string}`; delta: number } | null = null;
for (let i = 0; i <= 100; i += 1) {
const targetL = i / 100;
const probeHex = oklchToHex({ l: targetL, c: base.c, h: base.h });
const ratio = contrastRatio(probeHex, bgHex);
if (ratio >= minContrast) {
const delta = Math.abs(targetL - base.l);
if (!best || delta < best.delta) best = { hex: probeHex, delta };
}
}
if (best) return best.hex;
// Chroma reduction pass if pure lightness search was insufficient.
for (let i = 0; i <= 100; i += 1) {
const targetC = (base.c * (100 - i)) / 100;
const probeHex = oklchToHex({ l: base.l, c: targetC, h: base.h });
if (contrastRatio(probeHex, bgHex) >= minContrast) return probeHex;
}
return chooseReadableText(bgHex, fgHex, minContrast);
}
function enforceContrastPairs(tokens: ThemeTokens): {
tokens: ThemeTokens;
diagnostics: ContrastDiagnostic[];
} {
const out: ThemeTokens = { ...tokens };
const diagnostics: ContrastDiagnostic[] = [];
for (const pair of SEMANTIC.contrastPairs) {
const fgToken = pair.fg;
const bgToken = pair.bg;
const fgValue = out[fgToken];
const bgValue = out[bgToken];
const fgHex = parseHexOnly(fgValue);
const bgHex = parseHexOnly(bgValue);
if (!fgHex || !bgHex) continue;
const minRequired = Math.max(
pair.minContrast ?? 0,
wcagMinimumContrast(pair.largeText)
);
const solvedFg = solveForegroundContrast(fgHex, bgHex, minRequired);
out[fgToken] = solvedFg;
const ratio = contrastRatio(solvedFg, bgHex);
diagnostics.push({
fg: fgToken,
bg: bgToken,
ratio,
minRequired,
passes: ratio >= minRequired,
apcaLc: apcaContrastApprox(solvedFg, bgHex),
});
}
return { tokens: out, diagnostics };
}
function buildSemanticTokens(
contract: ThemeContractV2,
seed: ResolvedThemeV2['seed']
) {
const tokens: ThemeTokens = {};
const { elements, tones, emphasis, states } = SEMANTIC.axes;
const legacyNeutralTokens = buildLegacyNeutralContrastTokens(
seed,
contract.contrast
);
for (const tone of tones) {
for (const emph of emphasis) {
for (const state of states) {
const tokenSuffix = `${tone}.${emph}.${state}` as const;
const baseAdjustment = mergeAdjustment(
SEMANTIC.transforms.emphasis[emph],
SEMANTIC.transforms.state[state]
);
const axisOverride = mergeAdjustment(
contract.overrides?.tone?.[tone],
contract.overrides?.emphasis?.[emph],
contract.overrides?.state?.[state],
contract.overrides?.cell?.[tokenSuffix]
);
for (const element of elements) {
const tokenKey = `${element}.${tokenSuffix}` as TokenKey;
if (tone === 'neutral') {
const legacy = parseCssColor(legacyNeutralTokens[tokenKey]);
if (legacy) {
const adjusted = applyAdjustment(legacy.oklch, axisOverride);
const legacyHex = oklchToHex(adjusted);
const resolvedAlpha =
typeof axisOverride.alpha === 'number'
? axisOverride.alpha
: legacy.alpha;
tokens[tokenKey] =
typeof resolvedAlpha === 'number' && resolvedAlpha < 1
? alpha(legacyHex, resolvedAlpha)
: legacyHex;
continue;
}
}
const toneBase = toneBaseColor(tone, contract.mode, seed, element);
const adjustment = mergeAdjustment(
baseAdjustment,
SEMANTIC.transforms.element[element],
contrastBias(element, contract.mode, contract.contrast),
axisOverride
);
const colorHex = oklchToHex(applyAdjustment(toneBase, adjustment));
const value =
typeof adjustment.alpha === 'number' && adjustment.alpha < 1
? alpha(colorHex, adjustment.alpha)
: colorHex;
tokens[tokenKey] = value;
}
}
}
}
return tokens;
}
function buildFixedToneTokens(
mode: Mode,
background: `#${string}`,
ink: `#${string}`,
contrastT: number,
schema: FixedToneSchema = DEFAULT_FIXED_TONE_SCHEMA
/**
* Tones used for filled primary-style controls (`button` `TONE_PRIMARY`): same rule as
* brand prefer near-white on saturated fills (WCAG large-text ~3:1), else best black/white.
*/
const FILLED_ACCENT_INVERSE_TONES: Tone[] = [
'brand',
'success',
'error',
'warning',
'information',
];
function applyFilledAccentInverseTextHeuristic(
tokens: ThemeTokens
): ThemeTokens {
const tokens: ThemeTokens = {};
const fixed = schema[mode];
for (const [tone, values] of Object.entries(fixed) as Array<
[FixedTone, FixedToneSeed]
>) {
const base = values.color;
const bgSubtleDefault = mix(background, base, 0.1 + contrastT * 0.1);
const bgSubtleHover = mix(background, base, 0.14 + contrastT * 0.12);
const bgSubtleActive = mix(background, base, 0.18 + contrastT * 0.14);
const bgDefault = mix(background, base, 0.2 + contrastT * 0.14);
const textHover = mix(base, ink, 0.12 + contrastT * 0.06);
const textActive = mix(base, ink, 0.2 + contrastT * 0.08);
const borderDefault = alpha(base, 0.32 + contrastT * 0.28);
const borderHover = alpha(base, 0.38 + contrastT * 0.3);
const borderFocus = alpha(base, 0.45 + contrastT * 0.3);
// Text: full emphasis ramp (UI state `default`). Regular semantic color is
// `text.<tone>.default.default` (e.g. `text.error.default.default`).
tokens[`text.${tone}.subtle.default`] = mix(
base,
background,
0.48 + contrastT * 0.1
);
tokens[`text.${tone}.muted.default`] = mix(
base,
ink,
0.18 + contrastT * 0.06
);
tokens[`text.${tone}.default.default`] = base;
tokens[`text.${tone}.default.hover`] = textHover;
tokens[`text.${tone}.default.active`] = textActive;
tokens[`text.${tone}.strong.default`] = mix(
base,
ink,
0.4 + contrastT * 0.12
);
tokens[`text.${tone}.inverse.default`] = chooseReadableText(bgDefault, ink);
tokens[`icon.${tone}.default.default`] = base;
tokens[`icon.${tone}.default.hover`] = textHover;
tokens[`icon.${tone}.default.active`] = textActive;
tokens[`border.${tone}.default.default`] = borderDefault;
tokens[`border.${tone}.default.hover`] = borderHover;
tokens[`border.${tone}.default.focus`] = borderFocus;
tokens[`ring.${tone}.default.focus`] = alpha(base, 0.4 + contrastT * 0.35);
tokens[`bg.${tone}.subtle.default`] = bgSubtleDefault;
tokens[`bg.${tone}.subtle.hover`] = bgSubtleHover;
tokens[`bg.${tone}.subtle.active`] = bgSubtleActive;
tokens[`bg.${tone}.subtle.selected`] =
values.selectedBg ?? mix(background, base, 0.16 + contrastT * 0.12);
tokens[`bg.${tone}.default.default`] = bgDefault;
const out: ThemeTokens = { ...tokens };
for (const tone of FILLED_ACCENT_INVERSE_TONES) {
for (const state of SEMANTIC.axes.states) {
const bgKey = `bg.${tone}.default.${state}` as TokenKey;
const textKey = `text.${tone}.inverse.${state}` as TokenKey;
const bgHex = parseHexOnly(out[bgKey]);
if (!bgHex) continue;
out[textKey] = chooseReadableText(bgHex, '#ffffff', 3);
}
}
return out;
}
return tokens;
function buildComponentAliasVariables(
tokens: ThemeTokens
): Record<string, string> {
const resolved = resolveExtends(componentTokens as Record<string, unknown>);
const leaves = flattenDtcgTokens(resolved);
const out: Record<string, string> = {};
for (const leaf of leaves) {
if (typeof leaf.value !== 'string') continue;
const resolvedValue = resolveAliasReferences(leaf.value, (path) => {
const key = path as TokenKey;
return tokens[key];
});
const cssVar = (leaf.extensions?.cssVar as string | undefined) ?? null;
if (!cssVar || !resolvedValue) continue;
out[cssVar] = resolvedValue;
}
return out;
}
function toCssVariables(tokens: ThemeTokens): Record<string, string> {
@ -538,76 +704,59 @@ function toCssVariables(tokens: ThemeTokens): Record<string, string> {
return variables;
}
function resolveContract(contract: ThemeContractV1): ThemeContractV1 {
function normalizeContract(contract: ThemeContractV2): ThemeContractV2 {
return {
...contract,
contrast: clamp(Math.round(contract.contrast), 0, 100),
};
}
function getThemeSeed(
contract: ThemeContractV1,
catalog: ThemeCatalog
): {
colorThemeId: string;
seed: ResolvedThemeV1['seed'];
} {
const definition = getColorThemeDefinition(
function getThemeSeed(contract: ThemeContractV2, catalog: ThemeCatalogV2) {
const definition = getColorThemeDefinitionV2(
contract.mode,
contract.colorThemeId,
contract.themeId,
catalog
);
return {
colorThemeId: definition.id,
themeId: definition.id,
seed: definition.seed,
};
}
export function buildThemeV1(
contract: ThemeContractV1,
catalog: ThemeCatalog = DEFAULT_THEME_CATALOG
): ResolvedThemeV1 {
const resolvedContract = resolveContract(contract);
const { colorThemeId, seed } = getThemeSeed(resolvedContract, catalog);
const contrastT = resolvedContract.contrast / 100;
export function buildThemeV2(
contract: ThemeContractV2,
catalog: ThemeCatalogV2 = DEFAULT_THEME_CATALOG
): ResolvedThemeV2 {
const normalized = normalizeContract(contract);
const { seed, themeId } = getThemeSeed(normalized, catalog);
const core = buildCoreTokens(
seed.accent,
seed.ink,
seed.background,
contrastT
);
const status = buildStatusTokens(
seed.accent,
seed.ink,
seed.background,
contrastT
);
const fixedTone = buildFixedToneTokens(
resolvedContract.mode,
seed.background,
seed.ink,
contrastT
);
const tokens: ThemeTokens = {
...core,
...status,
...fixedTone,
const semantic = buildSemanticTokens(normalized, seed);
const accentInverseAdjusted = applyFilledAccentInverseTextHeuristic(semantic);
const enforced = enforceContrastPairs(accentInverseAdjusted);
const semanticCssVars = toCssVariables(enforced.tokens);
const componentVars = buildComponentAliasVariables(enforced.tokens);
const cssVariables = {
...semanticCssVars,
...componentVars,
'--ds-theme-contrast': String(normalized.contrast),
};
return {
contract: {
...resolvedContract,
colorThemeId,
...normalized,
themeId,
},
seed,
tokens,
cssVariables: toCssVariables(tokens),
tokens: enforced.tokens,
cssVariables,
diagnostics: {
contrast: enforced.diagnostics,
},
};
}
export function applyResolvedThemeToElement(
resolvedTheme: ResolvedThemeV1,
resolvedTheme: ResolvedThemeV2,
element: HTMLElement
): void {
for (const [name, value] of Object.entries(resolvedTheme.cssVariables)) {
@ -615,12 +764,25 @@ export function applyResolvedThemeToElement(
}
}
export function applyThemeContractV1(
contract: ThemeContractV1,
export function applyThemeContractV2(
contract: ThemeContractV2,
element: HTMLElement,
catalog: ThemeCatalog = DEFAULT_THEME_CATALOG
): ResolvedThemeV1 {
const resolved = buildThemeV1(contract, catalog);
catalog: ThemeCatalogV2 = DEFAULT_THEME_CATALOG
): ResolvedThemeV2 {
const resolved = buildThemeV2(contract, catalog);
applyResolvedThemeToElement(resolved, element);
return resolved;
}
export function createApcaDiagnosticsReport(
resolvedTheme: ResolvedThemeV2
): string {
return JSON.stringify(
{
contract: resolvedTheme.contract,
diagnostics: resolvedTheme.diagnostics,
},
null,
2
);
}

View file

@ -0,0 +1,306 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { describe, expect, it } from 'vitest';
import { DEFAULT_THEME_CATALOG, createDefaultThemeContractV2 } from './catalog';
import { contrastRatio } from './colorMath';
import {
applyThemeContractV2,
buildThemeV2,
createApcaDiagnosticsReport,
} from './engine';
import {
TOKEN_ELEMENTS,
TOKEN_EMPHASIS,
TOKEN_TONES,
TOKEN_UI_STATES,
type Mode,
type ThemeCatalogV2,
} from './types';
function isHex(value: string): boolean {
return /^#[0-9a-f]{6}$/i.test(value);
}
function isRgba(value: string): boolean {
return /^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(0|0?\.\d+|1(?:\.0+)?)\s*\)$/i.test(
value
);
}
function relativeLuminance(hex: string): number {
const clean = hex.replace('#', '');
const r = parseInt(clean.slice(0, 2), 16) / 255;
const g = parseInt(clean.slice(2, 4), 16) / 255;
const b = parseInt(clean.slice(4, 6), 16) / 255;
const linear = (channel: number) =>
channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
}
function randomHex(): `#${string}` {
const num = Math.floor(Math.random() * 0xffffff);
return `#${num.toString(16).padStart(6, '0')}` as `#${string}`;
}
describe('themeTokens v2 engine', () => {
it('is deterministic for a fixed contract and catalog', () => {
const contract = createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 52,
});
const first = buildThemeV2(contract, DEFAULT_THEME_CATALOG);
const second = buildThemeV2(contract, DEFAULT_THEME_CATALOG);
expect(first.tokens).toStrictEqual(second.tokens);
expect(first.cssVariables).toStrictEqual(second.cssVariables);
});
it('enforces override precedence with cell override as final authority', () => {
const baseContract = createDefaultThemeContractV2('light', {
themeId: 'codex',
contrast: 50,
});
const base = buildThemeV2(baseContract, DEFAULT_THEME_CATALOG);
const overridden = buildThemeV2(
{
...baseContract,
overrides: {
tone: { brand: { dL: 0.2, dC: 0.05 } },
emphasis: { default: { dL: -0.1 } },
state: { default: { dL: -0.08 } },
cell: { 'brand.default.default': { dL: 0.01, dC: 0.001, dH: 3 } },
},
},
DEFAULT_THEME_CATALOG
);
const key = 'bg.brand.default.default';
expect(overridden.tokens[key]).not.toBe(base.tokens[key]);
});
it('produces a full tone × emphasis × state × element matrix', () => {
const theme = buildThemeV2(
createDefaultThemeContractV2('dark', { themeId: 'camel', contrast: 63 }),
DEFAULT_THEME_CATALOG
);
const expected =
TOKEN_ELEMENTS.length *
TOKEN_TONES.length *
TOKEN_EMPHASIS.length *
TOKEN_UI_STATES.length;
expect(Object.keys(theme.tokens)).toHaveLength(expected);
});
it('ensures all generated tokens are valid CSS colors', () => {
const theme = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'claude',
contrast: 40,
}),
DEFAULT_THEME_CATALOG
);
for (const value of Object.values(theme.tokens)) {
if (!value) continue;
expect(isHex(value) || isRgba(value)).toBe(true);
}
});
it('enforces required WCAG pairs and emits APCA diagnostics', () => {
const theme = buildThemeV2(
createDefaultThemeContractV2('dark', { themeId: 'eigent', contrast: 80 }),
DEFAULT_THEME_CATALOG
);
expect(theme.diagnostics.contrast.length).toBeGreaterThan(0);
for (const item of theme.diagnostics.contrast) {
expect(item.ratio).toBeGreaterThanOrEqual(item.minRequired);
expect(Number.isFinite(item.apcaLc)).toBe(true);
}
const report = createApcaDiagnosticsReport(theme);
const parsed = JSON.parse(report) as {
diagnostics: { contrast: Array<{ apcaLc: number }> };
};
expect(parsed.diagnostics.contrast.length).toBe(
theme.diagnostics.contrast.length
);
});
it('applies and re-applies themes without stale root values', () => {
const root = document.createElement('div');
const light = applyThemeContractV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 40,
}),
root
);
const firstBg = root.style.getPropertyValue(
'--ds-bg-neutral-subtle-default'
);
expect(firstBg).toBe(light.cssVariables['--ds-bg-neutral-subtle-default']);
const dark = applyThemeContractV2(
createDefaultThemeContractV2('dark', { themeId: 'eigent', contrast: 70 }),
root
);
const secondBg = root.style.getPropertyValue(
'--ds-bg-neutral-subtle-default'
);
expect(secondBg).toBe(dark.cssVariables['--ds-bg-neutral-subtle-default']);
expect(secondBg).not.toBe(firstBg);
});
it('keeps neutral surface polarity aligned with mode', () => {
const light = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 50,
}),
DEFAULT_THEME_CATALOG
);
const dark = buildThemeV2(
createDefaultThemeContractV2('dark', { themeId: 'eigent', contrast: 50 }),
DEFAULT_THEME_CATALOG
);
const lightBg = light.tokens['bg.neutral.subtle.default'] as string;
const darkBg = dark.tokens['bg.neutral.subtle.default'] as string;
const lightText = light.tokens['text.neutral.default.default'] as string;
const darkText = dark.tokens['text.neutral.default.default'] as string;
expect(relativeLuminance(lightBg)).toBeGreaterThan(
relativeLuminance(darkBg)
);
expect(relativeLuminance(darkText)).toBeGreaterThan(
relativeLuminance(lightText)
);
});
it('uses legacy-style monotonic contrast response for neutral tokens', () => {
const lightLow = buildThemeV2(
createDefaultThemeContractV2('light', { themeId: 'eigent', contrast: 0 }),
DEFAULT_THEME_CATALOG
);
const lightHigh = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 100,
}),
DEFAULT_THEME_CATALOG
);
const darkLow = buildThemeV2(
createDefaultThemeContractV2('dark', { themeId: 'eigent', contrast: 0 }),
DEFAULT_THEME_CATALOG
);
const darkHigh = buildThemeV2(
createDefaultThemeContractV2('dark', {
themeId: 'eigent',
contrast: 100,
}),
DEFAULT_THEME_CATALOG
);
const lightBgLow = lightLow.tokens['bg.neutral.default.default'] as string;
const lightBgHigh = lightHigh.tokens[
'bg.neutral.default.default'
] as string;
const darkBgLow = darkLow.tokens['bg.neutral.default.default'] as string;
const darkBgHigh = darkHigh.tokens['bg.neutral.default.default'] as string;
const lightTextLow = lightLow.tokens[
'text.neutral.muted.default'
] as string;
const lightTextHigh = lightHigh.tokens[
'text.neutral.muted.default'
] as string;
const darkTextLow = darkLow.tokens['text.neutral.muted.default'] as string;
const darkTextHigh = darkHigh.tokens[
'text.neutral.muted.default'
] as string;
expect(relativeLuminance(lightBgHigh)).toBeLessThan(
relativeLuminance(lightBgLow)
);
expect(relativeLuminance(darkBgHigh)).toBeGreaterThan(
relativeLuminance(darkBgLow)
);
expect(relativeLuminance(lightTextHigh)).toBeLessThan(
relativeLuminance(lightTextLow)
);
expect(relativeLuminance(darkTextHigh)).toBeGreaterThan(
relativeLuminance(darkTextLow)
);
});
it('keeps brand inverse text white for black brand fills', () => {
const theme = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 50,
}),
DEFAULT_THEME_CATALOG
);
expect(theme.tokens['bg.brand.default.default']).toBe('#000000');
expect(theme.tokens['text.brand.inverse.default']).toBe('#ffffff');
});
it('keeps success inverse text as light as brand inverse on filled success (light)', () => {
const theme = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 50,
}),
DEFAULT_THEME_CATALOG
);
const successBg = theme.tokens['bg.success.default.default'] as string;
const successInverse = theme.tokens[
'text.success.inverse.default'
] as string;
expect(contrastRatio(successInverse, successBg)).toBeGreaterThanOrEqual(3);
expect(successInverse).toBe('#ffffff');
});
it('remains stable under randomized theme seeds', () => {
for (let i = 0; i < 20; i += 1) {
const mode: Mode = i % 2 === 0 ? 'light' : 'dark';
const themeId = `random-${i}`;
const catalog: ThemeCatalogV2 = {
...DEFAULT_THEME_CATALOG,
[mode]: {
...DEFAULT_THEME_CATALOG[mode],
[themeId]: {
id: themeId,
mode,
seed: {
accent: randomHex(),
background: randomHex(),
ink: randomHex(),
},
},
},
};
const theme = buildThemeV2(
createDefaultThemeContractV2(mode, {
themeId,
contrast: Math.floor(Math.random() * 101),
}),
catalog
);
for (const value of Object.values(theme.tokens)) {
if (!value) continue;
expect(isHex(value) || isRgba(value)).toBe(true);
}
}
});
});

View file

@ -1,96 +0,0 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import type { FixedTone, Mode } from './types';
export type FixedToneSeed = {
// Base tone color for text/icon and derived border/bg states.
color: `#${string}`;
// Optional explicit selected background. Falls back to derived value when omitted.
selectedBg?: string;
};
export type FixedToneSchema = Record<Mode, Record<FixedTone, FixedToneSeed>>;
// Developer-owned fixed tones. Not user-editable through theme import/customization UI.
export const DEFAULT_FIXED_TONE_SCHEMA: FixedToneSchema = {
light: {
'single-agent': {
color: '#7e22ce',
selectedBg: '#f3e8ff',
},
workforce: {
color: '#007a55',
selectedBg: '#d0fae5',
},
browser: {
color: '#0084d1',
},
terminal: {
color: '#009966',
},
document: {
color: '#e17100',
},
success: {
color: '#00a63e',
},
caution: {
color: '#e7000b',
},
error: {
color: '#e7000b',
},
warning: {
color: '#d08700',
},
information: {
color: '#155dfc',
},
},
dark: {
'single-agent': {
color: '#e9d5ff',
selectedBg: 'rgba(168, 85, 247, 0.22)',
},
workforce: {
color: '#6ee7b7',
selectedBg: 'rgba(52, 211, 153, 0.2)',
},
browser: {
color: '#7dd3fc',
},
terminal: {
color: '#6ee7b7',
},
document: {
color: '#ffd479',
},
success: {
color: '#4ade80',
},
caution: {
color: '#f87171',
},
error: {
color: '#f87171',
},
warning: {
color: '#facc15',
},
information: {
color: '#7ab3ff',
},
},
};

View file

@ -13,8 +13,7 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
export * from './catalog';
export * from './dtcg';
export * from './engine';
export * from './fixedToneSchema';
export * from './legacyMapping';
export * from './naming';
export * from './types';

View file

@ -1,172 +0,0 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { tokenKeyToCssVarValue } from './naming';
import type { TokenKey } from './types';
export type LegacyTokenMappingEntry = {
legacyVariable: string;
token: TokenKey;
notes?: string;
};
export const LEGACY_TOKEN_MAPPING: LegacyTokenMappingEntry[] = [
// Task/badge lifecycle semantics
{
legacyVariable: '--badge-running-surface',
token: 'bg.status-running.subtle.default',
},
{
legacyVariable: '--badge-running-surface-foreground',
token: 'text.status-running.strong.default',
},
{
legacyVariable: '--badge-splitting-surface',
token: 'bg.status-splitting.subtle.default',
},
{
legacyVariable: '--badge-splitting-surface-foreground',
token: 'text.status-splitting.strong.default',
},
{
legacyVariable: '--badge-paused-surface',
token: 'bg.status-paused.subtle.default',
},
{
legacyVariable: '--badge-paused-surface-foreground',
token: 'text.status-paused.strong.default',
},
{
legacyVariable: '--badge-error-surface',
token: 'bg.status-error.subtle.default',
},
{
legacyVariable: '--badge-error-surface-foreground',
token: 'text.status-error.strong.default',
},
{
legacyVariable: '--badge-complete-surface',
token: 'bg.status-completed.subtle.default',
},
{
legacyVariable: '--badge-complete-surface-foreground',
token: 'text.status-completed.strong.default',
},
{
legacyVariable: '--task-fill-running',
token: 'bg.status-running.subtle.default',
},
{
legacyVariable: '--task-fill-success',
token: 'bg.status-completed.subtle.default',
},
{
legacyVariable: '--task-fill-warning',
token: 'bg.status-blocked.subtle.default',
notes:
'In task cards this often also represents reassigning, which should migrate to status-reassigning.',
},
{
legacyVariable: '--task-fill-error',
token: 'bg.status-error.subtle.default',
},
{
legacyVariable: '--task-border-focus-success',
token: 'border.status-completed.default.focus',
},
{
legacyVariable: '--task-border-focus-warning',
token: 'border.status-blocked.default.focus',
},
{
legacyVariable: '--task-border-focus-error',
token: 'border.status-error.default.focus',
},
// Generic semantic colors currently used in status-like UI contexts
{
legacyVariable: '--surface-success',
token: 'bg.status-completed.subtle.default',
},
{
legacyVariable: '--surface-information',
token: 'bg.status-splitting.subtle.default',
},
{
legacyVariable: '--surface-warning',
token: 'bg.status-pending.subtle.default',
},
{
legacyVariable: '--surface-cuation',
token: 'bg.status-error.subtle.default',
notes:
'Spelling kept for legacy compatibility. New naming should use error.',
},
{
legacyVariable: '--text-success',
token: 'text.status-completed.strong.default',
},
{
legacyVariable: '--text-information',
token: 'text.status-splitting.strong.default',
},
{
legacyVariable: '--text-warning',
token: 'text.status-pending.strong.default',
},
{
legacyVariable: '--text-cuation',
token: 'text.status-error.strong.default',
},
{
legacyVariable: '--border-success',
token: 'border.status-completed.default.default',
},
{
legacyVariable: '--border-information',
token: 'border.status-splitting.default.default',
},
{
legacyVariable: '--border-warning',
token: 'border.status-pending.default.default',
},
{
legacyVariable: '--border-cuation',
token: 'border.status-error.default.default',
},
{
legacyVariable: '--icon-success',
token: 'icon.status-completed.default.default',
},
{
legacyVariable: '--icon-information',
token: 'icon.status-splitting.default.default',
},
{
legacyVariable: '--icon-warning',
token: 'icon.status-pending.default.default',
},
{
legacyVariable: '--icon-cuation',
token: 'icon.status-error.default.default',
},
];
export function buildLegacyAliasVariableValues(): Record<string, string> {
const out: Record<string, string> = {};
for (const item of LEGACY_TOKEN_MAPPING) {
out[item.legacyVariable] = tokenKeyToCssVarValue(item.token);
}
return out;
}

View file

@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
export const THEME_CONTRACT_VERSION = 1 as const;
export const THEME_CONTRACT_VERSION = 2 as const;
export const TOKEN_ELEMENTS = ['bg', 'text', 'border', 'icon', 'ring'] as const;
export const TOKEN_EMPHASIS = [
@ -30,21 +30,19 @@ export const TOKEN_UI_STATES = [
'focus',
'disabled',
] as const;
export const TASK_SEMANTIC_STATES = [
'running',
'splitting',
'pending',
'error',
'reassigning',
'completed',
'blocked',
'paused',
'skipped',
'cancelled',
] as const;
export const FIXED_TONES = [
export const TOKEN_TONES = [
'neutral',
'brand',
'status-running',
'status-splitting',
'status-pending',
'status-error',
'status-reassigning',
'status-completed',
'status-blocked',
'status-paused',
'status-skipped',
'status-cancelled',
'single-agent',
'workforce',
'browser',
@ -59,41 +57,85 @@ export const FIXED_TONES = [
export type Mode = 'light' | 'dark';
export type ThemeContractV1 = {
version: typeof THEME_CONTRACT_VERSION;
mode: Mode;
colorThemeId: string;
contrast: number; // 0..100
export type Element = (typeof TOKEN_ELEMENTS)[number];
export type Emphasis = (typeof TOKEN_EMPHASIS)[number];
export type State = (typeof TOKEN_UI_STATES)[number];
export type Tone = (typeof TOKEN_TONES)[number];
export type TokenKey = `${Element}.${Tone}.${Emphasis}.${State}`;
export type Adjustment = {
dL?: number;
dC?: number;
dH?: number;
alpha?: number;
};
export type ThemeSeed = {
export type ThemeContractV2 = {
version: typeof THEME_CONTRACT_VERSION;
mode: Mode;
themeId: string;
contrast: number; // 0..100
overrides?: {
tone?: Partial<Record<Tone, Adjustment>>;
emphasis?: Partial<Record<Emphasis, Adjustment>>;
state?: Partial<Record<State, Adjustment>>;
cell?: Partial<Record<`${Tone}.${Emphasis}.${State}`, Adjustment>>;
};
};
export type TokenGenerationContractV2 = {
baseToneAdjustments?: Partial<Record<Tone, Adjustment>>;
emphasisAdjustments?: Partial<Record<Emphasis, Adjustment>>;
stateAdjustments?: Partial<Record<State, Adjustment>>;
requiredContrastPairs?: Array<{
fg: TokenKey;
bg: TokenKey;
minContrast: number;
largeText?: boolean;
}>;
};
export type ThemeSeedV2 = {
accent: `#${string}`;
background: `#${string}`;
ink: `#${string}`;
};
export type ColorThemeDefinitionV1 = {
export type ColorThemeDefinitionV2 = {
id: string;
mode: Mode;
seed: ThemeSeed;
seed: ThemeSeedV2;
};
export type ThemeCatalog = Record<Mode, Record<string, ColorThemeDefinitionV1>>;
export type Element = (typeof TOKEN_ELEMENTS)[number];
export type Emphasis = (typeof TOKEN_EMPHASIS)[number];
export type UiState = (typeof TOKEN_UI_STATES)[number];
export type TaskSemanticState = (typeof TASK_SEMANTIC_STATES)[number];
export type StatusTone = `status-${TaskSemanticState}`;
export type FixedTone = (typeof FIXED_TONES)[number];
export type Tone = 'neutral' | 'brand' | StatusTone | FixedTone;
export type TokenKey = `${Element}.${Tone}.${Emphasis}.${UiState}`;
export type ThemeCatalogV2 = Record<
Mode,
Record<string, ColorThemeDefinitionV2>
>;
export type ThemeTokens = Partial<Record<TokenKey, string>>;
export type ResolvedThemeV1 = {
contract: ThemeContractV1;
seed: ThemeSeed;
export type ContrastDiagnostic = {
fg: TokenKey;
bg: TokenKey;
ratio: number;
minRequired: number;
passes: boolean;
apcaLc: number;
};
export type ThemeDiagnostics = {
contrast: ContrastDiagnostic[];
};
export type ResolvedThemeV2 = {
contract: ThemeContractV2;
seed: ThemeSeedV2;
tokens: ThemeTokens;
cssVariables: Record<string, string>;
diagnostics: ThemeDiagnostics;
};
// Backward-compatible type aliases during V2 cutover.
export type ThemeSeed = ThemeSeedV2;
export type ThemeCatalog = ThemeCatalogV2;
export type UiState = State;

View file

@ -0,0 +1,65 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { describe, expect, it } from 'vitest';
import { createDefaultThemeContractV2 } from './catalog';
import { buildThemeV2 } from './engine';
import { verifyThemeEngine } from './verifier';
describe('themeTokens v2 engine verifier', () => {
it('produces zero errors across every registered theme/mode/contrast', () => {
const report = verifyThemeEngine();
const errors = report.findings.filter((f) => f.severity === 'error');
if (errors.length > 0) {
const preview = errors
.slice(0, 5)
.map(
(e) =>
`[${e.code}] ${e.mode}/${e.themeId}@${e.contrast}: ${e.message}`
)
.join('\n');
throw new Error(
`Theme engine emitted ${errors.length} errors. First few:\n${preview}`
);
}
expect(errors).toHaveLength(0);
});
it('applies contrast clamping at bounds', () => {
const low = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: -50,
})
);
const high = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'eigent',
contrast: 500,
})
);
expect(low.contract.contrast).toBe(0);
expect(high.contract.contrast).toBe(100);
});
it('falls back to a registered theme for unknown ids', () => {
const resolved = buildThemeV2(
createDefaultThemeContractV2('light', {
themeId: 'this-theme-does-not-exist',
contrast: 43,
})
);
expect(resolved.tokens['bg.neutral.subtle.default']).toBeDefined();
});
});

View file

@ -0,0 +1,407 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed 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.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// V2 design-token engine verifier.
//
// The engine declares a short list of `contrastPairs` in semantic.color.json and
// actively solves the foreground color to meet each pair's threshold. This
// verifier:
// 1. Exercises every (mode x theme x contrast-grid) variant.
// 2. Forwards the engine's own declared-pair diagnostics.
// 3. Checks **additional, undeclared** pairings that the engine does NOT solve
// for — every status badge and every fixed tone's `strong` text on its
// `subtle` surface. Those are what typically drift as transforms are tuned,
// because the engine won't auto-correct them.
// 4. Validates token coverage and CSS-value syntax.
//
// Structured output lets both the CLI script and vitest consume the same result.
import {
DEFAULT_THEME_CATALOG,
createDefaultThemeContractV2,
getColorThemeDefinitionV2,
} from './catalog';
import { contrastRatio } from './colorMath';
import { buildThemeV2 } from './engine';
import {
TOKEN_ELEMENTS,
TOKEN_EMPHASIS,
TOKEN_TONES,
TOKEN_UI_STATES,
type Mode,
type ResolvedThemeV2,
type ThemeCatalogV2,
type TokenKey,
} from './types';
export type VerifySeverity = 'error' | 'warn';
export type VerifyFinding = {
severity: VerifySeverity;
mode: Mode;
themeId: string;
contrast: number;
code: string;
message: string;
tokenKey?: string;
value?: string;
ratio?: number;
threshold?: number;
};
export type VerifySummary = {
variantsChecked: number;
errors: number;
warnings: number;
};
export type VerifyReport = {
summary: VerifySummary;
findings: VerifyFinding[];
};
// WCAG 2.1 AA minimums. "Large text" applies to >=18pt or bold >=14pt.
const MIN_CONTRAST_NORMAL_AA = 4.5;
const MIN_CONTRAST_LARGE_AA = 3.0;
// Contrast grid covers the engine's full input range. 43 is the app default.
const DEFAULT_CONTRAST_GRID = [0, 25, 43, 75, 100];
// Tokens the engine is required to emit for every variant. Missing any of these
// is an error, not a warning, since downstream components hard-reference them.
const REQUIRED_CORE_TOKENS: TokenKey[] = [
'bg.neutral.subtle.default',
'bg.neutral.default.default',
'bg.neutral.muted.default',
'bg.neutral.strong.default',
'bg.brand.default.default',
'text.neutral.default.default',
'text.neutral.muted.default',
'text.neutral.subtle.default',
'text.brand.inverse.default',
'border.neutral.default.default',
'border.neutral.strong.default',
'ring.brand.default.focus',
'icon.neutral.default.default',
];
type Pairing = {
bg: TokenKey;
text: TokenKey;
threshold: number;
label: string;
};
// Status + fixed-tone badges typically render `text.<tone>.strong.default` on
// `bg.<tone>.subtle.default`. None of these are in semantic.color.json's
// `contrastPairs` so the engine doesn't auto-solve them — perfect place for the
// verifier to catch regressions.
const AUXILIARY_BADGE_TONES: Array<{
tone: string;
threshold: number;
label?: string;
}> = [
// Task lifecycle (10 tones)
{ tone: 'status-running', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-splitting', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-pending', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-error', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-reassigning', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-completed', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-blocked', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-paused', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-skipped', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'status-cancelled', threshold: MIN_CONTRAST_NORMAL_AA },
// Fixed tones (10)
{ tone: 'single-agent', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'workforce', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'browser', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'terminal', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'document', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'success', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'caution', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'error', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'warning', threshold: MIN_CONTRAST_NORMAL_AA },
{ tone: 'information', threshold: MIN_CONTRAST_NORMAL_AA },
];
function buildAuxiliaryPairings(): Pairing[] {
const pairs: Pairing[] = [];
for (const { tone, threshold, label } of AUXILIARY_BADGE_TONES) {
pairs.push({
bg: `bg.${tone}.subtle.default` as TokenKey,
text: `text.${tone}.strong.default` as TokenKey,
threshold,
label: label ?? `${tone} badge text on subtle surface`,
});
}
// Body text on muted surface — not declared but ubiquitous.
pairs.push({
bg: 'bg.neutral.muted.default' as TokenKey,
text: 'text.neutral.default.default' as TokenKey,
threshold: MIN_CONTRAST_NORMAL_AA,
label: 'body text on muted surface',
});
// Brand inverse text on brand hover/active (engine handles `default` via heuristic).
pairs.push({
bg: 'bg.brand.default.hover' as TokenKey,
text: 'text.brand.inverse.hover' as TokenKey,
threshold: MIN_CONTRAST_LARGE_AA,
label: 'brand button label (hover)',
});
pairs.push({
bg: 'bg.brand.default.active' as TokenKey,
text: 'text.brand.inverse.active' as TokenKey,
threshold: MIN_CONTRAST_LARGE_AA,
label: 'brand button label (active)',
});
return pairs;
}
const VALID_COLOR_RE =
/^(#[0-9a-fA-F]{3,8}|rgba?\(\s*[0-9.\s,%-]+\)|transparent)$/;
function validateTokenValue(value: string | undefined): boolean {
if (!value) return false;
if (value.includes('NaN')) return false;
return VALID_COLOR_RE.test(value.trim());
}
function extractHex(value: string | undefined): `#${string}` | null {
if (!value) return null;
const trimmed = value.trim().toLowerCase();
if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed as `#${string}`;
// rgba(r,g,b,a) — only safe to compare when alpha is 1; otherwise the visual
// contrast depends on what's behind the translucent layer.
const m = trimmed.match(
/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(0|0?\.\d+|1(?:\.0+)?)\s*)?\)$/
);
if (!m) return null;
const alpha = m[4] !== undefined ? Number(m[4]) : 1;
if (alpha < 1) return null;
const toHex = (n: string) => Number(n).toString(16).padStart(2, '0');
return `#${toHex(m[1])}${toHex(m[2])}${toHex(m[3])}` as `#${string}`;
}
function pushFinding(
findings: VerifyFinding[],
base: Omit<VerifyFinding, 'severity' | 'code' | 'message'>,
severity: VerifySeverity,
code: string,
message: string,
extra: Partial<VerifyFinding> = {}
) {
findings.push({ ...base, severity, code, message, ...extra });
}
export type VerifyOptions = {
catalog?: ThemeCatalogV2;
contrastGrid?: number[];
modes?: Mode[];
themeIds?: string[];
extraPairings?: Pairing[];
// When true, elevate auxiliary contrast failures to 'error' severity so CI
// blocks on them. Engine-declared pair failures are always errors.
strictAuxContrast?: boolean;
};
export function verifyThemeEngine(options: VerifyOptions = {}): VerifyReport {
const catalog = options.catalog ?? DEFAULT_THEME_CATALOG;
const contrastGrid = options.contrastGrid ?? DEFAULT_CONTRAST_GRID;
const modes: Mode[] = options.modes ?? ['light', 'dark'];
const auxSeverity: VerifySeverity = options.strictAuxContrast
? 'error'
: 'warn';
const auxPairings = [
...buildAuxiliaryPairings(),
...(options.extraPairings ?? []),
];
const findings: VerifyFinding[] = [];
let variantsChecked = 0;
for (const mode of modes) {
const modeCatalog = catalog[mode] ?? {};
const themeIds = options.themeIds ?? Object.keys(modeCatalog);
for (const themeId of themeIds) {
if (!modeCatalog[themeId]) {
findings.push({
severity: 'error',
mode,
themeId,
contrast: -1,
code: 'unknown-theme',
message: `Theme "${themeId}" not registered for mode "${mode}".`,
});
continue;
}
for (const contrast of contrastGrid) {
variantsChecked += 1;
const base = { mode, themeId, contrast };
let resolved: ResolvedThemeV2;
try {
resolved = buildThemeV2(
createDefaultThemeContractV2(mode, { themeId, contrast }),
catalog
);
} catch (err) {
pushFinding(
findings,
base,
'error',
'engine-throw',
`buildThemeV2 threw: ${(err as Error).message}`
);
continue;
}
// Contract sanity.
if (resolved.contract.mode !== mode) {
pushFinding(
findings,
base,
'error',
'contract-mode-drift',
`Resolved contract mode "${resolved.contract.mode}" !== requested "${mode}".`
);
}
if (resolved.contract.themeId !== themeId) {
pushFinding(
findings,
base,
'error',
'contract-theme-drift',
`Resolved contract themeId "${resolved.contract.themeId}" !== requested "${themeId}".`
);
}
// Required core tokens.
for (const key of REQUIRED_CORE_TOKENS) {
const value = resolved.tokens[key];
if (!validateTokenValue(value)) {
pushFinding(
findings,
base,
'error',
'missing-or-invalid-token',
`Required token "${key}" is missing or not a valid color.`,
{ tokenKey: key, value }
);
}
}
// All emitted tokens must be valid CSS colors.
for (const [key, value] of Object.entries(resolved.tokens)) {
if (!validateTokenValue(value)) {
pushFinding(
findings,
base,
'error',
'invalid-token-value',
`Token "${key}" resolved to invalid value "${value}".`,
{ tokenKey: key, value }
);
}
}
// Engine-declared pairs (the engine actively solves for these; if any
// fail, the solver couldn't converge — that's always an error).
for (const diag of resolved.diagnostics.contrast) {
if (!diag.passes) {
pushFinding(
findings,
base,
'error',
'engine-contrast-unsolved',
`Declared pair ${diag.fg} on ${diag.bg}: ratio ${diag.ratio.toFixed(2)} < ${diag.minRequired} (solver failed to converge).`,
{
tokenKey: diag.fg,
ratio: diag.ratio,
threshold: diag.minRequired,
}
);
}
}
// Auxiliary (undeclared) pairings — drift detector.
for (const pairing of auxPairings) {
const bg = resolved.tokens[pairing.bg];
const text = resolved.tokens[pairing.text];
if (!bg || !text) continue;
const bgHex = extractHex(bg);
const textHex = extractHex(text);
if (!bgHex || !textHex) continue;
const ratio = contrastRatio(bgHex, textHex);
if (ratio < pairing.threshold) {
pushFinding(
findings,
base,
auxSeverity,
'aux-contrast-below-threshold',
`${pairing.label}: contrast ${ratio.toFixed(2)} < ${pairing.threshold} (bg=${pairing.bg}, text=${pairing.text}).`,
{
tokenKey: pairing.text,
value: `bg=${bg} / text=${text}`,
ratio,
threshold: pairing.threshold,
}
);
}
}
}
}
}
const errors = findings.filter((f) => f.severity === 'error').length;
const warnings = findings.filter((f) => f.severity === 'warn').length;
return {
summary: { variantsChecked, errors, warnings },
findings,
};
}
export function getDefaultContrastGrid(): number[] {
return [...DEFAULT_CONTRAST_GRID];
}
export function listRegisteredThemes(
catalog: ThemeCatalogV2 = DEFAULT_THEME_CATALOG
): Array<{ mode: Mode; id: string }> {
const out: Array<{ mode: Mode; id: string }> = [];
for (const mode of ['light', 'dark'] as Mode[]) {
for (const id of Object.keys(catalog[mode] ?? {})) {
const def = getColorThemeDefinitionV2(mode, id, catalog);
if (def.id !== id) {
throw new Error(
`Catalog getter drift: asked for "${id}" in mode "${mode}", got "${def.id}".`
);
}
out.push({ mode, id });
}
}
return out;
}
// Expose axis constants for scripts that want to reason about token coverage.
export const TOKEN_AXES = {
elements: TOKEN_ELEMENTS,
emphasis: TOKEN_EMPHASIS,
states: TOKEN_UI_STATES,
tones: TOKEN_TONES,
} as const;

View file

@ -1262,24 +1262,38 @@ export default function SettingModels() {
</div>
<div className="gap-2 flex items-center">
{form[idx].prefer ? (
<span className="px-2 py-1 text-label-xs font-bold text-ds-text-status-completed-strong-default inline-flex items-center rounded-full">
<Button
variant="primary"
tone="success"
size="xs"
textWeight="bold"
disabled
buttonRadius="full"
>
{t('setting.default')}
</span>
) : (
</Button>
) : canSwitch ? (
<Button
variant="ghost"
tone="neutral"
size="xs"
disabled={!canSwitch || loading === idx}
textWeight="bold"
disabled={loading === idx}
buttonRadius="full"
onClick={() => handleSwitch(idx, true)}
className={
canSwitch
? 'bg-button-transparent-fill-hover !text-ds-text-neutral-muted-default hover:bg-button-transparent-fill-active inline-flex items-center rounded-full shadow-none'
: 'gap-1.5 inline-flex items-center'
}
>
{!canSwitch
? t('setting.not-configured')
: t('setting.set-as-default')}
{t('setting.set-as-default')}
</Button>
) : (
<Button
variant="secondary"
tone="neutral"
size="xs"
textWeight="bold"
disabled
buttonRadius="full"
>
{t('setting.not-configured')}
</Button>
)}
{form[idx].provider_id ? (
@ -1506,7 +1520,7 @@ export default function SettingModels() {
onClick={() => handleLocalSwitch(true)}
className={
isConfigured
? 'bg-button-transparent-fill-hover !text-ds-text-neutral-muted-default rounded-full shadow-none'
? 'bg-ds-bg-neutral-default-hover !text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-default-active rounded-full shadow-none'
: ''
}
>

View file

@ -52,7 +52,7 @@ export default function SkillDeleteDialog({
})}
confirmText={t('layout.delete')}
cancelText={t('layout.cancel')}
confirmVariant="cuation"
confirmVariant="caution"
/>
);
}

View file

@ -665,7 +665,7 @@ export default function SkillUploadDialog({
message="There's an existing skill with the same name. Uploading this skill will replace the existing one, which can't be restored."
confirmText="Update and Replace"
cancelText="Cancel"
confirmVariant="cuation"
confirmVariant="caution"
/>
)}
</>

View file

@ -195,7 +195,7 @@ export default function CDP() {
})}
confirmText={t('layout.remove')}
cancelText={t('layout.cancel')}
confirmVariant="cuation"
confirmVariant="caution"
/>
<div className="px-6 pb-6 pt-8 flex w-full items-center justify-between">
@ -212,10 +212,14 @@ export default function CDP() {
</Button>
<Button
variant="outline"
textWeight="semibold"
buttonContent="text"
buttonRadius="lg"
tone="neutral"
size="sm"
onClick={handleConnectExistingBrowser}
>
<Link2 className="h-4 w-4 text-button-tertiery-text-default" />
<Link2 />
{t('layout.connect-existing-browser')}
</Button>
</div>

View file

@ -28,31 +28,29 @@ import {
DEFAULT_THEME_CATALOG,
} from '@/lib/themeTokens/catalog';
import type {
ColorThemeDefinitionV1,
ColorThemeDefinitionV2,
Mode,
ThemeCatalog,
ThemeSeed,
} from '@/lib/themeTokens/types';
import { useAuthStore, type WorkspaceMainBackground } from '@/store/authStore';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
type SaveStatus = {
type: 'success' | 'error';
message: string;
} | null;
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
const NEW_TEMPLATE_TAB_ID = '__new_template__';
const DEFAULT_EDITABLE_THEME_IDS = [
'eigent',
'claude',
'codex',
'camel',
] as const;
const CUSTOM_THEME_IDS = ['custom-1', 'custom-2'] as const;
function normalizeThemeId(input: string): string {
return input
.trim()
.toLowerCase()
.replace(/[^a-z0-9-_]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
type ThemeOption = {
id: string;
label: string;
isDefault: boolean;
};
function normalizeHexColor(input: string): `#${string}` | null {
const trimmed = input.trim();
@ -74,21 +72,10 @@ function buildMergedCatalog(customThemeCatalog: ThemeCatalog): ThemeCatalog {
};
}
function extractThemeList(
mode: Mode,
catalog: ThemeCatalog
): ColorThemeDefinitionV1[] {
return Object.values(catalog[mode] ?? {}).sort((a, b) =>
a.id.localeCompare(b.id)
);
}
function nextAutoThemeId(mode: Mode, catalog: ThemeCatalog): string {
let index = 1;
while (catalog[mode][`custom-${index}`]) {
index += 1;
}
return `custom-${index}`;
function formatThemeLabel(id: string): string {
if (id === 'custom-1') return 'Custom 1';
if (id === 'custom-2') return 'Custom 2';
return id;
}
function ColorSeedEditor({
@ -101,12 +88,17 @@ function ColorSeedEditor({
onChange: (value: string) => void;
}) {
const normalizedPreview = normalizeHexColor(value) ?? '#000000';
return (
<div className="gap-2 grid grid-cols-[96px_minmax(0,1fr)_40px] items-center">
<div className="text-body-sm font-semibold text-ds-text-neutral-default-default">
{label}
</div>
<Input value={value} onChange={(e) => onChange(e.target.value)} />
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
note={normalizeHexColor(value) ? '' : 'Hex format: #1a2b3c'}
/>
<div
className="h-9 w-9 rounded-md border-ds-border-neutral-default-default border"
style={{ backgroundColor: normalizedPreview }}
@ -115,6 +107,38 @@ function ColorSeedEditor({
);
}
function ModePanel({
title,
description,
active,
onClick,
}: {
title: string;
description: string;
active: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={[
'rounded-xl px-4 py-4 border text-left transition-colors',
active
? 'border-ds-border-brand-default-focus bg-ds-bg-brand-subtle-default'
: 'border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default hover:bg-ds-bg-neutral-subtle-hover',
].join(' ')}
>
<div className="text-body-sm font-semibold text-ds-text-neutral-default-default">
{title}
</div>
<div className="mt-1 text-label-sm text-ds-text-neutral-muted-default">
{description}
</div>
</button>
);
}
export default function AppearanceSettings() {
const { t } = useTranslation();
const appearanceMode = useAuthStore((s) => s.appearanceMode);
@ -130,6 +154,8 @@ export default function AppearanceSettings() {
const removeCustomThemeTemplate = useAuthStore(
(s) => s.removeCustomThemeTemplate
);
const themeContrast = useAuthStore((s) => s.themeContrast);
const setThemeContrast = useAuthStore((s) => s.setThemeContrast);
const workspaceMainBackground = useAuthStore(
(s) => s.workspaceMainBackground
);
@ -139,142 +165,163 @@ export default function AppearanceSettings() {
const activeMode: Mode =
appearanceMode === 'system' ? appearance : appearanceMode;
const mergedCatalog = useMemo(
() => buildMergedCatalog(customThemeCatalog),
[customThemeCatalog]
);
const themeList = useMemo(
() => extractThemeList(activeMode, mergedCatalog),
[activeMode, mergedCatalog]
);
const selectedThemeId =
activeMode === 'dark' ? darkColorThemeId : lightColorThemeId;
const selectedTheme =
themeList.find((theme) => theme.id === selectedThemeId) ??
themeList[0] ??
null;
const [activeThemeTab, setActiveThemeTab] = useState<string>(
selectedThemeId || NEW_TEMPLATE_TAB_ID
const themeOptions = useMemo<ThemeOption[]>(
() => [
...DEFAULT_EDITABLE_THEME_IDS.map((id) => ({
id,
label: formatThemeLabel(id),
isDefault: true,
})),
...CUSTOM_THEME_IDS.map((id) => ({
id,
label: formatThemeLabel(id),
isDefault: false,
})),
],
[]
);
const allowedThemeIds = useMemo(
() => themeOptions.map((option) => option.id),
[themeOptions]
);
const modeThemeId =
activeMode === 'dark' ? darkColorThemeId : lightColorThemeId;
const [activeThemeId, setActiveThemeId] = useState<string>(
allowedThemeIds.includes(modeThemeId) ? modeThemeId : DEFAULT_COLOR_THEME_ID
);
const [templateName, setTemplateName] = useState('');
const [accent, setAccent] = useState('');
const [background, setBackground] = useState('');
const [ink, setInk] = useState('');
const [saveStatus, setSaveStatus] = useState<SaveStatus>(null);
useEffect(() => {
if (!selectedTheme && themeList.length > 0) {
setColorThemeForMode(activeMode, themeList[0].id);
const nextThemeId = allowedThemeIds.includes(modeThemeId)
? modeThemeId
: DEFAULT_COLOR_THEME_ID;
setActiveThemeId(nextThemeId);
if (modeThemeId !== nextThemeId) {
setColorThemeForMode(activeMode, nextThemeId);
}
}, [activeMode, selectedTheme, setColorThemeForMode, themeList]);
}, [activeMode, allowedThemeIds, modeThemeId, setColorThemeForMode]);
const fallbackSeed =
DEFAULT_THEME_CATALOG[activeMode][DEFAULT_COLOR_THEME_ID]?.seed ??
Object.values(DEFAULT_THEME_CATALOG[activeMode])[0]?.seed;
const activeTheme = useMemo<ColorThemeDefinitionV2 | null>(() => {
const fromMerged = mergedCatalog[activeMode]?.[activeThemeId];
if (fromMerged) return fromMerged;
const fromDefault = DEFAULT_THEME_CATALOG[activeMode]?.[activeThemeId];
if (fromDefault) {
return {
id: activeThemeId,
mode: activeMode,
seed: fromDefault.seed,
};
}
if (!fallbackSeed) return null;
return {
id: activeThemeId,
mode: activeMode,
seed: fallbackSeed,
};
}, [activeMode, activeThemeId, fallbackSeed, mergedCatalog]);
useEffect(() => {
if (activeThemeTab === NEW_TEMPLATE_TAB_ID) return;
const fallbackTab = selectedTheme?.id ?? themeList[0]?.id ?? '';
if (!fallbackTab) return;
if (!themeList.some((theme) => theme.id === activeThemeTab)) {
setActiveThemeTab(fallbackTab);
}
}, [activeThemeTab, selectedTheme?.id, themeList]);
if (!activeTheme) return;
setAccent(activeTheme.seed.accent);
setBackground(activeTheme.seed.background);
setInk(activeTheme.seed.ink);
}, [
activeTheme?.id,
activeTheme?.seed.accent,
activeTheme?.seed.background,
activeTheme?.seed.ink,
activeMode,
]);
const currentTheme =
activeThemeTab === NEW_TEMPLATE_TAB_ID
? null
: (themeList.find((theme) => theme.id === activeThemeTab) ??
selectedTheme ??
null);
const isCurrentCustom = Boolean(
currentTheme && customThemeCatalog[activeMode][currentTheme.id]
);
const commitThemeSeed = (
nextAccent: string,
nextBackground: string,
nextInk: string
) => {
const accentHex = normalizeHexColor(nextAccent);
const backgroundHex = normalizeHexColor(nextBackground);
const inkHex = normalizeHexColor(nextInk);
if (!accentHex || !backgroundHex || !inkHex || !activeTheme) return;
const resetDraftTemplate = useCallback(() => {
setTemplateName('');
setAccent('');
setBackground('');
setInk('');
}, []);
useEffect(() => {
if (activeThemeTab === NEW_TEMPLATE_TAB_ID) {
resetDraftTemplate();
setSaveStatus(null);
return;
}
if (!currentTheme) return;
setAccent(currentTheme.seed.accent);
setBackground(currentTheme.seed.background);
setInk(currentTheme.seed.ink);
setSaveStatus(null);
}, [activeThemeTab, currentTheme, resetDraftTemplate]);
const createTemplate = () => {
const accentHex = normalizeHexColor(accent);
const backgroundHex = normalizeHexColor(background);
const inkHex = normalizeHexColor(ink);
const explicitId = normalizeThemeId(templateName);
const themeId = explicitId || nextAutoThemeId(activeMode, mergedCatalog);
if (!accentHex || !backgroundHex || !inkHex) {
setSaveStatus({
type: 'error',
message: 'Colors must be valid hex values (example: #1a2b3c).',
});
return;
}
if (mergedCatalog[activeMode][themeId]) {
setSaveStatus({
type: 'error',
message: 'Theme id already exists. Choose another id.',
});
return;
}
const seed: ThemeSeed = {
const nextSeed: ThemeSeed = {
accent: accentHex,
background: backgroundHex,
ink: inkHex,
};
upsertCustomThemeTemplate(activeMode, themeId, seed);
resetDraftTemplate();
setSaveStatus({
type: 'success',
message: `Created "${themeId}" in ${activeMode} themes.`,
});
};
const deleteTemplate = () => {
if (!currentTheme) return;
if (!isCurrentCustom) {
setSaveStatus({
type: 'error',
message: 'Built-in themes cannot be deleted.',
});
const current = activeTheme.seed;
if (
current.accent === nextSeed.accent &&
current.background === nextSeed.background &&
current.ink === nextSeed.ink
) {
return;
}
const fallbackTheme =
themeList.find(
(theme) =>
theme.id !== currentTheme.id && theme.id === DEFAULT_COLOR_THEME_ID
) ?? themeList.find((theme) => theme.id !== currentTheme.id);
if (fallbackTheme) {
setColorThemeForMode(activeMode, fallbackTheme.id);
setActiveThemeTab(fallbackTheme.id);
}
removeCustomThemeTemplate(activeMode, currentTheme.id);
setSaveStatus({
type: 'success',
message: `Deleted "${currentTheme.id}".`,
});
upsertCustomThemeTemplate(activeMode, activeThemeId, nextSeed);
};
const openDraftTemplate = () => {
setActiveThemeTab(NEW_TEMPLATE_TAB_ID);
const handleAccentChange = (value: string) => {
setAccent(value);
commitThemeSeed(value, background, ink);
};
const handleBackgroundChange = (value: string) => {
setBackground(value);
commitThemeSeed(accent, value, ink);
};
const handleInkChange = (value: string) => {
setInk(value);
commitThemeSeed(accent, background, value);
};
const handleThemeChange = (themeId: string) => {
setActiveThemeId(themeId);
setColorThemeForMode(activeMode, themeId);
};
const resetActiveTheme = () => {
if (!activeTheme || !fallbackSeed) return;
const isDefaultTheme = DEFAULT_EDITABLE_THEME_IDS.includes(
activeThemeId as (typeof DEFAULT_EDITABLE_THEME_IDS)[number]
);
if (isDefaultTheme) {
const defaultSeed =
DEFAULT_THEME_CATALOG[activeMode][activeThemeId]?.seed;
if (defaultSeed) {
removeCustomThemeTemplate(activeMode, activeThemeId);
setAccent(defaultSeed.accent);
setBackground(defaultSeed.background);
setInk(defaultSeed.ink);
}
return;
}
upsertCustomThemeTemplate(activeMode, activeThemeId, fallbackSeed);
setAccent(fallbackSeed.accent);
setBackground(fallbackSeed.background);
setInk(fallbackSeed.ink);
};
return (
@ -294,30 +341,28 @@ export default function AppearanceSettings() {
<div className="text-body-base font-bold text-ds-text-neutral-default-default">
Mode
</div>
<Select
value={appearanceMode}
onValueChange={(value) =>
setAppearanceMode(value as Mode | 'system')
}
>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-input-bg-default border">
<SelectGroup>
<SelectItem value="light">{t('setting.light')}</SelectItem>
<SelectItem value="dark">{t('setting.dark')}</SelectItem>
<SelectItem value="system">
{t('setting.system-default')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-body-sm text-ds-text-neutral-muted-default">
{appearanceMode === 'system'
? `Following system. Current system mode: ${appearance}.`
: `Using ${appearanceMode} mode.`}
<div className="gap-3 grid grid-cols-2">
<ModePanel
title={t('setting.light')}
description="Use light mode as the active UI mode."
active={appearanceMode === 'light'}
onClick={() => setAppearanceMode('light')}
/>
<ModePanel
title={t('setting.dark')}
description="Use dark mode as the active UI mode."
active={appearanceMode === 'dark'}
onClick={() => setAppearanceMode('dark')}
/>
</div>
<ModePanel
title={t('setting.system-default')}
description={`Follow system. Current system mode: ${appearance}.`}
active={appearanceMode === 'system'}
onClick={() => setAppearanceMode('system')}
/>
</div>
<div className="item-center gap-4 rounded-2xl bg-ds-bg-neutral-default-default px-6 py-4 flex flex-col">
@ -326,120 +371,67 @@ export default function AppearanceSettings() {
Schema Customization
</div>
<div className="text-body-sm text-ds-text-neutral-muted-default">
Theme tabs are mode-specific. Select a current theme for
Add/Delete. Use New Template for Create.
4 default themes + 2 custom slots. Changes are auto-saved and
applied live.
</div>
</div>
<div className="gap-3 flex w-full flex-col">
<div className="w-full overflow-x-auto">
<Tabs
value={activeThemeTab}
onValueChange={(value) => {
setSaveStatus(null);
setActiveThemeTab(value);
if (value !== NEW_TEMPLATE_TAB_ID) {
setColorThemeForMode(activeMode, value);
}
}}
>
<div className="gap-3 flex w-full items-center justify-between">
<div className="min-w-0 flex-1 overflow-x-auto">
<Tabs value={activeThemeId} onValueChange={handleThemeChange}>
<TabsList className="min-w-max">
{themeList.map((theme) => {
const isCustom = Boolean(
customThemeCatalog[activeMode][theme.id]
);
return (
<TabsTrigger key={theme.id} value={theme.id}>
{isCustom ? `${theme.id} *` : theme.id}
</TabsTrigger>
);
})}
<TabsTrigger value={NEW_TEMPLATE_TAB_ID}>
+ New Template
</TabsTrigger>
{themeOptions.map((option) => (
<TabsTrigger key={option.id} value={option.id}>
{option.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
<div className="gap-2 flex">
{activeThemeTab === NEW_TEMPLATE_TAB_ID ? (
<Button variant="secondary" onClick={createTemplate}>
Create
</Button>
) : (
<>
<Button variant="secondary" onClick={openDraftTemplate}>
Add
</Button>
<Button
variant="secondary"
onClick={deleteTemplate}
disabled={!isCurrentCustom}
>
Delete
</Button>
</>
)}
</div>
<Button variant="secondary" onClick={resetActiveTheme}>
Reset
</Button>
</div>
<Input
title="New Template ID"
placeholder="my-theme-template (optional, auto-generated when empty)"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
note="Use lowercase letters, numbers, hyphen, or underscore."
/>
<div className="gap-3 flex flex-col">
<ColorSeedEditor
label="Accent"
value={accent}
onChange={setAccent}
onChange={handleAccentChange}
/>
<ColorSeedEditor
label="Background"
value={background}
onChange={setBackground}
onChange={handleBackgroundChange}
/>
<ColorSeedEditor
label="Ink"
value={ink}
onChange={handleInkChange}
/>
<ColorSeedEditor label="Ink" value={ink} onChange={setInk} />
</div>
<div
className="h-16 rounded-lg border-ds-border-neutral-default-default w-full overflow-hidden border"
style={{
backgroundColor: normalizeHexColor(background) ?? '#ffffff',
}}
>
<div className="flex h-full">
<div
className="w-4 shrink-0 self-stretch"
style={{
backgroundColor: normalizeHexColor(accent) ?? '#000000',
}}
/>
<div className="min-w-0 p-2 flex flex-1 flex-col justify-end">
<span
className="text-label-sm font-semibold truncate text-left capitalize"
style={{ color: normalizeHexColor(ink) ?? '#1d1d1d' }}
>
Preview
</span>
<div className="gap-2 flex flex-col">
<div className="flex items-center justify-between">
<div className="text-body-sm font-semibold text-ds-text-neutral-default-default">
Contract (Contrast)
</div>
<div className="text-body-sm font-semibold text-ds-text-neutral-muted-default">
{themeContrast}
</div>
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={themeContrast}
onChange={(e) => setThemeContrast(Number(e.target.value))}
className="h-2 bg-ds-bg-neutral-strong-default w-full cursor-pointer appearance-none rounded-full accent-[var(--ds-bg-brand-default-default)]"
aria-label="Theme contrast"
/>
</div>
{saveStatus ? (
<div
className={
saveStatus.type === 'error'
? 'text-body-sm text-ds-text-status-error-strong-default'
: 'text-body-sm text-ds-text-status-completed-strong-default'
}
>
{saveStatus.message}
</div>
) : null}
</div>
<div className="item-center rounded-2xl bg-ds-bg-neutral-default-default px-6 py-4 flex flex-row justify-between">

View file

@ -218,13 +218,21 @@ export default function SettingGeneral() {
window.location.href = `https://www.eigent.ai/dashboard?email=${authStore.email}`;
}}
variant="primary"
textWeight="semibold"
buttonContent="text"
buttonRadius="lg"
tone="neutral"
size="sm"
>
<Settings className="h-4 w-4 text-button-primary-icon-default" />
<Settings />
{t('setting.manage')}
</Button>
<Button
variant="outline"
textWeight="semibold"
buttonContent="text"
buttonRadius="lg"
tone="neutral"
size="sm"
onClick={() => {
chatStore.clearTasks();
@ -236,7 +244,7 @@ export default function SettingGeneral() {
navigate('/login');
}}
>
<LogOut className="h-4 w-4 text-button-tertiery-text-default" />
<LogOut />
{t('setting.log-out')}
</Button>
</div>

View file

@ -35,7 +35,7 @@ body {
@layer base {
body {
color: var(--text-body);
color: var(--ds-text-neutral-default-default);
}
p,
@ -53,20 +53,20 @@ body {
input,
textarea,
select {
color: var(--text-primary);
background-color: var(--surface-primary);
border: 1px solid var(--border-secondary);
color: var(--ds-text-neutral-default-default);
background-color: var(--ds-bg-neutral-subtle-default);
border: 1px solid var(--ds-border-neutral-default-default);
outline: none;
}
input::placeholder,
textarea::placeholder {
color: var(--text-tertiary);
color: var(--ds-text-neutral-subtle-default);
opacity: 1;
}
.lucide {
color: var(--icon-secondary);
color: var(--ds-icon-neutral-muted-default);
stroke: currentColor;
stroke-width: 1.5;
}
@ -74,7 +74,7 @@ body {
button .lucide,
a .lucide,
.lucide[data-state='active'] {
color: var(--icon-primary);
color: var(--ds-icon-neutral-default-default);
}
:is(
@ -84,13 +84,13 @@ body {
[class*='bg-white-100%'],
[class*='bg-white-50']
) {
color: var(--text-primary);
background-color: var(--surface-card) !important;
color: var(--ds-text-neutral-default-default);
background-color: var(--ds-bg-neutral-default-default) !important;
box-shadow: none;
}
[class*='bg-white-50'] {
background-color: var(--surface-tertiary) !important;
background-color: var(--ds-bg-neutral-strong-default) !important;
}
.theme-image-invert-dark {
@ -114,10 +114,10 @@ body {
box-shadow:
-2px -2px 100px rgba(255, 255, 255, 0.1) inset,
2px 2px 100px rgba(29, 29, 29, 0.1) inset;
background-color: var(--bg-page) !important;
background-color: var(--ds-bg-neutral-subtle-default) !important;
backdrop-filter: blur(75px);
z-index: 0;
color: var(--text-body);
color: var(--ds-text-neutral-default-default);
position: relative;
isolation: isolate;
}
@ -177,7 +177,7 @@ body {
}
.custom-resizable-handle:hover {
background: var(--border-information);
background: var(--ds-border-brand-default-focus);
width: 2px;
height: 40px;
transform: none;
@ -528,7 +528,7 @@ code {
}
.hover-style-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--text-label, rgba(0, 0, 0, 0.4));
background-color: var(--ds-text-neutral-muted-default, rgba(0, 0, 0, 0.4));
opacity: 0;
border-radius: 9999px;
transition: opacity 0.2s ease;
@ -546,7 +546,7 @@ code {
/* Dark mode overrides for FolderComponent (CSV/table content) */
[data-theme='dark'] .folder-component-content {
color: var(--text-primary);
color: var(--ds-text-neutral-default-default);
}
[data-theme='dark'] .folder-component-content * {
@ -560,7 +560,7 @@ code {
[data-theme='dark'] .folder-component-content table th,
[data-theme='dark'] .folder-component-content table td {
border-color: #30363d !important;
color: var(--text-primary) !important;
color: var(--ds-text-neutral-default-default) !important;
}
[data-theme='dark'] .folder-component-content table th {
@ -611,11 +611,11 @@ code {
color: transparent;
background: linear-gradient(
110deg,
var(--text-body) 0%,
var(--text-body) 35%,
var(--ds-text-neutral-default-default) 0%,
var(--ds-text-neutral-default-default) 35%,
var(--colors-primary-4) 50%,
var(--text-body) 65%,
var(--text-body) 100%
var(--ds-text-neutral-default-default) 65%,
var(--ds-text-neutral-default-default) 100%
);
background-size: 250% 100%;
background-position: 100% 0;

View file

@ -1,6 +1,5 @@
@layer base {
:root {
/* Red */
--colors-red-50: #fef2f2;
--colors-red-100: #ffe2e2;
--colors-red-200: #ffc9c9;
@ -12,7 +11,7 @@
--colors-red-900: #82181a;
--colors-red-950: #460809;
--colors-red-default: #e7000b;
/* Yellow */
--colors-yellow-50: #fefce8;
--colors-yellow-100: #fef9c2;
--colors-yellow-200: #fff085;
@ -24,7 +23,7 @@
--colors-yellow-900: #733e0a;
--colors-yellow-950: #432004;
--colors-yellow-default: #d08700;
/* Green */
--colors-green-50: #f0fdf4;
--colors-green-100: #dcfce7;
--colors-green-200: #b9f8cf;
@ -36,7 +35,7 @@
--colors-green-900: #0d542b;
--colors-green-950: #032e15;
--colors-green-default: #00a63e;
/* Indigo */
--colors-indigo-50: #eef2ff;
--colors-indigo-100: #e0e7ff;
--colors-indigo-200: #c6d2ff;
@ -48,7 +47,7 @@
--colors-indigo-900: #312c85;
--colors-indigo-950: #1e1a4d;
--colors-indigo-default: #4f39f6;
/* Blue */
--colors-blue-50: #eff6ff;
--colors-blue-100: #dbeafe;
--colors-blue-200: #bedbff;
@ -60,7 +59,7 @@
--colors-blue-900: #1c398e;
--colors-blue-950: #162456;
--colors-blue-default: #155dfc;
/* Amber */
--colors-amber-50: #fffbeb;
--colors-amber-100: #fef3c6;
--colors-amber-200: #fee685;
@ -72,7 +71,7 @@
--colors-amber-900: #7b3306;
--colors-amber-950: #461901;
--colors-amber-default: #e17100;
/* Emerald */
--colors-emerald-50: #ecfdf5;
--colors-emerald-100: #d0fae5;
--colors-emerald-200: #a4f4cf;
@ -84,7 +83,7 @@
--colors-emerald-900: #004f3b;
--colors-emerald-950: #002c22;
--colors-emerald-default: #009966;
/* Purple */
--colors-purple-50: #faf5ff;
--colors-purple-100: #f3e8ff;
--colors-purple-200: #e9d5ff;
@ -95,7 +94,7 @@
--colors-purple-800: #6b21a8;
--colors-purple-900: #581c87;
--colors-purple-default: #9333ea;
/* Orange */
--colors-orange-50: #fff7ed;
--colors-orange-100: #ffedd4;
--colors-orange-200: #ffd6a8;
@ -107,7 +106,7 @@
--colors-orange-900: #7e2a0c;
--colors-orange-950: #441306;
--colors-orange-default: #f54900;
/* Sky */
--colors-sky-50: #f0f9ff;
--colors-sky-100: #dff2fe;
--colors-sky-200: #b8e6fe;
@ -119,7 +118,7 @@
--colors-sky-900: #024a70;
--colors-sky-950: #052f4a;
--colors-sky-default: #0084d1;
/* Fuchsia */
--colors-fuchsia-50: #fdf4ff;
--colors-fuchsia-100: #fae8ff;
--colors-fuchsia-200: #f6cfff;
@ -131,14 +130,14 @@
--colors-fuchsia-900: #721378;
--colors-fuchsia-950: #4b004f;
--colors-fuchsia-default: #c800de;
/* Black */
--colors-black-0: #00000000;
--colors-black-10: #0000001a;
--colors-black-30: #0000004d;
--colors-black-50: #00000080;
--colors-black-80: #000000cc;
--colors-black-100: #000000;
/* Primary */
--colors-primary-1: #f5f5f5;
--colors-primary-2: #eeeeee;
--colors-primary-3: #cccccc;
@ -150,31 +149,30 @@
--colors-primary-10: #111111;
--colors-primary-11: #000000;
--colors-primary-default: #222222;
/* Off-White */
--colors-off-white-0: #f5f5f500;
--colors-off-white-10: #f5f5f51a;
--colors-off-white-30: #f5f5f54d;
--colors-off-white-50: #f5f5f580;
--colors-off-white-80: #f5f5f5cc;
--colors-off-white-100: #f5f5f5;
/* White */
--colors-white-0: #ffffff00;
--colors-white-10: #ffffff1a;
--colors-white-30: #ffffff4d;
--colors-white-50: #ffffff80;
--colors-white-80: #ffffffcc;
--colors-white-100: #ffffff;
/* Off-Black */
--colors-off-black-0: #1d1c1b00;
--colors-off-black-10: #1d1c1b1a;
--colors-off-black-30: #1d1c1b4d;
--colors-off-black-50: #1d1c1b80;
--colors-off-black-80: #1d1c1bcc;
--colors-off-black-100: #1d1c1b;
/* Gradient */
--colors-gradient: #ffffff;
/* Neon (Workforce / camel brand) */
--colors-neon-50: #f2f2ff;
--colors-neon-100: #e9e7ff;
--colors-neon-200: #d5d3ff;
@ -188,556 +186,17 @@
--colors-neon-950: #1e096c;
--colors-neon-default: #4c19e8;
/* Alias: same as --colors-neon-default */
--camel-color: var(--colors-neon-default);
/* Component Tokens */
/* Input Component */
--input-bg-input: var(--surface-tertiary);
--input-bg-spliting: var(--surface-information);
--input-bg-confirm: var(--surface-success);
--input-bg-default: var(--surface-primary);
--input-bg-hover: var(--surface-secondary);
--input-border-default: var(--border-secondary);
--input-border-hover: var(--border-action-hover);
--input-border-focus: var(--border-information);
--input-text-default: var(--text-secondary);
--input-text-focus: var(--text-primary);
--input-label-default: var(--text-secondary);
--input-border-success: var(--border-success);
--input-border-cuation: var(--border-cuation);
--input-border-warning: var(--border-warning);
/* Popup Component */
--popup-surface: var(--surface-primary);
--popup-bg: var(--fill-default);
--popup-border: var(--border-secondary);
/* Menu Tabs Component */
--menutabs-fill-default: var(--fill-fill-transparent);
--menutabs-fill-hover: var(--fill-fill-tertiary-hover);
--menutabs-fill-active: var(--fill-default);
--menutabs-fill-disabled: var(--fill-fill-tertiary-disabled);
--menutabs-border-disabled: var(--fill-fill-tertiary-disabled);
--menutabs-border-active: var(--fill-fill-tertiary-active);
--menutabs-border-hover: var(--fill-fill-tertiary-hover);
--menutabs-border-default: var(--fill-fill-tertiary);
--menutabs-text-active: var(--text-action);
--menutabs-text-disabled: var(--text-disabled);
--menutabs-text-hover: var(--text-action-hover);
--menutabs-text-default: var(--text-disabled);
--menutabs-icon-hover: var(--icon-action-hover);
--menutabs-icon-default: var(--icon-disabled);
--menutabs-icon-disabled: var(--icon-disabled);
--menutabs-icon-active: var(--icon-action);
--menutabs-bg-default: var(--surface-primary);
/* Progress Component */
--progress-fill-default: var(--fill-fill-success);
--progress-bg: var(--fill-default);
--progress-fill-complete: var(--fill-fill-success-active);
--progress-fill-past: var(--fill-fill-primary);
--progress-fill-new: var(--fill-fill-warning);
/* Button Primary Component */
--button-primary-fill-default: var(--fill-fill-primary);
--button-primary-fill-hover: var(--fill-fill-primary-hover);
--button-primary-fill-active: var(--fill-fill-primary-active);
--button-primary-fill-disabled: var(--fill-fill-primary-disabled);
--button-primary-icon-hover: var(--text-on-hover);
--button-primary-icon-default: var(--text-on-action);
--button-primary-text-disabled: var(--text-on-disabled);
--button-primary-text-active: var(--text-on-action);
--button-primary-text-hover: var(--text-on-hover);
--button-primary-text-default: var(--text-on-action);
--button-primary-icon-disabled: var(--text-on-disabled);
--button-primary-icon-active: var(--text-on-action);
/* Button Secondary Component */
--button-secondary-fill-disabled: var(--fill-fill-secondary-disabled);
--button-secondary-fill-active: var(--fill-fill-secondary-active);
--button-secondary-fill-hover: var(--fill-fill-secondary-hover);
--button-secondary-fill-default: var(--fill-fill-secondary);
--button-secondary-text-default: var(--text-on-action);
--button-secondary-text-hover: var(--text-on-hover);
--button-secondary-text-active: var(--text-on-action);
--button-secondary-text-disabled: var(--text-on-disabled);
--button-secondary-icon-disabled: var(--icon-on-disabled);
--button-secondary-icon-active: var(--icon-on-action);
--button-secondary-icon-hover: var(--icon-on-hover);
--button-secondary-icon-default: var(--icon-on-action);
/* Button Transparent Component */
--button-transparent-fill-disabled: var(--fill-fill-transparent-disabled);
--button-transparent-fill-active: var(--fill-fill-transparent-active);
--button-transparent-fill-hover: var(--fill-fill-transparent-hover);
--button-transparent-fill-default: var(--fill-fill-transparent);
--button-transparent-icon-default: var(--icon-action);
--button-transparent-text-disabled: var(--text-disabled);
--button-transparent-text-default: var(--text-action);
--button-transparent-text-active: var(--text-action);
--button-transparent-icon-hover: var(--icon-action-hover);
--button-transparent-text-hover: var(--text-action-hover);
--button-transparent-icon-disabled: var(--icon-disabled);
--button-transparent-icon-active: var(--icon-action);
/* Button Tertiary Component */
--button-tertiery-fill-hover: var(--fill-fill-tertiary-hover);
--button-tertiery-fill-default: var(--fill-fill-tertiary);
--button-tertiery-fill-disabled: var(--fill-fill-tertiary-disabled);
--button-tertiery-fill-active: var(--fill-fill-tertiary-active);
--button-tertiery-icon-hover: var(--icon-action-hover);
--button-tertiery-icon-default: var(--icon-action);
--button-tertiery-text-disabled: var(--text-disabled);
--button-tertiery-text-active: var(--text-action);
--button-tertiery-text-hover: var(--text-action-hover);
--button-tertiery-text-default: var(--text-action);
--button-tertiery-icon-disabled: var(--icon-disabled);
--button-tertiery-icon-active: var(--icon-action);
--button-tertiery-icon-hover-2: var(--icon-on-hover);
--button-tertiery-icon-default-2: var(--icon-on-action);
--button-tertiery-text-disabled-2: var(--text-on-disabled);
--button-tertiery-text-active-2: var(--text-on-action);
--button-tertiery-text-hover-2: var(--text-on-hover);
--button-tertiery-text-default-2: var(--text-on-action);
--button-tertiery-icon-disabled-2: var(--icon-on-disabled);
--button-tertiery-icon-active-2: var(--icon-on-action);
/* Button State Colors */
--button-fill-success: var(--fill-fill-success);
--button-fill-cuation: var(--fill-fill-cuation);
--button-fill-warning: var(--fill-fill-warning);
--button-fill-success-foreground: var(--text-on-action);
--button-fill-cuation-foreground: var(--text-on-action);
--button-fill-warning-foreground: var(--text-on-action);
--button-fill-information: var(--fill-fill-information);
--button-fill-information-foreground: var(--text-on-action);
/* Badge Component */
--badge-running-surface: var(--surface-success);
--badge-running-surface-foreground: var(--text-success);
--badge-paused-surface-foreground: var(--text-warning);
--badge-paused-surface: var(--surface-warning);
--badge-error-surface-foreground: var(--text-cuation);
--badge-error-surface: var(--surface-cuation);
--badge-complete-surface-foreground: var(--text-body);
--badge-complete-surface: var(--surface-primary);
--badge-splitting-surface-foreground: var(--text-information);
--badge-splitting-surface: var(--surface-information);
/* Switch Component */
--switch-off-fill-track-fill: var(--fill-fill-secondary);
--switch-off-fill-track-border: var(--border-primary);
--switch-off-fill-thumb-border: var(--border-primary);
--switch-off-fill-thumb-fill: var(--fill-default);
--switch-on-fill-thumb-border: var(--border-success);
--switch-on-fill-thumb-fill: var(--fill-default);
--switch-on-fill-track-border: var(--border-success);
--switch-on-fill-track-fill: var(--fill-fill-success);
--switch-disabled-fill-thumb-border: var(--border-disabled);
--switch-disabled-fill-track-border: var(--border-disabled);
--switch-disabled-fill-thumb-fill: var(--fill-default);
--switch-disabled-fill-track-fill: var(--fill-fill-primary-disabled);
/* Pill Component */
--pill-bg: var(--fill-default);
--pill-surface: var(--fill-fill-primary);
--pill-border: var(--border-primary);
/* Menu Button Component */
--menubutton-fill-default: var(--fill-fill-transparent);
--menubutton-fill-hover: var(--fill-fill-transparent-hover);
--menubutton-fill-active: var(--fill-default);
--menubutton-border-active: var(--border-primary);
--menubutton-border-default: var(--border-transparent);
--menubutton-border-hover: var(--border-disabled);
--menubutton-disabled: 1.25rem;
/* Dropdown Component */
--dropdown-bg: var(--fill-default);
--dropdown-border: var(--border-secondary);
--dropdown-item-bg-default: var(--fill-fill-transparent);
--dropdown-item-bg-hover: var(--fill-fill-tertiary-hover);
--dropdown-item-bg-active: var(--fill-fill-tertiary);
/* Search Component */
--search-bg: var(--fill-default);
--search-border-hover: var(--border-secondary);
--search-border-default: var(--border-disabled);
--search-default: 3.125rem;
/* Tag Component */
--tag-surface: var(--button-tertiery-fill-default);
--tag-fill-browser: var(--fill-browser);
--tag-fill-camel: var(--fill-camel);
--tag-fill-developer: var(--fill-developer);
--tag-fill-document: var(--fill-document);
--tag-fill-multimodal: var(--fill-multimodal);
--tag-fill-socialmedia: var(--fill-socialmedia);
--tag-fill-info: var(--surface-information);
--tag-foreground-info: var(--text-information);
--tag-surface-hover: var(--button-tertiery-fill-hover);
--tag-fill-success: var(--surface-success);
--tag-foreground-success: var(--text-success);
--tag-fill-warning: var(--surface-warning);
--tag-foreground-warning: var(--text-warning);
--tag-fill-cuation: var(--surface-cuation);
--tag-foreground-cuation: var(--text-cuation);
--tag-foreground-default: var(--text-body);
--tag-fill-default: var(--surface-tertiary);
--tag-fill-default-foreground: var(--surface-information);
/* Project Component */
--project-surface: var(--surface-tertiary);
--project-surface-hover: var(--surface-tertiary-hover);
--project-border-default: var(--border-tertiary);
--project-border-hover: var(--border-primary);
/* Message Component */
--message-fill-default: var(--surface-tertiary);
--message-fill-hover: var(--surface-secondary);
--message-fill-active: var(--surface-primary);
--message-border-default: var(--border-disabled);
--message-border-focus: var(--border-focus);
/* Task Component */
--task-surface: var(--surface-tertiary);
--task-border-default: var(--border-disabled);
--task-border-focus: var(--border-focus);
--task-fill-default: var(--fill-fill-tertiary);
--task-fill-hover: var(--fill-fill-tertiary-hover);
--task-fill-success: var(--surface-success);
--task-fill-warning: var(--surface-warning);
--task-fill-error: var(--surface-cuation);
--task-border-focus-success: var(--border-success);
--task-border-focus-warning: var(--border-warning);
--task-border-focus-error: var(--border-cuation);
--task-fill-running: var(--surface-primary);
/* Log Component */
--log-default: #f5f5f5;
/* Worker Component */
--worker-surface-primary: var(--surface-tertiary);
--worker-border-default: var(--border-disabled);
--worker-border-focus: var(--border-focus);
--worker-surface-secondary: var(--surface-disabled);
/* Mask Component */
--mask-default: var(--bg-secondary);
--mask-dark: var(--bg-dark-secondary);
/* Code Component */
--code-bg: var(--bg-dark-default);
--code-foreground: var(--text-on-action);
--code-surface: #f4f4f5;
/* zinc-100 equivalent */
/* Surface Variants */
--surface-error-subtle: #fee2e2;
/* red-100 equivalent */
--surface-hover-subtle: #f3f4f6;
/* gray-100 equivalent */
--surface-success-subtle: #d1fae5;
/* emerald-100 equivalent */
--surface-tertiary-subtle: #f9fafb;
/* gray-50 equivalent */
/* Text Variants */
--text-muted: var(--colors-primary-5);
--text-muted-strong: var(--colors-primary-7);
--text-link: var(--colors-blue-500);
--text-link-hover: var(--colors-blue-700);
--text-error: var(--colors-red-500);
--border-subtle: var(--colors-primary-2);
--border-subtle-strong: var(--colors-primary-3);
/* Shadow Tokens */
/* Perfect Shadow */
--shadow-perfect:
0 8px 20px -2px #1d21291a, 0 32px 48px -12px #1d21291f,
0 96px 120px -12px #414a5c0f, 0 108px 72px -16px #414a5c14,
0 32px 64px -8px #7199bd1f, 0 8px 10px 0 #7199bd1f;
/* Button Shadow */
--shadow-button:
inset 0 1px 0 0 #ffffff54, 0 3px 4px -1px #00000040, 0 0 0 1px #d4d4d440;
}
.root,
[data-theme='light'] {
--text-heading: var(--colors-primary-10);
--text-body: var(--colors-primary-default);
--text-label: var(--colors-primary-6);
--text-action: var(--colors-primary-default);
--text-action-hover: var(--colors-primary-10);
--text-disabled: var(--colors-primary-3);
--text-information: var(--colors-blue-default);
--text-success: var(--colors-green-default);
--text-warning: var(--colors-yellow-default);
--text-cuation: var(--colors-red-default);
--text-on-action: var(--colors-primary-1);
--text-on-disabled: var(--colors-primary-1);
--text-document: var(--colors-amber-default);
--text-socialmedia: var(--colors-purple-default);
--text-browser: var(--colors-sky-default);
--text-developer: var(--colors-emerald-default);
--text-multimodal: var(--colors-fuchsia-default);
--text-session-single-agent: var(--colors-purple-700);
--text-session-workforce: var(--colors-emerald-700);
--surface-session-single-agent-selected: var(--colors-purple-100);
--surface-session-workforce-selected: var(--colors-emerald-100);
--text-on-hover: var(--colors-primary-2);
--surface-primary: var(--colors-off-white-100);
--surface-secondary: var(--colors-primary-2);
--surface-success: var(--colors-green-50);
--surface-information: var(--colors-blue-50);
--surface-warning: var(--colors-yellow-50);
--surface-cuation: var(--colors-red-50);
--surface-action: var(--colors-primary-2);
--surface-action-hover: var(--colors-primary-1);
--surface-disabled: var(--colors-off-white-30);
--surface-tertiary: var(--colors-white-100);
--surface-tertiary-hover: var(--colors-white-50);
--surface-card: var(--colors-off-white-30);
--surface-card-hover: var(--colors-off-white-80);
--surface-card-focus: var(--colors-white-100);
--surface-card-default: 1.25rem;
--border-primary: var(--colors-primary-4);
--border-secondary: var(--colors-primary-3);
--border-tertiary: var(--colors-primary-1);
--border-information: var(--colors-blue-default);
--border-success: var(--colors-green-default);
--border-warning: var(--colors-yellow-default);
--border-cuation: var(--colors-red-default);
--border-focus: var(--colors-primary-4);
--border-action: var(--colors-primary-3);
--border-action-hover: var(--colors-primary-4);
--border-disabled: var(--colors-primary-2);
--border-developer: var(--colors-emerald-default);
--border-browser: var(--colors-sky-default);
--border-socialmedia: var(--colors-purple-default);
--border-multimodal: var(--colors-fuchsia-default);
--border-document: var(--colors-amber-default);
--border-camel: var(--colors-neon-500);
--border-transparent: var(--colors-white-0);
--text-camel: var(--colors-neon-default);
--fill-camel: var(--colors-neon-100);
--icon-primary: var(--colors-primary-default);
--icon-action: var(--colors-primary-default);
--icon-disabled: var(--colors-primary-3);
--icon-information: var(--colors-blue-default);
--icon-success: var(--colors-green-default);
--icon-warning: var(--colors-yellow-default);
--icon-cuation: var(--colors-red-default);
--icon-action-hover: var(--colors-primary-10);
--icon-multimodal: var(--colors-fuchsia-default);
--icon-socialmedia: var(--colors-purple-default);
--icon-document: var(--colors-amber-default);
--icon-browser: var(--colors-sky-default);
--icon-developer: var(--colors-emerald-default);
--icon-on-disabled: var(--colors-off-white-50);
--icon-on-hover: var(--colors-primary-2);
--icon-on-action: var(--colors-primary-1);
--icon-secondary: var(--colors-primary-5);
--developer: var(--colors-emerald-default);
--browser: var(--colors-sky-default);
--document: var(--colors-amber-default);
--multimodal: var(--colors-fuchsia-default);
--socialmedia: var(--colors-purple-default);
--fill-default: var(--colors-white-100);
--fill-fill-primary: var(--colors-primary-default);
--fill-fill-primary-hover: var(--colors-primary-10);
--fill-fill-primary-active: var(--colors-primary-11);
--fill-fill-primary-disabled: var(--colors-primary-5);
--fill-fill-tertiary: var(--colors-primary-1);
--fill-fill-transparent: var(--colors-white-0);
--fill-fill-transparent-hover: var(--colors-off-white-100);
--fill-fill-tertiary-hover: var(--colors-primary-2);
--fill-fill-tertiary-active: var(--colors-primary-3);
--fill-fill-tertiary-disabled: var(--colors-primary-2);
--fill-fill-transparent-active: var(--colors-white-100);
--fill-fill-transparent-disabled: var(--colors-white-0);
--fill-fill-secondary-disabled: var(--colors-primary-4);
--fill-fill-secondary-active: var(--colors-primary-7);
--fill-fill-secondary-hover: var(--colors-primary-6);
--fill-fill-secondary: var(--colors-primary-5);
--fill-fill-success: var(--colors-green-default);
--fill-fill-success-hover: var(--colors-green-700);
--fill-fill-success-active: var(--colors-green-800);
--fill-fill-success-disable: var(--colors-green-400);
--fill-fill-warning: var(--colors-yellow-default);
--fill-fill-cuation: var(--colors-red-default);
--fill-socialmedia: var(--colors-purple-100);
--fill-document: var(--colors-amber-100);
--fill-browser: var(--colors-sky-100);
--fill-multimodal: var(--colors-fuchsia-100);
--fill-developer: var(--colors-emerald-100);
--fill-scrollbar-dark: var(--colors-primary-3);
--fill-scrollbar-light: var(--colors-primary-1);
--fill-skeloten-default: var(--colors-primary-2);
--fill-fill-information: var(--colors-blue-default);
--bg-page: var(--colors-primary-1);
--bg-primary: var(--colors-primary-1);
--bg-secondary: var(--colors-primary-2);
--bg-tertiary: var(--colors-primary-3);
--bg-dark: var(--colors-off-black-80);
--bg-dark-primary: var(--colors-off-black-50);
--bg-dark-secondary: var(--colors-off-black-30);
--bg-dark-tertiary: var(--colors-off-black-10);
--bg-dark-default: var(--colors-off-black-100);
--bg-page-default: var(--colors-off-white-100);
--background: var(--bg-page);
}
.root,
[data-theme='dark'] {
--text-heading: #e8ecff;
--text-body: #f4f6ff;
--text-label: #b0bdd8;
--text-action: #f8fafc;
--text-action-hover: #d9e2ff;
--text-disabled: rgba(148, 163, 184, 0.35);
--text-information: #7ab3ff;
--text-success: #4ade80;
--text-warning: #facc15;
--text-cuation: #f87171;
--text-on-action: #0b1020;
--text-on-disabled: rgba(15, 23, 42, 0.7);
--text-document: #ffd479;
--text-socialmedia: #d8b4fe;
--text-browser: #7dd3fc;
--text-developer: #6ee7b7;
--text-multimodal: #f5a8ff;
--text-session-single-agent: #e9d5ff;
--text-session-workforce: var(--text-developer);
--surface-session-single-agent-selected: rgba(168, 85, 247, 0.22);
--surface-session-workforce-selected: rgba(52, 211, 153, 0.2);
--text-on-hover: #ffffff;
--text-primary: #f4f6ff;
--text-secondary: var(--text-label);
--text-tertiary: var(--text-disabled);
--text-inverse-primary: var(--text-on-action);
--text-success-primary: var(--text-success);
--text-success-default: var(--text-success);
--text-caution: var(--text-cuation);
--text-cuation-default: var(--text-cuation);
--surface-primary: #131b2b;
--surface-secondary: #1b2435;
--surface-success: rgba(15, 118, 110, 0.25);
--surface-information: rgba(30, 64, 175, 0.22);
--surface-warning: rgba(161, 98, 7, 0.26);
--surface-cuation: rgba(153, 27, 27, 0.26);
--surface-action: #2c3a55;
--surface-action-hover: #35476a;
--surface-disabled: rgba(148, 163, 184, 0.14);
--surface-tertiary: #222d41;
--surface-tertiary-hover: #2c3950;
--surface-card: #161f30;
--surface-card-hover: #1f2a40;
--surface-card-focus: #2a3a55;
--surface-card-default: 1.25rem;
--border-primary: rgba(148, 163, 184, 0.24);
--border-secondary: rgba(148, 163, 184, 0.12);
--border-tertiary: rgba(148, 163, 184, 0.08);
--border-information: rgba(125, 179, 255, 0.65);
--border-success: rgba(74, 222, 128, 0.6);
--border-cuation: rgba(248, 113, 113, 0.6);
--border-warning: rgba(250, 204, 21, 0.6);
--border-focus: rgba(226, 232, 240, 0.2);
--border-action: rgba(148, 163, 184, 0.24);
--border-action-hover: rgba(226, 232, 240, 0.38);
--border-disabled: rgba(148, 163, 184, 0.08);
--border-developer: rgba(110, 231, 183, 0.6);
--border-browser: rgba(125, 211, 252, 0.6);
--border-socialmedia: rgba(216, 180, 254, 0.6);
--border-multimodal: rgba(245, 168, 255, 0.6);
--border-document: rgba(255, 212, 121, 0.6);
--border-camel: var(--colors-neon-500);
--border-transparent: var(--colors-black-0);
/* Dark: lighter default for readability on dark surfaces */
--camel-color: var(--colors-neon-400);
--text-camel: var(--camel-color);
--fill-camel: var(--colors-neon-100);
--icon-primary: #d0dcff;
--icon-action: #f1f5ff;
--icon-disabled: rgba(148, 163, 184, 0.4);
--icon-information: #7ab3ff;
--icon-success: #4ade80;
--icon-warning: #facc15;
--icon-cuation: #f87171;
--icon-action-hover: #ffffff;
--icon-multimodal: #f5a8ff;
--icon-socialmedia: #d8b4fe;
--icon-document: #ffd479;
--icon-browser: #7dd3fc;
--icon-developer: #6ee7b7;
--icon-on-disabled: rgba(15, 23, 42, 0.7);
--icon-on-hover: #ffffff;
--icon-on-action: #0b1020;
--icon-secondary: rgba(226, 232, 240, 0.55);
--developer: #34d399;
--browser: #38bdf8;
--document: #fbbf24;
--multimodal: #f472b6;
--socialmedia: #c084fc;
--fill-default: #131b2b;
--fill-fill-primary: #e2e8ff;
--fill-fill-primary-hover: #ffffff;
--fill-fill-primary-active: #ffffff;
--fill-fill-primary-disabled: rgba(209, 213, 219, 0.4);
--fill-fill-tertiary: #1f2937;
--fill-fill-transparent: rgba(15, 23, 42, 0.4);
--fill-fill-transparent-hover: rgba(59, 74, 99, 0.6);
--fill-fill-tertiary-hover: #2c3a55;
--fill-fill-tertiary-active: #35476a;
--fill-fill-tertiary-disabled: rgba(31, 41, 55, 0.6);
--fill-fill-transparent-active: rgba(59, 74, 99, 0.75);
--fill-fill-transparent-disabled: rgba(15, 23, 42, 0.2);
--fill-fill-secondary-disabled: rgba(148, 163, 184, 0.25);
--fill-fill-secondary-active: #f1f5ff;
--fill-fill-secondary-hover: rgba(226, 232, 240, 0.85);
--fill-fill-secondary: rgba(226, 232, 240, 0.65);
--fill-fill-success: #22c55e;
--fill-fill-success-hover: #4ade80;
--fill-fill-success-active: #a7f3d0;
--fill-fill-success-disable: rgba(15, 118, 110, 0.45);
--fill-fill-warning: #facc15;
--fill-fill-cuation: #f87171;
--fill-socialmedia: rgba(134, 94, 189, 0.45);
--fill-document: rgba(180, 128, 41, 0.45);
--fill-browser: rgba(40, 94, 151, 0.45);
--fill-multimodal: rgba(161, 60, 190, 0.45);
--fill-developer: rgba(23, 121, 96, 0.45);
--fill-scrollbar-dark: rgba(148, 163, 184, 0.2);
--fill-scrollbar-light: rgba(209, 213, 219, 0.35);
--fill-skeloten-default: rgba(148, 163, 184, 0.18);
--fill-fill-information: #7ab3ff;
--bg-page: #0d1424;
--bg-primary: #121a2a;
--bg-secondary: #161f30;
--bg-tertiary: #1c2640;
--bg-dark: #131b2b;
--bg-dark-primary: #1b2435;
--bg-dark-secondary: #222d41;
--bg-dark-tertiary: #2c3950;
--bg-dark-default: #0d1424;
--bg-page-default: #0d1424;
--background: var(--bg-page);
--task-fill-running: #1f2937;
--log-default: #1f2937;
--code-surface: #27272a;
--surface-error-subtle: rgba(153, 27, 27, 0.3);
--surface-hover-subtle: rgba(55, 65, 81, 0.5);
--surface-success-subtle: rgba(6, 95, 70, 0.35);
--surface-tertiary-subtle: rgba(31, 41, 55, 0.5);
--text-muted: var(--colors-primary-4);
--text-muted-strong: var(--colors-primary-3);
--text-link: var(--colors-blue-400);
--text-link-hover: var(--colors-blue-300);
--text-error: var(--colors-red-400);
--border-subtle: var(--colors-primary-7);
--border-subtle-strong: var(--colors-primary-6);
}
}

View file

@ -1,38 +1,61 @@
/** @type {import('tailwindcss').Config} */
const DS_TOKEN_ELEMENTS = ['bg', 'text', 'border', 'icon', 'ring'];
const DS_TOKEN_EMPHASIS = ['subtle', 'muted', 'default', 'strong', 'inverse'];
const DS_TOKEN_STATES = [
'default',
'hover',
'active',
'selected',
'focus',
'disabled',
];
const DS_TOKEN_TONES = [
'neutral',
'brand',
'status-running',
'status-splitting',
'status-pending',
'status-error',
'status-reassigning',
'status-completed',
'status-blocked',
'status-paused',
'status-skipped',
'status-cancelled',
'single-agent',
'workforce',
'browser',
'terminal',
'document',
'success',
'caution',
'error',
'warning',
'information',
];
const fs = require('node:fs');
const path = require('node:path');
function loadTokenManifest() {
const fallback = {
elements: ['bg', 'text', 'border', 'icon', 'ring'],
emphasis: ['subtle', 'muted', 'default', 'strong', 'inverse'],
states: ['default', 'hover', 'active', 'selected', 'focus', 'disabled'],
tones: [
'neutral',
'brand',
'status-running',
'status-splitting',
'status-pending',
'status-error',
'status-reassigning',
'status-completed',
'status-blocked',
'status-paused',
'status-skipped',
'status-cancelled',
'single-agent',
'workforce',
'browser',
'terminal',
'document',
'success',
'caution',
'error',
'warning',
'information',
],
};
const manifestPath = path.join(__dirname, 'tokens', 'manifest.json');
try {
const raw = fs.readFileSync(manifestPath, 'utf8');
const parsed = JSON.parse(raw);
if (
Array.isArray(parsed.elements) &&
Array.isArray(parsed.emphasis) &&
Array.isArray(parsed.states) &&
Array.isArray(parsed.tones)
) {
return parsed;
}
return fallback;
} catch (_error) {
return fallback;
}
}
const TOKEN_MANIFEST = loadTokenManifest();
const DS_TOKEN_ELEMENTS = TOKEN_MANIFEST.elements;
const DS_TOKEN_EMPHASIS = TOKEN_MANIFEST.emphasis;
const DS_TOKEN_STATES = TOKEN_MANIFEST.states;
const DS_TOKEN_TONES = TOKEN_MANIFEST.tones;
function buildDsTokenColorMap() {
const map = {};
@ -316,19 +339,19 @@ module.exports = {
input: {
'bg-default': 'var(--input-bg-default)',
'bg-hover': 'var(--input-bg-hover)',
'bg-spliting': 'var(--input-bg-spliting)',
'bg-splitting': 'var(--input-bg-splitting)',
'bg-confirm': 'var(--input-bg-confirm)',
'bg-input': 'var(--input-bg-input)',
'border-default': 'var(--input-border-default)',
'border-hover': 'var(--input-border-hover)',
'border-focus': 'var(--input-border-focus)',
'border-success': 'var(--input-border-success)',
'border-cuation': 'var(--input-border-cuation)',
'border-caution': 'var(--input-border-caution)',
'border-warning': 'var(--input-border-warning)',
'text-default': 'var(--input-text-default)',
'text-focus': 'var(--input-text-focus)',
'text-success': 'var(--text-success)',
'text-cuation': 'var(--text-cuation)',
'text-caution': 'var(--text-caution)',
'text-warning': 'var(--text-warning)',
'label-default': 'var(--input-label-default)',
},
@ -406,33 +429,33 @@ module.exports = {
'icon-disabled': 'var(--button-transparent-icon-disabled)',
'icon-active': 'var(--button-transparent-icon-active)',
},
tertiery: {
'fill-hover': 'var(--button-tertiery-fill-hover)',
'fill-default': 'var(--button-tertiery-fill-default)',
'fill-disabled': 'var(--button-tertiery-fill-disabled)',
'fill-active': 'var(--button-tertiery-fill-active)',
'icon-hover': 'var(--button-tertiery-icon-hover)',
'icon-default': 'var(--button-tertiery-icon-default)',
'text-disabled': 'var(--button-tertiery-text-disabled)',
'text-active': 'var(--button-tertiery-text-active)',
'text-hover': 'var(--button-tertiery-text-hover)',
'text-default': 'var(--button-tertiery-text-default)',
'icon-disabled': 'var(--button-tertiery-icon-disabled)',
'icon-active': 'var(--button-tertiery-icon-active)',
'icon-hover 2': 'var(--button-tertiery-icon-hover-2)',
'icon-default 2': 'var(--button-tertiery-icon-default-2)',
'text-disabled 2': 'var(--button-tertiery-text-disabled-2)',
'text-active 2': 'var(--button-tertiery-text-active-2)',
'text-hover 2': 'var(--button-tertiery-text-hover-2)',
'text-default 2': 'var(--button-tertiery-text-default-2)',
'icon-disabled 2': 'var(--button-tertiery-icon-disabled-2)',
'icon-active 2': 'var(--button-tertiery-icon-active-2)',
tertiary: {
'fill-hover': 'var(--button-tertiary-fill-hover)',
'fill-default': 'var(--button-tertiary-fill-default)',
'fill-disabled': 'var(--button-tertiary-fill-disabled)',
'fill-active': 'var(--button-tertiary-fill-active)',
'icon-hover': 'var(--button-tertiary-icon-hover)',
'icon-default': 'var(--button-tertiary-icon-default)',
'text-disabled': 'var(--button-tertiary-text-disabled)',
'text-active': 'var(--button-tertiary-text-active)',
'text-hover': 'var(--button-tertiary-text-hover)',
'text-default': 'var(--button-tertiary-text-default)',
'icon-disabled': 'var(--button-tertiary-icon-disabled)',
'icon-active': 'var(--button-tertiary-icon-active)',
'icon-hover 2': 'var(--button-tertiary-icon-hover-2)',
'icon-default 2': 'var(--button-tertiary-icon-default-2)',
'text-disabled 2': 'var(--button-tertiary-text-disabled-2)',
'text-active 2': 'var(--button-tertiary-text-active-2)',
'text-hover 2': 'var(--button-tertiary-text-hover-2)',
'text-default 2': 'var(--button-tertiary-text-default-2)',
'icon-disabled 2': 'var(--button-tertiary-icon-disabled-2)',
'icon-active 2': 'var(--button-tertiary-icon-active-2)',
},
'fill-success': 'var(--button-fill-success)',
'fill-cuation': 'var(--button-fill-cuation)',
'fill-caution': 'var(--button-fill-caution)',
'fill-warning': 'var(--button-fill-warning)',
'fill-success-foreground': 'var(--button-fill-success-foreground)',
'fill-cuation-foreground': 'var(--button-fill-cuation-foreground)',
'fill-caution-foreground': 'var(--button-fill-caution-foreground)',
'fill-warning-foreground': 'var(--button-fill-warning-foreground)',
'fill-information': 'var(--button-fill-information)',
'fill-information-foreground':
@ -515,8 +538,8 @@ module.exports = {
'text-success': 'var(--tag-text-success)',
'fill-warning': 'var(--tag-fill-warning)',
'foreground-warning': 'var(--tag-foreground-warning)',
'fill-cuation': 'var(--tag-fill-cuation)',
'foreground-cuation': 'var(--tag-foreground-cuation)',
'fill-caution': 'var(--tag-fill-caution)',
'foreground-caution': 'var(--tag-foreground-caution)',
'fill-default': 'var(--tag-fill-default)',
'foreground-default': 'var(--tag-foreground-default)',
'fill-default-foreground': 'var(--tag-fill-default-foreground)',
@ -564,10 +587,10 @@ module.exports = {
surface: 'var(--code-surface)',
},
surface: {
'error-subtle': 'var(--surface-error-subtle)',
'hover-subtle': 'var(--surface-hover-subtle)',
'success-subtle': 'var(--surface-success-subtle)',
'tertiary-subtle': 'var(--surface-tertiary-subtle)',
'error-subtle': 'var(--ds-bg-status-error-subtle-default)',
'hover-subtle': 'var(--ds-bg-neutral-subtle-hover)',
'success-subtle': 'var(--ds-bg-status-completed-subtle-default)',
'tertiary-subtle': 'var(--ds-bg-neutral-strong-default)',
},
'text-muted': 'var(--text-muted)',
'text-muted-strong': 'var(--text-muted-strong)',
@ -592,7 +615,7 @@ module.exports = {
'text-information': 'var(--text-information)',
'text-success': 'var(--text-success)',
'text-warning': 'var(--text-warning)',
'text-cuation': 'var(--text-cuation)',
'text-caution': 'var(--text-caution)',
'text-on-action': 'var(--text-on-action)',
'text-on-disabled': 'var(--text-on-disabled)',
'text-document': 'var(--text-document)',
@ -605,25 +628,24 @@ module.exports = {
'session-workforce': 'var(--text-session-workforce)',
'text-on-hover': 'var(--text-on-hover)',
'surface-primary': 'var(--surface-primary)',
'surface-secondary': 'var(--surface-secondary)',
'surface-success': 'var(--surface-success)',
'surface-information': 'var(--surface-information)',
'surface-warning': 'var(--surface-warning)',
'surface-cuation': 'var(--surface-cuation)',
'surface-caution': 'var(--surface-cuation)',
'surface-action': 'var(--surface-action)',
'surface-action-hover': 'var(--surface-action-hover)',
'surface-disabled': 'var(--surface-disabled)',
'surface-tertiary': 'var(--surface-tertiary)',
'surface-card': 'var(--surface-card)',
'surface-card-hover': 'var(--surface-card-hover)',
'surface-card-focus': 'var(--surface-card-focus)',
'surface-card-default': 'var(--surface-card-default)',
'surface-primary': 'var(--ds-bg-neutral-subtle-default)',
'surface-secondary': 'var(--ds-bg-neutral-default-default)',
'surface-success': 'var(--ds-bg-status-completed-subtle-default)',
'surface-information': 'var(--ds-bg-status-splitting-subtle-default)',
'surface-warning': 'var(--ds-bg-status-pending-subtle-default)',
'surface-caution': 'var(--ds-bg-status-error-subtle-default)',
'surface-action': 'var(--ds-bg-neutral-default-default)',
'surface-action-hover': 'var(--ds-bg-neutral-default-hover)',
'surface-disabled': 'var(--ds-bg-neutral-muted-disabled)',
'surface-tertiary': 'var(--ds-bg-neutral-strong-default)',
'surface-card': 'var(--ds-bg-neutral-default-default)',
'surface-card-hover': 'var(--ds-bg-neutral-default-hover)',
'surface-card-focus': 'var(--ds-bg-neutral-default-focus)',
'surface-card-default': '1.25rem',
'surface-session-single-agent-selected':
'var(--surface-session-single-agent-selected)',
'var(--ds-bg-single-agent-subtle-selected)',
'surface-session-workforce-selected':
'var(--surface-session-workforce-selected)',
'var(--ds-bg-workforce-subtle-selected)',
'border-primary': 'var(--border-primary)',
'border-secondary': 'var(--border-secondary)',
@ -631,7 +653,7 @@ module.exports = {
'border-information': 'var(--border-information)',
'border-success': 'var(--border-success)',
'border-warning': 'var(--border-warning)',
'border-cuation': 'var(--border-cuation)',
'border-caution': 'var(--border-caution)',
'border-focus': 'var(--border-focus)',
'border-action': 'var(--border-action)',
'border-action-hover': 'var(--border-action-hover)',
@ -650,8 +672,7 @@ module.exports = {
'icon-information': 'var(--icon-information)',
'icon-success': 'var(--icon-success)',
'icon-warning': 'var(--icon-warning)',
'icon-caution': 'var(--icon-cuation)',
'icon-cuation': 'var(--icon-cuation)',
'icon-caution': 'var(--icon-caution)',
'icon-action-hover': 'var(--icon-action-hover)',
'icon-multimodal': 'var(--icon-multimodal)',
'icon-socialmedia': 'var(--icon-socialmedia)',
@ -688,7 +709,7 @@ module.exports = {
'fill-fill-success-active': 'var(--fill-fill-success-active)',
'fill-fill-success-disable': 'var(--fill-fill-success-disable)',
'fill-fill-warning': 'var(--fill-fill-warning)',
'fill-fill-cuation': 'var(--fill-fill-cuation)',
'fill-fill-caution': 'var(--fill-fill-caution)',
'fill-socialmedia': 'var(--fill-socialmedia)',
'fill-document': 'var(--fill-document)',
'fill-browser': 'var(--fill-browser)',
@ -700,6 +721,9 @@ module.exports = {
'fill-skeloten-default': 'var(--fill-skeloten-default)',
'fill-fill-information': 'var(--fill-fill-information)',
/** Embedded xterm viewport backdrop; fixed black (see tokens/component.color.json). */
'terminal-viewport-surface': 'var(--terminal-viewport-surface)',
'bg-page': 'var(--bg-page)',
'bg-primary': 'var(--bg-primary)',
'bg-secondary': 'var(--bg-secondary)',

75
tokens/base.color.json Normal file
View file

@ -0,0 +1,75 @@
{
"$description": "Base color seeds and fixed role anchors for Design Tokens V2.",
"themes": {
"light": {
"eigent": {
"accent": "#000000",
"background": "#faf7f6",
"ink": "#1d1d1d"
},
"claude": {
"accent": "#cc7d5e",
"background": "#f9f9f7",
"ink": "#2d2d2b"
},
"codex": {
"accent": "#0169cc",
"background": "#ffffff",
"ink": "#0d0d0d"
},
"camel": {
"accent": "#4c19e8",
"background": "#ffffff",
"ink": "#1d1d1d"
}
},
"dark": {
"eigent": {
"accent": "#ede1db",
"background": "#1f1f1f",
"ink": "#ffffff"
},
"claude": {
"accent": "#cc7d5e",
"background": "#2d2d2b",
"ink": "#f9f9f7"
},
"codex": {
"accent": "#0169cc",
"background": "#111111",
"ink": "#fcfcfc"
},
"camel": {
"accent": "#b5afff",
"background": "#1f1f1f",
"ink": "#fafafa"
}
}
},
"fixedAnchors": {
"light": {
"single-agent": "#7e22ce",
"workforce": "#007a55",
"browser": "#0084d1",
"terminal": "#009966",
"document": "#e17100",
"success": "#00a63e",
"caution": "#e7000b",
"error": "#e7000b",
"warning": "#d08700",
"information": "#155dfc"
},
"dark": {
"single-agent": "#e9d5ff",
"workforce": "#6ee7b7",
"browser": "#7dd3fc",
"terminal": "#6ee7b7",
"document": "#ffd479",
"success": "#4ade80",
"caution": "#f87171",
"error": "#f87171",
"warning": "#facc15",
"information": "#7ab3ff"
}
}
}

403
tokens/component.color.json Normal file
View file

@ -0,0 +1,403 @@
{
"$description": "Component aliases resolved from semantic tokens.",
"component": {
"text": {
"heading": {
"$type": "color",
"$value": "{text.neutral.default.default}",
"$extensions": { "cssVar": "--text-heading" }
},
"body": {
"$type": "color",
"$value": "{text.neutral.default.default}",
"$extensions": { "cssVar": "--text-body" }
},
"label": {
"$type": "color",
"$value": "{text.neutral.muted.default}",
"$extensions": { "cssVar": "--text-label" }
},
"primary": {
"$type": "color",
"$value": "{text.neutral.default.default}",
"$extensions": { "cssVar": "--text-primary" }
},
"secondary": {
"$type": "color",
"$value": "{text.neutral.muted.default}",
"$extensions": { "cssVar": "--text-secondary" }
},
"tertiary": {
"$type": "color",
"$value": "{text.neutral.subtle.default}",
"$extensions": { "cssVar": "--text-tertiary" }
},
"action": {
"$type": "color",
"$value": "{text.neutral.default.default}",
"$extensions": { "cssVar": "--text-action" }
},
"action-hover": {
"$type": "color",
"$value": "{text.neutral.default.hover}",
"$extensions": { "cssVar": "--text-action-hover" }
},
"disabled": {
"$type": "color",
"$value": "{text.neutral.muted.disabled}",
"$extensions": { "cssVar": "--text-disabled" }
},
"information": {
"$type": "color",
"$value": "{text.status-splitting.strong.default}",
"$extensions": { "cssVar": "--text-information" }
},
"success": {
"$type": "color",
"$value": "{text.status-completed.strong.default}",
"$extensions": { "cssVar": "--text-success" }
},
"warning": {
"$type": "color",
"$value": "{text.status-pending.strong.default}",
"$extensions": { "cssVar": "--text-warning" }
},
"caution": {
"$type": "color",
"$value": "{text.status-error.strong.default}",
"$extensions": { "cssVar": "--text-caution" }
},
"on-action": {
"$type": "color",
"$value": "{text.brand.inverse.default}",
"$extensions": { "cssVar": "--text-on-action" }
},
"on-hover": {
"$type": "color",
"$value": "{text.brand.inverse.default}",
"$extensions": { "cssVar": "--text-on-hover" }
},
"on-disabled": {
"$type": "color",
"$value": "{text.neutral.muted.disabled}",
"$extensions": { "cssVar": "--text-on-disabled" }
}
},
"border": {
"primary": {
"$type": "color",
"$value": "{border.neutral.strong.default}",
"$extensions": { "cssVar": "--border-primary" }
},
"secondary": {
"$type": "color",
"$value": "{border.neutral.default.default}",
"$extensions": { "cssVar": "--border-secondary" }
},
"tertiary": {
"$type": "color",
"$value": "{border.neutral.subtle.default}",
"$extensions": { "cssVar": "--border-tertiary" }
},
"focus": {
"$type": "color",
"$value": "{border.brand.default.focus}",
"$extensions": { "cssVar": "--border-focus" }
},
"information": {
"$type": "color",
"$value": "{border.status-splitting.default.default}",
"$extensions": { "cssVar": "--border-information" }
},
"success": {
"$type": "color",
"$value": "{border.status-completed.default.default}",
"$extensions": { "cssVar": "--border-success" }
},
"warning": {
"$type": "color",
"$value": "{border.status-pending.default.default}",
"$extensions": { "cssVar": "--border-warning" }
},
"caution": {
"$type": "color",
"$value": "{border.status-error.default.default}",
"$extensions": { "cssVar": "--border-caution" }
},
"disabled": {
"$type": "color",
"$value": "{border.neutral.subtle.disabled}",
"$extensions": { "cssVar": "--border-disabled" }
}
},
"icon": {
"primary": {
"$type": "color",
"$value": "{icon.neutral.default.default}",
"$extensions": { "cssVar": "--icon-primary" }
},
"secondary": {
"$type": "color",
"$value": "{icon.neutral.muted.default}",
"$extensions": { "cssVar": "--icon-secondary" }
},
"action": {
"$type": "color",
"$value": "{icon.neutral.default.default}",
"$extensions": { "cssVar": "--icon-action" }
},
"action-hover": {
"$type": "color",
"$value": "{icon.neutral.default.hover}",
"$extensions": { "cssVar": "--icon-action-hover" }
},
"disabled": {
"$type": "color",
"$value": "{icon.neutral.muted.disabled}",
"$extensions": { "cssVar": "--icon-disabled" }
},
"information": {
"$type": "color",
"$value": "{icon.status-splitting.default.default}",
"$extensions": { "cssVar": "--icon-information" }
},
"success": {
"$type": "color",
"$value": "{icon.status-completed.default.default}",
"$extensions": { "cssVar": "--icon-success" }
},
"warning": {
"$type": "color",
"$value": "{icon.status-pending.default.default}",
"$extensions": { "cssVar": "--icon-warning" }
},
"caution": {
"$type": "color",
"$value": "{icon.status-error.default.default}",
"$extensions": { "cssVar": "--icon-caution" }
}
},
"app": {
"background": {
"$type": "color",
"$value": "{bg.neutral.subtle.default}",
"$extensions": { "cssVar": "--bg-page" }
},
"fill-default": {
"$type": "color",
"$value": "{bg.neutral.strong.default}",
"$extensions": { "cssVar": "--fill-default" }
},
"code-bg": {
"$type": "color",
"$value": "{bg.neutral.inverse.default}",
"$extensions": { "cssVar": "--code-bg" }
},
"code-foreground": {
"$type": "color",
"$value": "{text.neutral.inverse.default}",
"$extensions": { "cssVar": "--code-foreground" }
},
"code-surface": {
"$type": "color",
"$value": "{bg.neutral.inverse.hover}",
"$extensions": { "cssVar": "--code-surface" }
},
"mask-default": {
"$type": "color",
"$value": "{bg.neutral.subtle.default}",
"$extensions": { "cssVar": "--mask-default" }
},
"mask-dark": {
"$type": "color",
"$value": "{bg.neutral.inverse.default}",
"$extensions": { "cssVar": "--mask-dark" }
}
},
"terminal": {
"viewport-surface": {
"$type": "color",
"$description": "Fixed black backdrop for the embedded terminal viewport (xterm); not theme-tinted.",
"$value": "#000000",
"$extensions": { "cssVar": "--terminal-viewport-surface" }
}
},
"compat": {
"text-session-single-agent": {
"$type": "color",
"$value": "{text.single-agent.default.default}",
"$extensions": { "cssVar": "--text-session-single-agent" }
},
"text-session-workforce": {
"$type": "color",
"$value": "{text.workforce.default.default}",
"$extensions": { "cssVar": "--text-session-workforce" }
},
"fill-fill-primary": {
"$type": "color",
"$value": "{bg.brand.default.default}",
"$extensions": { "cssVar": "--fill-fill-primary" }
},
"fill-fill-primary-hover": {
"$type": "color",
"$value": "{bg.brand.default.hover}",
"$extensions": { "cssVar": "--fill-fill-primary-hover" }
},
"fill-fill-primary-active": {
"$type": "color",
"$value": "{bg.brand.default.active}",
"$extensions": { "cssVar": "--fill-fill-primary-active" }
},
"fill-fill-primary-disabled": {
"$type": "color",
"$value": "{bg.neutral.muted.disabled}",
"$extensions": { "cssVar": "--fill-fill-primary-disabled" }
},
"fill-fill-tertiary": {
"$type": "color",
"$value": "{bg.neutral.subtle.default}",
"$extensions": { "cssVar": "--fill-fill-tertiary" }
},
"fill-fill-tertiary-hover": {
"$type": "color",
"$value": "{bg.neutral.subtle.hover}",
"$extensions": { "cssVar": "--fill-fill-tertiary-hover" }
},
"fill-fill-tertiary-active": {
"$type": "color",
"$value": "{bg.neutral.subtle.active}",
"$extensions": { "cssVar": "--fill-fill-tertiary-active" }
},
"fill-fill-tertiary-disabled": {
"$type": "color",
"$value": "{bg.neutral.muted.disabled}",
"$extensions": { "cssVar": "--fill-fill-tertiary-disabled" }
},
"fill-fill-transparent": {
"$type": "color",
"$value": "transparent",
"$extensions": { "cssVar": "--fill-fill-transparent" }
},
"fill-fill-transparent-hover": {
"$type": "color",
"$value": "{bg.neutral.subtle.hover}",
"$extensions": { "cssVar": "--fill-fill-transparent-hover" }
},
"fill-fill-transparent-active": {
"$type": "color",
"$value": "{bg.neutral.subtle.active}",
"$extensions": { "cssVar": "--fill-fill-transparent-active" }
},
"fill-fill-transparent-disabled": {
"$type": "color",
"$value": "transparent",
"$extensions": { "cssVar": "--fill-fill-transparent-disabled" }
},
"fill-fill-secondary": {
"$type": "color",
"$value": "{bg.neutral.default.default}",
"$extensions": { "cssVar": "--fill-fill-secondary" }
},
"fill-fill-secondary-hover": {
"$type": "color",
"$value": "{bg.neutral.default.hover}",
"$extensions": { "cssVar": "--fill-fill-secondary-hover" }
},
"fill-fill-secondary-active": {
"$type": "color",
"$value": "{bg.neutral.default.active}",
"$extensions": { "cssVar": "--fill-fill-secondary-active" }
},
"fill-fill-secondary-disabled": {
"$type": "color",
"$value": "{bg.neutral.muted.disabled}",
"$extensions": { "cssVar": "--fill-fill-secondary-disabled" }
},
"fill-fill-success": {
"$type": "color",
"$value": "{bg.status-completed.default.default}",
"$extensions": { "cssVar": "--fill-fill-success" }
},
"fill-fill-success-hover": {
"$type": "color",
"$value": "{bg.status-completed.subtle.hover}",
"$extensions": { "cssVar": "--fill-fill-success-hover" }
},
"fill-fill-success-active": {
"$type": "color",
"$value": "{bg.status-completed.subtle.active}",
"$extensions": { "cssVar": "--fill-fill-success-active" }
},
"fill-fill-success-disable": {
"$type": "color",
"$value": "{bg.status-completed.subtle.default}",
"$extensions": { "cssVar": "--fill-fill-success-disable" }
},
"fill-fill-warning": {
"$type": "color",
"$value": "{bg.status-pending.default.default}",
"$extensions": { "cssVar": "--fill-fill-warning" }
},
"fill-fill-caution": {
"$type": "color",
"$value": "{bg.status-error.default.default}",
"$extensions": { "cssVar": "--fill-fill-caution" }
},
"fill-fill-information": {
"$type": "color",
"$value": "{bg.status-splitting.default.default}",
"$extensions": { "cssVar": "--fill-fill-information" }
},
"fill-browser": {
"$type": "color",
"$value": "{bg.browser.subtle.default}",
"$extensions": { "cssVar": "--fill-browser" }
},
"fill-document": {
"$type": "color",
"$value": "{bg.document.subtle.default}",
"$extensions": { "cssVar": "--fill-document" }
},
"fill-socialmedia": {
"$type": "color",
"$value": "{bg.single-agent.subtle.default}",
"$extensions": { "cssVar": "--fill-socialmedia" }
},
"fill-multimodal": {
"$type": "color",
"$value": "{bg.information.subtle.default}",
"$extensions": { "cssVar": "--fill-multimodal" }
},
"fill-developer": {
"$type": "color",
"$value": "{bg.workforce.subtle.default}",
"$extensions": { "cssVar": "--fill-developer" }
},
"icon-on-action": {
"$type": "color",
"$value": "{text.brand.inverse.default}",
"$extensions": { "cssVar": "--icon-on-action" }
},
"icon-on-hover": {
"$type": "color",
"$value": "{text.brand.inverse.default}",
"$extensions": { "cssVar": "--icon-on-hover" }
},
"icon-on-disabled": {
"$type": "color",
"$value": "{text.neutral.muted.disabled}",
"$extensions": { "cssVar": "--icon-on-disabled" }
},
"bg-page-default": {
"$type": "color",
"$value": "{bg.neutral.subtle.default}",
"$extensions": { "cssVar": "--bg-page-default" }
},
"background": {
"$type": "color",
"$value": "{bg.neutral.subtle.default}",
"$extensions": { "cssVar": "--background" }
}
}
}
}

View file

@ -0,0 +1,15 @@
{
"contracts": {
"base": {
"version": 2,
"themeId": "eigent",
"contrast": 43,
"overrides": {
"tone": {},
"emphasis": {},
"state": {},
"cell": {}
}
}
}
}

View file

@ -0,0 +1,4 @@
{
"$extends": "contracts.base",
"mode": "dark"
}

View file

@ -0,0 +1,4 @@
{
"$extends": "contracts.base",
"mode": "light"
}

29
tokens/manifest.json Normal file
View file

@ -0,0 +1,29 @@
{
"elements": ["bg", "text", "border", "icon", "ring"],
"emphasis": ["subtle", "muted", "default", "strong", "inverse"],
"states": ["default", "hover", "active", "selected", "focus", "disabled"],
"tones": [
"neutral",
"brand",
"status-running",
"status-splitting",
"status-pending",
"status-error",
"status-reassigning",
"status-completed",
"status-blocked",
"status-paused",
"status-skipped",
"status-cancelled",
"single-agent",
"workforce",
"browser",
"terminal",
"document",
"success",
"caution",
"error",
"warning",
"information"
]
}

143
tokens/semantic.color.json Normal file
View file

@ -0,0 +1,143 @@
{
"$description": "Semantic token axes and generation transforms for V2.",
"axes": {
"elements": ["bg", "text", "border", "icon", "ring"],
"tones": [
"neutral",
"brand",
"status-running",
"status-splitting",
"status-pending",
"status-error",
"status-reassigning",
"status-completed",
"status-blocked",
"status-paused",
"status-skipped",
"status-cancelled",
"single-agent",
"workforce",
"browser",
"terminal",
"document",
"success",
"caution",
"error",
"warning",
"information"
],
"emphasis": ["subtle", "muted", "default", "strong", "inverse"],
"states": ["default", "hover", "active", "selected", "focus", "disabled"]
},
"transforms": {
"emphasis": {
"subtle": { "dL": 0.18, "dC": -0.02 },
"muted": { "dL": 0.1, "dC": -0.01 },
"default": { "dL": 0, "dC": 0 },
"strong": { "dL": -0.08, "dC": 0.02 },
"inverse": { "dL": -0.22, "dC": 0.01 }
},
"state": {
"default": { "dL": 0, "dC": 0 },
"hover": { "dL": -0.03, "dC": 0.01 },
"active": { "dL": -0.06, "dC": 0.015 },
"selected": { "dL": -0.08, "dC": 0.015 },
"focus": { "dL": -0.05, "dC": 0.03 },
"disabled": { "dL": 0.08, "dC": -0.03, "alpha": 0.5 }
},
"element": {
"bg": { "dL": 0, "dC": 0 },
"text": { "dL": -0.12, "dC": -0.005 },
"border": { "dL": -0.06, "dC": -0.01, "alpha": 0.75 },
"icon": { "dL": -0.1, "dC": -0.005, "alpha": 0.92 },
"ring": { "dL": -0.04, "dC": 0.01, "alpha": 0.62 }
}
},
"toneSource": {
"neutral": {
"source": "background",
"sourceByElement": {
"text": "ink",
"icon": "ink"
},
"dC": -0.08,
"dLLight": 0.08,
"dLDark": -0.08
},
"brand": { "source": "accent", "dC": 0.03 },
"status-running": { "source": "accent", "dH": 0, "dC": 0.02 },
"status-splitting": { "source": "accent", "dH": 25, "dC": 0.015 },
"status-pending": { "source": "accent", "dH": 72, "dC": 0.02 },
"status-error": {
"source": "accent",
"dH": 18,
"dC": 0.03,
"dLLight": -0.03,
"dLDark": 0.03
},
"status-reassigning": { "source": "accent", "dH": 58, "dC": 0.015 },
"status-completed": { "source": "accent", "dH": 140, "dC": 0.02 },
"status-blocked": { "source": "accent", "dH": 62, "dC": 0.015 },
"status-paused": { "source": "accent", "dH": 50, "dC": 0.01 },
"status-skipped": {
"source": "ink",
"dC": -0.06,
"dLLight": 0.12,
"dLDark": -0.12
},
"status-cancelled": {
"source": "ink",
"dC": -0.05,
"dLLight": 0.09,
"dLDark": -0.09
},
"single-agent": { "source": "fixed" },
"workforce": { "source": "fixed" },
"browser": { "source": "fixed" },
"terminal": { "source": "fixed" },
"document": { "source": "fixed" },
"success": { "source": "fixed" },
"caution": { "source": "fixed" },
"error": { "source": "fixed" },
"warning": { "source": "fixed" },
"information": { "source": "fixed" }
},
"contrastPairs": [
{
"fg": "text.neutral.default.default",
"bg": "bg.neutral.subtle.default",
"minContrast": 4.5
},
{
"fg": "text.neutral.muted.default",
"bg": "bg.neutral.subtle.default",
"minContrast": 4.5
},
{
"fg": "text.brand.inverse.default",
"bg": "bg.brand.default.default",
"minContrast": 3,
"largeText": true
},
{
"fg": "text.status-completed.strong.default",
"bg": "bg.status-completed.subtle.default",
"minContrast": 4.5
},
{
"fg": "text.status-error.strong.default",
"bg": "bg.status-error.subtle.default",
"minContrast": 4.5
},
{
"fg": "text.status-splitting.strong.default",
"bg": "bg.status-splitting.subtle.default",
"minContrast": 4.5
},
{
"fg": "text.status-pending.strong.default",
"bg": "bg.status-pending.subtle.default",
"minContrast": 4.5
}
]
}

View file

@ -6,5 +6,5 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json"]
"include": ["vite.config.ts", "package.json", "scripts/**/*.ts"]
}