Browse Source

feat: 添加主题支持和全局样式,包含亮色和暗色主题变量

mono
dash 2 months ago
parent
commit
b94114513b
  1. BIN
      bun.lockb
  2. 2
      packages/client/components.d.ts
  3. 1
      packages/client/package.json
  4. 2
      packages/client/src/App.vue
  5. 206
      packages/client/src/assets/styles/scss/_global.scss
  6. 190
      packages/client/src/assets/styles/scss/_theme-helpers.scss
  7. 60
      packages/client/src/assets/styles/scss/common.scss
  8. 2
      packages/client/src/entry-client.ts
  9. 53
      packages/client/src/pages/index.vue
  10. 7
      packages/client/vite.config.ts

BIN
bun.lockb

Binary file not shown.

2
packages/client/components.d.ts

@ -9,12 +9,10 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AiDemo: typeof import('./src/components/AiDemo/index.vue')['default'] AiDemo: typeof import('./src/components/AiDemo/index.vue')['default']
AXBubble: typeof import('ant-design-x-vue')['Bubble']
ClientOnly: typeof import('./../../internal/x/components/ClientOnly.vue')['default'] ClientOnly: typeof import('./../../internal/x/components/ClientOnly.vue')['default']
CookieDemo: typeof import('./src/components/CookieDemo.vue')['default'] CookieDemo: typeof import('./src/components/CookieDemo.vue')['default']
DataFetch: typeof import('./src/components/DataFetch.vue')['default'] DataFetch: typeof import('./src/components/DataFetch.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
MazBtn: typeof import('maz-ui/components/MazBtn')['default']
QuillEditor: typeof import('./src/components/QuillEditor/index.vue')['default'] QuillEditor: typeof import('./src/components/QuillEditor/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

1
packages/client/package.json

@ -8,6 +8,7 @@
"check": "vue-tsc" "check": "vue-tsc"
}, },
"devDependencies": { "devDependencies": {
"sass-embedded": "^1.93.2",
"unplugin-vue-components": "^29.1.0", "unplugin-vue-components": "^29.1.0",
"vue-tsc": "^3.1.0" "vue-tsc": "^3.1.0"
}, },

2
packages/client/src/App.vue

