mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-04 21:15:35 +08:00
Co-authored-by: gujiaming <52187003+AtomsH4@users.noreply.github.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: kangfenmao <kangfenmao@qq.com> Signed-off-by: gujiaming <52187003+AtomsH4@users.noreply.github.com>
183 lines
5.9 KiB
TypeScript
183 lines
5.9 KiB
TypeScript
import { promises as fs } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
|
|
const STYLES_DIR = path.resolve(__dirname, '../src/styles')
|
|
const THEME_OUTPUT_PATH = path.join(STYLES_DIR, 'theme.css')
|
|
|
|
const RUNTIME_THEME_INPUT_LINES = [
|
|
'--cs-theme-primary: var(--cs-primary);',
|
|
'--cs-theme-ring: color-mix(in srgb, var(--cs-theme-primary) 40%, transparent);'
|
|
]
|
|
|
|
const COMPATIBILITY_ALIAS_LINES = ['--primary: var(--color-primary);', '--ring: var(--color-ring);']
|
|
|
|
const PRIMARY_SEMANTIC_LINES = [
|
|
'--color-primary: var(--cs-theme-primary);',
|
|
'--color-primary-hover: var(--cs-primary-hover);',
|
|
'--color-primary-soft: color-mix(in srgb, var(--color-primary) 60%, transparent);',
|
|
'--color-primary-mute: color-mix(in srgb, var(--color-primary) 30%, transparent);',
|
|
'--color-ring: var(--cs-theme-ring);'
|
|
]
|
|
|
|
const SPACING_COMMENT_LINES = [
|
|
'/* Keep spacing opt-in for now to avoid overriding Tailwind container names. */',
|
|
'/* --spacing-5xs: var(--cs-size-5xs);',
|
|
'--spacing-4xs: var(--cs-size-4xs);',
|
|
'--spacing-3xs: var(--cs-size-3xs);',
|
|
'--spacing-2xs: var(--cs-size-2xs);',
|
|
'--spacing-xs: var(--cs-size-xs);',
|
|
'--spacing-sm: var(--cs-size-sm);',
|
|
'--spacing-md: var(--cs-size-md);',
|
|
'--spacing-lg: var(--cs-size-lg);',
|
|
'--spacing-xl: var(--cs-size-xl);',
|
|
'--spacing-2xl: var(--cs-size-2xl);',
|
|
'--spacing-3xl: var(--cs-size-3xl);',
|
|
'--spacing-4xl: var(--cs-size-4xl);',
|
|
'--spacing-5xl: var(--cs-size-5xl);',
|
|
'--spacing-6xl: var(--cs-size-6xl);',
|
|
'--spacing-7xl: var(--cs-size-7xl);',
|
|
'--spacing-8xl: var(--cs-size-8xl); */'
|
|
]
|
|
|
|
const ANIMATION_LINES = [
|
|
'--animate-checkbox-bounce: checkbox-bounce 300ms cubic-bezier(0.4, 0, 0.2, 1);',
|
|
'--animate-checkbox-icon-in: checkbox-icon-in 160ms ease-out both;',
|
|
'',
|
|
'@keyframes checkbox-bounce {',
|
|
' 0%,',
|
|
' 100% {',
|
|
' transform: scale(1);',
|
|
' }',
|
|
'',
|
|
' 50% {',
|
|
' transform: scale(1.08);',
|
|
' }',
|
|
'}',
|
|
'',
|
|
'@keyframes checkbox-icon-in {',
|
|
' from {',
|
|
' opacity: 0;',
|
|
' transform: scale(0.75);',
|
|
' }',
|
|
'',
|
|
' to {',
|
|
' opacity: 1;',
|
|
' transform: scale(1);',
|
|
' }',
|
|
'}'
|
|
]
|
|
|
|
export interface ThemeContractInputs {
|
|
primitiveColors: string[]
|
|
semanticColors: string[]
|
|
statusColors: string[]
|
|
radiusTokens: string[]
|
|
typographyTokens: string[]
|
|
}
|
|
|
|
function dedupe(values: string[]): string[] {
|
|
return [...new Set(values)]
|
|
}
|
|
|
|
export function extractTokenNames(source: string): string[] {
|
|
return dedupe([...source.matchAll(/^\s*--cs-([a-z0-9-]+)\s*:/gm)].map((match) => match[1]))
|
|
}
|
|
|
|
function toPrefixedMappings(tokenNames: string[], targetPrefix: string, sourcePrefix = '--cs-'): string[] {
|
|
return tokenNames.map((tokenName) => `--${targetPrefix}${tokenName}: var(${sourcePrefix}${tokenName});`)
|
|
}
|
|
|
|
function toDirectMappings(tokenNames: string[], sourcePrefix = '--cs-'): string[] {
|
|
return tokenNames.map((tokenName) => `--${tokenName}: var(${sourcePrefix}${tokenName});`)
|
|
}
|
|
|
|
function buildSection(title: string, lines: string[]): string {
|
|
const indentedLines = lines.map((line) => (line ? ` ${line}` : '')).join('\n')
|
|
|
|
return ` /* ==================== */\n /* ${title} */\n /* ==================== */\n${indentedLines}`
|
|
}
|
|
|
|
export function buildThemeContractCss(inputs: ThemeContractInputs): string {
|
|
const semanticContractTokens = inputs.semanticColors.filter(
|
|
(token) => !['primary', 'primary-hover', 'ring'].includes(token)
|
|
)
|
|
|
|
const sections = [
|
|
buildSection('Primitive Colors', toPrefixedMappings(inputs.primitiveColors, 'color-')),
|
|
buildSection('Runtime Theme Inputs', RUNTIME_THEME_INPUT_LINES),
|
|
buildSection('Compatibility Aliases', COMPATIBILITY_ALIAS_LINES),
|
|
buildSection('Semantic Colors', [
|
|
...PRIMARY_SEMANTIC_LINES,
|
|
...toPrefixedMappings(semanticContractTokens, 'color-')
|
|
]),
|
|
buildSection('Status Colors', toPrefixedMappings(inputs.statusColors, 'color-')),
|
|
buildSection('Spacing', SPACING_COMMENT_LINES),
|
|
buildSection('Radius', toDirectMappings(inputs.radiusTokens)),
|
|
buildSection('Typography', toDirectMappings(inputs.typographyTokens)),
|
|
buildSection('Animation', ANIMATION_LINES)
|
|
]
|
|
|
|
return `/**
|
|
* Generated from design tokens.
|
|
*
|
|
* ⚠️ DO NOT EDIT DIRECTLY!
|
|
* This file is generated by \`pnpm theme:build\`.
|
|
* Update \`src/styles/tokens/*\` to change the design source.
|
|
*/
|
|
|
|
@import './tokens.css';
|
|
|
|
@theme {
|
|
${sections.join('\n\n')}
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border outline-ring/50;
|
|
}
|
|
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
}
|
|
`
|
|
}
|
|
|
|
export async function loadThemeContractInputs(stylesDir = STYLES_DIR): Promise<ThemeContractInputs> {
|
|
const tokensDir = path.join(stylesDir, 'tokens')
|
|
const [primitiveColorsSource, semanticColorsSource, statusColorsSource, radiusSource, typographySource] =
|
|
await Promise.all([
|
|
fs.readFile(path.join(tokensDir, 'colors/primitive.css'), 'utf8'),
|
|
fs.readFile(path.join(tokensDir, 'colors/semantic.css'), 'utf8'),
|
|
fs.readFile(path.join(tokensDir, 'colors/status.css'), 'utf8'),
|
|
fs.readFile(path.join(tokensDir, 'radius.css'), 'utf8'),
|
|
fs.readFile(path.join(tokensDir, 'typography.css'), 'utf8')
|
|
])
|
|
|
|
return {
|
|
primitiveColors: extractTokenNames(primitiveColorsSource),
|
|
semanticColors: extractTokenNames(semanticColorsSource),
|
|
statusColors: extractTokenNames(statusColorsSource),
|
|
radiusTokens: extractTokenNames(radiusSource),
|
|
typographyTokens: extractTokenNames(typographySource)
|
|
}
|
|
}
|
|
|
|
export async function writeThemeContractCss(outputPath = THEME_OUTPUT_PATH, stylesDir = STYLES_DIR): Promise<void> {
|
|
const inputs = await loadThemeContractInputs(stylesDir)
|
|
const css = buildThemeContractCss(inputs)
|
|
await fs.writeFile(outputPath, css, 'utf8')
|
|
}
|
|
|
|
async function main() {
|
|
await writeThemeContractCss()
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
|
void main()
|
|
}
|