关于 design-system
面向 Sentry 前端开发的组件使用指南,介绍 `@sentry/scraps` 的布局、文本和信息提示原语,强调复用组件与响应式属性,而非自行编写样式组件。
主要功能
- 01Container、Flex、Grid 与 Stack 布局原语
- 02Text 与 Heading 排版原语
- 03InfoTip 与 InfoText 辅助说明
- 04支持容器查询的响应式属性
- 05组件抽象须先征求确认
适用场景
- 01构建一致的 Sentry 页面布局
- 02为文本和标题应用统一样式
- 03添加可访问的提示与术语说明
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ getsentry/ sentry --skill design-system安装后在 Claude Code 中输入/design-system确认已加载。
详细介绍
Sentry 中的布局和文本原语
核心原则
始终使用 @sentry/scraps 中的核心组件,而不是用 Emotion 创建样式化组件。
核心组件在整个代码库中提供一致的样式、响应式设计和更好的可维护性。
组件实现参考
有关受支持 props 及其类型的完整列表,请参阅实现文件:
- 布局组件:
/static/app/components/core/layout/container.tsx- 包含所有布局 props 的基础容器flex.tsx- Flex 布局原语grid.tsx- Grid 布局原语stack.tsx- Stack 布局原语(默认方向为 column 的 Flex)
- 排版组件:
/static/app/components/core/text/text.tsx- 文本原语heading.tsx- 标题原语
布局原语
重要:
Flex、Grid和Stack都继承自Container。这意味着 Container 上可用的每个 prop 在 Flex、Grid 和 Stack 上也可用。当你使用<Flex>时,你将获得所有 Container props(position、padding、border、overflow 等)以及 Flex 特有的 props。Grid 和 Stack 也是如此。
Container
支持所有常见布局属性的基础布局组件。Flex、Grid 和 Stack 继承自 Container,继承其所有 props。
关键 Props(完整列表见 container.tsx):
position:"static" | "relative" | "absolute" | "fixed" | "sticky"padding、paddingTop、paddingBottom、paddingLeft、paddingRight:SpaceSize tokensmargin、marginTop等:SpaceSize tokens(已弃用,建议使用 gap)width、height、minWidth、maxWidth、minHeight、maxHeightborder、borderTop、borderBottom、borderLeft、borderRight:BorderVariant tokensradius:RadiusSize tokensoverflow、overflowX、overflowY:"visible" | "hidden" | "scroll" | "auto"background:SurfaceVariant("primary" | "secondary" | "tertiary")display:各种 display 类型- Flex item props:
flex、flexGrow、flexShrink、flexBasis、alignSelf、order - Grid item props:
area、row、column
import {Container} from '@sentry/scraps/layout';
// ❌ Don't create styled components
const Component = styled('div')`
padding: ${p => p.theme.space.md};
border: 1px solid ${p => p.theme.tokens.border.primary};
`;
// ✅ Use Container primitive
<Container padding="md" border="primary">
Content
</Container>;
Flex
使用 <Flex> 进行 Flex 布局。继承自 Container,继承所有 Container props 以及 Flex 特有的 props。
Flex 特有的 Props(完整列表见 flex.tsx):
direction:"row" | "row-reverse" | "column" | "column-reverse"align:"start" | "end" | "center" | "baseline" | "stretch"justify:"start" | "end" | "center" | "between" | "around" | "evenly" | "left" | "right"gap:SpaceSize 或"${SpaceSize} ${SpaceSize}",用于行/列间距wrap:"nowrap" | "wrap" | "wrap-reverse"display:"flex" | "inline-flex" | "none"
外加所有 Container props:position、padding、margin、width、height、border、radius、overflow、background、flex/grid item props 等(见上文 Container 部分)。
import {Flex} from '@sentry/scraps/layout';
// ❌ Don't create styled components
const Component = styled('div')`
display: flex;
flex-direction: column;
position: relative;
`;
// ✅ Use Flex primitive with props
<Flex direction="column" position="relative" gap="md">
<Child1 />
<Child2 />
</Flex>;
Grid
使用 <Grid> 进行 Grid 布局。继承自 Container,继承所有 Container props 以及 Grid 特有的 props。
Grid 特有的 Props(完整列表见 grid.tsx):
columns:Grid 模板列(数字或 CSS 值)rows:Grid 模板行areas:命名的 grid 区域gap:SpaceSize 或"${SpaceSize} ${SpaceSize}",用于行/列间距align:"start" | "end" | "center" | "baseline" | "stretch"(align-items)alignContent:"start" | "end" | "center" | "between" | "around" | "evenly" | "stretch"justify:"start" | "end" | "center" | "between" | "around" | "evenly" | "stretch"(justify-content)justifyItems:"start" | "end" | "center" | "stretch"flow:"row" | "column" | "row dense" | "column dense"autoColumns、autoRows:自动生成的轨道大小
外加所有 Container props:position、padding、margin、width、height、border、radius、overflow、background、flex/grid item props 等(见上文 Container 部分)。
import {Grid} from '@sentry/scraps/layout';
// ❌ Don't create styled components
const Component = styled('div')`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: ${p => p.theme.space.md};
`;
// ✅ Use Grid primitive
<Grid columns="repeat(3, 1fr)" gap="md">
<Item1 />
<Item2 />
<Item3 />
</Grid>;
Stack
使用 <Stack> 实现垂直布局。Stack 本质上就是默认 direction="column" 的 Flex。它还提供 Stack.Separator,用于在项目之间添加分隔符。
Props(完整列表见 stack.tsx):
- 与 Flex props 相同(继承所有 Flex 和 Container props)
direction默认为 "column"(但可以覆盖)Stack.Separator组件用于在 stack 项目之间添加分隔线
import {Stack} from '@sentry/scraps/layout';
// ❌ Don't create styled components for vertical layouts
const Component = styled('div')`
display: flex;
flex-direction: column;
gap: ${p => p.theme.space.md};
`;
// ✅ Use Stack primitive (automatically column direction)
<Stack gap="md">
<Item1 />
<Item2 />
<Item3 />
</Stack>;
// ✅ With separators between items
<Stack gap="md">
<Item1 />
<Stack.Separator />
<Item2 />
<Stack.Separator />
<Item3 />
</Stack>;
// ✅ Stack supports all Flex and Container props
<Stack gap="md" padding="lg" position="relative" border="primary">
<Item1 />
<Item2 />
</Stack>;
排版基础组件
Text
使用 <Text> 承载所有文本内容。切勿使用带文本样式的原生 <p>、<span> 或 <div> 元素。
关键 Props(完整列表见 text.tsx):
as:"span" | "p" | "label" | "div"(语义化 HTML 元素)size:TextSize("xs" | "sm" | "md" | "lg" | "xl" | "2xl")variant:ContentVariant | "muted"(见下方内容变体 Token)align:"left" | "center" | "right" | "justify"bold:booleanitalic:booleanuppercase:booleanmonospace:booleantabular:boolean(等宽数字)ellipsis:boolean(以省略号截断)wrap:"nowrap" | "normal" | "pre" | "pre-line" | "pre-wrap"textWrap:"wrap" | "nowrap" | "balance" | "pretty" | "stable"wordBreak:"normal" | "break-all" | "keep-all" | "break-word"density:"compressed" | "comfortable"(行高)underline:boolean | "dotted"strikethrough:boolean
import {Text} from '@sentry/scraps/text';
// ❌ Don't create styled text components
const Label = styled('span')`
color: ${p => p.theme.tokens.content.secondary};
font-size: ${p => p.theme.font.size.sm};
`;
// ❌ Don't use raw elements
<p>This is a paragraph</p>
<span>Status: Active</span>
// ✅ Use Text primitive with semantic 'as' prop
<Text as="p" variant="muted" density="comfortable">
This is a paragraph
</Text>
<Text as="span" bold uppercase>
Status: Active
</Text>
Heading
使用 <Heading> 承载所有标题。切勿使用原生 <h1>、<h2> 等元素。
关键 Props(完整列表见 heading.tsx):
as:"h1" | "h2" | "h3" | "h4" | "h5" | "h6"(必填)size:HeadingSize("xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl")variant:与 Text 相同align:与 Text 相同italic、monospace、tabular:与 Text 相同ellipsis、wrap、textWrap、wordBreak:与 Text 相同density:与 Text 相同underline、strikethrough:与 Text 相同
注意:Heading 不支持 bold 和 uppercase(标题始终为粗体)。
import {Heading} from '@sentry/scraps/text';
// ❌ Don't style heading elements
const Title = styled('h2')`
font-size: ${p => p.theme.font.size.md};
font-weight: bold;
`;
// ❌ Don't use raw heading elements
<h2>My Title</h2>
// ✅ Use Heading primitive with semantic 'as' prop
<Heading as="h2">My Title</Heading>
// ✅ With custom size
<Heading as="h3" size="xl">Large H3</Heading>
信息组件
重要:始终优先使用
InfoTip和InfoText,而不是原生<Tooltip>组件。它们为上下文帮助提供一致且可访问的模式。
InfoTip
使用 <InfoTip> 在标签或标题旁添加带 tooltip 的信息图标。它支持键盘访问,并为补充帮助提供一致的模式。
import {InfoTip} from '@sentry/scraps/info';
import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
// ❌ Don't use Tooltip with arbitrary icons
<Flex gap="xs" align="center">
<Text>Retention Period</Text>
<Tooltip title="The number of days...">
<IconInfo size="xs" />
</Tooltip>
</Flex>
// ✅ Use InfoTip for contextual help icons
<Flex gap="xs" align="center">
<Text>Retention Period</Text>
<InfoTip title="The number of days event data is stored before being automatically deleted." />
</Flex>
关键 Props:
title:Tooltip 内容(必填)size:"xs" | "sm"(默认)| "md"
使用场景:
- 为标题或章节标题添加上下文
- 在不添加行内文本的情况下显示补充信息
- 解释设置或配置选项
InfoText
使用 <InfoText> 呈现带 tooltip 的行内文本。它会渲染带虚线下划线的文本,在悬停/聚焦时显示 tooltip。
import {InfoText} from '@sentry/scraps/info';
// ❌ Don't wrap text with raw Tooltip
<Tooltip title="Time to First Byte measures the time...">
<span style={{textDecoration: 'underline dotted'}}>TTFB</span>
</Tooltip>
// ✅ Use InfoText for inline explanations
<InfoText title="Time to First Byte measures the time from the request start until the first byte of the response is received.">
TTFB
</InfoText>
关键 Props:
title:Tooltip 内容(必填)- 继承
Text,因此支持所有 Text props:size、variant、bold等。
// With Text styling props
<InfoText title="Small muted text" size="sm" variant="muted">
Hint text
</InfoText>
<InfoText title="Bold text" bold>
Important term
</InfoText>
使用场景:
- 在行内定义技术术语或缩写
- 在不增加视觉杂乱的情况下提供额外上下文
- 创建一致且可访问的行内帮助模式
创建轻量抽象
⚠️ 关键:当意图是通过 DRY(Don't Repeat Yourself,不要重复自己)消除重复的 props 时,在基于布局原语(
Container、Flex、Grid、Stack、Text、Heading)创建抽象之前,务必先向用户请求确认。
你可以基于原语创建轻量抽象,目的是通过使用有意义的名称(例如 TableCell 与通用的 Flex)来改进语义结构,并提供一些默认 props。非常重要的是,你要谨慎地这样做,只有当它对可读性有净收益时才这样做。例如,如果重复的 props 只有两个实例,而且它们彼此相邻,那么抽象带来的成本就超过了简洁性的收益。
在创建抽象之前,你必须:
- 向用户请求确认
- 说明你计划创建什么抽象
- 论证为什么该抽象值得增加复杂度
- 在继续之前等待明确批准
import {Flex, type FlexProps} from '@sentry/scraps/layout';
// ❌ Don't repeat the same props everywhere
<Flex align="center" gap="xs" flex="1" padding="sm">Content 1</Flex>
<Flex align="center" gap="xs" flex="1" padding="sm">Content 2</Flex>
<Flex align="center" gap="xs" flex="1" padding="sm">Content 3</Flex>
<Flex align="center" gap="xs" flex="1" padding="sm">Content 4</Flex>
// ✅ Create a thin wrapper with default props (AFTER USER CONFIRMATION)
function TableCell(props: FlexProps) {
return <Flex align="center" gap="md" {...props} />;
}
<TableCell>Content 1</TableCell>
<TableCell>Content 2</TableCell>
<TableCell align="start">Content 3</TableCell>{/* Can override defaults */}
关键要点:
- 在创建抽象之前,务必先向用户请求确认
- 扩展原语的 props 类型(
extends FlexProps) - 在 JSX 组件上设置默认值,并展开
{...props}以允许覆盖 - 不要使用 styled components —— 而应组合原语
通用指南
1. 使用响应式 Props
大多数 props 支持使用断点键的响应式语法。
// ❌ Don't use styled media queries
const Component = styled('div')`
display: flex;
flex-direction: column;
@media screen and (min-width: ${p => p.theme.breakpoints.md}) {
flex-direction: row;
}
`;
// ✅ Use responsive prop signature. `screen:` keys match the viewport, like the
// @media query above.
<Flex direction={{'screen:xs': 'column', 'screen:md': 'row'}}>
响应式 prop 键有两种形式,并且可以在同一个 prop 上组合使用:
- 裸键(
{xs: …})会针对最近的查询容器解析 —— 容器查询是默认方式,因此不需要前缀。 screen:前缀键({'screen:md': …})会针对 视口解析。
要响应组件的可用空间,请使用 containerType="inline-size" 将父级声明为查询容器,并在子级上使用裸键(元素永远无法查询自身大小,因此容器必须是祖先元素)。
// ✅ Reflow based on the parent's width, not the viewport's
<Container containerType="inline-size">
<Flex direction={{'2xs': 'column', md: 'row'}}>{/* ... */}</Flex>
</Container>
当你需要在 JS 中获取解析后的断点时,请使用 useContainerBreakpoint(ref)(这是基于 ResizeObserver、作用域为容器的替代方案,用于替代基于宽度的 useMedia)。
2. 优先使用 Gap/Padding 而不是 Margin
Container 支持 margin props,但它们已弃用。请改为在父容器上使用 gap。
// ❌ Don't use margin between children
const Child = styled('div')`
margin-right: ${p => p.theme.space.lg};
`;
// ✅ Use gap on parent container
<Flex gap="lg">
<Child1 />
<Child2 />
</Flex>;
3. 将布局与排版分离
不要在单个 styled component 中耦合布局和排版。请使用独立的原语。
// ❌ Don't couple layout and typography
const Component = styled('div')`
display: flex;
flex-direction: column;
color: ${p => p.theme.tokens.content.secondary};
font-size: ${p => p.theme.font.size.lg};
`;
// ✅ Split into layout and typography primitives
<Flex direction="column">
<Text variant="muted" size="lg">
Content
</Text>
</Flex>;
4. 查看实现文件以了解所有 Props
实现文件包含完整、最新的受支持 props 列表及其 TypeScript 类型。如有疑问:
- 阅读
/static/app/components/core/layout/container.tsx了解基础布局 props - 阅读
/static/app/components/core/layout/flex.tsx了解 Flex 专属 props - 阅读
/static/app/components/core/layout/grid.tsx了解 Grid 专属 props - 阅读
/static/app/components/core/layout/stack.tsx了解 Stack 专属 props - 阅读
/static/app/components/core/text/text.tsx了解 Text props - 阅读
/static/app/components/core/text/heading.tsx了解 Heading props
Token 参考
间距 Token(SpaceSize)
将它们用于 gap、padding:
"0"、"2xs"、"xs"、"sm"、"md"、"lg"、"xl"、"2xl"、"3xl"- 多个值:
"md lg"(垂直 水平) - 响应式:
{{xs: "sm", md: "lg"}}
边框 Token(BorderVariant)
将它们用于 border prop:
"primary"、"muted"、"accent"、"danger"、"promotion"、"success"、"warning"
圆角 Token(RadiusSize)
将它们用于 radius prop:
"0"、"2xs"、"xs"、"sm"、"md"、"lg"、"xl"、"2xl"、"full"
文本尺寸 Token
- TextSize:"xs" | "sm" | "md" | "lg" | "xl" | "2xl"
- HeadingSize:"xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"
表面变体 Token(SurfaceVariant)
将它们用于布局组件上的 background prop:
"primary"、"secondary"、"tertiary"
ContentVariant(内容变体)
在 Text 和 Heading 的 variant 属性中使用以下值:
- ContentVariant: "primary" | "secondary" | "accent" | "danger" | "promotion" | "success" | "warning"
- 以及 "muted":除 ContentVariant 的值外,Text 和 Heading 也接受 "muted"
快速参考清单
创建样式化组件前,先问问自己:
- ✅ 能否使用
<Flex>、<Grid>或<Stack>来实现布局? - ✅ 能否使用
<Stack>,利用其默认的列方向来实现垂直布局? - ✅ 能否使用
<Container>来设置边框、内边距或定位? - ✅ 能否使用
<Text>或<Heading>来设置排版? - ✅ 能否使用
<InfoTip>或<InfoText>代替<Tooltip>? - ✅ 能否使用响应式属性代替媒体查询?
- ✅ 能否使用
gap代替外边距? - ✅ 这个基础组件是否支持我需要的属性?(请检查实现文件)
如果有任何一项的答案是肯定的,请改用基础组件。
其他核心组件
菜单和选择器
操作菜单使用 DropdownMenu,选项使用 CompactSelect。在表单中,使用表单系统的 field.Select 或 field.CompactSelect。来自 @sentry/scraps/menu 的 Menu 是内部展示基础组件;仅在实现 DropdownMenu 和 CompactSelect 等共享控件时直接使用。
头像
使用 static/app/components/core/avatar 中的核心头像组件(<UserAvatar/>、<TeamAvatar/>、<ProjectAvatar/>、<OrganizationAvatar/>、<SentryAppAvatar/>、<DocIntegrationAvatar/>)。头像列表使用 <AvatarList>。
// ✅ Use Avatar component and useUser
import {UserAvatar} from '@sentry/scraps/avatar';
import {useUser} from 'sentry/utils/useUser';
<UserAvatar user={user}>
// ❌ Do not use raw intrinsic elements or static paths
function Component() {
return (
<img
src="/path/to/image.jpg"
style={{
border,
width: 20,
height: 20,
borderRadius: '50%',
objectFit: 'cover',
display: 'inline-block',
}}
/>
);
}
折叠面板
使用核心折叠面板组件,不要自行构建。
// ✅ Use Disclosure component
<Disclosure>
<Disclosure.Title>Title</Disclosure.Title>
<Disclosure.Content>Content that is toggled based on expanded state</Disclosure.Content>
</Disclosure>;
// ❌ Do not reimplement disclosure pattern manually
function Component() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div>
<Button
onClick={() => setIsExpanded(!isExpanded)}
icon={<IconChevron direction={isExpanded ? 'down' : 'right'} />}
>
Title
</Button>
{isExpanded && (
<Container>Content that is toggled based on expanded state</Container>
)}
</div>
);
}
图片和图标
将基于 SvgIcon 的图标放在 static/packages/icons/src 中,并从明确的 @sentry/icons 子路径导入。将应用资产封装组件放在 static/app/icons 中。图标包不得依赖 scraps 或应用代码。切勿内联 SVG。使用 svgo 或 svgomg 优化 SVG。
// ❌ Never inline SVGs
function Component(){
return (
<Button icon={
<svg viewbox="0 0 16 16>"}>
// ❌ paths have excessive precision, optimize them with SVGO
<circle cx="8.00134" cy="8.4314" r="5.751412" />
<circle cx="8.00134" cy="8.4314" r="12.751412" />
<line x1="8.41334" y1="5.255361" x2="8" y2="8.255421" />
</svg>
</Button>
)
}
// ❌ Never place SVGs outside of icons folder.
import {CustomIcon} from "./customIcon"
// ✅ Import icon from our icon set
import {IconExclamation} from "@sentry/icons/exclamation"
所有图片都应放在 static/app/images 中,并且必须通过 webpack 加载器(sentry-images 别名)导入,切勿通过静态路径引用。
// ✅ Images are imported from sentry-images alias
import image from 'sentry-images/example.png';
function Component() {
return <Image src={image} />;
}
// ❌ All images need to be imported using the webpack loader!
function Component() {
return <Image src="/path/to/image.png" />;
}