项目文件夹

文件
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

385 行
18 KiB
TypeScript

import type { ChromeTheme, ThemeMode } from './types'
/* ─────────────────────────────────────────────────────────────────────────
* Codex theme derivation, reverse-engineered from the running app.
*
* 5 user inputs (accent, ink, surface, contrast, opaqueWindows) + fonts +
* semanticColors drive 60+ derived CSS custom properties on `<html>`.
*
* Every contrast-driven alpha token has the form:
* value(c) = base + delta · (c/100)^p
* with mode-specific p (≈1.5 light, ≈1.66 dark) and per-token (base, delta)
* coefficients fit empirically from snapshots of the running Codex app.
*
* See packages/codium/design-system.md for the full treatment.
* ──────────────────────────────────────────────────────────────────────── */
type RGB = [number, number, number]
type Curve = { base: number; delta: number }
interface ModeCoeffs {
p: number
/* — contrast-shaped rgba(ink, …) overlays — */
fgSecondary: Curve
fgTertiary: Curve
border: Curve
borderHeavy: Curve
borderLight: Curve
/** Light: passthrough ink. Dark: rgba(ink, base+delta·t). */
iconPrimary: Curve | 'passthrough'
btnPrimaryActive: Curve
btnPrimaryHover: Curve
btnPrimaryInactive: Curve
btnSecondary: Curve
btnSecondaryActive: Curve
btnSecondaryHover: Curve
btnSecondaryInactive: Curve
btnTertiary: Curve
btnTertiaryActive: Curve
btnTertiaryHover: Curve
/* — contrast-shaped mixes — */
/** Light: mix(surface, ink, …). Dark: mix(surface, BLACK, …). */
surfUnder: Curve
/** Light: mix(surface, accent, …). Dark: mix(BLACK, accent, …). */
bgAccent: Curve
bgAccentHover: Curve
bgAccentActive: Curve
/** Dark: mix(accent, WHITE, …) for legible accent foreground. */
accentForegroundMix: Curve
/* — fixed brand colors per mode — */
accentPurple: string
decorationAdded: string
decorationDeleted: string
/** alpha applied to decoration colors for `editor-added` / `editor-deleted`. */
editorOverlayAlpha: number
}
const COEFFS: Record<ThemeMode, ModeCoeffs> = {
light: {
p: 1.5,
fgSecondary: { base: 0.598, delta: 0.335 },
fgTertiary: { base: 0.398, delta: 0.335 },
border: { base: 0.04, delta: 0.133 },
borderHeavy: { base: 0.06, delta: 0.20 },
borderLight: { base: 0.03, delta: 0.067 },
iconPrimary: 'passthrough',
btnPrimaryActive: { base: 0.04, delta: 0.40 },
btnPrimaryHover: { base: 0.02, delta: 0.20 },
btnPrimaryInactive: { base: 0.151, delta: 0.306 },
btnSecondary: { base: 0.037, delta: 0.041 },
btnSecondaryActive: { base: 0.027, delta: 0.041 },
btnSecondaryHover: { base: 0.035, delta: 0.061 },
btnSecondaryInactive: { base: 0.007, delta: 0.041 },
btnTertiary: { base: 0, delta: 0 }, // always transparent
btnTertiaryActive: { base: 0.140, delta: 0.184 },
btnTertiaryHover: { base: 0.073, delta: 0.082 },
surfUnder: { base: 0.0, delta: 0.10 }, // mix(surface, ink)
bgAccent: { base: 0.09, delta: 0.13 }, // mix(surface, accent)
bgAccentHover: { base: 0.10, delta: 0.15 },
bgAccentActive: { base: 0.11, delta: 0.16 },
accentForegroundMix: { base: 0, delta: 0 }, // light: passthrough
accentPurple: '#751ed9',
decorationAdded: '#00a240',
decorationDeleted: '#e02e2a',
editorOverlayAlpha: 0.15,
},
dark: {
p: 1.66,
fgSecondary: { base: 0.58, delta: 0.303 },
fgTertiary: { base: 0.329, delta: 0.394 },
border: { base: 0.032, delta: 0.121 },
borderHeavy: { base: 0.078, delta: 0.182 },
borderLight: { base: 0.016, delta: 0.061 },
iconPrimary: { base: 0.79, delta: 0.27 },
btnPrimaryActive: { base: 0.035, delta: 0.152 },
btnPrimaryHover: { base: 0.019, delta: 0.091 },
btnPrimaryInactive: { base: 0.017, delta: 0.036 },
btnSecondary: { base: 0.037, delta: 0.036 },
btnSecondaryActive: { base: 0.078, delta: 0.099 },
btnSecondaryHover: { base: 0.051, delta: 0.063 },
btnSecondaryInactive: { base: 0.015, delta: 0.054 },
btnTertiary: { base: 0.017, delta: 0.027 },
btnTertiaryActive: { base: 0.058, delta: 0.099 },
btnTertiaryHover: { base: 0.041, delta: 0.063 },
surfUnder: { base: 0.05, delta: 0.16 }, // mix(surface, BLACK)
bgAccent: { base: 0.17, delta: 0.19 }, // mix(BLACK, accent)
bgAccentHover: { base: 0.18, delta: 0.21 },
bgAccentActive: { base: 0.20, delta: 0.23 },
accentForegroundMix: { base: 0.19, delta: 0.46 },
accentPurple: '#ad7bf9',
decorationAdded: '#40c977',
decorationDeleted: '#fa423e',
editorOverlayAlpha: 0.23,
},
}
/** Dark-mode contrast-shaped surface↔ink mixes (in light, surface=white, no headroom). */
const DARK_SURFACE_MIX = {
/** mix(surface, ink, …) → control-opaque */
controlOpaque: { base: 0.049, delta: 0.099 },
/** mix(surface, ink, …) → editor-opaque + elevated-secondary-opaque */
editorOpaque: { base: 0.058, delta: 0.027 },
/** mix(surface, ink, …) → elevated-primary[-opaque] */
elevatedPrimary: { base: 0.053, delta: 0.180 },
/** mix(surface, BLACK, …) → button-primary bg + text-button-primary */
buttonPrimaryFill: { base: 0.46, delta: 0 },
/** mix(surface, ink, …) → text-button-secondary */
textBtnSecondary: { base: 0.20, delta: 0.09 },
}
/* ─────────── color helpers ─────────── */
function parseHex(hex: string): RGB {
const h = hex.startsWith('#') ? hex.slice(1) : hex
const expanded = h.length === 3 ? h.split('').map((c) => c + c).join('') : h
return [
parseInt(expanded.slice(0, 2), 16),
parseInt(expanded.slice(2, 4), 16),
parseInt(expanded.slice(4, 6), 16),
]
}
const clampByte = (n: number) => Math.max(0, Math.min(255, Math.round(n)))
const toHex = (n: number) => clampByte(n).toString(16).padStart(2, '0')
const rgbToHex = ([r, g, b]: RGB) => '#' + toHex(r) + toHex(g) + toHex(b)
function rgba(rgb: RGB, alpha: number) {
const a = Math.max(0, Math.min(1, alpha))
return `rgba(${clampByte(rgb[0])}, ${clampByte(rgb[1])}, ${clampByte(rgb[2])}, ${Math.round(a * 1000) / 1000})`
}
function rgb(rgb: RGB) {
return `rgb(${clampByte(rgb[0])}, ${clampByte(rgb[1])}, ${clampByte(rgb[2])})`
}
function mix(a: RGB, b: RGB, t: number): RGB {
const k = Math.max(0, Math.min(1, t))
return [a[0] * (1 - k) + b[0] * k, a[1] * (1 - k) + b[1] * k, a[2] * (1 - k) + b[2] * k]
}
const BLACK: RGB = [0, 0, 0]
const WHITE: RGB = [255, 255, 255]
/* ─────────── derivation ─────────── */
function shape(contrast: number, mode: ThemeMode) {
const c = Math.max(0, Math.min(100, contrast)) / 100
return Math.pow(c, COEFFS[mode].p)
}
const apply = (curve: Curve, t: number) => curve.base + curve.delta * t
interface DerivedTokens { [key: string]: string }
const TYPOGRAPHY: DerivedTokens = {
'--text-xs': '11px',
'--text-sm': '12px',
'--text-base': '13px',
'--text-lg': '16px',
'--text-heading-sm': '18px',
'--text-heading-md': '20px',
'--text-heading-lg': '24px',
'--text-xl': '28px',
'--text-2xl': '36px',
'--text-3xl': '48px',
'--text-4xl': '72px',
'--vscode-font-size': '13px',
'--vscode-editor-font-size': '12px',
}
export function deriveTokens(theme: ChromeTheme, mode: ThemeMode): DerivedTokens {
const ink = parseHex(theme.ink)
const surface = parseHex(theme.surface)
const accent = parseHex(theme.accent)
const t = shape(theme.contrast, mode)
const C = COEFFS[mode]
const inkA = (alpha: number) => rgba(ink, alpha)
const inkOverlay = (curve: Curve) => inkA(apply(curve, t))
/* — accent foreground (legibility) — */
const accentFg = mode === 'dark'
? rgbToHex(mix(accent, WHITE, apply(C.accentForegroundMix, t)))
: theme.accent
/* — surface "below" (page/sidebar bg) — */
const surfUnderTarget = mode === 'dark' ? BLACK : ink
const surfaceUnder = rgbToHex(mix(surface, surfUnderTarget, apply(C.surfUnder, t)))
/* — surface "above" (panel/elevated/control/dropdown) — */
let bgControlRgb: RGB
let bgEditorRgb: RGB
let bgElevatedPrimaryRgb: RGB
let panel: string
if (mode === 'dark') {
bgControlRgb = mix(surface, ink, apply(DARK_SURFACE_MIX.controlOpaque, t))
bgEditorRgb = mix(surface, ink, apply(DARK_SURFACE_MIX.editorOpaque, t))
bgElevatedPrimaryRgb = mix(surface, ink, apply(DARK_SURFACE_MIX.elevatedPrimary, t))
panel = rgbToHex(mix(surface, ink, 0.07))
} else {
bgControlRgb = surface
bgEditorRgb = surface
bgElevatedPrimaryRgb = surface
panel = theme.surface
}
const bgControlOpaque = rgb(bgControlRgb)
const bgControl = rgba(bgControlRgb, 0.96)
const bgEditorOpaque = rgb(bgEditorRgb)
const bgElevatedPrimary = rgba(bgElevatedPrimaryRgb, 0.96)
const bgElevatedPrimaryOpaque = rgb(bgElevatedPrimaryRgb)
const bgElevatedSecondaryOpaque = rgbToHex(bgEditorRgb)
const bgElevatedSecondary = mode === 'dark'
? inkOverlay(C.btnPrimaryInactive) // matches Codex value (≈ rgba(ink, 0.032) at c=60)
: rgba(surface, 0.96)
/* — bg-accent family (selection accent backgrounds) — */
const bgAccentBase: RGB = mode === 'dark' ? BLACK : surface
const bgAccent = rgbToHex(mix(bgAccentBase, accent, apply(C.bgAccent, t)))
const bgAccentHover = rgbToHex(mix(bgAccentBase, accent, apply(C.bgAccentHover, t)))
const bgAccentActive = rgbToHex(mix(bgAccentBase, accent, apply(C.bgAccentActive, t)))
/* — primary button fill (contrast-independent in our model) — */
const buttonPrimaryFillRgb: RGB = mode === 'dark'
? mix(surface, BLACK, apply(DARK_SURFACE_MIX.buttonPrimaryFill, t))
: ink
const buttonPrimaryFill = mode === 'dark' ? rgb(buttonPrimaryFillRgb) : rgbToHex(buttonPrimaryFillRgb)
/* — text-button colors — */
const textBtnPrimary = mode === 'dark' ? rgb(buttonPrimaryFillRgb) : theme.surface
const textBtnSecondary = mode === 'dark'
? rgbToHex(mix(surface, ink, apply(DARK_SURFACE_MIX.textBtnSecondary, t)))
: theme.ink
const textBtnTertiary = inkOverlay(C.fgTertiary) // = text-foreground-tertiary
/* — decoration / editor overlays — */
const decorationAdded = C.decorationAdded
const decorationDeleted = C.decorationDeleted
const editorAdded = rgba(parseHex(decorationAdded), C.editorOverlayAlpha)
const editorDeleted = rgba(parseHex(decorationDeleted), C.editorOverlayAlpha)
/* — icon-primary (contrast-shaped in dark only) — */
const iconPrimary = C.iconPrimary === 'passthrough'
? theme.ink
: inkA(apply(C.iconPrimary, t))
return {
/* === BASE INPUTS (echoed) === */
'--codex-base-accent': theme.accent,
'--codex-base-ink': theme.ink,
'--codex-base-surface': theme.surface,
'--codex-base-contrast': String(theme.contrast),
/* === FONTS === */
'--vscode-font-family': appendFontFallback(theme.fonts.ui ?? 'Geist, Inter', 'sans'),
'--vscode-editor-font-family': appendFontFallback(theme.fonts.code ?? '"Geist Mono", ui-monospace, "SFMono-Regular"', 'mono'),
/* === TYPOGRAPHY (fixed) === */
...TYPOGRAPHY,
/* === PASS-THROUGHS / ACCENT === */
'--color-text-foreground': theme.ink,
'--color-icon-primary': iconPrimary,
'--color-text-accent': accentFg,
'--color-icon-accent': accentFg,
'--color-background-surface': theme.surface,
'--color-accent-blue': theme.accent,
'--color-accent-purple': C.accentPurple,
/* === STATIC-ALPHA INK OVERLAYS === */
'--color-hover': inkA(0.05),
'--color-token-list-hover-background': inkA(0.05),
'--color-token-toolbar-hover-background': inkA(0.05),
'--color-token-badge-background': inkA(0.047),
'--color-token-input-border': inkA(0.074),
'--color-token-bg-fog': inkA(0.025),
/* === CONTRAST-SHAPED INK OVERLAYS === */
'--color-text-foreground-secondary': inkOverlay(C.fgSecondary),
'--color-text-foreground-tertiary': inkOverlay(C.fgTertiary),
'--color-icon-secondary': inkOverlay(C.fgSecondary),
'--color-icon-tertiary': inkOverlay(C.fgTertiary),
'--color-border': inkOverlay(C.border),
'--color-border-heavy': inkOverlay(C.borderHeavy),
'--color-border-light': inkOverlay(C.borderLight),
'--color-active': inkA(0.04 + 0.06 * t),
'--color-button-bg': inkA(0.04 + 0.06 * t),
'--color-button-hover': inkA(0.04 + 0.04 * t),
// Light: black overlay over light surfaces. Dark: ink overlay over dark.
'--color-simple-scrim': mode === 'light'
? rgba(BLACK, 0.06 + 0.13 * t)
: inkA(0.06 + 0.13 * t),
/* button background family */
'--color-background-button-primary': buttonPrimaryFill,
'--color-background-button-primary-active': inkOverlay(C.btnPrimaryActive),
'--color-background-button-primary-hover': inkOverlay(C.btnPrimaryHover),
'--color-background-button-primary-inactive': inkOverlay(C.btnPrimaryInactive),
'--color-background-button-secondary': inkOverlay(C.btnSecondary),
'--color-background-button-secondary-active': inkOverlay(C.btnSecondaryActive),
'--color-background-button-secondary-hover': inkOverlay(C.btnSecondaryHover),
'--color-background-button-secondary-inactive': inkOverlay(C.btnSecondaryInactive),
'--color-background-button-tertiary': inkOverlay(C.btnTertiary),
'--color-background-button-tertiary-active': inkOverlay(C.btnTertiaryActive),
'--color-background-button-tertiary-hover': inkOverlay(C.btnTertiaryHover),
/* text on buttons */
'--color-text-button-primary': textBtnPrimary,
'--color-text-button-secondary': textBtnSecondary,
'--color-text-button-tertiary': textBtnTertiary,
/* === SURFACE FAMILY === */
'--color-background-surface-under': surfaceUnder,
'--color-background-panel': panel,
'--color-background-control': bgControl,
'--color-background-control-opaque': bgControlOpaque,
'--color-background-editor-opaque': bgEditorOpaque,
'--color-background-elevated': bgElevatedPrimary, // legacy alias
'--color-background-elevated-primary': bgElevatedPrimary,
'--color-background-elevated-primary-opaque': bgElevatedPrimaryOpaque,
'--color-background-elevated-secondary': bgElevatedSecondary,
'--color-background-elevated-secondary-opaque': bgElevatedSecondaryOpaque,
/* === ACCENT BACKGROUNDS === */
'--color-background-accent': bgAccent,
'--color-background-accent-hover': bgAccentHover,
'--color-background-accent-active': bgAccentActive,
/* === FOCUS ring === */
// Light: passthrough accent (full opacity).
// Dark: accent-foreground (auto-lightened) at 0.76 alpha.
'--color-border-focus':
mode === 'dark'
? rgba(parseHex(accentFg), 0.76)
: theme.accent,
/* === DIFF / EDITOR DECORATIONS (fixed brand per mode) === */
'--color-decoration-added': decorationAdded,
'--color-decoration-deleted': decorationDeleted,
'--color-editor-added': editorAdded,
'--color-editor-deleted': editorDeleted,
/* legacy semantic-color shortcuts (unchanged from theme.semanticColors) */
'--color-accent-green': theme.semanticColors.diffAdded,
'--color-accent-red': theme.semanticColors.diffRemoved,
}
}
/** Codex appends `, var(--font-{sans,mono}-default)` to user-supplied font stacks. */
function appendFontFallback(value: string, kind: 'sans' | 'mono'): string {
const tail = `var(--font-${kind}-default)`
return value.includes(tail) ? value : `${value}, ${tail}`
}
/** Apply derived tokens to <html>'s inline style and toggle theme classes. */
export function applyTheme(theme: ChromeTheme, mode: ThemeMode) {
const root = document.documentElement
const tokens = deriveTokens(theme, mode)
for (const [k, v] of Object.entries(tokens)) {
root.style.setProperty(k, v)
}
root.classList.toggle('electron-light', mode === 'light')
root.classList.toggle('electron-dark', mode === 'dark')
root.classList.toggle('electron-opaque', !!theme.opaqueWindows)
root.style.colorScheme = mode
}