From 850bd58ac27d2fc5eb11a8de4db4a676a6add1c2 Mon Sep 17 00:00:00 2001 From: vaebe <18137693952@163.com> Date: Thu, 4 Jun 2026 10:33:23 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=B7=B1?= =?UTF-8?q?=E8=89=B2=E6=A8=A1=E5=BC=8F=E2=80=94=E2=80=94=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=20token=20=E5=8C=96=E3=80=81=E7=A7=BB=E9=99=A4=20cssVar=20?= =?UTF-8?q?=E6=AD=BB=E6=8E=A5=E5=8F=A3=E3=80=81=E4=B8=BB=E9=A2=98=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=8E=20demo=20=E6=B7=B1=E8=89=B2=E9=A2=84?= =?UTF-8?q?=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 深色基础设施(ConfigProvider algorithm:'dark'、token 体系、.dark 级联)此前已就绪, 本次补齐使其在组件、API、文档三层完整: - 组件:auto-complete / button / button-3d / color-picker / mentions / popover / tooltip / transfer / upload 的硬编码灰阶与白色表面填充改为 $ccui-* token, 深色下随主题级联;保留恒为白的元素(彩色按钮白字、滑块手柄描边、 SV/hue 渐变停靠色、alpha 棋盘格、switch 旋钮) - token:themes/light.ts + dark.ts 新增 button-info* 与 color-picker-alpha-checker (均带 light/dark 双值) - API:移除 ThemeConfig.cssVar 死接口(ccui 全程 CSS 变量驱动,该标志为 no-op) - 文档:新增「主题定制 / 深色模式」指南页(主推根元素 .dark 类切换),登记侧边栏入口 - 文档站:每个 demo 注入浅/深就地预览开关,仅切容器自身 .dark,不影响全站 appearance - 修复:tooltip / popover 原本纯硬编码、缺 style-var 引入,换 token 后真实构建报 Undefined variable,补 @use '../../style-var/index.scss' as *; Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ui/auto-complete/src/auto-complete.scss | 14 +- packages/ccui/ui/button-3d/src/button-3d.scss | 4 +- packages/ccui/ui/button/src/button.scss | 10 +- .../ui/color-picker/src/color-picker.scss | 34 +++-- .../src/config-provider-types.ts | 1 - packages/ccui/ui/mentions/src/mentions.scss | 10 +- packages/ccui/ui/popover/src/popover.scss | 18 ++- packages/ccui/ui/tooltip/src/tooltip.scss | 14 +- packages/ccui/ui/transfer/src/transfer.scss | 20 +-- packages/ccui/ui/upload/src/upload.scss | 18 +-- packages/cli/templates/vitepress-sidebar.js | 24 +++ .../docs/.vitepress/theme/demoDarkToggle.ts | 77 ++++++++++ packages/docs/.vitepress/theme/index.ts | 6 + .../docs/.vitepress/theme/styles/index.css | 47 ++++++ .../docs/components/config-provider/index.md | 2 +- packages/docs/components/theme/index.md | 140 ++++++++++++++++++ packages/theme/themes/dark.ts | 6 + packages/theme/themes/light.ts | 6 + 18 files changed, 382 insertions(+), 69 deletions(-) create mode 100644 packages/docs/.vitepress/theme/demoDarkToggle.ts create mode 100644 packages/docs/components/theme/index.md diff --git a/packages/ccui/ui/auto-complete/src/auto-complete.scss b/packages/ccui/ui/auto-complete/src/auto-complete.scss index e338e582..21d45d59 100644 --- a/packages/ccui/ui/auto-complete/src/auto-complete.scss +++ b/packages/ccui/ui/auto-complete/src/auto-complete.scss @@ -15,9 +15,9 @@ width: 100%; height: 32px; padding: 0 11px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 6px; - background: #fff; + background: $ccui-color-bg-container; box-sizing: border-box; cursor: text; transition: @@ -34,7 +34,7 @@ } &.is-disabled { - background: #f5f5f5; + background: $ccui-list-item-hover-bg; cursor: not-allowed; input { @@ -50,10 +50,10 @@ } &--status-error { - border-color: #ff4d4f; + border-color: $ccui-color-error; } &--status-warning { - border-color: #faad14; + border-color: $ccui-color-warning; } } @@ -93,8 +93,8 @@ } &__panel { - background: #fff; - border: 1px solid #f0f0f0; + background: $ccui-base-bg; + border: 1px solid $ccui-dividing-line; border-radius: 6px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), diff --git a/packages/ccui/ui/button-3d/src/button-3d.scss b/packages/ccui/ui/button-3d/src/button-3d.scss index b50e62f4..a9152b83 100644 --- a/packages/ccui/ui/button-3d/src/button-3d.scss +++ b/packages/ccui/ui/button-3d/src/button-3d.scss @@ -1,7 +1,7 @@ @use '../../style-var/index.scss' as *; -$button-3d-info-color: #909399; -$button-3d-info-active-color: #73767a; +$button-3d-info-color: $ccui-button-info; +$button-3d-info-active-color: $ccui-button-info-active; .#{$cls-prefix}-button-3d { position: relative; diff --git a/packages/ccui/ui/button/src/button.scss b/packages/ccui/ui/button/src/button.scss index 5ccc5238..56926c1b 100644 --- a/packages/ccui/ui/button/src/button.scss +++ b/packages/ccui/ui/button/src/button.scss @@ -1,10 +1,10 @@ @use '../../style-var/index.scss' as *; -$button-info-color: #909399; -$button-info-hover-color: #a6a9ad; -$button-info-active-color: #73767a; -$button-info-plain-bg: #f4f4f5; -$button-info-plain-border: #dcdfe6; +$button-info-color: $ccui-button-info; +$button-info-hover-color: $ccui-button-info-hover; +$button-info-active-color: $ccui-button-info-active; +$button-info-plain-bg: $ccui-button-info-plain-bg; +$button-info-plain-border: $ccui-button-info-plain-border; .#{$cls-prefix}-button { box-sizing: border-box; diff --git a/packages/ccui/ui/color-picker/src/color-picker.scss b/packages/ccui/ui/color-picker/src/color-picker.scss index 071ecd6d..9ddc31aa 100644 --- a/packages/ccui/ui/color-picker/src/color-picker.scss +++ b/packages/ccui/ui/color-picker/src/color-picker.scss @@ -17,9 +17,9 @@ align-items: center; gap: 6px; padding: 4px 6px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 6px; - background: #fff; + background: $ccui-color-bg-container; cursor: pointer; line-height: 1; transition: @@ -36,8 +36,8 @@ } &.is-disabled { - border-color: #d9d9d9; - background: #f5f5f5; + border-color: $ccui-form-control-line; + background: $ccui-list-item-hover-bg; cursor: not-allowed; pointer-events: none; } @@ -65,8 +65,10 @@ border-radius: 4px; overflow: hidden; background-image: - linear-gradient(45deg, #ddd 25%, transparent 25%), linear-gradient(-45deg, #ddd 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #ddd 75%), linear-gradient(-45deg, transparent 75%, #ddd 75%); + linear-gradient(45deg, $ccui-color-picker-alpha-checker 25%, transparent 25%), + linear-gradient(-45deg, $ccui-color-picker-alpha-checker 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, $ccui-color-picker-alpha-checker 75%), + linear-gradient(-45deg, transparent 75%, $ccui-color-picker-alpha-checker 75%); background-size: 8px 8px; background-position: 0 0, @@ -98,8 +100,8 @@ &__panel { width: 240px; padding: 12px; - background: #fff; - border: 1px solid #f0f0f0; + background: $ccui-color-bg-elevated; + border: 1px solid $ccui-dividing-line; border-radius: 8px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), @@ -169,8 +171,10 @@ &__alpha { background-image: - linear-gradient(45deg, #ddd 25%, transparent 25%), linear-gradient(-45deg, #ddd 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #ddd 75%), linear-gradient(-45deg, transparent 75%, #ddd 75%); + linear-gradient(45deg, $ccui-color-picker-alpha-checker 25%, transparent 25%), + linear-gradient(-45deg, $ccui-color-picker-alpha-checker 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, $ccui-color-picker-alpha-checker 75%), + linear-gradient(-45deg, transparent 75%, $ccui-color-picker-alpha-checker 75%); background-size: 8px 8px; background-position: 0 0, @@ -211,9 +215,9 @@ align-items: center; padding: 0 8px; height: 28px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 4px; - background: #fff; + background: $ccui-color-bg-container; transition: border-color 0.2s; &:focus-within { @@ -245,9 +249,9 @@ width: 64px; height: 28px; padding: 0 8px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 4px; - background: #fff; + background: $ccui-color-bg-container; &:focus-within { border-color: $ccui-color-primary; @@ -324,7 +328,7 @@ &__footer { margin-top: 12px; padding-top: 12px; - border-top: 1px solid #f0f0f0; + border-top: 1px solid $ccui-dividing-line; } } diff --git a/packages/ccui/ui/config-provider/src/config-provider-types.ts b/packages/ccui/ui/config-provider/src/config-provider-types.ts index aef14fd8..a23018d0 100644 --- a/packages/ccui/ui/config-provider/src/config-provider-types.ts +++ b/packages/ccui/ui/config-provider/src/config-provider-types.ts @@ -5,7 +5,6 @@ export type ComponentSize = 'small' | 'middle' | 'large' export interface ThemeConfig { token?: Record algorithm?: 'default' | 'dark' | 'compact' - cssVar?: boolean } export interface ModalLocale { diff --git a/packages/ccui/ui/mentions/src/mentions.scss b/packages/ccui/ui/mentions/src/mentions.scss index 534d611e..05ba50b4 100644 --- a/packages/ccui/ui/mentions/src/mentions.scss +++ b/packages/ccui/ui/mentions/src/mentions.scss @@ -46,9 +46,9 @@ &__textarea { width: 100%; padding: 6px 12px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 6px; - background: #fff; + background: $ccui-color-bg-container; color: rgba(0, 0, 0, 0.88); font-size: 14px; line-height: 1.5; @@ -74,7 +74,7 @@ } &:disabled { - background: #f5f5f5; + background: $ccui-list-item-hover-bg; color: rgba(0, 0, 0, 0.4); cursor: not-allowed; } @@ -86,8 +86,8 @@ z-index: 1050; min-width: 160px; max-width: 320px; - background: #fff; - border: 1px solid #f0f0f0; + background: $ccui-base-bg; + border: 1px solid $ccui-dividing-line; border-radius: 6px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), diff --git a/packages/ccui/ui/popover/src/popover.scss b/packages/ccui/ui/popover/src/popover.scss index 95acbf9e..f1839be9 100644 --- a/packages/ccui/ui/popover/src/popover.scss +++ b/packages/ccui/ui/popover/src/popover.scss @@ -1,3 +1,5 @@ +@use '../../style-var/index.scss' as *; + .ccui-popover { position: relative; display: inline-block; @@ -19,16 +21,16 @@ box-sizing: border-box; &--dark { - background: #303133; - color: #fff; - border: 1px solid #303133; + background: $ccui-feedback-overlay-bg; + color: $ccui-light-text; + border: 1px solid $ccui-feedback-overlay-bg; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } &--light { - background: #fff; - color: #606266; - border: 1px solid #e4e7ed; + background: $ccui-base-bg; + color: $ccui-aide-text-stress; + border: 1px solid $ccui-color-border; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } } @@ -37,11 +39,11 @@ padding: 0 0 8px 0; font-weight: 500; font-size: 16px; - color: #303133; + color: $ccui-text; border-radius: inherit; .ccui-popover__popper--dark & { - color: #fff; + color: $ccui-light-text; } } diff --git a/packages/ccui/ui/tooltip/src/tooltip.scss b/packages/ccui/ui/tooltip/src/tooltip.scss index 3c1f2bc7..231d2e51 100644 --- a/packages/ccui/ui/tooltip/src/tooltip.scss +++ b/packages/ccui/ui/tooltip/src/tooltip.scss @@ -1,3 +1,5 @@ +@use '../../style-var/index.scss' as *; + .ccui-tooltip { position: relative; display: inline-block; @@ -15,16 +17,16 @@ word-wrap: break-word; &--dark { - background: #303133; - color: #fff; - border: 1px solid #303133; + background: $ccui-feedback-overlay-bg; + color: $ccui-light-text; + border: 1px solid $ccui-feedback-overlay-bg; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } &--light { - background: #fff; - color: #606266; - border: 1px solid #e4e7ed; + background: $ccui-base-bg; + color: $ccui-aide-text-stress; + border: 1px solid $ccui-color-border; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } } diff --git a/packages/ccui/ui/transfer/src/transfer.scss b/packages/ccui/ui/transfer/src/transfer.scss index 29d00db0..a7d13272 100644 --- a/packages/ccui/ui/transfer/src/transfer.scss +++ b/packages/ccui/ui/transfer/src/transfer.scss @@ -15,9 +15,9 @@ flex-direction: column; width: 200px; max-height: 320px; - border: 1px solid #e8e8e8; + border: 1px solid $ccui-color-border; border-radius: 6px; - background: #fff; + background: $ccui-color-bg-container; overflow: hidden; } @@ -26,10 +26,10 @@ align-items: center; gap: 8px; padding: 8px 12px; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid $ccui-dividing-line; color: rgba(0, 0, 0, 0.85); font-size: 13px; - background: #fafafa; + background: $ccui-area; } &__header-checkbox { @@ -58,16 +58,16 @@ &__search { padding: 8px 12px; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid $ccui-dividing-line; } &__search-input { width: 100%; height: 28px; padding: 0 8px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 4px; - background: #fff; + background: $ccui-color-bg-container; color: rgba(0, 0, 0, 0.88); font-size: 13px; outline: 0; @@ -153,9 +153,9 @@ min-width: 32px; height: 28px; padding: 0 8px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-color-border; border-radius: 4px; - background: #fff; + background: $ccui-color-bg-container; color: rgba(0, 0, 0, 0.88); font-size: 14px; line-height: 1; @@ -172,7 +172,7 @@ &:disabled { color: rgba(0, 0, 0, 0.25); - background: #f5f5f5; + background: $ccui-list-item-hover-bg; cursor: not-allowed; } } diff --git a/packages/ccui/ui/upload/src/upload.scss b/packages/ccui/ui/upload/src/upload.scss index d10b3164..7d31edb0 100644 --- a/packages/ccui/ui/upload/src/upload.scss +++ b/packages/ccui/ui/upload/src/upload.scss @@ -19,9 +19,9 @@ display: inline-block; padding: 4px 16px; height: 32px; - border: 1px solid #d9d9d9; + border: 1px solid $ccui-form-control-line; border-radius: 6px; - background: #fff; + background: $ccui-color-bg-container; color: rgba(0, 0, 0, 0.88); font-size: 14px; line-height: 24px; @@ -34,7 +34,7 @@ } &.is-disabled { - background: #f5f5f5; + background: $ccui-list-item-hover-bg; color: rgba(0, 0, 0, 0.25); cursor: not-allowed; } @@ -47,9 +47,9 @@ justify-content: center; gap: 8px; padding: 32px 16px; - border: 1px dashed #d9d9d9; + border: 1px dashed $ccui-color-border; border-radius: 8px; - background: #fafafa; + background: $ccui-area; color: rgba(0, 0, 0, 0.65); text-align: center; cursor: pointer; @@ -67,12 +67,12 @@ &.is-disabled { cursor: not-allowed; - background: #f5f5f5; + background: $ccui-list-item-hover-bg; color: rgba(0, 0, 0, 0.25); &:hover { - border-color: #d9d9d9; - background: #f5f5f5; + border-color: $ccui-color-border; + background: $ccui-list-item-hover-bg; } } } @@ -107,7 +107,7 @@ } &--status-error { - color: #ff4d4f; + color: $ccui-color-error; } &--status-uploading { diff --git a/packages/cli/templates/vitepress-sidebar.js b/packages/cli/templates/vitepress-sidebar.js index 8fb80b83..dc2e6f77 100644 --- a/packages/cli/templates/vitepress-sidebar.js +++ b/packages/cli/templates/vitepress-sidebar.js @@ -12,6 +12,13 @@ function buildCategoryOptions(text, items = []) { return { text, items } } +// 非组件的纯文档指南页(在 docs/components/ 下手写、没有对应 ui/ 组件, +// 因而不会被 discoverComponents 扫到)。这里按分类显式登记,保证 sidebar 重新生成时 +// 这些页面入口不会丢失。key 用中文分类名,与 VITEPRESS_SIDEBAR_CATEGORY 对齐。 +const EXTRA_GUIDE_PAGES = { + 其他: [{ slug: 'theme', zh: '主题定制 / 深色模式', en: 'Theming / Dark Mode' }], +} + function generateZhMenus(componentsInfo) { const categoryMap = VITEPRESS_SIDEBAR_CATEGORY.reduce((map, cate) => map.set(cate, []), new Map()) @@ -28,6 +35,14 @@ function generateZhMenus(componentsInfo) { logger.warning(`组件 ${info.name} 的分类 ${info.category} 不存在!`) } }) + + for (const [cate, pages] of Object.entries(EXTRA_GUIDE_PAGES)) { + if (!categoryMap.has(cate)) continue + pages.forEach((p) => { + categoryMap.get(cate).push({ text: p.zh, link: `/${SITES_COMPONENTS_DIR_NAME}/${p.slug}/` }) + }) + } + return Array.from(categoryMap).map(([k, v]) => buildCategoryOptions(k, v)) } @@ -42,6 +57,15 @@ function generateEnMenus(componentsInfo) { }) } }) + + for (const [cate, pages] of Object.entries(EXTRA_GUIDE_PAGES)) { + const enCate = VITEPRESS_SIDEBAR_CATEGORY_ZH_TO_EN[cate] + if (!categoryMapEn.has(enCate)) continue + pages.forEach((p) => { + categoryMapEn.get(enCate).push({ text: p.en, link: `/${SITES_COMPONENTS_DIR_NAME_EN}/${p.slug}/` }) + }) + } + return Array.from(categoryMapEn).map(([k, v]) => buildCategoryOptions(k, v)) } diff --git a/packages/docs/.vitepress/theme/demoDarkToggle.ts b/packages/docs/.vitepress/theme/demoDarkToggle.ts new file mode 100644 index 00000000..6cc1a889 --- /dev/null +++ b/packages/docs/.vitepress/theme/demoDarkToggle.ts @@ -0,0 +1,77 @@ +// 给每个 demo 容器(@vitepress-code-preview/container 渲染的 [class*='_example-showcase_']) +// 注入一个浮动「浅色 / 深色」开关。点击只 toggle 该容器自身的 `.dark` 类, +// darkTheme.css 的规则全部 scope 在 `.dark` 下、组件全走 CSS 变量, +// 因此该容器内的 ccui 组件即就地变深色,而不触碰 VitePress 全站 appearance(html.dark)。 +// +// 仅客户端执行:本模块只在 onMounted / 浏览器环境里被调用。 + +// 标记位:避免对同一容器重复注入按钮。 +const INJECTED_FLAG = 'data-ccui-dark-toggle' +const SHOWCASE_SELECTOR = "[class*='_example-showcase_']" + +function createToggleButton(container: HTMLElement): HTMLButtonElement { + const btn = document.createElement('button') + btn.type = 'button' + btn.className = 'ccui-demo-dark-toggle' + btn.setAttribute('aria-pressed', 'false') + + const sync = () => { + const isDark = container.classList.contains('dark') + btn.setAttribute('aria-pressed', String(isDark)) + btn.title = isDark ? '切换为浅色' : '切换为深色' + btn.setAttribute('aria-label', btn.title) + // 用文字图标,零依赖、不需要额外资源 + btn.textContent = isDark ? '☀' : '☾' + } + + btn.addEventListener('click', (e) => { + e.preventDefault() + e.stopPropagation() + container.classList.toggle('dark') + sync() + }) + + sync() + return btn +} + +function enhanceShowcase(container: HTMLElement) { + if (container.hasAttribute(INJECTED_FLAG)) return + container.setAttribute(INJECTED_FLAG, '') + + // 容器默认 position 未必是 relative(active 规则里没设), + // 绝对定位按钮前确保有定位上下文。 + const pos = getComputedStyle(container).position + if (pos === 'static') container.style.position = 'relative' + + container.appendChild(createToggleButton(container)) +} + +function scan(root: ParentNode = document) { + root.querySelectorAll(SHOWCASE_SELECTOR).forEach(enhanceShowcase) +} + +let observer: MutationObserver | null = null + +/** + * 在客户端启动 demo 深色开关注入。多次调用安全(幂等)。 + */ +export function setupDemoDarkToggle() { + if (typeof window === 'undefined' || typeof document === 'undefined') return + + // 首屏 + 后续懒加载的 demo(VitePress SPA 路由切换会换页面内容)。 + scan() + + if (observer) return + + observer = new MutationObserver((mutations) => { + for (const m of mutations) { + m.addedNodes.forEach((node) => { + if (!(node instanceof HTMLElement)) return + if (node.matches(SHOWCASE_SELECTOR)) enhanceShowcase(node) + scan(node) + }) + } + }) + observer.observe(document.body, { childList: true, subtree: true }) +} diff --git a/packages/docs/.vitepress/theme/index.ts b/packages/docs/.vitepress/theme/index.ts index c362c4e7..c9ab9ee2 100644 --- a/packages/docs/.vitepress/theme/index.ts +++ b/packages/docs/.vitepress/theme/index.ts @@ -3,6 +3,7 @@ import DemoPreview, { useComponents } from '@vitepress-code-preview/container' import DefaultTheme from 'vitepress/theme' import ccui from '@vaebe/ccui/ui/vue-ccui' import IconShowcase from './components/IconShowcase.vue' +import { setupDemoDarkToggle } from './demoDarkToggle' import './styles/index.css' // 暗色主题:从 workspace 源码 (@vaebe/ccui-theme) 引入,规则用 `.dark` // 选择器作用域,与 VitePress 默认 html.dark 切换约定一致;替代了之前从 @@ -20,5 +21,10 @@ export default { ctx.app.component('IconShowcase', IconShowcase) useComponents(ctx.app, DemoPreview) + + // 仅客户端:给每个 demo 容器注入「浅色 / 深色」就地切换按钮。 + // VitePress SSR 阶段 enhanceApp 也会跑,用浏览器环境守卫; + // 内部再用 MutationObserver 兜住路由切换后新渲染的 demo。 + if (typeof window !== 'undefined') setupDemoDarkToggle() }, } diff --git a/packages/docs/.vitepress/theme/styles/index.css b/packages/docs/.vitepress/theme/styles/index.css index 8af6ce1c..763d4d64 100644 --- a/packages/docs/.vitepress/theme/styles/index.css +++ b/packages/docs/.vitepress/theme/styles/index.css @@ -37,6 +37,53 @@ margin-top: 0; } +/* demo 容器右上角的「浅色 / 深色」就地切换按钮。 + - JS(theme/demoDarkToggle.ts)注入到每个 [class*='_example-showcase_'] 容器内, + 容器若为 static 定位会被就地改成 relative 以承载本绝对定位按钮。 + - 不带任何 ccui- 类,因此不会被 showcase 的相邻兄弟间距 / 列表抹平规则命中。 + - 切换只 toggle 容器自身 .dark;按钮自身样式写死、不依赖 ccui 变量, + 使其在浅 / 深两态下都清晰可见。 */ +.vp-doc [class*='_example-showcase_'] .ccui-demo-dark-toggle { + position: absolute; + top: 8px; + inset-inline-end: 8px; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + margin: 0; + font-size: 14px; + line-height: 1; + cursor: pointer; + border-radius: 6px; + border: 1px solid var(--vp-c-divider); + /* 用 VitePress 自身变量,浅 / 深态都有合适对比;不受容器 .dark 影响 */ + color: var(--vp-c-text-2); + background-color: var(--vp-c-bg); + opacity: 0.55; + transition: + opacity 0.2s ease, + color 0.2s ease, + border-color 0.2s ease; +} + +.vp-doc [class*='_example-showcase_'] .ccui-demo-dark-toggle:hover, +.vp-doc [class*='_example-showcase_'] .ccui-demo-dark-toggle:focus-visible { + opacity: 1; + color: var(--vp-c-brand-1, var(--vp-c-brand)); + border-color: var(--vp-c-brand-1, var(--vp-c-brand)); + outline: none; +} + +/* 容器进入深色态时,按钮仍保持自身配色(已用 vp 变量), + 这里仅微调让它在深底上更显眼。 */ +.vp-doc [class*='_example-showcase_'].dark .ccui-demo-dark-toggle { + opacity: 0.7; +} + .mt-10 { margin-top: 10px; } diff --git a/packages/docs/components/config-provider/index.md b/packages/docs/components/config-provider/index.md index 6466d42a..5b0d65de 100644 --- a/packages/docs/components/config-provider/index.md +++ b/packages/docs/components/config-provider/index.md @@ -185,7 +185,7 @@ const cfg = useConfig() | componentSize | `'small' \| 'middle' \| 'large'` | `'middle'` | 默认组件尺寸 | | direction | `'ltr' \| 'rtl'` | `'ltr'` | 文字方向 | | locale | `Locale` | — | 语言包 | -| theme | `{ token, algorithm, cssVar }` | — | 主题配置:`token` 用 camelCase(colorPrimary / borderRadius 等),自动映射为 CSS 变量并下传 | +| theme | `{ token, algorithm }` | — | 主题配置:`token` 用 camelCase(colorPrimary / borderRadius 等),自动映射为 CSS 变量并下传 | | iconPrefixCls | string | `'ccui-icon'` | 图标类名前缀 | ### useConfig diff --git a/packages/docs/components/theme/index.md b/packages/docs/components/theme/index.md new file mode 100644 index 00000000..5448d8dd --- /dev/null +++ b/packages/docs/components/theme/index.md @@ -0,0 +1,140 @@ +# 主题定制 / 深色模式 + +ccui 的所有组件样式都建立在一套 `--ccui-*` CSS 变量之上。深色模式、品牌色定制、圆角调整都归结为一件事:**覆盖这些 CSS 变量**。本页梳理三种由浅入深的用法。 + +## 深色模式怎么生效 + +深色样式集中在 `darkTheme.css`,所有规则都 scope 在 `.dark` 选择器下,形如: + +```css +.dark { + --ccui-color-bg-container: #141414; + --ccui-color-text: rgba(255, 255, 255, 0.85); + /* …整套 token 的深色取值 */ +} +``` + +只要某个祖先元素带上 `.dark` 类,其子树内的 ccui 组件就会就地切换到深色取值。因此「切换深色」=「在合适的元素上挂 `.dark` 类」。`darkTheme.css` 已在文档站全局引入;接入你自己的应用时,从 `@vaebe/ccui-theme/darkTheme.css` 引入一次即可。 + +## 应用级深色切换(主推) + +整站统一切换深色,推荐把 `.dark` 类挂在根元素(`` 或 ``)上。这样所有组件、布局、自定义样式同时切换,无需逐处包裹。 + +下面是一个「跟随系统 + 手动切换」的最小实现:首屏跟随系统偏好,用户手动点击后记住选择并落到 `localStorage`。 + +```ts +const STORAGE_KEY = 'ccui-theme' + +function applyDark(isDark: boolean) { + document.documentElement.classList.toggle('dark', isDark) +} + +// 1. 初始化:优先读取用户已保存的选择,否则跟随系统 +const media = window.matchMedia('(prefers-color-scheme: dark)') +const saved = localStorage.getItem(STORAGE_KEY) // 'dark' | 'light' | null +applyDark(saved ? saved === 'dark' : media.matches) + +// 2. 手动切换:写入存储并即时应用 +function setTheme(isDark: boolean) { + localStorage.setItem(STORAGE_KEY, isDark ? 'dark' : 'light') + applyDark(isDark) +} + +// 3. 未手动指定时,继续跟随系统变化 +media.addEventListener('change', (e) => { + if (!localStorage.getItem(STORAGE_KEY)) applyDark(e.matches) +}) +``` + +把 `setTheme(true / false)` 接到你的开关按钮即可。Vue 项目里也可以直接用 `useDark` / `useToggle`(来自 VueUse)托管同样的逻辑,行为一致。 + +## 局部 / 子树方案 + +只想让页面的某一块走深色(例如预览面板、对比区域),无需触碰根元素,用 `c-config-provider` 的 `theme.algorithm` 即可。设为 `'dark'` 时,ConfigProvider 会在其包裹层挂上 `.dark` 类,从而只让这棵子树切换深色。 + +:::demo + +```vue + +``` + +::: + +`algorithm` 与 `token` 可以同时使用:先由 `algorithm: 'dark'` 给出整套深色基线,再用 `token` 覆盖个别值。 + +## token 体系简介 + +ccui 的设计变量统一以 `--ccui-` 开头,覆盖颜色、圆角、字号、间距、动效、层级等。组件样式只读这些变量,所以你既可以直接在 CSS 里覆盖变量,也可以通过 `c-config-provider` 的 `theme.token` 用 camelCase 形式传入(如 `colorPrimary`、`borderRadius`),ConfigProvider 会自动映射成对应的 `--ccui-*` 变量并下传给子组件。 + +### 关键色 light / dark 对照 + +下表节选几个最常用的 token,列出浅色与深色两套取值,方便对照理解变量在两种模式下的差异: + +| token(camelCase) | CSS 变量 | Light | Dark | +| ------------------ | --------------------------- | ----------------- | ----------------------- | +| colorPrimary | `--ccui-color-primary` | `#1677ff` | `#1668dc` | +| colorSuccess | `--ccui-color-success` | `#52c41a` | `#49aa19` | +| colorWarning | `--ccui-color-warning` | `#faad14` | `#d89614` | +| colorError | `--ccui-color-error` | `#ff4d4f` | `#dc4446` | +| colorText | `--ccui-color-text` | `rgba(0,0,0,.88)` | `rgba(255,255,255,.85)` | +| colorBgContainer | `--ccui-color-bg-container` | `#ffffff` | `#141414` | +| colorBgBase | `--ccui-color-bg-base` | `#ffffff` | `#000000` | +| colorBorder | `--ccui-color-border` | `#d9d9d9` | `#424242` | +| borderRadius | `--ccui-border-radius` | `6px` | `6px` | + +### 自定义 token 覆盖品牌色 + +最常见的需求是换一套品牌主色。把 camelCase 的 token 传给 `theme.token`,作用域内组件就会跟着走新主色。下面的示例可以在几个预设主色之间实时切换: + +:::demo + +```vue + + + +``` + +::: + +`theme.token` 接收的字段与上表的 camelCase 名一一对应,`colorPrimary` / `borderRadius` / `colorError` 等都可以一起传。需要更细的派生色(hover / active / 边框 / 浅底)时,直接覆盖对应的 `--ccui-color-primary-hover` 等 CSS 变量即可。 + +## 三种方案怎么选 + +- 整站统一深色:在根元素切 `.dark` 类(**主推**),配合全局引入的 `darkTheme.css`。 +- 仅局部子树深色:用 `c-config-provider` 的 `theme.algorithm: 'dark'` 包裹。 +- 定制品牌色 / 圆角:用 `c-config-provider` 的 `theme.token`,或直接覆盖 `--ccui-*` 变量。 + +::: tip 在文档站内试试 +本站每个 `:::demo` 演示框右上角都有一个浅色 / 深色开关,点击只会切换该演示框自身的 `.dark`,方便就地预览组件在两种模式下的表现。 +::: diff --git a/packages/theme/themes/dark.ts b/packages/theme/themes/dark.ts index 28a522ca..ee10d665 100644 --- a/packages/theme/themes/dark.ts +++ b/packages/theme/themes/dark.ts @@ -72,6 +72,7 @@ export default { 'float-block-shadow': 'rgba(22, 104, 220, 0.32)', 'highlight-overlay': 'rgba(0, 0, 0, 0.45)', 'range-item-hover-bg': '#111a2c', + 'color-picker-alpha-checker': '#424242', // 按钮 primary: '#1668dc', 'primary-hover': '#3c89e8', @@ -79,6 +80,11 @@ export default { 'contrast-hover': '#e86e6b', 'contrast-active': '#ad2d2c', secondary: 'rgba(255, 255, 255, 0.65)', + 'button-info': '#a6a9ad', + 'button-info-hover': '#b8bbbf', + 'button-info-active': '#85888c', + 'button-info-plain-bg': '#2a2a2a', + 'button-info-plain-border': '#3a3a3a', // 状态 'danger-line': '#58181c', 'danger-bg': '#2a1215', diff --git a/packages/theme/themes/light.ts b/packages/theme/themes/light.ts index 3f51f38a..73ab576f 100644 --- a/packages/theme/themes/light.ts +++ b/packages/theme/themes/light.ts @@ -73,6 +73,7 @@ export default { 'float-block-shadow': 'rgba(22, 119, 255, 0.16)', 'highlight-overlay': 'rgba(255, 255, 255, 0.8)', 'range-item-hover-bg': '#e6f4ff', + 'color-picker-alpha-checker': '#dddddd', // 按钮 primary: '#1677ff', 'primary-hover': '#4096ff', @@ -80,6 +81,11 @@ export default { 'contrast-hover': '#ff7875', 'contrast-active': '#d9363e', secondary: 'rgba(0, 0, 0, 0.65)', + 'button-info': '#909399', + 'button-info-hover': '#a6a9ad', + 'button-info-active': '#73767a', + 'button-info-plain-bg': '#f4f4f5', + 'button-info-plain-border': '#dcdfe6', // 状态 'danger-line': '#ffccc7', 'danger-bg': '#fff2f0', From 4e8b77bf38e1e24a217da378e13e4cead3e688fb Mon Sep 17 00:00:00 2001 From: vaebe <18137693952@163.com> Date: Thu, 4 Jun 2026 14:21:31 +0800 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20ls-lint=20ignore=20docs-notes=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=EF=BC=88=E5=91=BD=E5=90=8D=E4=B8=8D=E5=8F=97?= =?UTF-8?q?=20kebab-case=20=E7=BA=A6=E6=9D=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs-notes 是面向贡献者的内部维护笔记(含 README.md 等非 kebab 命名), 与 docs-notes 已是 Ant 提及 / 格式 debt 的豁免区一致,显式加入 .ls-lint.yml ignore,使命名检查跳过该目录。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .ls-lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ls-lint.yml b/.ls-lint.yml index c18d47b4..f934afc6 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -14,6 +14,8 @@ ls: .d.ts: kebab-case ignore: + # 内部维护笔记,命名不受 kebab-case 约束(如 README.md) + - docs-notes # ccui - packages/ccui/node_modules - packages/docs From f710b4e11c4bd29fa890c827e6bd58088723ff64 Mon Sep 17 00:00:00 2001 From: vaebe <18137693952@163.com> Date: Thu, 4 Jun 2026 14:23:10 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=E4=BF=AE=E5=A4=8D=20divider=20?= =?UTF-8?q?=E7=B1=B3=E8=89=B2=E8=83=8C=E6=99=AF=20demo=20=E6=B7=B1?= =?UTF-8?q?=E8=89=B2=E4=B8=8B=E7=99=BD=E5=AD=97=E4=B8=8D=E5=8F=AF=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit content-background-color="#fff7e6" 钉死了浅米色底,但未设 content-color, 文字跟随主题 → 深色模式下变白,白字落在米色底上糊成一片。补一个深暖色 content-color="#874d00",浅/深两模式均可读;说明里同步补上「钉死浅底时 须一并钉死文字色」的配对原则。 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/docs/components/divider/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/components/divider/index.md b/packages/docs/components/divider/index.md index 28842425..4e0d2199 100644 --- a/packages/docs/components/divider/index.md +++ b/packages/docs/components/divider/index.md @@ -77,14 +77,14 @@ ## 自定义文案样式 -`content-color` 改文字颜色,`content-background-color` 改文字底色(在彩色背景上常用)。 +`content-color` 改文字颜色,`content-background-color` 改文字底色(在彩色背景上常用)。钉死一个固定浅色底时,记得同时钉死文字色,否则深色模式下文字会变白、落在浅底上不可读。 :::demo ```vue ``` From 462b527d9115a06dc2304482447a6e614de34a7d Mon Sep 17 00:00:00 2001 From: vaebe <18137693952@163.com> Date: Thu, 4 Jun 2026 14:33:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20demo=20=E6=B5=85/=E6=B7=B1=E5=B0=B1?= =?UTF-8?q?=E5=9C=B0=E5=88=87=E6=8D=A2=E5=9C=A8=E5=85=A8=E7=AB=99=E6=B7=B1?= =?UTF-8?q?=E8=89=B2=E4=B8=8B=E4=B9=9F=E8=83=BD=E5=88=87=E5=9B=9E=E6=B5=85?= =?UTF-8?q?=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧实现只 toggle demo 容器自身的 .dark,但 .dark 会从任意祖先级联。全站深色 (html.dark)时容器即便不带 .dark 也继承深色,单纯增删容器的 .dark 切不回浅色, 按钮状态也读错(恒显示 ☾)。 - generate-theme.js:darkTheme.css 在 .dark{} 外再产出对称的 .light{}(light 全集)。 CSS 自定义属性按元素就近解析,子树容器上挂 .light 即可覆盖外层 html.dark 下传值。 - demoDarkToggle.ts:改为给容器挂【显式】互斥的 .dark / .light,并以容器当前实际 渲染态(含全站继承)为基准取反,首次点击在浅/深任一全站模式下都正确翻转; 另监听 html.class 变化,跟随全站的未显式选择容器同步按钮图标。 darkTheme.css 是 gitignored 生成产物,随 postinstall/bootstrap 重生成。 pnpm docs:build 通过(exit 0,无 Undefined variable)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/commands/generate-theme.js | 11 +- .../docs/.vitepress/theme/demoDarkToggle.ts | 101 ++++++++++++------ 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/packages/cli/commands/generate-theme.js b/packages/cli/commands/generate-theme.js index 00c0c824..bdb8f2e4 100644 --- a/packages/cli/commands/generate-theme.js +++ b/packages/cli/commands/generate-theme.js @@ -36,7 +36,16 @@ export const generateTheme = async () => { const darkCssVars = Object.entries(mergedDarkTheme) .map(([key, value]) => `--${CSS_CLASS_PREFIX}-${key}: ${value}`) .join(';\n') - const darkFileStr = `.dark{\n${darkCssVars}\n}` + + // 同时产出一个 `.light{}` 块,内容为 light 全集。它的用途是「在已处于 + // 深色(祖先带 .dark)的子树里把某一块强制切回浅色」:CSS 自定义属性按元素 + // 就近解析,离用得最近的祖先上的声明胜出,因此在子树容器上挂 `.light` 即可 + // 覆盖更外层 `html.dark` 下传的取值。`.dark` / `.light` 互为对称的作用域类, + // 任一模式都能就地反向覆盖,文档站的 demo 浅/深就地预览即依赖此能力。 + const lightScopeVars = Object.entries(lightTheme) + .map(([key, value]) => `--${CSS_CLASS_PREFIX}-${key}: ${value}`) + .join(';\n') + const darkFileStr = `.dark{\n${darkCssVars}\n}\n.light{\n${lightScopeVars}\n}` const lightThemeFilePath = path.resolve(__dirname, '../../theme/theme.scss') const darkThemeFilePath = path.resolve(__dirname, '../../theme/darkTheme.css') diff --git a/packages/docs/.vitepress/theme/demoDarkToggle.ts b/packages/docs/.vitepress/theme/demoDarkToggle.ts index 6cc1a889..0e456050 100644 --- a/packages/docs/.vitepress/theme/demoDarkToggle.ts +++ b/packages/docs/.vitepress/theme/demoDarkToggle.ts @@ -1,37 +1,65 @@ // 给每个 demo 容器(@vitepress-code-preview/container 渲染的 [class*='_example-showcase_']) -// 注入一个浮动「浅色 / 深色」开关。点击只 toggle 该容器自身的 `.dark` 类, -// darkTheme.css 的规则全部 scope 在 `.dark` 下、组件全走 CSS 变量, -// 因此该容器内的 ccui 组件即就地变深色,而不触碰 VitePress 全站 appearance(html.dark)。 +// 注入一个浮动「浅色 / 深色」开关,只影响该容器自身,不触碰 VitePress 全站 appearance。 // -// 仅客户端执行:本模块只在 onMounted / 浏览器环境里被调用。 +// 为什么不能只 toggle 容器的 `.dark`:`.dark` 会从**任意祖先**级联下来。当全站 +// 处于深色(html.dark)时,容器即便不带 `.dark` 也会继承深色,单纯增删容器自己的 +// `.dark` 根本切不回浅色。darkTheme.css 同时产出了对称的 `.dark` / `.light` 作用域类 +// (CSS 自定义属性按元素就近解析,离得最近的祖先声明胜出),因此这里改为给容器挂 +// **显式**的 `.dark` 或 `.light`,无论全站是浅是深都能就地反向覆盖。 +// +// 仅客户端执行:本模块只在浏览器环境里被调用。 // 标记位:避免对同一容器重复注入按钮。 const INJECTED_FLAG = 'data-ccui-dark-toggle' +// 记录用户对该容器的显式选择:'dark' | 'light' | 不存在(跟随全站)。 +const FORCED_ATTR = 'data-ccui-theme' const SHOWCASE_SELECTOR = "[class*='_example-showcase_']" +// 全站是否深色:VitePress 的 appearance 切换挂在 .dark 上。 +function ambientIsDark(): boolean { + return document.documentElement.classList.contains('dark') +} + +// 容器当前实际渲染为深色与否:有显式选择则以选择为准,否则跟随全站。 +function effectiveIsDark(container: HTMLElement): boolean { + const forced = container.getAttribute(FORCED_ATTR) + if (forced === 'dark') return true + if (forced === 'light') return false + return ambientIsDark() +} + +// 把目标模式落到容器上:互斥地挂 `.dark` / `.light`,并记录显式选择。 +function applyForced(container: HTMLElement, dark: boolean) { + container.setAttribute(FORCED_ATTR, dark ? 'dark' : 'light') + container.classList.toggle('dark', dark) + container.classList.toggle('light', !dark) +} + +const buttons = new Set<{ container: HTMLElement; btn: HTMLButtonElement }>() + +function syncButton(container: HTMLElement, btn: HTMLButtonElement) { + const isDark = effectiveIsDark(container) + btn.setAttribute('aria-pressed', String(isDark)) + // 图标表示「点一下会切到」的目标:当前深色就给太阳(切浅),反之给月亮(切深)。 + btn.title = isDark ? '切换为浅色' : '切换为深色' + btn.setAttribute('aria-label', btn.title) + btn.textContent = isDark ? '☀' : '☾' +} + function createToggleButton(container: HTMLElement): HTMLButtonElement { const btn = document.createElement('button') btn.type = 'button' btn.className = 'ccui-demo-dark-toggle' - btn.setAttribute('aria-pressed', 'false') - - const sync = () => { - const isDark = container.classList.contains('dark') - btn.setAttribute('aria-pressed', String(isDark)) - btn.title = isDark ? '切换为浅色' : '切换为深色' - btn.setAttribute('aria-label', btn.title) - // 用文字图标,零依赖、不需要额外资源 - btn.textContent = isDark ? '☀' : '☾' - } btn.addEventListener('click', (e) => { e.preventDefault() e.stopPropagation() - container.classList.toggle('dark') - sync() + // 以「当前实际效果」为基准取反,保证首次点击在任何全站模式下都正确翻转。 + applyForced(container, !effectiveIsDark(container)) + syncButton(container, btn) }) - sync() + syncButton(container, btn) return btn } @@ -39,12 +67,13 @@ function enhanceShowcase(container: HTMLElement) { if (container.hasAttribute(INJECTED_FLAG)) return container.setAttribute(INJECTED_FLAG, '') - // 容器默认 position 未必是 relative(active 规则里没设), - // 绝对定位按钮前确保有定位上下文。 + // 容器默认 position 未必是 relative,绝对定位按钮前确保有定位上下文。 const pos = getComputedStyle(container).position if (pos === 'static') container.style.position = 'relative' - container.appendChild(createToggleButton(container)) + const btn = createToggleButton(container) + container.appendChild(btn) + buttons.add({ container, btn }) } function scan(root: ParentNode = document) { @@ -52,6 +81,7 @@ function scan(root: ParentNode = document) { } let observer: MutationObserver | null = null +let htmlObserver: MutationObserver | null = null /** * 在客户端启动 demo 深色开关注入。多次调用安全(幂等)。 @@ -62,16 +92,27 @@ export function setupDemoDarkToggle() { // 首屏 + 后续懒加载的 demo(VitePress SPA 路由切换会换页面内容)。 scan() - if (observer) return + if (!observer) { + observer = new MutationObserver((mutations) => { + for (const m of mutations) { + m.addedNodes.forEach((node) => { + if (!(node instanceof HTMLElement)) return + if (node.matches(SHOWCASE_SELECTOR)) enhanceShowcase(node) + scan(node) + }) + } + }) + observer.observe(document.body, { childList: true, subtree: true }) + } - observer = new MutationObserver((mutations) => { - for (const m of mutations) { - m.addedNodes.forEach((node) => { - if (!(node instanceof HTMLElement)) return - if (node.matches(SHOWCASE_SELECTOR)) enhanceShowcase(node) - scan(node) + // 全站 appearance 变化时,未做过显式选择的容器要跟着更新按钮图标 + // (它们没挂 .dark/.light,视觉本就跟随全站,只是按钮文案需同步)。 + if (!htmlObserver) { + htmlObserver = new MutationObserver(() => { + buttons.forEach(({ container, btn }) => { + if (!container.getAttribute(FORCED_ATTR)) syncButton(container, btn) }) - } - }) - observer.observe(document.body, { childList: true, subtree: true }) + }) + htmlObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) + } } From d1f8f0ef4d2f131fad9d00b71de9577df12a3509 Mon Sep 17 00:00:00 2001 From: vaebe <18137693952@163.com> Date: Thu, 4 Jun 2026 14:41:21 +0800 Subject: [PATCH 5/5] =?UTF-8?q?revert:=20=E7=A7=BB=E9=99=A4=20demo=20?= =?UTF-8?q?=E6=B5=85/=E6=B7=B1=E5=B0=B1=E5=9C=B0=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实际意义不大,整体撤掉 Track D: - 删 demoDarkToggle.ts 及 theme/index.ts 中的注入调用 - 删 styles/index.css 中 .ccui-demo-dark-toggle 按钮样式 - 撤 generate-theme.js 中专为该功能加的 .light{} 生成(darkTheme.css 复原为仅 .dark{}) - 删主题文档末尾「文档站内试试开关」提示段 全站深色仍由 html.dark / ConfigProvider algorithm:'dark' 提供,组件 token 化与 主题指南页保留。pnpm docs:build 通过(exit 0)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/commands/generate-theme.js | 11 +- .../docs/.vitepress/theme/demoDarkToggle.ts | 118 ------------------ packages/docs/.vitepress/theme/index.ts | 6 - .../docs/.vitepress/theme/styles/index.css | 47 ------- packages/docs/components/theme/index.md | 4 - 5 files changed, 1 insertion(+), 185 deletions(-) delete mode 100644 packages/docs/.vitepress/theme/demoDarkToggle.ts diff --git a/packages/cli/commands/generate-theme.js b/packages/cli/commands/generate-theme.js index bdb8f2e4..00c0c824 100644 --- a/packages/cli/commands/generate-theme.js +++ b/packages/cli/commands/generate-theme.js @@ -36,16 +36,7 @@ export const generateTheme = async () => { const darkCssVars = Object.entries(mergedDarkTheme) .map(([key, value]) => `--${CSS_CLASS_PREFIX}-${key}: ${value}`) .join(';\n') - - // 同时产出一个 `.light{}` 块,内容为 light 全集。它的用途是「在已处于 - // 深色(祖先带 .dark)的子树里把某一块强制切回浅色」:CSS 自定义属性按元素 - // 就近解析,离用得最近的祖先上的声明胜出,因此在子树容器上挂 `.light` 即可 - // 覆盖更外层 `html.dark` 下传的取值。`.dark` / `.light` 互为对称的作用域类, - // 任一模式都能就地反向覆盖,文档站的 demo 浅/深就地预览即依赖此能力。 - const lightScopeVars = Object.entries(lightTheme) - .map(([key, value]) => `--${CSS_CLASS_PREFIX}-${key}: ${value}`) - .join(';\n') - const darkFileStr = `.dark{\n${darkCssVars}\n}\n.light{\n${lightScopeVars}\n}` + const darkFileStr = `.dark{\n${darkCssVars}\n}` const lightThemeFilePath = path.resolve(__dirname, '../../theme/theme.scss') const darkThemeFilePath = path.resolve(__dirname, '../../theme/darkTheme.css') diff --git a/packages/docs/.vitepress/theme/demoDarkToggle.ts b/packages/docs/.vitepress/theme/demoDarkToggle.ts deleted file mode 100644 index 0e456050..00000000 --- a/packages/docs/.vitepress/theme/demoDarkToggle.ts +++ /dev/null @@ -1,118 +0,0 @@ -// 给每个 demo 容器(@vitepress-code-preview/container 渲染的 [class*='_example-showcase_']) -// 注入一个浮动「浅色 / 深色」开关,只影响该容器自身,不触碰 VitePress 全站 appearance。 -// -// 为什么不能只 toggle 容器的 `.dark`:`.dark` 会从**任意祖先**级联下来。当全站 -// 处于深色(html.dark)时,容器即便不带 `.dark` 也会继承深色,单纯增删容器自己的 -// `.dark` 根本切不回浅色。darkTheme.css 同时产出了对称的 `.dark` / `.light` 作用域类 -// (CSS 自定义属性按元素就近解析,离得最近的祖先声明胜出),因此这里改为给容器挂 -// **显式**的 `.dark` 或 `.light`,无论全站是浅是深都能就地反向覆盖。 -// -// 仅客户端执行:本模块只在浏览器环境里被调用。 - -// 标记位:避免对同一容器重复注入按钮。 -const INJECTED_FLAG = 'data-ccui-dark-toggle' -// 记录用户对该容器的显式选择:'dark' | 'light' | 不存在(跟随全站)。 -const FORCED_ATTR = 'data-ccui-theme' -const SHOWCASE_SELECTOR = "[class*='_example-showcase_']" - -// 全站是否深色:VitePress 的 appearance 切换挂在 .dark 上。 -function ambientIsDark(): boolean { - return document.documentElement.classList.contains('dark') -} - -// 容器当前实际渲染为深色与否:有显式选择则以选择为准,否则跟随全站。 -function effectiveIsDark(container: HTMLElement): boolean { - const forced = container.getAttribute(FORCED_ATTR) - if (forced === 'dark') return true - if (forced === 'light') return false - return ambientIsDark() -} - -// 把目标模式落到容器上:互斥地挂 `.dark` / `.light`,并记录显式选择。 -function applyForced(container: HTMLElement, dark: boolean) { - container.setAttribute(FORCED_ATTR, dark ? 'dark' : 'light') - container.classList.toggle('dark', dark) - container.classList.toggle('light', !dark) -} - -const buttons = new Set<{ container: HTMLElement; btn: HTMLButtonElement }>() - -function syncButton(container: HTMLElement, btn: HTMLButtonElement) { - const isDark = effectiveIsDark(container) - btn.setAttribute('aria-pressed', String(isDark)) - // 图标表示「点一下会切到」的目标:当前深色就给太阳(切浅),反之给月亮(切深)。 - btn.title = isDark ? '切换为浅色' : '切换为深色' - btn.setAttribute('aria-label', btn.title) - btn.textContent = isDark ? '☀' : '☾' -} - -function createToggleButton(container: HTMLElement): HTMLButtonElement { - const btn = document.createElement('button') - btn.type = 'button' - btn.className = 'ccui-demo-dark-toggle' - - btn.addEventListener('click', (e) => { - e.preventDefault() - e.stopPropagation() - // 以「当前实际效果」为基准取反,保证首次点击在任何全站模式下都正确翻转。 - applyForced(container, !effectiveIsDark(container)) - syncButton(container, btn) - }) - - syncButton(container, btn) - return btn -} - -function enhanceShowcase(container: HTMLElement) { - if (container.hasAttribute(INJECTED_FLAG)) return - container.setAttribute(INJECTED_FLAG, '') - - // 容器默认 position 未必是 relative,绝对定位按钮前确保有定位上下文。 - const pos = getComputedStyle(container).position - if (pos === 'static') container.style.position = 'relative' - - const btn = createToggleButton(container) - container.appendChild(btn) - buttons.add({ container, btn }) -} - -function scan(root: ParentNode = document) { - root.querySelectorAll(SHOWCASE_SELECTOR).forEach(enhanceShowcase) -} - -let observer: MutationObserver | null = null -let htmlObserver: MutationObserver | null = null - -/** - * 在客户端启动 demo 深色开关注入。多次调用安全(幂等)。 - */ -export function setupDemoDarkToggle() { - if (typeof window === 'undefined' || typeof document === 'undefined') return - - // 首屏 + 后续懒加载的 demo(VitePress SPA 路由切换会换页面内容)。 - scan() - - if (!observer) { - observer = new MutationObserver((mutations) => { - for (const m of mutations) { - m.addedNodes.forEach((node) => { - if (!(node instanceof HTMLElement)) return - if (node.matches(SHOWCASE_SELECTOR)) enhanceShowcase(node) - scan(node) - }) - } - }) - observer.observe(document.body, { childList: true, subtree: true }) - } - - // 全站 appearance 变化时,未做过显式选择的容器要跟着更新按钮图标 - // (它们没挂 .dark/.light,视觉本就跟随全站,只是按钮文案需同步)。 - if (!htmlObserver) { - htmlObserver = new MutationObserver(() => { - buttons.forEach(({ container, btn }) => { - if (!container.getAttribute(FORCED_ATTR)) syncButton(container, btn) - }) - }) - htmlObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) - } -} diff --git a/packages/docs/.vitepress/theme/index.ts b/packages/docs/.vitepress/theme/index.ts index c9ab9ee2..c362c4e7 100644 --- a/packages/docs/.vitepress/theme/index.ts +++ b/packages/docs/.vitepress/theme/index.ts @@ -3,7 +3,6 @@ import DemoPreview, { useComponents } from '@vitepress-code-preview/container' import DefaultTheme from 'vitepress/theme' import ccui from '@vaebe/ccui/ui/vue-ccui' import IconShowcase from './components/IconShowcase.vue' -import { setupDemoDarkToggle } from './demoDarkToggle' import './styles/index.css' // 暗色主题:从 workspace 源码 (@vaebe/ccui-theme) 引入,规则用 `.dark` // 选择器作用域,与 VitePress 默认 html.dark 切换约定一致;替代了之前从 @@ -21,10 +20,5 @@ export default { ctx.app.component('IconShowcase', IconShowcase) useComponents(ctx.app, DemoPreview) - - // 仅客户端:给每个 demo 容器注入「浅色 / 深色」就地切换按钮。 - // VitePress SSR 阶段 enhanceApp 也会跑,用浏览器环境守卫; - // 内部再用 MutationObserver 兜住路由切换后新渲染的 demo。 - if (typeof window !== 'undefined') setupDemoDarkToggle() }, } diff --git a/packages/docs/.vitepress/theme/styles/index.css b/packages/docs/.vitepress/theme/styles/index.css index 763d4d64..8af6ce1c 100644 --- a/packages/docs/.vitepress/theme/styles/index.css +++ b/packages/docs/.vitepress/theme/styles/index.css @@ -37,53 +37,6 @@ margin-top: 0; } -/* demo 容器右上角的「浅色 / 深色」就地切换按钮。 - - JS(theme/demoDarkToggle.ts)注入到每个 [class*='_example-showcase_'] 容器内, - 容器若为 static 定位会被就地改成 relative 以承载本绝对定位按钮。 - - 不带任何 ccui- 类,因此不会被 showcase 的相邻兄弟间距 / 列表抹平规则命中。 - - 切换只 toggle 容器自身 .dark;按钮自身样式写死、不依赖 ccui 变量, - 使其在浅 / 深两态下都清晰可见。 */ -.vp-doc [class*='_example-showcase_'] .ccui-demo-dark-toggle { - position: absolute; - top: 8px; - inset-inline-end: 8px; - z-index: 1; - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - padding: 0; - margin: 0; - font-size: 14px; - line-height: 1; - cursor: pointer; - border-radius: 6px; - border: 1px solid var(--vp-c-divider); - /* 用 VitePress 自身变量,浅 / 深态都有合适对比;不受容器 .dark 影响 */ - color: var(--vp-c-text-2); - background-color: var(--vp-c-bg); - opacity: 0.55; - transition: - opacity 0.2s ease, - color 0.2s ease, - border-color 0.2s ease; -} - -.vp-doc [class*='_example-showcase_'] .ccui-demo-dark-toggle:hover, -.vp-doc [class*='_example-showcase_'] .ccui-demo-dark-toggle:focus-visible { - opacity: 1; - color: var(--vp-c-brand-1, var(--vp-c-brand)); - border-color: var(--vp-c-brand-1, var(--vp-c-brand)); - outline: none; -} - -/* 容器进入深色态时,按钮仍保持自身配色(已用 vp 变量), - 这里仅微调让它在深底上更显眼。 */ -.vp-doc [class*='_example-showcase_'].dark .ccui-demo-dark-toggle { - opacity: 0.7; -} - .mt-10 { margin-top: 10px; } diff --git a/packages/docs/components/theme/index.md b/packages/docs/components/theme/index.md index 5448d8dd..ad551c26 100644 --- a/packages/docs/components/theme/index.md +++ b/packages/docs/components/theme/index.md @@ -134,7 +134,3 @@ const radius = ref(6) - 整站统一深色:在根元素切 `.dark` 类(**主推**),配合全局引入的 `darkTheme.css`。 - 仅局部子树深色:用 `c-config-provider` 的 `theme.algorithm: 'dark'` 包裹。 - 定制品牌色 / 圆角:用 `c-config-provider` 的 `theme.token`,或直接覆盖 `--ccui-*` 变量。 - -::: tip 在文档站内试试 -本站每个 `:::demo` 演示框右上角都有一个浅色 / 深色开关,点击只会切换该演示框自身的 `.dark`,方便就地预览组件在两种模式下的表现。 -:::