feat: implement theme toggling and enhance Button styles with new color variables

feat/vapor
tangjinzhou 2025-07-30 23:02:30 +08:00
parent 428fdfc182
commit d7ca354b87
8 changed files with 144 additions and 132 deletions

View File

@ -1,3 +1,15 @@
<template>
<RouterView></RouterView>
<button @click="toggleTheme" class="fixed top-2 right-2">toggle {{ appearance }}</button>
<a-theme :appearance="appearance">
<RouterView />
</a-theme>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const appearance = ref('light')
const toggleTheme = () => {
appearance.value = appearance.value === 'light' ? 'dark' : 'light'
}
</script>

View File

@ -14,6 +14,7 @@ import { buttonProps, buttonEmits, ButtonSlots } from './meta'
import { getCssVarColor } from '@/utils/colorAlgorithm'
import { useThemeInject } from '../theme/hook'
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'
import { defaultColor } from '../theme/meta'
const props = defineProps(buttonProps)
@ -28,7 +29,7 @@ const color = computed(() => {
}
if (props.danger) {
return theme.dangerColor
return 'red'
}
return theme.primaryColor
@ -46,7 +47,12 @@ const rootClass = computed(() => {
}
})
const cssVars = computed(() => {
return getCssVarColor(color.value)
return color.value.toLowerCase() !== defaultColor.toLowerCase()
? getCssVarColor(color.value, {
appearance: theme.appearance,
backgroundColor: theme.backgroundColor,
})
: {}
})
const handleClick = (event: MouseEvent) => {

View File

@ -11,38 +11,40 @@
}
&:where(.ant-btn-solid:not(:disabled)) {
@apply border-none bg-[var(--accent-color)] text-[var(--accent-color-content)];
@apply hover:bg-[var(--accent-color-hover)] active:bg-[var(--accent-color-active)];
@apply bg-accent text-accent-content border-none;
@apply hover:bg-accent-hover active:bg-accent-active;
}
&:where(.ant-btn-outlined:not(:disabled)),
&:where(.ant-btn-dashed:not(:disabled)) {
@apply border-[var(--accent-color)] bg-transparent text-[var(--accent-color)];
@apply hover:text-[var(--accent-color-hover)] active:border-[var(--accent-color-active)] active:text-[var(--accent-color-active)];
@apply border-[var(--accent-color-active)] hover:border-[var(--accent-color-hover)];
@apply border-accent text-accent bg-transparent;
@apply hover:text-accent-hover active:border-accent-active active:text-accent-active;
@apply border-accent-active hover:border-accent-hover;
}
&:where(.ant-btn-text:not(.ant-btn-custom-color):not(:disabled)) {
@apply border-none bg-transparent text-[var(--neutral-color)];
@apply hover:bg-[var(--neutral-disabled-bg)];
@apply text-neutral border-none bg-transparent;
@apply hover:bg-neutral-disabled-bg;
}
&:where(.ant-btn-text.ant-btn-custom-color:not(:disabled)) {
@apply border-none bg-transparent text-[var(--accent-color)];
@apply hover:bg-[var(--accent-color-1)] hover:text-[var(--accent-color-hover)];
@apply text-accent border-none bg-transparent;
@apply hover:bg-accent-1 hover:text-accent-hover;
}
&:where(.ant-btn-link:not(:disabled)) {
@apply border-none bg-transparent text-[var(--accent-color)] hover:text-[var(--accent-color-hover)];
@apply text-accent border-none bg-transparent;
@apply hover:text-accent-hover;
}
&:where(.ant-btn-dashed) {
@apply border-dashed;
}
&:where(.ant-btn-filled:not(:disabled)) {
@apply border-none bg-[var(--accent-color-1)] text-[var(--accent-color)] hover:text-[var(--accent-color-hover)];
@apply hover:bg-[var(--accent-color-2)] active:bg-[var(--accent-color-3)] active:text-[var(--accent-color-active)];
@apply text-accent bg-accent-1 border-none;
@apply hover:bg-accent-2 active:bg-accent-3 active:text-accent-active;
@apply hover:text-accent-hover;
}
&:where(.ant-btn-disabled) {
@apply cursor-not-allowed;
@apply border-[var(--neutral-border)] bg-[var(--neutral-disabled-bg)] text-[var(--neutral-disabled)];
@apply text-neutral-disabled bg-neutral-disabled-bg border-neutral-border;
}
&:where(.ant-btn-disabled.ant-btn-text),
&:where(.ant-btn-disabled.ant-btn-link) {

View File

@ -5,8 +5,30 @@
<script setup lang="ts">
import { themeProps } from './meta'
import { useThemeProvide } from './hook'
import { getCssVarColor } from '@/utils/colorAlgorithm'
import { watchEffect } from 'vue'
const props = defineProps(themeProps)
useThemeProvide(props)
const style = document.createElement('style')
watchEffect(() => {
const cssVars = getCssVarColor(props.primaryColor, {
appearance: props.appearance,
backgroundColor: props.backgroundColor,
})
document.documentElement.classList.remove('light-theme', 'dark-theme')
document.documentElement.classList.add(`${props.appearance}-theme`)
style.textContent = `:root.${props.appearance}-theme {
${Object.entries(cssVars)
.map(([key, value]) => `${key}: ${value};`)
.join('\n')}
}`
document.head.appendChild(style)
return () => {
document.head.removeChild(style)
}
})
</script>

View File

@ -1,21 +1,17 @@
import { inject, InjectionKey, provide, Reactive } from 'vue'
import { inject, InjectionKey, provide } from 'vue'
import { ThemeProps } from './meta'
type ThemeType = Reactive<{
appearance: 'light' | 'dark'
primaryColor: string
dangerColor: string
}>
const ThemeSymbol: InjectionKey<ThemeType> = Symbol('theme')
const ThemeSymbol: InjectionKey<ThemeProps> = Symbol('theme')
export const useThemeInject = () => {
return inject(ThemeSymbol, {
appearance: 'light',
primaryColor: '#1677ff',
dangerColor: '#ff4d4f',
})
darkBackgroundColor: '#141414',
} as ThemeProps)
}
export const useThemeProvide = (theme: ThemeType) => {
export const useThemeProvide = (theme: ThemeProps) => {
provide(ThemeSymbol, theme)
}

View File

@ -1,5 +1,6 @@
import { PropType, ExtractPublicPropTypes } from 'vue'
export const defaultColor = '#1677FF'
// Theme Props
export const themeProps = {
/**
@ -16,15 +17,15 @@ export const themeProps = {
*/
primaryColor: {
type: String,
default: '#1677FF',
default: defaultColor,
},
/**
* Specifies the danger color of the component
* @default '#ff4d4f'
* Specifies the background color of the component, only used in dark mode
* @default '#141414'
*/
dangerColor: {
backgroundColor: {
type: String,
default: '#ff4d4f',
default: '#141414',
},
} as const

View File

@ -1,29 +1,33 @@
@theme static {
--color-base-100: #ffffff;
--color-base-200: #f7f7f7;
--color-base-300: #ededed;
--color-base-content: #222222;
--color-primary: #151415;
--color-primary-content: #ffffff;
--color-secondary: #0d58fc;
--color-secondary-content: #ffffff;
--color-accent: #0289ff;
--color-accent-1: #e6f4ff;
--color-accent-2: #bae0ff;
--color-accent-3: #91caff;
--color-accent-4: #69b1ff;
--color-accent-5: #4096ff;
--color-accent-6: #1677ff;
--color-accent-7: #0958d9;
--color-accent-8: #003eb3;
--color-accent-9: #002c8c;
--color-accent-10: #001d66;
--color-accent: #1677ff;
--color-accent-hover: #4096ff;
--color-accent-active: #0958d9;
--color-accent-content: #ffffff;
--color-neutral: #666666;
--color-neutral-content: #ffffff;
--color-info: #0d58fc;
--color-info-content: #ffffff;
--color-success: #00c573;
--color-success-content: #ffffff;
--color-warning: #ff9900;
--color-warning-content: #ffffff;
--color-error: #ff3333;
--color-error-content: #ffffff;
--neutral-color: #000000e0;
--neutral-secondary: #000000a6;
--neutral-disabled: #00000040;
--neutral-border: #d9d9d9;
--neutral-separator: #0505050f;
--neutral-bg: #f5f5f5;
--color-neutral: #000000e0;
--color-neutral-secondary: #000000a6;
--color-neutral-disabled: #00000040;
--color-neutral-disabled-bg: #0000000a;
--color-neutral-border: #d9d9d9;
--color-neutral-separator: #0505050f;
--color-neutral-bg: #f5f5f5;
--color-error: #ff4d4f;
--color-warning: #faad14;
--color-success: #52c41a;
--color-info: #1677ff;
}
.dark-theme {
background-color: #141414;
}

View File

@ -1,51 +1,41 @@
import { TinyColor } from '@ctrl/tinycolor'
import { generate, presetPalettes, presetDarkPalettes } from '@ant-design/colors'
export const getAlphaColor = (baseColor: string, alpha: number) =>
new TinyColor(baseColor).setAlpha(alpha).toRgbString()
export const getSolidColor = (baseColor: string, brightness: number) => {
const instance = new TinyColor(baseColor)
return instance.darken(brightness).toHexString()
}
export const getTintColor = (baseColor: string, tintNumber: number) => {
return new TinyColor(baseColor).tint(tintNumber).toString()
}
export const getShadeColor = (baseColor: string, shadeNumber: number) => {
return new TinyColor(baseColor).shade(shadeNumber).toString()
}
export const getLightNeutralColor = () => {
return {
'--neutral-color': '#000000e0',
'--neutral-secondary': '#000000a6',
'--neutral-disabled': '#00000040',
'--neutral-disabled-bg': '#0000000a',
'--neutral-border': '#d9d9d9',
'--neutral-separator': '#0505050f',
'--neutral-bg': '#f5f5f5',
'--color-neutral': '#000000e0',
'--color-neutral-secondary': '#000000a6',
'--color-neutral-disabled': '#00000040',
'--color-neutral-disabled-bg': '#0000000a',
'--color-neutral-border': '#d9d9d9',
'--color-neutral-separator': '#0505050f',
'--color-neutral-bg': '#f5f5f5',
}
}
export const getDarkNeutralColor = () => {
return {
'--neutral-color': '#FFFFFFD9',
'--neutral-secondary': '#FFFFFFA6',
'--neutral-disabled': '#FFFFFF40',
'--neutral-disabled-bg': 'rgba(255, 255, 255, 0.08)',
'--neutral-border': '#424242',
'--neutral-separator': '#FDFDFD1F',
'--neutral-bg': '#000000',
'--color-neutral': '#FFFFFFD9',
'--color-neutral-secondary': '#FFFFFFA6',
'--color-neutral-disabled': '#FFFFFF40',
'--color-neutral-disabled-bg': 'rgba(255, 255, 255, 0.08)',
'--color-neutral-border': '#424242',
'--color-neutral-separator': '#FDFDFD1F',
'--color-neutral-bg': '#000000',
}
}
const cacheColors = new Map<string, Record<string, string>>()
export const getCssVarColor = (
baseColor: string,
opts?: { appearance: 'light' | 'dark'; backgroundColor: string },
opts: { appearance: 'light' | 'dark'; backgroundColor: string },
) => {
const { appearance = 'light', backgroundColor = '#141414' } = opts || {}
const { appearance = 'light', backgroundColor = '#141414' } = opts
const cacheKey = `${baseColor}-${appearance}-${backgroundColor}`
if (cacheColors.has(cacheKey)) {
return cacheColors.get(cacheKey)
}
const color = new TinyColor(baseColor)
const preset = appearance === 'dark' ? presetDarkPalettes : presetPalettes
const colors =
@ -55,50 +45,29 @@ export const getCssVarColor = (
appearance === 'dark' ? { theme: appearance, backgroundColor } : undefined,
)
const accentColor = colors[5]
return {
'--accent-color-1': colors[0],
'--accent-color-2': colors[1],
'--accent-color-3': colors[2],
'--accent-color-4': colors[3],
'--accent-color-5': colors[4],
'--accent-color-6': colors[5],
'--accent-color-7': colors[6],
'--accent-color-8': colors[7],
'--accent-color-9': colors[8],
'--accent-color-10': colors[9],
'--accent-color': accentColor,
'--accent-color-hover': colors[4],
'--accent-color-active': colors[5],
'--accent-color-content': '#ffffff',
...(appearance === 'dark' ? getDarkNeutralColor() : getLightNeutralColor()),
'--bg-color': baseColor,
'--bg-color-hover': getTintColor(baseColor, 10),
'--bg-color-active': getTintColor(baseColor, 20),
'--bg-color-content': '#ffffff',
const cssVars = {
'--color-accent-1': colors[0],
'--color-accent-2': colors[1],
'--color-accent-3': colors[2],
'--color-accent-4': colors[3],
'--color-accent-5': colors[4],
'--color-accent-6': colors[5],
'--color-accent-7': colors[6],
'--color-accent-8': colors[7],
'--color-accent-9': colors[8],
'--color-accent-10': colors[9],
'--color-accent': accentColor,
'--color-accent-hover': colors[4],
'--color-accent-active': colors[6],
'--color-accent-content': '#ffffff',
'--border-color': baseColor,
'--border-color-hover': getTintColor(baseColor, 10),
'--border-color-active': getTintColor(baseColor, 20),
'--border-color-tint-10': getTintColor(baseColor, 10),
'--border-color-tint-20': getTintColor(baseColor, 20),
'--border-color-tint-30': getTintColor(baseColor, 30),
'--border-color-tint-40': getTintColor(baseColor, 40),
'--border-color-tint-50': getTintColor(baseColor, 50),
'--border-color-tint-60': getTintColor(baseColor, 60),
'--border-color-tint-70': getTintColor(baseColor, 70),
'--border-color-tint-80': getTintColor(baseColor, 80),
'--border-color-tint-90': getTintColor(baseColor, 90),
'--bg-color-tint-10': getTintColor(baseColor, 10),
'--bg-color-tint-20': getTintColor(baseColor, 20),
'--bg-color-tint-30': getTintColor(baseColor, 30),
'--bg-color-tint-40': getTintColor(baseColor, 40),
'--bg-color-tint-50': getTintColor(baseColor, 50),
'--bg-color-tint-60': getTintColor(baseColor, 60),
'--bg-color-tint-70': getTintColor(baseColor, 70),
'--bg-color-tint-80': getTintColor(baseColor, 80),
'--bg-color-tint-90': getTintColor(baseColor, 90),
'--text-color': baseColor,
'--text-color-hover': getTintColor(baseColor, 10),
'--text-color-active': getTintColor(baseColor, 20),
'--color-error': preset.red[4],
'--color-warning': preset.yellow[4],
'--color-success': preset.green[4],
'--color-info': preset.blue[4],
...(appearance === 'dark' ? getDarkNeutralColor() : getLightNeutralColor()),
}
cacheColors.set(cacheKey, cssVars)
return cssVars
}