Files
apes-sidebar/src/client/theme.ts
T
Atlas Zeng 3c196fffc3 feat: 皮肤兼容(令牌驱动)+ CSS 修复(#106 #105 #90 #60,附带 #52 #57 #92) (#110)
令牌驱动方案(主目标:与 dsh-web-ui 皮肤中心兼容):
- 面板表面改用通用卡片令牌 --dsw-alias-bg-layer-1,不再消费宿主左导航专属
  --dsw-specific-sidebar-fill——dsh-web-ui 10 款皮肤全部覆盖 --dsw-alias-*
  层,换肤后侧边栏自动跟随,零每皮肤适配(#60/#105/#106)。
- effectiveTokenValue 增加 alpha 阈值(新增 colorAlpha 解析 computed 颜色):
  transparent 与 alpha<0.9 的半透明玻璃值(dsh-web-ui 皮肤 bg-base 为
  rgba 0.16–0.7)一律回退不透明底色,终端/编辑器文字永不叠在皮肤背景画上
  (#90);≥0.9 的近不透明值放行,皮肤仍可控终端表面。

独立修复(保留):
- z-index 降到 DSH 浮层栈(100+)之下:面板 40 / 按钮簇 45 / 角手柄面板内
  局部 2,Cordis 弹出框不再被底部工作台遮挡(#52)。
- 角手柄移入右面板、CSS 相对定位(bottom: calc(var(--dsh-sidebar-height)
  + 6px)),删除 JS 内联 viewport 坐标。
- 刷新按钮统一 14px(#57 前半)。
- 拖拽逐帧回归 e2e(#92):真实挂载下 rAF 采样(拖条以 cursor: col-resize
  语义定位),断言拖拽期间 data-dsh-sidebar-dragging 存在、#root transition
  为 none、会话列与面板边单调 1:1 跟随(实测通过)。

按 KISS 回退首版契约:类名 kebab-case 改名与 data-bs-* 语义钩子全部移除
(令牌驱动下不需要,改名引入无谓 breaking),skin-hooks 测试删除;
mount e2e 改为断言 --dsh-sidebar-width 布局变量随面板挂载生效。

文档:AGENTS.md §8 重写为「皮肤兼容(令牌驱动)」;设计文档记录决策与
回退。README 与版本号不在本批更新(发布流程统一处理)。

测试:tests/theme.spec.ts(colorAlpha + effectiveTokenValue 阈值);
typecheck/vitest(511)/build/consumer-types/test:mount 全绿。
2026-08-16 16:37:11 +08:00

111 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Live theme access for surfaces that cannot consume the token colors
* directly — xterm's palette and CodeMirror's theme extensions need concrete
* values, but the app's scheme flips at runtime (ui-layout's ThemePresenter
* projects prefers-color-scheme and the user's choice onto
* body[data-ds-dark-theme] and html { color-scheme }). This module reads the
* resolved scheme and token values, and notifies subscribers on flips, so
* the terminal and the editor re-theme in place instead of freezing in the
* scheme they happened to be created under.
*/
/** Whether the app shell resolved to the dark scheme.
*
* The presenter sets `html { color-scheme }` together with the body palette
* attribute, so a set color-scheme means the decision is authoritative (an
* absent attribute is then LIGHT even when the OS prefers dark — the user
* chose light). Before the presenter has run, fall back to the OS media
* query as the best guess.
*/
export function isDarkScheme(): boolean {
if (typeof document === 'undefined') return true
const decided = document.documentElement.style.colorScheme !== ''
if (decided) return document.body.hasAttribute('data-ds-dark-theme')
return typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: dark)').matches
}
/** One token's computed value on <body> ('' while the theme has not applied). */
export function tokenValue(name: string): string {
if (typeof document === 'undefined') return ''
return getComputedStyle(document.body).getPropertyValue(name).trim()
}
/** Minimal alpha for a token color to count as effectively opaque. Skin
* systems turn `--dsw-alias-bg-base` translucent for glass panels (the
* dsh-web-ui skins use rgba 0.16–0.7; `transparent` is 0); below this
* floor a text surface (terminal, editor) would render over the skin's
* backdrop art, so callers fall back to an opaque color. Values at or
* above the floor (e.g. a skin's scoped 0.96 porcelain) pass through —
* the skin still controls the surface. */
const OPAQUE_ALPHA_MIN = 0.9
/** The alpha channel of a computed CSS color, or null when the format is
* not parseable (named colors, `color()`… — treated as opaque). Handles
* the shapes getComputedStyle actually returns: the rgb()/rgba() and
* hsl()/hsla() function forms (comma or space syntax, with or without the
* `/ alpha` slot) and the #rgb/#rgba/#rrggbb/#rrggbbaa hex family. */
export function colorAlpha(color: string): number | null {
const s = color.trim()
// #rgb / #rgba / #rrggbb / #rrggbbaa
const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(s)
if (hex !== null) {
const digits = hex[1]!
if (digits.length === 3 || digits.length === 4) {
const a = digits.length === 4 ? digits[3]! : 'f'
return parseInt(a + a, 16) / 255
}
const alphaHex = digits.length === 8 ? digits.slice(6) : 'ff'
return parseInt(alphaHex, 16) / 255
}
// rgb(r g b) / rgb(r g b / a) / rgba(r, g, b, a) — the trailing slot is alpha.
const fn = /^(rgba?|hsla?)\(([^)]+)\)$/i.exec(s)
if (fn !== null) {
const parts = fn[2]!.split(/[,\s/]+/).filter(Boolean)
const alphaPart = parts[3]
if (alphaPart === undefined) return 1
const alpha = Number.parseFloat(alphaPart)
return Number.isFinite(alpha) ? alpha : 1
}
return null
}
/**
* A token value that actually PAINTS something — the guard for text
* surfaces (issue #90). Skin systems routinely set global tokens to
* `transparent` (glass skins) or translucent glass values (`rgba(…,0.16–0.7)`,
* e.g. the dsh-web-ui skins) — both are truthy strings, so callers using
* `|| fallback` never fire and the terminal/editor goes see-through over
* the skin's backdrop. This returns '' for visually inert values (unset
* keywords, transparent, and any color below the opacity floor) so the
* caller's fallback chain engages; effectively opaque values pass through.
*/
export function effectiveTokenValue(name: string): string {
const raw = tokenValue(name)
switch (raw) {
case '':
case 'transparent':
case 'initial':
case 'inherit':
case 'unset':
return ''
default: {
const alpha = colorAlpha(raw)
if (alpha !== null && alpha < OPAQUE_ALPHA_MIN) return ''
return raw
}
}
}
/**
* Subscribe to color-scheme flips (the presenter toggles the body
* attribute). The callback fires after the attribute changed; re-read the
* scheme inside it.
* @returns the disposer.
*/
export function subscribeColorScheme(callback: () => void): () => void {
if (typeof document === 'undefined') return () => {}
const observer = new MutationObserver(() => { callback() })
observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
return () => { observer.disconnect() }
}