关于 frontend-style-guide
为在 Lightdash 前端代码中开发组件的 Agent 提供 Mantine 8 样式指南,强调主题令牌、共享组件以及明暗模式的一致设计。
主要功能
- 01Mantine 8 主题与组件规范
- 02颜色、排版和间距令牌
- 03CSS Modules 使用规则
- 04明暗模式设计检查
- 05组件状态与无障碍规范
适用场景
- 01创建或更新前端组件
- 02统一界面样式与主题令牌
- 03检查明暗模式和按钮层级
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lightdash/ lightdash --skill frontend-style-guide安装后在 Claude Code 中输入/frontend-style-guide确认已加载。
详细介绍
Lightdash 前端样式指南
在处理 packages/frontend/ 中的任何前端组件时,请遵循以下规则。
Mantine 8
应用使用 Mantine 8(@mantine/core),主题位于 src/theme/。
设计原则
主题中性、简洁,风格接近 shadcn、Radix 和 Kumo:一种墨色强调色、扁平表面、柔和边框、紧凑的字号比例。大多数时候,使用默认值就能带来「恰到好处」的观感。添加颜色、阴影或尺寸前,请先逐条检查以下规则。
- 只用一种强调色,而且是墨色。 表面上的主要操作使用主题主色(浅色模式下接近黑色,深色模式下接近白色)。切勿为了强调而将 Button 或 ActionIcon 设为
blue、dark、indigo或ldDark。控件的颜色用于表示状态:red表示危险操作,yellow表示警告,teal表示「已复制」,orange表示收藏,green表示已验证状态(勾选图标、徽章)以及现有的合并操作,绝不能用于填充验证按钮。靛蓝色用于 AI 表面。链接是唯一使用蓝色文字的元素。 - 层级由变体而非颜色体现。
filled是每张卡片、每个页眉或模态框页脚中的唯一主要操作。default(带边框)是次要操作。light用于第三级或类似切换的操作(「添加筛选器」)。subtle用于图标按钮和行内操作;它是 ActionIcon 的默认值。如果一个表面上有两个filled按钮,其中一个就不合适。 - 表面保持扁平。 Paper 和 Card 默认带边框、无阴影、圆角为 12px;阴影只用于浮层(Menu 和 Popover 使用
md,Modal 使用lg)。不要再传入withBorder、shadow或radius重复声明这些默认值。空白或占位内容区域使用<Paper variant="dotted">。 - 中性色使用令牌,不要手动配对。 使用
--mantine-color-body(表面)、--ld-color-page(画布)、default-border、default-hover、text、dimmed、placeholder。ldGray.N已根据配色方案解析(0 为画布,1 为弱化填充,2 为边框,3 为醒目边框,5 为第三级文本,6 = 弱化文本,7 为标签,9 为文本),所以light-dark(ldGray-x, ldDark-y)和用于中性色的@mixin dark代码块都是不良信号。次要文本使用c="dimmed"。 - 字号遵循比例,不是滑块。 正文为 14/20。标题使用
Title的 1 到 6 级,字重为 600(28 到 14px);标签和表格标题字重为 500;其他所有内容字重为 400。使用fz="xs|sm|md",不要使用fz={13}。卡片和区域标题使用Title order={5};页面顶部使用PageHeader。 - 间距遵循令牌网格。 卡片内边距使用
md,组间距使用xs/sm,区域间距使用lg,页面边距使用lg/xl。不要使用像素边距;如果布局需要margin-top: 20px,就使用mt="lg"。 - 图标保持低调。
MantineIcon的描边宽度为 1.5,文本旁的图标为 16px,xs控件中的图标为 14px;作为次要元素时使用dimmed着色。每个纯图标按钮都必须有aria-label和 Tooltip。 - 输入控件保持平和。 使用默认变体和柔和边框,聚焦时边框颜色加深,不显示环形效果。
size="xs"控件会自动带有紧凑的次要标签。Select 使用勾选标记显示选中的选项;筛选值选择器使用标准组合框,不要使用自定义列表。 - 动效服务于功能。 下拉菜单从锚点弹出,模态框淡入淡出,两者都使用主题中的设置。除此之外,除非用于显示进度,否则不要添加动画。
- 深色模式需要专门设计,而非从浅色模式推导。 完成前请检查两种配色方案。切勿读取
useMantineColorScheme来选择颜色;请使用令牌。编辑器使用useEditorTheme();只有没有 CSS 的 JS 使用场景(ECharts、Leaflet)才读取useComputedColorScheme。 - 状态使用共享组件。 加载时使用
EmptyStateLoader,区域加载失败时使用InlineErrorState,页面加载失败时使用SuboptimalState,表示「此处为空」时使用<Paper variant="dotted">。 - 编写 CSS 前,先检查是否已有现成方案: 变体、令牌、
ld-*工具类或共享控件(CopyActionIcon、FavoriteActionIcon、ConfirmDeleteButton、TruncatedText、FilterFacet、NumberInput、MantineModal)。 **交接页面前的自查:**分别在浅色和深色模式下打开页面;统计每个区域的实心按钮数量(最多一个);检查是否有非链接的蓝色元素;检查卡片是否带有阴影;检查是否有未使用主题令牌的字体大小或灰色;检查图标按钮是否缺少工具提示。如有疑问,请以 Explorer 页面为参考。
组件检查清单
创建或更新组件时:
- 使用
@mantine/core导入 - 不使用
style或styles属性 - 查看 Mantine 文档和类型,确认可用的组件属性
- 尽可能使用内联样式组件属性进行样式设置(并遵循最多 3 个属性的规则)
- 当组件属性不可用,或需要超过 3 个内联样式属性时,使用 CSS modules
- 错误消息中不得用“Lightdash”指代“此应用”,可通过嵌入访问的组件标签中也不得如此(参见
docs/white-label-copy.md) - 嵌入内容中不得出现 Lightdash 品牌元素或文档链接:使用
useIsEmbedded()控制这些内容(参见packages/frontend/AGENTS.md中的“嵌入内容应采用白标形式”) - 使用主题值('md'、'lg'、'xl' 或 'ldGray.1'、'ldGray.2'、'ldDark.1'、'ldDark.2' 等),而非硬编码数值
- 在 CSS modules 中使用 Mantine 颜色时,始终使用了解主题的变量:
--mantine-color-${color}-text:用于实心背景上的文本--mantine-color-${color}-filled:用于实心背景(强色)--mantine-color-${color}-filled-hover:用于悬停时的实心背景--mantine-color-${color}-light:用于浅色背景--mantine-color-${color}-light-hover:用于悬停时的浅色背景(浅色)--mantine-color-${color}-light-color:用于浅色背景上的文本--mantine-color-${color}-outline:用于轮廓--mantine-color-${color}-outline-hover:用于悬停时的轮廓
样式最佳实践
核心原则:优先使用主题
**目标是尽可能使用主题默认值。**样式覆盖应当是例外,而非惯例。
样式层级
- **最佳:**不使用自定义样式(使用主题默认值和变体)
- **扩展主题:**对于重复使用的样式,在
src/theme/components/<Component>.module.css中添加变体或规则(并在src/theme/components/index.ts中注册) - **组件属性:**简单覆盖(1–3 个属性,例如
mt="xl" w={240}) - **CSS modules:**复杂样式、超过 3 个属性,或 Mantine 没有对应属性的单条规则(
align-self、overflow、cursor、white-space);在该文件的模块中使用以角色命名的类。flex-shrink/flex-grow属于flex样式属性。不要使用全局工具类。
绝不要使用
styles属性(始终改用 CSS modules)style属性(内联样式)
主题扩展(用于重复模式)
如果发现自己多次应用相同的样式覆盖,请将其放入主题中。每个组件在 src/theme/components/ 中都有一个 CSS module 文件,并在 src/theme/components/index.ts 中有一个条目:
/* src/theme/components/Badge.module.css */
.root[data-variant='light'] {
text-transform: none;
font-weight: 500;
}
// src/theme/components/index.ts
Badge: Badge.extend({
defaultProps: { variant: 'light', color: 'gray' },
classNames: badgeClasses,
}),
只有在 Mantine 将值以内联方式写入时(按钮和徽章颜色、NavLink 填充、输入框字体大小),才使用 vars 回调,因为 CSS 无法覆盖内联自定义属性。
特定上下文的覆盖
内联样式组件属性(1–3 个简单属性)
// ✅ Good
<Button mt="xl" w={240} c="blue.6">Submit</Button>
// ❌ Bad - Too many props, use CSS modules instead
<Button mt={20} mb={20} ml={10} mr={10} w={240} c="blue.6" bg="white">Submit</Button>
常用的内联样式属性:
- 布局:
mt、mb、ml、mr、m、p、pt、pb、pl、pr - 尺寸:
w、h、maw、mah、miw、mih - 颜色:
c(颜色)、bg(背景) - 字体:
ff、fs、fw - 文本:
ta、lh
CSS Modules(复杂样式或超过 3 个属性)
在与组件相同的文件夹中创建一个 .module.css 文件:
/* Component.module.css */
.customCard {
transition: transform 0.2s ease;
cursor: pointer;
}
.customCard:hover {
transform: translateY(-2px);
box-shadow: var(--mantine-shadow-lg);
}
import styles from './Component.module.css';
<Card className={styles.customCard}>{/* content */}</Card>;
不要包含 .css.d.ts 文件——Vite 会自动处理。
颜色指南
**优先使用组件默认颜色。**按钮、ActionIcons 和徽章会从主题中获取合适的中性色或墨色;控件上的 color 属性只能用于指定状态(参见设计原则)。
// ❌ Bad - restates the theme, and is a near-black button in dark mode
<Button color="dark">Apply</Button>
<ActionIcon color="ldGray.6" variant="subtle" />
// ✅ Good - the theme already renders these
<Button>Apply</Button>
<ActionIcon />
// ❌ Bad - hand-picked grey
<Text c="ldGray.6">Secondary text</Text>
// ✅ Good - semantic token
<Text c="dimmed">Secondary text</Text>
中性色 token
| Token | 用途 |
|---|---|
--mantine-color-body | 表面(卡片、输入框、菜单) |
--ld-color-page | 表面下方的页面画布 |
--mantine-color-default-border / default-hover | 中性控件的边框和悬停填充色 |
--mantine-color-text / dimmed / placeholder | 主文本、次级文本和三级文本 |
ldGray.0-9 | 在两种配色方案中作用相同:0 画布、1 柔和填充色、2 边框、3 强边框、4 淡色图标、5 三级文本、6 暗淡文本、7 标签、9 文本 |
CSS Modules 中的深色模式
中性色无需深色模式分支:这些 token 和 ldGray.N 已会根据配色方案解析。
/* ❌ Bad */
.row:hover {
background-color: var(--mantine-color-ldGray-0);
@mixin dark {
background-color: var(--mantine-color-ldDark-5);
}
}
/* ✅ Good */
.row:hover {
background-color: var(--mantine-color-default-hover);
}
仅当某个非中性色对没有对应 token 时,才使用 light-dark(),例如强调色的色调:
.highlight {
background-color: light-dark(
var(--mantine-color-indigo-0),
var(--mantine-color-indigo-9)
);
}
始终使用主题 token
// ❌ Bad - Magic numbers
<Box p={16} mt={24}>
// ✅ Good - Theme tokens
<Box p="md" mt="lg">
移除无用样式
将样式迁移到 CSS modules 之前,先检查这些样式是否确实需要:
// ❌ Unnecessary - display: block has no effect on flex children
<Flex justify="flex-end">
<Button style={{display: 'block'}}>Submit</Button>
</Flex>
// ✅ Better - Remove the style entirely
<Flex justify="flex-end">
<Button>Submit</Button>
</Flex>
共享布局 CSS 变量(高度、宽度、z-index)
跨组件的布局常量(导航栏/页眉/横幅/页脚高度、页面内容宽度、侧边栏尺寸、仪表板页眉/标签页高度和 z-index)会作为全局 CSS 变量公开,以便 CSS modules 直接使用:
/* ✅ Reference the global var — resolves on :root everywhere */
.myPanel {
top: var(--dashboard-header-height);
max-width: var(--page-content-max-width-large);
}
/* ❌ Don't hardcode the literal — drifts from the source of truth */
.myPanel {
top: 50px;
}
// ❌ Don't bridge a constant into CSS via an inline style object
<div style={{ '--dashboard-header-height': `${DASHBOARD_HEADER_HEIGHT}px` }}>
**唯一事实来源:**数值存放在各自的 */constants.ts 文件中
(例如,components/common/Page/constants.ts、
components/common/Dashboard/dashboard.constants.ts),并在
src/theme/cssVariablesResolver.ts 中注册为 CSS 变量(通过 Mantine 的
cssVariablesResolver 接入 Mantine provider)。定义自己的变量之前,请查看该文件,了解所有可用的
var(--...) 名称。
**添加新的共享布局常量:**将数值添加到相应的
constants.ts,在 src/theme/cssVariablesResolver.ts 中注册,然后在 CSS 中引用
var(--your-name)。不要在 .module.css 文件中重新声明字面值,也不要通过内联
style 传递。需要在 TS 中将其用作 JS 值时(例如作为 Mantine 的 h= prop),仍直接使用数值常量。
适配主题的组件逻辑
不要读取配色方案来选择颜色;传入 token,让 CSS 负责解析。只有代码编辑器和图表库会使用纯 JS 值,因此读取配色方案是合理的:
// Monaco / Ace theme names
const { monaco, ace } = useEditorTheme();
// ECharts, Leaflet and other non-CSS consumers
const isDark = useComputedColorScheme('light') === 'dark';
使用 @mantine/core 导出的 clsx 工具
import { clsx } from '@mantine/core';
const MyComponent = () => {
return (
<div className={clsx('my-class', 'my-other-class')}>My Component</div>
);
};
Select/MultiSelect 分组
<Select
label="Your favorite library"
placeholder="Pick value"
data={[
{ group: 'Frontend', items: ['React', 'Angular'] },
{ group: 'Backend', items: ['Express', 'Django'] },
]}
/>
可复用组件
模态框
- 始终使用
components/common/MantineModal中的MantineModal——切勿直接使用 Mantine 的 Modal - 用法示例见
stories/Modal.stories.tsx - 模态框内的表单:在表单上使用
id,并在提交按钮上使用form="form-id" - 模态框内的提示框:使用
Callout,变体为danger、warning、info
共享控件
- 复制到剪贴板:使用
components/common/CopyActionIcon中的CopyActionIcon(value、可选的copyLabel/copiedLabel/icon/tooltipPosition,以及 ActionIcon props)。切勿手动组合CopyButton、ActionIcon和图标切换。 - 收藏切换:使用
components/common/FavoriteActionIcon中的FavoriteActionIcon(isFavorite、onToggle,以及可选的标签名称name)。 - 双击删除:使用
components/common/ConfirmDeleteButton中的ConfirmDeleteButton(onConfirm、aria-label,以及可选的tooltip);首次点击时进入待确认状态,第二次点击时执行,失焦或超时后取消待确认状态。 - 多维筛选器(带搜索、计数、分组和全选的弹出框):使用
components/common/FilterFacet中的FilterFacet。
提示框
- 使用
components/common/Callout中的Callout - 变体:
danger、warning、info
空 / 不可用区块(点线样式)
<Paper variant="dotted">(Card也可以)会渲染带虚线边框和透明背景的容器——这是空、占位或不可用区块的统一样式。定义于src/theme/components/Paper.module.css;例如,FavoritesPanel和AiAgentKnowledgeFilesSection中会用到。- 区块加载失败:使用
components/common/InlineErrorState中的InlineErrorState——它是一个点线样式的 Paper,带有低调的提示消息和可选的onRetry按钮。保持低调;次要面板加载失败时不应大张旗鼓。 - 占位值(例如没有数据的统计磁贴):在点线容器中使用
ldGray.5颜色的长破折号(—),而不是伪造零值或无限期显示骨架屏。骨架屏表示“正在加载”,点线样式表示“此处无内容”。 - 整页加载失败时才使用
ErrorState/SuboptimalState。
多态可点击容器
当你需要兼具可点击功能的布局容器时,请使用这些组件——可避免原生 <button> 的背景 / 边框重置问题。
components/common/PolymorphicGroupButton中的PolymorphicGroupButton——一个多态Group(弹性行布局),并设置了cursor: pointer。适用于作为单个按钮的水平元素组。components/common/PolymorphicPaperButton中的PolymorphicPaperButton——一个多态Paper(卡片表面),并设置了cursor: pointer。适用于类似卡片的可点击表面。
两者都接受其基础组件(GroupProps / PaperProps)的所有属性,以及用于指定底层元素的 component 属性。
// ✅ Clickable row without native button style bleed
<PolymorphicGroupButton component="div" gap="sm" onClick={handleClick}>
<MantineIcon icon={IconFolder} />
<Text>Label</Text>
</PolymorphicGroupButton>
// ✅ Clickable card surface
<PolymorphicPaperButton component="div" p="md" onClick={handleClick}>
Card content
</PolymorphicPaperButton>
// ❌ Avoid - native <button> brings unwanted background/border in menus and panels
<UnstyledButton>
<Group>...</Group>
</UnstyledButton>
NumberInput
- 始终使用
components/common/NumberInput中的NumberInput——不要直接使用 Mantine 的 NumberInput - Mantine 8 的
onChange会发出number | string(字段为空、输入到一半的值,如-/12.、超出安全范围的大整数)。封装组件的onNumberChange会为你处理:触发时值为number;字段清空时则为undefined——不会触发临时字符串值 - 默认仅允许整数——封装组件默认设置
decimalScale={0}(大多数字段用于端口、数量和超时)。小数字段可通过decimalScale={2}等设置启用,或设置decimalScale="unlimited"以移除限制 - 原始
onChange属性仅适用于展开form.getInputProps()的场景,此时由表单库负责解析
// ✅ Good - cleared field maps to a domain decision at the call site
<NumberInput onNumberChange={(v) => setLimit(v ?? DEFAULT)} />
// ✅ Good - number-or-undefined sinks take the callback directly
<NumberInput decimalScale={2} onNumberChange={setThreshold} />
// ✅ OK - form spread owns value/onChange
<NumberInput {...form.getInputProps('warehouse.port')} />
// ❌ Avoid - hand-rolled typeof guards on the raw Mantine component
<MantineNumberInput onChange={(v) => { if (typeof v === 'number') setX(v); }} />
EmptyStateLoader
- 对于任何居中的加载状态,请使用
components/common/EmptyStateLoader中的EmptyStateLoader:页面级守卫、面板、表格、空容器等 - 基于
SuboptimalState(Mantine v8)构建——会渲染一个旋转指示器和可选标题,并在其父容器中完全居中
TruncatedText
- 当文本可能超出受限宽度时,请使用
components/common/TruncatedText中的TruncatedText - 传入
maxWidth(数字或字符串)以控制截断边界 - 仅当文本确实被截断时,才会自动显示包含完整文本的工具提示(短名称不会产生多余提示)
- 默认值为
fz="sm";可通过标准Text属性覆盖
// ✅ Good - truncates long names, tooltip only appears when needed
<TruncatedText maxWidth={200}>{item.name}</TruncatedText>
// ✅ Accepts any Text prop
<TruncatedText maxWidth="100%" fw={500}>{space.name}</TruncatedText>
带搜索、分页和排序的表格
对于带搜索、分页和排序的表格,请使用 components/common/ContentTable 中的 ContentTable 组件。
如果需要筛选器,请使用 FilterFacet
Mantine 文档
所有组件及其文档链接的列表,采用便于大模型使用的格式:https://mantine.dev/llms.txt