news 2026/9/13 17:04:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/posthog

@posthog/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.css@theme+@custom-variant+ base 层应用(apps)
tailwind-lib.css@theme+@custom-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 类名。

两条取值路线

  1. 主题派生(theme-derived):表面色与中性色由共享的主题色相 + 着色度(tint)推导。surface()辅助函数构造的是引用 CSS 自定义属性的oklch()表达式,而非写死的颜色值(colors.ts#L62-L68):

    function 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-L46):

    export 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" 的一组变量,生成时写入:root(colors.ts#L283-L291)。

  2. 静态值(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, [theme="dark"], [data-theme="dark"]) { 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-* … */ }

为防止漏配导致"局部主题覆盖静默失效",模块加载时会执行一个构建期守卫assertThemeDerivedSyncedWithColors(colors.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, [theme="dark"], [data-theme="dark"]), :is(.dark, [theme="dark"], [data-theme="dark"]) *));

三种形态分别覆盖 Tailwind 惯用的.darkclass、裸theme="dark"属性,以及data-theme="dark"属性(源码注释指出的一个真实动机:@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..15

export 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: Record<string, string> = { '--color-graph-axis-label': 'var(--muted-foreground)', '--color-graph-axis-line': 'var(--border)', '--color-graph-crosshair': 'var(--muted-foreground)', }

双份副本 + CI 断言防漂移

JS 侧还导出三个非 CSS 消费者用的 API:dataColorsdataColorPalette(浅色 hex 的普通数组,兼作 CSS 变量未加载时的回退)、dataColorVarName(index)(把 0 基索引映射为--data-color-{index+1})。

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.25rem(16px 根字号下即 4px),与 Tailwind v4 默认一致。这一模型带来四个好处(源码注释归纳,spacing.ts#L3-L16):随根字号等比缩放(无障碍)、连续刻度(p-7p-[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包含三段:

  1. @custom-variant dark:如上所述的三合一暗色选择器;
  2. @theme inline:颜色映射--color-{key}: var(--{key})(使bg-cardtext-foregroundborder-border等 Tailwind 工具类解析到 quill 的值)、间距基值、字号、字体族、阴影、圆角派生量,以及skeleton/pulse-glow/horizontal-shake/radar四个动画的 keyframes;
  3. @layer 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 与守卫实现确认):

  1. 编辑对应的src/*.ts模块。例如给dataColors数组加一项,或往semanticColors加一个[light, dark, tailwindClass]三元组。
  2. 如果新语义令牌直接引用了主题变量--theme-hue/--theme-dark-hue/--theme-tint/--primary-light/--primary-dark),把它登记进 THEME_DERIVED_TOKENS;忘了的话模块加载时会直接抛错,错误信息会告诉你该往哪里加。
  3. 重跑构建pnpm --filter @posthog/quill-tokens build
  4. 验证产物:确认新变量出现在dist/color-system.css(以及.scoped.css变体)里。

如果改动的是dataColors,还有一条隐含步骤:同步 quill-charts 的DEFAULT_CHART_COLORS(packages/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:04:00

手工实现KNN与朴素贝叶斯:鸢尾花分类算法全解析

简介&#xff1a;这一项目以鸢尾花数据集为对象&#xff0c;手工实现KNN与朴素贝叶斯两种经典分类算法&#xff0c;适合机器学习初学者对照理论动手实践。压缩包内共5个文件&#xff0c;包含两个Python代码文件、鸢尾花数据csv、结果txt以及README说明&#xff0c;整个资源仅4K…

作者头像 李华
网站建设 2026/9/13 17:03:53

Python人工智能案例包:环境配置与代码实战全流程指南

简介&#xff1a;一套聚焦Python人工智能入门与进阶的经典案例合集&#xff0c;面向希望结合真实数据动手实践的学习者&#xff0c;覆盖数据加载、特征分析、模型训练与结果可视化等典型环节。压缩包内共104个文件&#xff0c;主要包含28个csv数据集、24个py案例脚本与10张jpg效…

作者头像 李华
网站建设 2026/9/13 17:03:40

MATLAB BP神经网络分类实战:从蠓虫到鸢尾花全流程

简介&#xff1a;这是一份面向MATLAB初学者的ANN神经网络入门资料&#xff0c;聚焦BP神经网络在分类问题中的完整实现&#xff0c;适合刚接触神经网络或希望在MATLAB中动手实践分类任务的读者。压缩包内共7个文件&#xff0c;含5个txt数据文件与2个m源程序&#xff0c;结构清晰…

作者头像 李华
网站建设 2026/9/13 17:02:51

LabVIEW UDS上位机从TOOMOSS到ZLG的CAN硬件移植指南

1. 项目概述&#xff1a;为什么一个CAN UDS上位机的移植值得专门写十三篇&#xff1f;“基于周立功的CAN UDS升级上位机-LabVIEW版本&#xff08;十三&#xff09;&#xff1a;从图莫斯到ZLG的移植指南”——这个标题里藏着三个关键信号&#xff1a;CAN总线、UDS诊断协议、LabV…

作者头像 李华