@ -10,5 +10,3 @@ onServerPrefetch(() => {
<template> <template>
<RouterView></RouterView> <RouterView></RouterView>
</template> </template>
<style lang="scss" scoped></style>

206
packages/client/src/assets/styles/scss/_global.scss

@ -0,0 +1,206 @@
/*
theme-helpers.scss
为主题开发提供的 SCSS 帮助函数 / mixin / 示例
说明中文注释:
- 提供把 SCSS map 转换为 CSS 自定义属性variables mixin
- 提供生成主题light/dark选择器的 mixin 示例
- 提供读取 CSS 变量的辅助函数 `css-var()`
- 提供颜色可读性判断函数 `readable-text()`基于简单亮度公式
- 提供颜色微调辅助 `tone()`
使用方式在你的主样式中导入此文件并调用 mixin/map示例在文件底部有注释
*/
// -----------------------------
// CSS 变量相关帮助
// -----------------------------
// 返回一个 var(...) 字符串方便在 SCSS 中使用 CSS 变量
@function css-var($name, $fallback: null) {
@if $fallback == null {
@return unquote("var(--#{$name})");
} @else {
@return unquote("var(--#{$name}, #{$fallback})");
}
}
// 将一个 map 转换为 CSS 变量声明需在选择器块内使用
// 用法
// :root { @include declare-theme-variables($my-theme-map); }
@mixin declare-theme-variables($map) {
@each $token, $val in $map {
// 允许传入颜色字符串或数字
--#{$token}: #{$val};
}
}
// 生成主题选择器selector 可以是 ":root"":root[data-theme=\"dark\"]" ".theme-dark"
// 用法@include generate-theme(':root', $theme-light);
@mixin generate-theme($selector, $map) {
#{$selector} {
@include declare-theme-variables($map);
}
}
// -----------------------------
// 颜色工具函数
// -----------------------------
// 计算近似亮度0-255用于对比判定基于 ITU BT.601 近似
@function _luma($color) {
// 期望 $color color 类型
$r: red($color);
$g: green($color);
$b: blue($color);
@return ($r * 0.299) + ($g * 0.587) + ($b * 0.114);
}
// 根据背景色返回可读的文字颜色#000 #fff
// 示例 color: readable-text(#0d1117);
@function readable-text($bg, $light: #ffffff, $dark: #000000) {
// 如果传入的不是 color 类型尝试转换如果是变量字符串则无法计算
@if type-of($bg) != 'color' {
// 无法在构建时计算 CSS 变量的对比默认返回白色以便在暗色环境下可读
@return $light;
}
@if _luma($bg) > 186 {
@return $dark;
}
@return $light;
}
// 基于 lighten/darken 的简单色调微调函数正值变亮负值变暗
@function tone($color, $percent) {
@if type-of($color) != 'color' {
@warn "tone(): first argument is not a color; returned value will be unchanged.";
@return $color;
}
@if $percent == 0 {
@return $color;
}
@if $percent > 0 {
@return lighten($color, $percent);
} @else {
@return darken($color, abs($percent));
}
}
// 使颜色变浅的辅助函数
// 用法
// lighten-by(#0d1117, 20) -> 20% 变亮
// lighten-by(#0d1117, 20%) -> 20% 变亮
// lighten-by(#0d1117, 0.2) -> 20% 变亮小数形式
@function lighten-by($color, $amount) {
@if type-of($color) != 'color' {
@warn "lighten-by(): first argument is not a color; returned value will be unchanged.";
@return $color;
}
@if type-of($amount) != 'number' {
@warn "lighten-by(): amount must be a number (e.g. 20, 20% or 0.2). Returning original color.";
@return $color;
}
// 规范化为百分比单位Sass percent 类型
$pct: $amount;
@if unit($amount) != '%' {
// 无单位数字如果在 (0,1] 范围内视为小数比例否则当作百分比数值
@if $amount > 0 and $amount <= 1 {
$pct: $amount * 100%;
} @else {
$pct: $amount * 1%;
}
}
@return lighten($color, $pct);
}
// -----------------------------
// 常用组件/场景 mixin
// -----------------------------
// 简单的背景/文字组合接收背景颜色或变量名
// 用法@include bg-fg('color-canvas-default'); // 传入变量名
// @include bg-fg(#0d1117); // 传入 color 类型
@mixin bg-fg($bg, $fg: null) {
@if type-of($bg) == 'string' {
// 假定传入的是变量名使用 css-var
background: css-var($bg);
@if $fg == null {
// 无法静态计算对比留空或用户自行指定
color: inherit;
} else {
color: css-var($fg);
}
} @else if type-of($bg) == 'color' {
background: $bg;
@if $fg == null {
color: readable-text($bg);
} @else if type-of($fg) == 'color' {
color: $fg;
} @else {
color: css-var($fg);
}
} @else {
@warn "bg-fg(): unsupported bg type";
}
}
// 一个可重用的按钮样式 mixin支持传入变量名或颜色
// 用法
// .btn { @include theme-button('color-accent-emphasis'); }
@mixin theme-button($bg, $fg: null, $radius: 6px, $pad-y: 8px, $pad-x: 12px) {
display: inline-flex;
align-items: center;
justify-content: center;
padding: $pad-y $pad-x;
border-radius: $radius;
border: none;
cursor: pointer;
@if type-of($bg) == 'string' {
background: css-var($bg);
@if $fg == null { color: css-var('color-fg-default'); } @else { color: css-var($fg); }
} @else if type-of($bg) == 'color' {
background: $bg;
@if $fg == null { color: readable-text($bg); } @else if type-of($fg) == 'color' { color: $fg; } @else { color: css-var($fg); }
}
// 微交互
&:hover { filter: brightness(0.95); }
&:active { transform: translateY(1px); }
}
// -----------------------------
// 示例注释掉直接拷贝到你的样式里使用
// -----------------------------
// 示例主题 maps键名与全局 CSS 变量中的命名保持一致但不包含前缀 --
// $theme-light: (
// 'color-fg-default': #24292f,
// 'color-fg-muted': #57606a,
// 'color-canvas-default': #ffffff,
// 'color-border-default': #d0d7de,
// 'color-accent-fg': #0969da,
// );
//
// $theme-dark: (
// 'color-fg-default': #c9d1d9,
// 'color-fg-muted': #8b949e,
// 'color-canvas-default': #0d1117,
// 'color-border-default': #30363d,
// 'color-accent-fg': #58a6ff,
// );
//
// 生成到 :root 和手动切换器
// @include generate-theme(':root', $theme-light);
// @include generate-theme(':root[data-theme="dark"]', $theme-dark);
//
// 使用 CSS 变量
// .markdown-body { color: css-var('color-fg-default'); background: css-var('color-canvas-default'); }
//
// 使用 mixin 快速为按钮应用主题颜色
// .btn { @include theme-button('color-accent-emphasis'); }
// -----------------------------
// 结束
// -----------------------------

190
packages/client/src/assets/styles/scss/_theme-helpers.scss

@ -0,0 +1,190 @@
// 返回一个 var(...) 字符串方便在 SCSS 中使用 CSS 变量
@function css-var($name, $fallback: null) {
@if $fallback == null {
@return unquote("var(--#{$name})");
} @else {
@return unquote("var(--#{$name}, #{$fallback})");
}
}
// 将一个 map 转换为 CSS 变量声明需在选择器块内使用
// 用法
// :root { @include declare-theme-variables($my-theme-map); }
@mixin declare-theme-variables($map) {
@each $token, $val in $map {
// 允许传入颜色字符串或数字
--#{$token}: #{$val};
}
}
// 生成主题选择器selector 可以是 ":root"":root[data-theme=\"dark\"]" ".theme-dark"
// 用法@include generate-theme(':root', $theme-light);
@mixin generate-theme($selector, $map) {
#{$selector} {
@include declare-theme-variables($map);
}
}
// -----------------------------
// 颜色工具函数
// -----------------------------
// 计算近似亮度0-255用于对比判定基于 ITU BT.601 近似
@function _luma($color) {
// 期望 $color color 类型
$r: red($color);
$g: green($color);
$b: blue($color);
@return ($r * 0.299) + ($g * 0.587) + ($b * 0.114);
}
// 根据背景色返回可读的文字颜色#000 #fff
// 示例 color: readable-text(#0d1117);
@function readable-text($bg, $light: #ffffff, $dark: #000000) {
// 如果传入的不是 color 类型尝试转换如果是变量字符串则无法计算
@if type-of($bg) != 'color' {
// 无法在构建时计算 CSS 变量的对比默认返回白色以便在暗色环境下可读
@return $light;
}
@if _luma($bg) > 186 {
@return $dark;
}
@return $light;
}
// 基于 lighten/darken 的简单色调微调函数正值变亮负值变暗
@function tone($color, $percent) {
@if type-of($color) != 'color' {
@warn "tone(): first argument is not a color; returned value will be unchanged.";
@return $color;
}
@if $percent == 0 {
@return $color;
}
@if $percent > 0 {
@return lighten($color, $percent);
} @else {
@return darken($color, abs($percent));
}
}
// 使颜色变浅的辅助函数
// 用法
// lighten-by(#0d1117, 20) -> 20% 变亮
// lighten-by(#0d1117, 20%) -> 20% 变亮
// lighten-by(#0d1117, 0.2) -> 20% 变亮小数形式
@function lighten-by($color, $amount) {
@if type-of($color) != 'color' {
@warn "lighten-by(): first argument is not a color; returned value will be unchanged.";
@return $color;
}
@if type-of($amount) != 'number' {
@warn "lighten-by(): amount must be a number (e.g. 20, 20% or 0.2). Returning original color.";
@return $color;
}
// 规范化为百分比单位Sass percent 类型
$pct: $amount;
@if unit($amount) != '%' {
// 无单位数字如果在 (0,1] 范围内视为小数比例否则当作百分比数值
@if $amount > 0 and $amount <= 1 {
$pct: $amount * 100%;
} @else {
$pct: $amount * 1%;
}
}
@return lighten($color, $pct);
}
// -----------------------------
// 常用组件/场景 mixin
// -----------------------------
// 简单的背景/文字组合接收背景颜色或变量名
// 用法@include bg-fg('color-canvas-default'); // 传入变量名
// @include bg-fg(#0d1117); // 传入 color 类型
@mixin bg-fg($bg, $fg: null) {
@if type-of($bg) == 'string' {
// 假定传入的是变量名使用 css-var
background: css-var($bg);
@if $fg == null {
// 无法静态计算对比留空或用户自行指定
color: inherit;
} else {
color: css-var($fg);
}
} @else if type-of($bg) == 'color' {
background: $bg;
@if $fg == null {
color: readable-text($bg);
} @else if type-of($fg) == 'color' {
color: $fg;
} @else {
color: css-var($fg);
}
} @else {
@warn "bg-fg(): unsupported bg type";
}
}
// 一个可重用的按钮样式 mixin支持传入变量名或颜色
// 用法
// .btn { @include theme-button('color-accent-emphasis'); }
@mixin theme-button($bg, $fg: null, $radius: 6px, $pad-y: 8px, $pad-x: 12px) {
display: inline-flex;
align-items: center;
justify-content: center;
padding: $pad-y $pad-x;
border-radius: $radius;
border: none;
cursor: pointer;
@if type-of($bg) == 'string' {
background: css-var($bg);
@if $fg == null { color: css-var('color-fg-default'); } @else { color: css-var($fg); }
} @else if type-of($bg) == 'color' {
background: $bg;
@if $fg == null { color: readable-text($bg); } @else if type-of($fg) == 'color' { color: $fg; } @else { color: css-var($fg); }
}
// 微交互
&:hover { filter: brightness(0.95); }
&:active { transform: translateY(1px); }
}
// -----------------------------
// 示例注释掉直接拷贝到你的样式里使用
// -----------------------------
// 示例主题 maps键名与全局 CSS 变量中的命名保持一致但不包含前缀 --
// $theme-light: (
// 'color-fg-default': #24292f,
// 'color-fg-muted': #57606a,
// 'color-canvas-default': #ffffff,
// 'color-border-default': #d0d7de,
// 'color-accent-fg': #0969da,
// );
//
// $theme-dark: (
// 'color-fg-default': #c9d1d9,
// 'color-fg-muted': #8b949e,
// 'color-canvas-default': #0d1117,
// 'color-border-default': #30363d,
// 'color-accent-fg': #58a6ff,
// );
//
// 生成到 :root 和手动切换器
// @include generate-theme(':root', $theme-light);
// @include generate-theme(':root[data-theme="dark"]', $theme-dark);
//
// 使用 CSS 变量
// .markdown-body { color: css-var('color-fg-default'); background: css-var('color-canvas-default'); }
//
// 使用 mixin 快速为按钮应用主题颜色
// .btn { @include theme-button('color-accent-emphasis'); }
// -----------------------------
// 结束
// -----------------------------

