多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PostHog Quill 设计系统:@posthog/quill-tokens 设计令牌生成管线与运行时主题实现

PostHog Quill 设计系统:@posthog/quill-tokens 设计令牌生成管线与运行时主题实现 PostHog Quill 设计系统posthog/quill-tokens 设计令牌生成管线与运行时主题实现【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthogposthog/quill-tokens是 PostHog Quill 组件库的设计令牌design tokens唯一事实来源令牌在 TypeScript 中编写构建时生成 CSS消费方应用、图表包、带自有设计系统的第三方宿主按各自形态引入生成产物。读完本文你将掌握它的TypeScript 源 → CSS 产物生成管线、语义色 / 数据可视化令牌的定义规则、THEME_DERIVED_TOKENS运行时主题守卫的底层原理以及新增或修改一个令牌的完整标准流程。定位令牌在哪里编写、在哪里生成Quill 仓库位于 packages/quill其中packages/quill/packages/tokens是独立的 npm 包 packages/quill/packages/tokens/package.json包名posthog/quill-tokens。其核心约定只有一句话令牌在src/下用 TypeScript 编写CSS 是构建产物永远不要手工编辑dist/*.css——每个产物文件开头都印有Auto-generated by posthog/quill-tokens — do not edit manually的戳记。src/目录下的源码按令牌分组组织各模块职责如下均可从 index.ts 的导出看到全貌模块内容colors.ts语义色映射、主题配置、CSS 生成器核心data-viz.ts数据可视化分类色板 图表外壳graph chrome变量spacing.ts间距基值与spacing()/spacingPx()工具函数typography.ts字号、字体族令牌shadow.ts阴影令牌border-radius.ts圆角令牌css.tscssVars/cssVarsFlat等 CSS 拼装工具生成管线一条命令从 TypeScript 到六份 CSS官方管线的形状是src/*.ts ──(tsx src/build.ts)──▶ dist/*.css在仓库中执行pnpm --filter posthog/quill-tokens build实际触发的是 package.json 中的两步tsx src/build.ts vite build。前者用 build.ts 把各生成器调用的结果写盘后者用 Vite 打包 JS 入口dist/index.js/dist/index.cjs及类型声明。build.ts的写盘顺序是build.ts#L26-L49产物文件内容消费方color-system.css:root浅色 暗色选择器覆盖——运行时 CSS 变量应用与包在运行时直接引入tailwind.cssthemecustom-variant base 层应用appstailwind-lib.css仅themecustom-variant库包library packages*.scoped.css同上但所有变量 gated 在[data-quill]选择器之后带自有设计系统的宿主避免变量名冲突一个容易踩坑的细节tailwind.css/tailwind-lib.css不包含import tailwindcss引入 Tailwind 本身是消费应用自己的责任build.ts#L10-L11。这些产物同时通过包exports暴露为子路径消费方可以精确引入某一份package.json#L25-L31./tailwind.css: ./dist/tailwind.css, ./tailwind-lib.css: ./dist/tailwind-lib.css, ./color-system.css: ./dist/color-system.css, ./tailwind.scoped.css: ./dist/tailwind.scoped.css, ./tailwind-lib.scoped.css: ./dist/tailwind-lib.scoped.css, ./color-system.scoped.css: ./dist/color-system.scoped.css改完src/后必须重跑构建保证dist/与源码一致。开发体验上Quill Storybook 的main.ts里有quillTokensWatcher监听src/改令牌可热更新无需手动 build。语义色主题色相派生与静态值两条线语义色是令牌体系的核心定义在 colors.ts 的buildSemanticColors()中。每个令牌是一个三元组[light, dark, tailwindClass]类型ColorTuple分别给出浅色值、暗色值和对应的 Tailwind 类名。两条取值路线主题派生theme-derived表面色与中性色由共享的主题色相 着色度tint推导。surface()辅助函数构造的是引用 CSS 自定义属性的oklch()表达式而非写死的颜色值colors.ts#L62-L68function surface(lightness: number, chromaScale: number, mode: light | dark, alpha?: number): string { const hueVar mode light ? var(--theme-hue) : var(--theme-dark-hue) const chromaExpr chromaScale 1 ? var(--theme-tint) : chromaScale 0 ? 0 : calc(var(--theme-tint) * ${chromaScale}) const alphaSuffix alpha ! undefined ? / ${alpha * 100}% : return oklch(${lightness} ${chromaExpr} ${hueVar}${alphaSuffix}) }因为色相与着色度落在var(--theme-hue)/var(--theme-tint)上消费方只要在运行时改这几个变量就能整体平移调色板无需重新构建。PostHog 默认主题是暖黄灰色表面 橙/琥珀品牌色colors.ts#L40-L46export const DEFAULT_THEME: ThemeConfig { hue: 90, darkHue: 264, tint: 0.006, primaryLight: oklch(0.65 0.21 37.41), primaryDark: oklch(0.83 0.16 84.71), }其中--theme-hue、--theme-dark-hue、--theme-tint、--primary-light、--primary-dark就是产物里注释为 Theme knobs 的一组变量生成时写入:rootcolors.ts#L283-L291。静态值static状态色destructive、success、warning、info、completed与品牌前景色是写死的oklch()常量独立于主题色相保证成功绿不会因宿主换主题而变色。值得注意的取值细节fill-hover/fill-selected/fill-expanded这些交互填充令牌不是灰色固定值而是color-mix(in oklab, var(--foreground) N%, transparent)——以前景色为基准的相对覆盖层使得 hover/选中态在任何表面background、muted、card上都保持正确对比度源码注释明确解释了动机固定的半透明灰色在本身接近灰色的bg-muted上会不可见colors.ts#L148-L167。THEME_DERIVED_TOKENS加载期守卫主题派生令牌有一个特殊的输出位置要求它们必须挂在*选择器上而不是:root这样才能让容器级的局部覆盖如在某个子树上写[--theme-hue:200]按元素重新求值。生成器会按 THEME_DERIVED_TOKENS 集合把令牌分成两批输出colors.ts#L293-L305 的partition()/* color-system.css 的实际形态节选 */ :root { color-scheme: light; } :is(.dark, [themedark], [data-themedark]) { color-scheme: dark; } /* Theme knobs — override these to shift the palette */ :root { --radius: 0.58rem; --theme-hue: 90; --theme-dark-hue: 264; --theme-tint: 0.006; --primary-light: oklch(0.65 0.21 37.41); --primary-dark: oklch(0.83 0.16 84.71); } /* 静态色不引用主题变量放 :root 安全 */ :root { /* --foreground、--success、--data-color-1… */ } /* 主题派生色 — 挂在 * 上使元素级 [--theme-hue:X] 覆盖生效 */ * { /* --background、--card、--muted、--border、--fill-* … */ }为防止漏配导致局部主题覆盖静默失效模块加载时会执行一个构建期守卫assertThemeDerivedSyncedWithColorscolors.ts#L246-L266任何令牌的值只要直接引用了var(--theme-hue)、var(--theme-dark-hue)、var(--theme-tint)、var(--primary-light)、var(--primary-dark)之一就必须登记在THEME_DERIVED_TOKENS中否则直接抛错if (refsThemeVar !THEME_DERIVED_TOKENS.has(key)) { throw new Error( [posthog/quill-tokens] Token ${key} references a theme variable but is missing from THEME_DERIVED_TOKENS. ... ) }注意守卫只捕获直接引用像fill-*这类经由var(--foreground)间接依赖主题的传递性令牌无法自动检测必须在集合里手工登记集合内注释也写明了这一点colors.ts#L229-L233。暗色选择器兼容三种暗色标记约定generateStylesCSS()会生成 Tailwind v4 的custom-variant dark其选择器由resolveDarkSelector()归一化colors.ts#L268-L273。默认值是一个三合一的:is()选择器custom-variant dark (:is(:is(.dark, [themedark], [data-themedark]), :is(.dark, [themedark], [data-themedark]) *));三种形态分别覆盖 Tailwind 惯用的.darkclass、裸themedark属性以及data-themedark属性源码注释指出的一个真实动机modelcontextprotocol/ext-appsSDK 的applyDocumentTheme()使用的是data-*约定colors.ts#L190-L200。darkSelector可在StylesConfig中整体替换。数据可视化令牌有序分类色板与图表外壳变量data-viz.ts 提供两类令牌消费方是posthog/quill-charts——图表包不带任何 CSS在运行时把这些 CSS 变量读进ChartTheme通过其themeFromCssVars/useChartTheme。有序分类色板--data-color-1..15export const dataColors: readonly DataColorTuple[] [ [#1d4aff], [#621da6, #7f26d9], [#42827e, #3e7a76], [#ce0e74, #bf0d6c], [#f14f58, #f0474f], [#7c440e, #b36114], /* … 共 15 项data-viz.ts#L27-L43 */ ] as const设计约束有三条顺序有意义图表按系列索引取色、色相互不混淆15 种色相在图上同时出现仍可分辨、取静态品牌 hex 而非主题派生系列色不应随宿主换主题而漂移。类型是[light, dark?]元组——只有需要在暗色模式下额外提对比度的条目才带第二个值generateDataVizVars()生成浅色块时输出全部 15 个变量暗色块只输出带覆盖值的条目其余沿用浅色值data-viz.ts#L69-L79。图表外壳变量别名到语义令牌轴标签、轴线、十字线的变量不定义新颜色而是别名到既有语义令牌从而自动跟随明暗切换无需单独的暗色块const graphChrome: Recordstring, string { --color-graph-axis-label: var(--muted-foreground), --color-graph-axis-line: var(--border), --color-graph-crosshair: var(--muted-foreground), }双份副本 CI 断言防漂移JS 侧还导出三个非 CSS 消费者用的 APIdataColors、dataColorPalette浅色 hex 的普通数组兼作 CSS 变量未加载时的回退、dataColorVarName(index)把 0 基索引映射为--data-color-{index1}。quill-charts 中持有一份无 CSS 回退副本DEFAULT_CHART_COLORS位于 packages/quill/packages/charts/src/core/theme.ts。两份副本的一致性由 theme.test.ts 断言与dataColorPalette相等来保证——一旦dataColors改了而回退副本没跟上测试失败直接挡住 CI而不是让用户在运行时看到两张颜色对不上的图。此外变量名刻意与 PostHog 应用历史使用的--data-color-*/--color-graph-*保持一致使应用侧可以删除本地定义、直接继承 quill 的产物。间距、字号、圆角、阴影各自的模块与生成器这四类令牌各自独立成模块、各自带生成器最终全部汇入generateStylesCSS()的theme inline块。间距采用 Tailwind v4 的单基值模型整个间距刻度由一个--spacing变量驱动所有间距工具类解析为calc(var(--spacing) * N)spacing.ts。默认基值0.25rem16px 根字号下即 4px与 Tailwind v4 默认一致。这一模型带来四个好处源码注释归纳spacing.ts#L3-L16随根字号等比缩放无障碍、连续刻度p-7、p-[17]、p-1.5都合法无需预定义离散档位、参数化在任何节点上覆盖--spacing即可为子树调整密度、与字号令牌单位一致rem。TS 消费方拿到的是纯函数import { spacing, spacingPx } from posthog/quill-tokens spacing(4) // 1rem —— CSS-in-JS spacingPx(4) // 16 —— React Native / Figma 插件 / 导出器圆角以--radius默认0.58rem为基值派生出--radius-xs到--radius-4xl共 8 档全部是calc(var(--radius) ± Npx)表达式colors.ts#L436-L445 的RADIUS_VARS。这里有个实现细节圆角与阴影变量会同时写进两处——theme inline供 Tailwind 生成工具类和运行时:root或作用域选择器块因为 BEM 样式的 CSS 直接引用var(--radius-*)/var(--shadow-*)时仅靠theme inline是解析不到的colors.ts#L511-L523。字号 / 字体族typography.ts与阴影shadow.ts同理模块内定义源数据generateFontSizeCSS()/generateFontFamilyCSS()/generateShadowCSS()各自产出theme片段。Tailwind 产物theme 映射与 base 层generateStylesCSS()colors.ts#L447-L545生成的tailwind.css/tailwind-lib.css包含三段custom-variant dark如上所述的三合一暗色选择器theme inline块颜色映射--color-{key}: var(--{key})使bg-card、text-foreground、border-border等 Tailwind 工具类解析到 quill 的值、间距基值、字号、字体族、阴影、圆角派生量以及skeleton/pulse-glow/horizontal-shake/radar四个动画的 keyframeslayer base仅 app 形态includeBaseLayer: true全局重置* { apply border-border outline-ring/50; }与body { apply bg-background text-foreground; }。lib 形态includeBaseLayer: false刻意不含 base 层——库包不该替宿主做全局样式重置。作用域形态scope: [data-quill]还会额外做两件事把 base 层的apply收进[data-quill]子树内并在作用域内重写 Tailwind--color-*映射使bg-card等工具类在 quill 包裹区域内解析到 quill 的值而非宿主的colors.ts#L352-L355。作用域模式对暗色选择器的位置也做了兼容无论.dark在[data-quill]的祖先上还是同一元素上:is(.dark [data-quill], [data-quill].dark)及其后代变体暗色模式都能生效colors.ts#L317-L322。源码注释解释了作用域迁移策略迁移期间data-quill属性沿 DOM 树向上挪到达html时作用域即等价于全局届时可移除。实操新增或修改一个令牌的标准流程这是日常改动的工作流源自包内 agent 指南并结合 build.ts 与守卫实现确认编辑对应的src/*.ts模块。例如给dataColors数组加一项或往semanticColors加一个[light, dark, tailwindClass]三元组。如果新语义令牌直接引用了主题变量--theme-hue/--theme-dark-hue/--theme-tint/--primary-light/--primary-dark把它登记进 THEME_DERIVED_TOKENS忘了的话模块加载时会直接抛错错误信息会告诉你该往哪里加。重跑构建pnpm --filter posthog/quill-tokens build。验证产物确认新变量出现在dist/color-system.css以及.scoped.css变体里。如果改动的是dataColors还有一条隐含步骤同步 quill-charts 的DEFAULT_CHART_COLORSpackages/quill/packages/charts/src/core/theme.ts否则theme.test.ts的相等性断言会让 CI 变红——这其实是好事说明防线在起作用。小结posthog/quill-tokens把设计令牌当作一等工程问题来处理TypeScript 是单一事实来源CSS 全部可再生主题派生与静态取色分轨且用加载期守卫把局部覆盖静默失效这类隐性 bug 挡在构建阶段数据可视化色板与图表包之间用测试断言锁住双副本一致性四种产物app / lib × scoped / 非 scoped覆盖从自有应用到第三方宿主的全部引入形态。对这些机制的理解是消费或扩展 Quill 设计体系的前提。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表