mcpField
D
design-system

design-system

github.com/getsentry设计与 UI45.7k111 次安装今天更新
代码仓库

关于 design-system

面向 Sentry 前端开发的组件使用指南,介绍 `@sentry/scraps` 的布局、文本和信息提示原语,强调复用组件与响应式属性,而非自行编写样式组件。

主要功能

  1. 01Container、Flex、Grid 与 Stack 布局原语
  2. 02Text 与 Heading 排版原语
  3. 03InfoTip 与 InfoText 辅助说明
  4. 04支持容器查询的响应式属性
  5. 05组件抽象须先征求确认

适用场景

  1. 01构建一致的 Sentry 页面布局
  2. 02为文本和标题应用统一样式
  3. 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 tokens
  • margin、marginTop 等:SpaceSize tokens(已弃用,建议使用 gap)
  • width、height、minWidth、maxWidth、minHeight、maxHeight
  • border、borderTop、borderBottom、borderLeft、borderRight:BorderVariant tokens
  • radius:RadiusSize tokens
  • overflow、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:boolean
  • italic:boolean
  • uppercase:boolean
  • monospace:boolean
  • tabular: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 只有两个实例,而且它们彼此相邻,那么抽象带来的成本就超过了简洁性的收益。

在创建抽象之前,你必须:

  1. 向用户请求确认
  2. 说明你计划创建什么抽象
  3. 论证为什么该抽象值得增加复杂度
  4. 在继续之前等待明确批准
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" />;
}

评论

支持 Markdown