60
packages/client/src/assets/styles/scss/common.scss

@ -0,0 +1,60 @@
html,
body {
height: 100%;
}
/* 全局主题变量使用 _theme-helpers.scss 中的 mixin/map
- :root 中生成默认亮色主题变量
- 支持手动切换data-theme="dark" / .theme-dark
- 保留 prefers-color-scheme 媒体查询用于自动切换
*/
// 亮色主题变量 map键不带 -- 前缀
$theme-light: (
"color-fg-default": #24292f,
"color-fg-muted": #57606a,
"color-fg-subtle": #6e7781,
"color-canvas-default": #ffffff,
"color-canvas-subtle": #f6f8fa,
"color-border-default": #d0d7de,
"color-border-muted": hsla(210, 18%, 87%, 1),
"color-neutral-muted": rgba(175, 184, 193, 0.2),
"color-accent-fg": #0969da,
"color-accent-emphasis": #0969da,
"color-attention-subtle": #fff8c5,
"color-danger-fg": #cf222e,
"color-mark-default": rgb(255, 255, 0),
"color-mark-fg": rgb(255, 187, 0),
);
// 暗色主题变量 map对应亮色变量的语义
$theme-dark: (
"color-fg-default": #c9d1d9,
"color-fg-muted": #8b949e,
"color-fg-subtle": #6e7681,
"color-canvas-default": #0d1117,
"color-canvas-subtle": #010409,
"color-border-default": #30363d,
"color-border-muted": hsla(210, 18%, 20%, 1),
"color-neutral-muted": rgba(175, 184, 193, 0.12),
"color-accent-fg": #58a6ff,
"color-accent-emphasis": #2389ff,
"color-attention-subtle": rgba(255, 214, 10, 0.07),
"color-danger-fg": #ff7b72,
"color-mark-default": rgb(255, 214, 10),
"color-mark-fg": rgb(255, 165, 0),
);
// :root 中生成默认亮色变量便于组件直接使用 css var
@include generate-theme(":root", $theme-light);
// 手动主题切换支持data-theme class
@include generate-theme(':root[data-theme="dark"]', $theme-dark);
@include generate-theme(".theme-dark", $theme-dark);
#app {
height: 100%;
background-color: css-var(color-canvas-default);
color: css-var(color-fg-default);
line-height: 1.2;
}

