10 changed files with 501 additions and 22 deletions
Binary file not shown.
@ -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'); } |
|||
|
|||
// ----------------------------- |
|||
// 结束 |
|||
// ----------------------------- |
|||
@ -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'); } |
|||
|
|||
// ----------------------------- |
|||
// 结束 |
|||
// ----------------------------- |
|||
@ -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; |
|||
} |
|||
Loading…
Reference in new issue