ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Scalar Storybook 主题与全局配置:基于 `@scalar/helpers/storybook` 的共享可视化基础设施

Scalar Storybook 主题与全局配置:基于 `@scalar/helpers/storybook` 的共享可视化基础设施 Scalar Storybook 主题与全局配置基于scalar/helpers/storybook的共享可视化基础设施【免费下载链接】scalarScalar is an open-source API platform: Modern REST API Client Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalarscalar/helpers/storybook是 Scalar 开源 API 平台中面向内部包的共享 Storybook 配置模块为scalar/components与scalar/api-reference两个包提供统一的主题切换与颜色模式控制能力。在引入该模块之前这两个包的.storybook目录各自维护着一份字节级相同的复制代码如今它们统一消费同一套themes与globals子模块。读完本文你将掌握如何把 Scalar 的全部主题预设接入 Storybook 工具栏、如何通过?globalsURL 参数驱动视觉测试以及这套机制在 CSS 级联层Cascade Layers与自定义属性Custom Properties层面的底层原理。模块定位内部工具而非公共 API该模块位于 packages/helpers/src/storybook/README.md其定位在文档开头就写得很清楚这个目录下的任何内容都不是给scalar/helpers的消费方使用的它是恰好寄居在同一包内的内部工具——正如scalar/helpers/playwright一样参见 packages/helpers 包结构。当前只有两个包实际运行 Storybook 并消费该模块scalar/components组件库scalar/api-referenceAPI 参考文档渲染器二者此前各自维护着字节级一致的复制代码如今统一由该模块提供。目录下仅有三个文件packages/helpers/src/storybook/ ├── README.md ├── globals.ts └── themes.ts其中themes.ts负责把 Scalar 主题预设表达为 Storybook 工具栏选项globals.ts负责把主题与颜色模式两个全局量应用到文档并暴露给预览Preview配置。主题预设themes子模块packages/helpers/src/storybook/themes.ts 从scalar/themes导入themeIds、themeLabels与getThemeStyles并向外导出三个核心对象/类型themeVariants、defaultThemeVariant与ThemeVariantId。themeVariants全部主题 两个圆角极端值themeVariants是“一个 story 可以在其下渲染的每一种主题”的映射以 id 为键。它由两部分拼合而成1. 预设变体presetVariants来自scalar/themes的themeIds。当前仓库中 packages/themes/src/index.ts 定义了完整的主题 id 列表export const themeIds [ alternate, default, moon, purple, solarized, bluePlanet, deepSpace, saturn, kepler, elysiajs, fastify, mars, laserwave, none, ] as const构造逻辑中有两个值得注意的取舍default变体的css为null不注入任何样式因为default本来就是preview.ts已加载的主题保持原样可以让 stories 的渲染结果与没有该装饰器时完全一致none预设被显式过滤掉因为getThemeStyles会把none解析回默认预设留着没有意义。2. 圆角变体radiusVariants两个额外注入的变体把--scalar-radius推到刻度两端以便对组件在两个极端外观下做快照const radiusVariants { rounded-none: { label: Rounded None (0), css: layer scalar-theme { :root { --scalar-radius: 0px; } }, }, rounded-full: { label: Rounded Full (9999px), css: layer scalar-theme { :root { --scalar-radius: 9999px; } }, }, } as const之所以命名为rounded-none/rounded-full是因为它们强制整个界面呈现 Tailwind 同名类的外观前者把所有圆角拍平为直角后者把所有圆角封顶为胶囊形。源码注释明确指出所有预设主题都达不到这两个极端——唯一碰过圆角的 Laserwave 主题也只是固定了自己更大的圆角值而不是从--scalar-radius派生见 packages/themes/src/presets/laserwave.css。关于圆角为什么要覆盖在:root上源码给出了精辟的解释整个圆角刻度都由--scalar-radius派生自定义属性custom property是在声明它的元素上完成var()代入的而派生的 token 也在:root上声明。因此如果把覆盖放在更深的元素上只会移动基准值而不会带动派生值所以必须覆盖在:root。applyThemeVariant(id)替换而非堆叠样式export const applyThemeVariant (id: ThemeVariantId) { document.getElementById(STYLE_ELEMENT_ID)?.remove() const css themeVariants[id]?.css if (!css) { return } const style document.createElement(style) style.id STYLE_ELEMENT_ID style.textContent css document.head.append(style) }该函数把变体的 CSS 注入到一个由它独占的 style 元素id 为scalar-storybook-theme中每次调用先移除旧元素再追加新元素从而做到“替换”而非“叠加”——反复切换主题不会在head里累积样式。注入的预设 CSS 被包裹在layer scalar-theme中而默认预设加载进的是scalar-base层因此主题样式依靠层顺序layer order胜出而不是选择器特异性。源码注释特别强调这正是 API reference 在运行时应用主题所用的同一机制对应getThemeStyles的layer参数默认值为scalar-theme见 packages/themes/src/index.ts。defaultThemeVariant 与 ThemeVariantIddefaultThemeVariant: ThemeVariantId default除非某个 story 或测试指定了其他变体否则 story 默认在此主题下渲染ThemeVariantId keyof typeof themeVariants所有合法变体 id 的联合类型13 个预设主题中除none外的 12 个 2 个圆角变体。在scalar/components的测试基建中可以看到它的实际用法packages/components/test/helpers.ts引入defaultThemeVariant作为 theme 的默认参数packages/components/test/gallery/main.ts在渲染画廊时调用applyThemeVariant(window.__scalarTheme ?? defaultThemeVariant)第 11、161 行packages/components/test/shared.ts还据此拼接快照文件名后缀当主题不是默认值时追加-{slug}第 133 行。全局控制globals子模块packages/helpers/src/storybook/globals.ts 为所有 Scalar Storybook 提供两个共享的工具栏控制项全局名工具栏标题说明themeTheme枚举themeVariants的全部选项画笔图标paintbrushdynamicTitle: truecolorModeColor modelight/dark两个选项太阳/月亮图标dynamicTitle: true颜色模式的默认值跟随操作系统颜色模式默认值取自操作系统偏好在预览加载时解析一次export const defaultColorMode: DarkLightMode getSystemColorMode()getSystemColorMode的实现位于 packages/helpers/src/theme/color-mode.ts细节很有参考价值无windowSSR 环境时返回light——没有偏好可读也没有任何东西在渲染有window但没有matchMedia时返回dark——这个看似不一致的行为是为兼容useColorMode的历史解析方式而保留的实践中只有 stub 过的测试环境会走到该分支正常情况用window.matchMedia((prefers-color-scheme: dark))判定。所以 Storybook 打开时会自动落在整台机器当前所处的模式上。这里用的是“解析一次”而非“持续监听”因为工具栏本身就是为了覆盖该默认值而存在的。快照测试永远不会看到这个默认值测试会为每张截图包括浅色截图自行设置模式 class因此基线不依赖运行测试的机器偏好。scalarGlobalTypes / scalarInitialGlobals纯对象字面量这两个导出被设计成普通的对象字面量而非 Storybook 的GlobalTypes类型目的在源码注释里写得很直白这样本包就不必依赖 Storybook——每个 preview 在把它赋给Preview.globalTypes/initialGlobals时才做类型检查。export const scalarGlobalTypes { theme: { description: Scalar theme, toolbar: { title: Theme, icon: paintbrush, items: /* themeVariants 全部条目 */, dynamicTitle: true }, }, colorMode: { description: Color mode, toolbar: { title: Color mode, icon: contrast, items: [light/dark], dynamicTitle: true }, }, } export const scalarInitialGlobals { theme: defaultThemeVariant, colorMode: defaultColorMode, }声明 globals 还有一个关键副作用Storybook 会丢弃任何未声明的 global。因此“声明”本身就让视觉测试可以通过 story URL 挑选主题与颜色模式?globalstheme:laserwave;colorMode:darkapplyScalarGlobals(globals)装饰器内联动应用export const applyScalarGlobals (globals: Recordstring, unknown): void { applyThemeVariant(globals.theme as ThemeVariantId) applyColorMode(globals.colorMode dark ? dark : light) }它同时应用两个全局量主题交给applyThemeVariant颜色模式交给applyColorMode。applyColorMode的实现color-mode.ts基于一个设计事实每个 Scalar 主题都以light-mode/dark-mode一对 class 而非媒体查询发布两种模式所以切换模式就是交换这两个 class且两个都被 toggle确保元素永远不会同时带上两个 mode class。调用方需要自行确认 DOM 已存在——因为默认参数会触及document在 SSR 下会抛错而不是静默无操作。文档推荐的用法是放进 decorator这样工具栏每次变化都会重新执行decorators: [ (story, context) { applyScalarGlobals(context.globals) return story() }, ]在 preview 中装配两个包的消费方式scalar/components与scalar/api-reference的.storybook/preview.ts几乎一模一样都以三行完成接入import { applyScalarGlobals, scalarGlobalTypes, scalarInitialGlobals } from scalar/helpers/storybook/globals const preview: Preview { globalTypes: scalarGlobalTypes, initialGlobals: scalarInitialGlobals, decorators: [ (story, context) { applyScalarGlobals(context.globals) return story() }, ], }可对照的完整实现见 packages/api-reference/.storybook/preview.ts 与 packages/components/.storybook/preview.ts。两个 preview 的差异主要体现在补充样式上api-reference的 preview 引入scalar/themes/fonts.css与自身的src/style.css其全局样式会拉入scalar/themes/style.css和 Tailwind 主题让 stories 与 reference 本身使用完全相同的 token 和 resetcomponents的 preview 额外引入scalar/themes/style.css并在 controls 中把--scalar-color-1、--scalar-background-1、--scalar-border-color等 16 个设计 token 注册为 preset colors方便在 controls 面板直接引用。两个 preview 都在document.body上添加了scalar-appclass以复用 Scalar 应用级的全局样式上下文。从 URL 参数到视觉测试的完整链路将上述机制串起来就是一条“从 URL 到快照基线”的完整链路测试在 story URL 上附加?globalstheme:laserwave;colorModedark;分隔、key:value形式Storybook 校验该 global 已在initialGlobals中声明否则直接丢弃渲染 story 时 decorator 读取context.globals调用applyScalarGlobalsapplyThemeVariant移除旧的scalar-storybook-themestyle 元素、按 id 查找themeVariants并注入新 CSS被layer scalar-theme包裹applyColorMode在 body 上切换light-mode/dark-modeclass快照测试为每张截图显式设置 mode class使基线不依赖运行机器。这套机制在scalar/components的测试与画廊基建中被实际消费packages/components/test/helpers.ts、packages/components/test/gallery/main.ts、packages/components/test/shared.ts均直接引用defaultThemeVariant/applyThemeVariant/ThemeVariantIdpackages/api-reference/test/helpers.ts也引入了同一条 storybook 路径印证了 README 中“两个包共用同一份配置”的陈述。环境依赖devDependency 约定模块引入前需要留意一个环境约定themes.ts导入了scalar/themes而它是本包的devDependency。因此任何引入scalar/helpers/storybook/*的包都必须自行提供该依赖——这与scalar/helpers/playwright/docker期望调用方提供playwright/test的约定完全同构参见 packages/helpers/src/playwright 相关实现。这也再次印证该模块是面向包内部工具链而非终端消费者的。推荐的最小导入方式import { applyScalarGlobals, scalarGlobalTypes, scalarInitialGlobals } from scalar/helpers/storybook/globals import { type ThemeVariantId, defaultThemeVariant } from scalar/helpers/storybook/themes小结scalar/helpers/storybook用约 150 行代码解决了 Scalar 多个包之间 Storybook 配置重复的问题并在细节上体现了几处值得借鉴的设计决策用“独占 style 元素 移除再追加”避免样式堆叠、用级联层而非特异性控制主题优先级、用纯对象字面量保持包对 Storybook 的零依赖、用“解析一次的系统偏好”作为颜色模式默认值以及通过声明 globals 顺带获得 URL 驱动的视觉测试能力。对于任何需要让 Storybook 与运行时主题保持一致的多包前端仓库这套模式都提供了可直接复制的参考实现。【免费下载链接】scalarScalar is an open-source API platform: Modern REST API Client Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表