2
packages/client/src/entry-client.ts

@ -5,6 +5,8 @@ import { createHead } from '@unhead/vue/client'
import "@/assets/styles/css/reset.css" import "@/assets/styles/css/reset.css"
import 'vue-final-modal/style.css' import 'vue-final-modal/style.css'
import "@/assets/styles/scss/common.scss"
import { MazUi } from 'maz-ui/plugins/maz-ui' import { MazUi } from 'maz-ui/plugins/maz-ui'
import { mazUi, ocean, pristine, obsidian } from '@maz-ui/themes' import { mazUi, ocean, pristine, obsidian } from '@maz-ui/themes'
import { zhCN } from '@maz-ui/translations' import { zhCN } from '@maz-ui/translations'

53
packages/client/src/pages/index.vue

@ -1,7 +1,3 @@
<template>
<MazBtn @click="open"> Open Modal </MazBtn>
</template>
<script setup lang="ts"> <script setup lang="ts">
definePage({ definePage({
name: "home", name: "home",
@ -14,19 +10,40 @@ defineOptions({
name: "home", name: "home",
}); });
import { useModal } from "vue-final-modal"; // import { useModal } from "vue-final-modal";
import ModalConfirmPlainCss from "./_M.vue"; // import ModalConfirmPlainCss from "./_M.vue";
const { open, close } = useModal({ // const { open, close } = useModal({
component: ModalConfirmPlainCss, // component: ModalConfirmPlainCss,
attrs: { // attrs: {
title: "Hello World!", // title: "Hello World!",
onConfirm() { // onConfirm() {
close(); // close();
}, // },
}, // },
slots: { // slots: {
default: "<p>The content of the modal</p>", // default: "<p>The content of the modal</p>",
}, // },
}); // });
</script> </script>
<template>
<div>
<div>safda</div>
<div>safda</div>
<div>safda</div>
<div>safda</div>
<div class="card">sada</div>
<div class="btn">asd</div>
</div>
</template>
<style lang="scss" scoped>
.card {
background: lighten-by(css-var("color-canvas-default"), 6%);
color: lighten-by(#0d1117, 80%);
}
.btn {
// @include theme-button("color-accent-emphasis");
}
</style>

7
packages/client/vite.config.ts

@ -31,6 +31,13 @@ export default defineConfig({
ssr: { ssr: {
noExternal: process.env.NODE_ENV === 'development' ? ['vue-router'] : [] noExternal: process.env.NODE_ENV === 'development' ? ['vue-router'] : []
}, },
css: {
preprocessorOptions: {
"scss": {
additionalData: `@use "@/assets/styles/scss/_global.scss" as *;\n`
}
}
},
plugins: [ plugins: [
devtoolsJson(), devtoolsJson(),
VueRouter({ VueRouter({

Loading…
Cancel
Save