拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Ghost Shade 设计系统实战:用 Stack、Inline、Box、Grid、Container、Text 原语取代裸 flex div

Ghost Shade 设计系统实战用 Stack、Inline、Box、Grid、Container、Text 原语取代裸 flex div【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost本文围绕 Ghost 仓库中的 Agent 技能文档 shade-use-primitives/SKILL.md 展开讲清楚 Ghost 前端设计系统「Shade」中原语primitives层的定位与用法何时用哪个原语、语义化间距刻度如何映射到 Tailwind 类名、以及className与 prop API 的边界。读完本文你能在tryghost/shade/primitives之上写出以「意图」而非「类名字符串」表达的布局代码并能对照 源码实现 验证每个 prop 的实际行为。规则起点什么样的 div 是「原语调用点」SKILL 文档给出了一条明确的判定标准一个div如果它的 className 只由flex / grid / gap / items-* / justify-*这类布局工具类组成那么它就是原语的调用点应当替换为对应的 Shade 原语。这样做的目标是让布局代码「按意图阅读read by intent而不是按类名字符串阅读not by class string」。正确的导入方式是import {Stack, Inline, Box, Grid, Container, Text} from tryghost/shade/primitives;这条规则并不是静态文档而是一条会自动触发的 Agent 技能。SKILL.md 的 frontmatter 声明了触发条件name: Shade use primitives description: Replace bare divs that only carry flex/grid/gap utilities with Shade primitives (Stack, Inline, Box, Grid, Container, Text). Use semantic gapmd instead of gap-4. Trigger when editing TSX in Shade-consuming apps. autoTrigger: - fileEdit: apps/{shade,admin,admin-x-framework,activitypub}/**/*.tsx也就是说凡是编辑apps/shade、apps/admin、apps/admin-x-framework、apps/activitypub这四个消费方应用下的.tsx文件时该技能都会被触发。这也界定了原语层的适用边界它是 Ghost 管理端与联邦ActivityPub应用共享的一套布局语言。如何选择原语六个组件对照表SKILL 文档给出了核心选择表下面完整保留并补充源码中可确认的默认值你需要使用底层元素源码默认值可从实现确认纵向排列子元素Stackflex-colgapmd、alignstretch、justifystart横向排列子元素Inlineflex-row可选wrap、asgapmd、aligncenter、justifystart、wrapfalse为内容加内边距 / 圆角Box带padding、radiusprops 的 div全部 props 可选按需输出对应类二维布局Griddisplay: gridcolumns1、gapmd、alignstretch、justifystart宽度受约束的外壳Containermax-width wrappersizepage、centeredtrue带尺寸 / 色调 / 字重的文本Textas、size、tone、weightasp、sizemd、weightregular、toneprimary、leadingbody各默认值均可在源码中逐一对应Stack 的签名是gap md, align stretch, justify start渲染固定为flex flex-col加三张映射表查出的类名Inline 额外支持as取值为div | header | section | footer | nav | span与wrap映射到flex-wrap/flex-nowrap注意它默认aligncenter与 Stack 不同Grid 的columns只接受1 | 2 | 3 | 4 | 5 | 6 | 12这几种档位Container 的size支持xs到9xl共 12 个 Tailwind max-width 档位外加prose、page、page-with-sidebar三个语义档位centered控制是否加mx-autopaddingX独立于垂直方向控制横向留白Text 的as覆盖p | span | div | label | small | strong | em | h1–h6size从2xs到3xlweight为regular | medium | semibold | boldtone为primary | secondary | tertiary对应text-text-primary等语义色类还支持leading与truncate。另外primitives入口并非只有这六个组件。入口文件 中还有注释为「Legacy layout/structure compatibility exports」的兼容导出ErrorPage与components/layout/heading的全部导出。从源码结构看可以推断H1–H4等标题组件就是通过该兼容层从tryghost/shade/primitives导出的——activitypub 的组件 中就有import { H1 } from tryghost/shade/primitives这样的实际用法而 feed-item 则直接使用了H4与Text。语义化间距刻度gapmd 而不是 gap-4SKILL 文档要求「用语义刻度不要用裸数字」完整映射表如下刻度对应 Tailwind适用场景nonegap-0贴边flushxsgap-1行内图标与文字的间距smgap-2密集列表、徽标mdgap-3行 / 列默认间距lggap-5区块内间距xlgap-6主要大块之间2xlgap-8页面级节奏同一套刻度同样适用于Box的 paddingpaddingmd→p-3。这份表不是文档层面的口头约定而是在 types.ts 中落地的两张查找表export const GAP_CLASSES: RecordSpaceStep, string { none: gap-0, xs: gap-1, sm: gap-2, md: gap-3, lg: gap-5, xl: gap-6, 2xl: gap-8, }; export const PADDING_CLASSES: RecordSpaceStep, string { none: p-0, xs: p-1, sm: p-2, md: p-3, lg: p-5, xl: p-6, 2xl: p-8, };其中SpaceStep类型正是none | xs | sm | md | lg | xl | 2xl。同文件还定义了PADDING_X_CLASSESpx-*、PADDING_Y_CLASSESpy-*分别支撑Box的paddingX/paddingY以及Container的paddingXALIGN_ITEMS_CLASSES与JUSTIFY_CONTENT_CLASSES则统一了Stack、Inline、Grid三个布局原语的align/justify取值——Align为start | center | end | stretch | baselineJustify为start | center | end | between | around | evenly。值得注意的一个细节刻度并非线性递增lg跳到gap-5、2xl到gap-8这是刻意设计出的「节奏感」语义刻度的价值正在于此——所有消费方共享同一份间距语义而不是各自随手写gap-4。正确与错误的写法SKILL 文档给出的对照示例完整保留如下。正确写法Box paddinglg radiusmd classNameborder border-border-default Inline aligncenter gapmd justifybetween Stack gapxs Text weightsemiboldEmail notifications/Text Text sizesm tonesecondaryGet notified about engagement./Text /Stack Switch / /Inline /Box错误写法// BAD — bare div doing flex layout div classNameflex flex-col gap-2 div classNamefont-semiboldTitle/div div classNametext-sm text-gray-600Hint/div /div // BAD — raw gap-4 instead of gapmd Stack classNamegap-4两个反例分别踩中了文档定义的两类错误第一类是完全绕过原语、用裸 div 拼 flex 布局同时text-gray-600这类裸灰度色也不符合语义 tone 的约定对照 Text 的色调实现 应使用tonesecondary第二类是原语本身用了却用classNamegap-4覆盖了语义间距违背了gapmd的意图表达。什么时候不要动用原语SKILL 文档明确列出了三种「不该伸手拿原语」的场景该形态已有现成模式组件。例如页面头部应该用PageHeader、列表页应该用ListPage而不是自己用ContainerStack从零拼。仓库中确有 patterns 层承载这类组合tryghost/shade也通过 package.json 的./patterns子路径单独导出与./primitives是平行的两个消费入口。包装层开始感知 Ghost 业务数据。此时它已经是 Pattern模式而非原语组合——原语层刻意不 import 任何数据保持与业务解耦。你正在 Shade 原语自身的实现内部。原语的实现代码如 stack.tsx本身就是直接写flex flex-col这类基础类名规则不适用于自己。className 依然可用靠 cn() 合并SKILL 文档对className的立场是「可用但有边界」原语会转发className并用cn()合并适合处理 prop API 覆盖不到的一次性需求例如classNameborder border-border-default但不要用className去设置flex、gap、align、justify——那是 props 的职责。这个合并行为在源码中一目了然。以 Stack 的实现 为例const Stack React.forwardRefHTMLDivElement, StackProps(function Stack( { className, gap md, align stretch, justify start, ...props }: StackProps, ref, ) { return ( div ref{ref} className{cn( flex flex-col, GAP_CLASSES[gap], ALIGN_ITEMS_CLASSES[align], JUSTIFY_CONTENT_CLASSES[justify], className, )} {...props} / ); });其中cn定义在 ds-utils.tsexport function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }即clsx拼接 tailwind-merge去冲突。由于className排在参数列表末尾twMerge会让传入的外部类在冲突时覆盖内置类——这也解释了为什么classNamegap-4能盖掉gapmd以及为什么文档禁止这么做它「能覆盖」恰恰说明你在用类名对抗 prop API 的语义约定。消费方式子路径导出与真实用例tryghost/shade在 package.json 中声明了./primitives子路径导出指向构建产物es/primitives.js因此各应用通过tryghost/shade/primitives深路径引入原语而不是从包根混引。仓库内已有实际消费案例可以参照header.tsximport { H1 } from tryghost/shade/primitivesfeed-item.tsximport { H4, Text } from tryghost/shade/primitives各原语均附带 Storybook 故事文件如 box.stories.tsx、stack.stories.tsx配合 AGENTS.md 可在apps/shade内本地预览原语效果。原语的汇总出口见 primitives/index.ts导出Box、Container含ContainerSize类型、Grid、Inline、Stack、Text含TextElement、TextSize、TextTone、TextWeight、TextLeading类型以及Align、Justify、SpaceStep三个共享类型。小结Shade 原语层的实践规则可以浓缩为四句裸布局 div 一律升级className 只有 flex/grid/gap/align/justify 类名的 div替换为Stack/Inline/Grid/Box/Container/Text间距只说语义gapmd而非gap-4同一刻度贯穿 gap 与 padding模式优先于原语已有PageHeader、ListPage等 Pattern或组件开始感知业务数据时不要用原语硬拼className 只做增量留给 prop API 覆盖不到的一次性样式冲突合并由cn()clsx tailwind-merge保证。按这四条落地Ghost 各前端的布局代码就能保持「意图可读」并在types.ts这一份共享刻度表上对齐全部间距语义。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门