)
PostHog Quill 组件库posthog/quill-components组合层深度指南DataTable、日期选择器与 Metric【免费下载链接】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 仓库中的 组件层 Agent 参考文档结合packages/quill/packages/components/src下的真实源码、测试与包配置系统讲解 Quill 设计系统“组合组件层”的五个核心导出DataTable、DateTimePicker、DatePicker、useCalendar与Metric。读完后你将掌握每个组件的完整 API、行为边界何时分页、何时提交、何时忽略行点击、底层 TanStack Table / date-fns / React Context 的实现原理以及该包在 Quill monorepo 中的构建与引用方式。组件层在 Quill 中的位置Quill 是 PostHog 的统一设计系统覆盖 web、MCP 与 Electron 三类 UI 表面构建在 Base UI 与 shadcn 风格原语之上且要求消费端使用 Tailwind v4见 Quill README。在packages/quill/packages/下按分层组织packages/quill/ ├── packages/ │ ├── tokens/ posthog/quill-tokens (published) │ ├── primitives/ posthog/quill-primitives (private, bundled into posthog/quill) │ ├── components/ posthog/quill-components (private, bundled into posthog/quill) │ ├── blocks/ posthog/quill-blocks (private, bundled into posthog/quill) │ └── quill/ posthog/quill (published — the aggregate consumers install) └── apps/ └── storybook/ posthog/quill-storybook (private, dev tool)posthog/quill-components处于中间层它不直接面向 npm 消费者package.json 中private: true当前版本0.3.0-beta.15而是在构建时被打进聚合包posthog/quill。其定位在 桶文件注释 中写得很明确——“把多个原语按合理默认值接起来的较高层组合否则每个应用都得手搓”如 FormField、ConfirmDialog、DataTable。依赖上该包依赖posthog/quill-primitives、posthog/quill-charts、date-fns与lucide-react而tanstack/react-table是 devDependency供类型声明使用。该包当前导出五个组合能力见 index.ts导出说明DataTableTanStack Table 接到 quillTablePagination上自带排序、可选分页与空态DateTimePicker带快捷范围预设quickRanges、CUSTOM_RANGE的日历区间选择器DatePicker单日期选择器一个日历、可选时间、无快捷范围useCalendarheadless 日历网格 hookDay、Month枚举Metric可组合统计瓦片CardBadge变更胶囊 Sparkline须从posthog/quill-components/metric子路径导入需要说明若你要使用原语级别的选型指南组件选择、间距、renderprop应先行阅读 原语层参考文档——这是本文档在源文件开头给出的前置条件。DataTableTanStack Table quill Table 的组合封装基本用法与 API原文档给出的标准用法如下列定义就是标准的 TanStackColumnDefquill 专属选项放在meta里import { type ColumnDef } from tanstack/react-table import { DataTable } from posthog/quill-components const columns: ColumnDefPerson[] [ { accessorKey: name, header: Name, meta: { expand: true } }, { accessorKey: status, header: Status, meta: { align: center } }, { accessorKey: amount, header: Amount, meta: { align: right }, enableSorting: false }, ] DataTable columns{columns} data{data} pageSize{10} // 省略则为不分页表格 pageSizeOptions{[10, 25, 50]} // 渲染每页条数选择器 stickyHeader // 或传 page 使其吸附到文档滚动 fullWidth sizesm // 收紧单元格内边距配合 Card sizesm onRowClick{(row) open(row)} // 忽略行内的链接、按钮与表单控件 /完整 props定义在>declare module tanstack/react-table { interface ColumnMetaTData extends RowData, TValue { align?: left | center | right expand?: boolean } }align同时作用于表头与单元格在渲染TableHead/TableCell时分别读取columnDef.metaexpand使该列在fullWidth表格中吸收剩余宽度。因此规则是fullWidth需要恰好一个列标记meta: { expand: true }否则无法决定哪一列拉伸。2. 行点击的交互排除是一份 CSS 选择器白名单。ROW_CLICK_IGNORE_SELECTOR 覆盖了原生交互元素a、button、input、select、textarea、label、summary、带controls的音视频、contenteditable内容、tabindex非-1的元素、全部常见role...控件以及一个消费者出口给自定义后代元素加data-row-click-ignore即可阻止其激活行点击。点击处理用target.closest(ROW_CLICK_IGNORE_SELECTOR)判定且要求命中的不是行本身。键盘可达性同样内置开启onRowClick后行获得tabIndex{0}在行上按 Enter 或 Space 即触发onKeyDown 分支。这些行为有测试直接印证data-table.test.tsx 用it.each参数化验证了label、contenteditable元素、rolebutton自定义控件、data-row-click-ignore四种情况都不会触发onRowClick而点击普通文本则会带原始行数据调用回调。3. 排序是客户端行为默认开启。组件内部用useState持有SortingState通过getSortedRowModel生效可排序列的表头渲染为Button点击循环 asc → desc → off并用ArrowUp/ArrowDown/ChevronsUpDown图标指示。真实语义写在th的aria-sort上映射关系见 ARIA_SORTasc → ascending、desc → descending按钮上的aria-selected只是视觉强化。某列不需要排序时传enableSorting: false即可退出。4. 分页状态归组件所有且页大小变更会重置到第一页。源码中pagination是内部 state默认pageSize ?? 10并有一段useEffect当外部pageSizeprop 运行时变化时把状态重置为{ pageIndex: 0, pageSize }保证新页大小从一致的偏移开始应用data-table.tsx。分页器由内部组件DataTablePagination渲染左侧是start–end of total行范围摘要与可选的每页条数Select右侧是基于getPaginationRange(pageCount, pageIndex)的兄弟窗口页码按钮含省略号。一个细节值得注意当过滤后零行时组件直接只返回表格本体不渲染分页器——空表格只展示空态而不是“0–0 of 0”的分页噪音短路判断 有注释说明此意图。5. 默认空态被刻意做“无观点”。DEFAULT_EMPTY是一个模块级常量避免每次渲染重建只有Inbox图标 No results 文案不含任何应用专属文案或操作按钮需要更丰富的空态请通过emptyprop 注入data-table.tsx。选型规则文档原话整理数据是行/列形状且需要排序或分页时不要从Table原语重新拼装表格——这正是DataTable存在的意义只有完全自定义的布局才降级回Table原语。DateTimePicker带快捷范围预设的区间选择器用法与 APIimport { CUSTOM_RANGE, DateTimePicker, quickRanges } from posthog/quill-components DateTimePicker value{{ start, end, range: CUSTOM_RANGE }} onApply{(value) setRange(value)} onCancel{() close()} minDate{minDate} maxDate{new Date()} dateFormatMDY // 或 DMY | YMD compact // 单日历 水平排列的快捷范围 /props 全集见 date-time-picker.tsxvalue{ start, end, range }、onApply、onCancel?、minDate?、maxDate?、dateFormat?默认MDY、weekStartsOn?、onDateTimeSettings?、compact?默认false、ranges?默认quickRanges、showHeader?默认true、showTime?默认true、className?。快捷范围quickRanges与CUSTOM_RANGE默认预设定义在 date-time-ranges.ts共 15 个预设加 1 个自定义项从 Last 5 minutes 一直覆盖到 Last 2 yearsexport const quickRanges: DateTimeRange[] [ CUSTOM_RANGE, { id: 1, name: Last 5 minutes, rangeSetter: (d) subMinutes(d, 5) }, { id: 2, name: Last 15 minutes, rangeSetter: (d) subMinutes(d, 15) }, { id: 3, name: Last 30 minutes, rangeSetter: (d) subMinutes(d, 30) }, { id: 4, name: Last 1 hour, rangeSetter: (d) subHours(d, 1) }, { id: 5, name: Last 3 hours, rangeSetter: (d) subHours(d, 3) }, { id: 6, name: Last 6 hours, rangeSetter: (d) subHours(d, 6) }, { id: 7, name: Last 12 hours, rangeSetter: (d) subHours(d, 12) }, { id: 8, name: Last 24 hours, rangeSetter: (d) subDays(d, 1) }, { id: 9, name: Last 2 days, rangeSetter: (d) subDays(d, 2) }, { id: 10, name: Last 7 days, rangeSetter: (d) subDays(d, 7) }, { id: 11, name: Last 30 days, rangeSetter: (d) subDays(d, 30) }, { id: 12, name: Last 90 days, rangeSetter: (d) subDays(d, 90) }, { id: 13, name: Last 6 months, rangeSetter: (d) subMonths(d, 6) }, { id: 14, name: Last 1 year, rangeSetter: (d) subYears(d, 1) }, { id: 15, name: Last 2 years, rangeSetter: (d) subYears(d, 2) }, ]预设的数据结构是函数式的基于 date-fns 的sub*族理解它的两条规则DateTimeRange通过rangeSetter从当前时刻 now计算出 startend 默认为 now 本身除非显式提供endSetter——后者是给Last month这类闭合周期用的name是任意字符串id: number用于标识CUSTOM_RANGE固定id: 0。选择任何日历手动操作后组件会把内部range置为CUSTOM_RANGEhandleSelect 末行setRange(CUSTOM_RANGE)即手动选区自动脱离预设高亮。消费侧覆盖预设的方式传ranges提供自己的DateTimeRange[]其中任何CUSTOM_RANGE条目会在渲染列表前被过滤掉见 presetRanges 过滤传ranges{[]}则完全隐藏快捷范围列——适合宿主自己渲染预设列表的场景。提交模型与布局行为变更是暂存的直到onApply触发才算提交。日历点击、输入框编辑都只是内部 state 的中间态不要把中间点击当成已确认的值value/onApply是受控契约onCancel丢弃暂存态。双日历布局在 Tailwindlg断点出现除非compact强制单日历。从源码看这个断点被硬编码为 CSS media query(min-width: 64rem)组件用内部useMediaQueryhook 监听LG_QUERYtwoCalendars !compact isLargeScreen。双日历下左历显示 start 所在月、右历显示 end 所在月单历时只有一本历跟随最近变更的边缘。minDate/maxDate是天粒度的边界时间输入段不受它们约束。未显式传maxDate时默认为new Date()。weekStartsOn只影响日历网格的周起始日不影响快捷范围的计算。嵌入宿主表面的调参组合例如放在宿主自带分区的 popover 内showHeader{false}去掉 Choose date range / Quick ranges 头部色带showTime{false}切换为天粒度模式无时间分段、无 Now、页脚读数只到日期——通常再配一个去掉卡片外观的classNameshadow-none ring-0。DatePicker单日期孪生组件DatePicker是DateTimePicker的单日期版本一个日历、无快捷范围、value是普通Date而非{ start, end, range }。文档指明它服务于 PostHog 的单日期调用方当前是LemonCalendarSelect需要 start→end 区间时才选DateTimePicker。import { DatePicker } from posthog/quill-components DatePicker value{date} onApply{(next) setDate(next)} onCancel{() close()} minDate{minDate} maxDate{new Date()} dateFormatMDY // 或 DMY | YMD showTime // 初始值中包含时间显示时/分输入 showTimeToggle // 渲染 Include time 开关默认等于 showTime。false 固定精度 onIncludeTimeChange{(includeTime) ...} // 开关翻转时触发 /props 与默认值date-picker.tsxvalue: Date、onApply: (Date) void、onCancel?、minDate?、maxDate?、dateFormat?默认MDY、weekStartsOn?、onDateTimeSettings?、showTime?默认false、showTimeToggle?默认取showTime的值、onIncludeTimeChange?、className?。关键行为规则时间的开关语义showTime决定是否包含时间的初始种子showTimeToggle决定 Include time 开关是否渲染。showTimeshowTimeToggle{false}得到固定含时间精度用户不能关掉只传showTimeToggle则是初始仅日期、允许用户加上时间。onIncludeTimeChange上报开关变化供外层 wrapper 镜像状态例如更新触发器按钮上的标签文案。不含时间时应用的值向下取整到天起点。源码中的提交处理是一行onApply(includeTime ? selected : startOfDay(selected))handleApply含时间时则保留时/分。精度差异是刻意的DatePicker的时间格式到分钟为止MM/dd/yy HH:mm而DateTimePicker到秒MM/dd/yy HH:mm:ss源码注释明确写着 deliberately differs … dont unifydate-picker.tsx。与DateTimePicker共享日历网格与天粒度边界两者都渲染 calendar-grid.tsx 中的Calendar。一个实现细节分段日期输入框本身只在上方受maxDate约束、没有下界所以输入低于minDate的值会被 clamp 回minDatehandleInputChange 的注释解释了这是为了防止输入绕过声明的边界。永远单日历没有compact/ 双日历模式。useCalendarheadless 月网格状态当你需要完全自定义的日历 UI 而DateTimePicker不合适时useCalendar提供 headless 的月网格状态。它返回三层结构calendar月 周 日、视图导航viewToday、viewNextMonth、viewPreviousYear等与选择助手select、selectRange、isSelected、toggle选中日期统一归一化到午夜clearTime把时分秒毫秒清零见 use-calendar.ts。可选配置UseCalendarOptions选项默认说明weekStartsOnDay.SUNDAY周起始日Day枚举SUNDAY: 0…SATURDAY: 6viewingnew Date()当前查看的月份selected[]初始选中集合会被归一化到午夜numberOfMonths1渲染的月份数返回值的完整 APIUseCalendarReturn包括视图侧的viewing/setViewing/viewToday/viewMonth(month: Month)/viewPreviousMonth/viewNextMonth/viewYear(year)/viewPreviousYear/viewNextYear选择侧的selected/setSelected/clearSelected/isSelected/select(date, replaceExisting?)/deselect/toggle/selectRange(start, end, replaceExisting?)/deselectRange以及clearTime、inRange两个纯函数助手与calendar: Date[][][]网格本身。Month是JANUARY: 0…DECEMBER: 11的 const 对象不是数字枚举便于在年月切换 UI 中做下拉项。从源码结构看网格通过 date-fns 的eachMonthOfInterval→eachWeekOfInterval→eachDayOfInterval三级展开并在useMemo中缓存依赖viewing、weekStartsOn、numberOfMonths文件头注释标明该 hook 移植自开源项目 use-lilius。Metric跨包依赖隔离在独立子路径下的统计瓦片Metric是一个可组合的统计瓦片大数字标题、Badge变更胶囊、可选Sparkline。它是本层唯一依赖posthog/quill-charts的组件为了Sparkline与 headless 指标计算间接引入 d3。这带来一条硬性引用规则它不走主桶文件也不在posthog/quill伞包里而是必须从posthog/quill-components/metric子路径导入——index.ts 的注释 与 package.json 的 exports 映射 都印证了这一点./metric指向dist/metric.js等产物。动机是把 charts/d3 挡在“永远急切加载的应用壳图”之外只有真正导入 metric 子路径的代码才付出这份体积代价。组合式用法import { Metric, MetricHeader, MetricTitle, MetricDelta, MetricValue, MetricSubtitle, MetricSparkline, } from posthog/quill-components/metric import { Card } from posthog/quill-primitives import { useChartTheme } from posthog/quill-charts const theme useChartTheme() Card flush classNameh-40 Metric data{series} labels{labels} theme{theme} color#22d3ee sparklineFill MetricHeader MetricTitleTotal revenue/MetricTitle MetricDelta / {/* Badge按 goodDirection 取 success/destructive无 delta 时隐藏 */} /MetricHeader MetricValue classNamemt-2 / {/* 跟随 hover 的大数字传 text-* 类可改尺寸 */} MetricSubtitle classNamemt-1 / MetricSparkline / {/* 向卡片左右两侧出血Card flush 让它能触底 */} /Metric /Card各子组件均为独立导出metric.tsx 起行号 326/330/336/348/385/403 分别定义MetricHeader、MetricTitle、MetricValue、MetricDelta、MetricSubtitle、MetricSparkline根组件Metric计算一次数据/hover 行为各 part 通过 React Context 读取——part 脱离Metric使用会直接抛错useMetric在 context 为 null 时throw new Error(… must be rendered inside Metric)见 metric.tsx。布局规则Metric 是内容不是表面必须包在Card flush里。Metric只是布局/内容层像CardContent一样自带内边距边框、块级内边距与底边归 Card 管。flush去掉卡片底部内边距让MetricSparkline触底纯数字瓦片用普通Card即可。底边对齐归MetricSparkline所有它内置一个 6px 位移把 canvas 的 hover 环外沿推到卡片边缘之外、让线条恰好落在边缘上。自定义className只管边距-mx-*/-mb-*/mt-*永远不要再叠加那个偏移。需要固定高度时给 Card 设高classNameh-40或定高容器里的h-full——使用sparklineFill或想让固定高度 sparkline 钉在底部时尤其必要否则按内容尺寸生长Metric本身是h-full填满所在卡片。数据入口根组件负责数据/hover向 part 供给上下文。纯数字瓦片传value带 sparkline 传datalabelstheme。labels必须唯一它同时充当 sparkline 的 x 轴刻度键重复值会把两个点压到同一位置、导致序列反向绘制。正确做法是传原始键ISO 日期展示文本用formatLabel渲染——预先格式化成June 16这类文本在跨年区间必然重复。formatLabel覆盖副标题sparklineTooltip则通过 charts 自己的labelFormatter格式化同样的键。MetricDelta渲染BadgegoodDirection默认up决定 success 还是 destructive 语义色它自带TooltipProvider所以changeTooltip无需在应用根部做任何配置。尺寸靠className定制metric insight 场景传入更大的胶囊类并通过MetricHeader并排放在大数字旁——没有changeInline或尺寸 prop在调用点自行组合样式。兼容旧MetricCard的行为restingSubtitle静止时副标题、hoverChangeFromPreviousPointhover 时变更胶囊切换为相对前一点的变化、changeTooltip、以及positiveColor/negativeColor用户自定义胶囊色覆盖语义 Badge 变体省略颜色 props 则保留语义 Badge 变体。多序列series画多条线的 sparklinecharts 的Series[]形状每序列一条线替代单条data线。它是纯视觉的——大数字、hover 与变更胶囊仍读data所以应把瓦片真正要读的数字如每索引的合计作为data一并传入类型上禁止只传series不传data。单线便利 propscolor、sparklineDashedFromIndex不适用需在每个序列上分别设置。sparklineTooltip是直透 sparkline 的 render prop默认关闭因为 hover 已经在擦扫大数字了想额外显示 hover 提示例如多序列合计背后的逐序列明细传(ctx) DefaultTooltip {...ctx} /。刻意不提供 prop 的能力请自行组合可点击瓦片把 handler 放在包裹的Card上、页脚MetricSparkline之后的兄弟节点、标题旁的 info 图标MetricTitle内放Tooltip、加载态用Skeleton替代整个Metric。与旧组件的关系posthog/quill-charts里的MetricCard是更老、自包含prop 驱动、无原语依赖的瓦片当你想组合布局或依赖 quill 的Card/Badge时选Metric。维护约定、测试与开发工作流源文档最后的 Maintenance 一节给出该包的硬性协作规则在此新增或修改任何组件时必须在同一个 PR 中更新本参考文档并在组件旁添加一个 story。仓库现状与之吻合每个组件都成对出现*.stories.tsxdata-table.stories.tsx、date-picker.stories.tsx、date-time-picker.stories.tsx、metric.stories.tsx与测试data-table.test.tsx、date-picker.test.tsx、date-time-picker.test.tsx、metric.test.tsx。开发时不必发布 npm 包在 PostHog monorepo 根目录安装依赖后运行pnpm quill:build或pnpm quill:storybook见 Quill README 的 Development 一节。Storybook 的 Vite 配置直接source扫描packages/{primitives,components,blocks}/src/*.{ts,tsx}修改packages/quill/packages/components/src下的任何.tsx会在数百毫秒内热重载——组件层的 dist 产物只服务于从 npm 拉取的消费者。快速决策表场景选择行/列数据 排序/分页DataTable别从Table原语重拼完全自定义表格布局降级到Table原语start→end 时间区间含快捷预设DateTimePicker单一日期可含可选时间精度DatePicker自绘日历 UI 的 headless 状态useCalendar统计瓦片数字 变更胶囊 sparklineMetric从posthog/quill-components/metric子路径导入并包Card flush嵌入宿主 popover 的时间区间选择DateTimePickershowHeader{false}showTime{false}shadow-none ring-0【免费下载链接】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),仅供参考