mcpField
F
frontend-style-guide

frontend-style-guide

github.com/lightdash设计与 UI6.2k416 次安装今天更新
代码仓库

关于 frontend-style-guide

为在 Lightdash 前端代码中开发组件的 Agent 提供 Mantine 8 样式指南,强调主题令牌、共享组件以及明暗模式的一致设计。

主要功能

  1. 01Mantine 8 主题与组件规范
  2. 02颜色、排版和间距令牌
  3. 03CSS Modules 使用规则
  4. 04明暗模式设计检查
  5. 05组件状态与无障碍规范

适用场景

  1. 01创建或更新前端组件
  2. 02统一界面样式与主题令牌
  3. 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:一种墨色强调色、扁平表面、柔和边框、紧凑的字号比例。大多数时候,使用默认值就能带来「恰到好处」的观感。添加颜色、阴影或尺寸前,请先逐条检查以下规则。

  1. 只用一种强调色,而且是墨色。 表面上的主要操作使用主题主色(浅色模式下接近黑色,深色模式下接近白色)。切勿为了强调而将 Button 或 ActionIcon 设为 blue、dark、indigo 或 ldDark。控件的颜色用于表示状态:red 表示危险操作,yellow 表示警告,teal 表示「已复制」,orange 表示收藏,green 表示已验证状态(勾选图标、徽章)以及现有的合并操作,绝不能用于填充验证按钮。靛蓝色用于 AI 表面。链接是唯一使用蓝色文字的元素。
  2. 层级由变体而非颜色体现。 filled 是每张卡片、每个页眉或模态框页脚中的唯一主要操作。default(带边框)是次要操作。light 用于第三级或类似切换的操作(「添加筛选器」)。subtle 用于图标按钮和行内操作;它是 ActionIcon 的默认值。如果一个表面上有两个 filled 按钮,其中一个就不合适。
  3. 表面保持扁平。 Paper 和 Card 默认带边框、无阴影、圆角为 12px;阴影只用于浮层(Menu 和 Popover 使用 md,Modal 使用 lg)。不要再传入 withBorder、shadow 或 radius 重复声明这些默认值。空白或占位内容区域使用 <Paper variant="dotted">。
  4. 中性色使用令牌,不要手动配对。 使用 --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"。
  5. 字号遵循比例,不是滑块。 正文为 14/20。标题使用 Title 的 1 到 6 级,字重为 600(28 到 14px);标签和表格标题字重为 500;其他所有内容字重为 400。使用 fz="xs|sm|md",不要使用 fz={13}。卡片和区域标题使用 Title order={5};页面顶部使用 PageHeader。
  6. 间距遵循令牌网格。 卡片内边距使用 md,组间距使用 xs/sm,区域间距使用 lg,页面边距使用 lg/xl。不要使用像素边距;如果布局需要 margin-top: 20px,就使用 mt="lg"。
  7. 图标保持低调。 MantineIcon 的描边宽度为 1.5,文本旁的图标为 16px,xs 控件中的图标为 14px;作为次要元素时使用 dimmed 着色。每个纯图标按钮都必须有 aria-label 和 Tooltip。
  8. 输入控件保持平和。 使用默认变体和柔和边框,聚焦时边框颜色加深,不显示环形效果。size="xs" 控件会自动带有紧凑的次要标签。Select 使用勾选标记显示选中的选项;筛选值选择器使用标准组合框,不要使用自定义列表。
  9. 动效服务于功能。 下拉菜单从锚点弹出,模态框淡入淡出,两者都使用主题中的设置。除此之外,除非用于显示进度,否则不要添加动画。
  10. 深色模式需要专门设计,而非从浅色模式推导。 完成前请检查两种配色方案。切勿读取 useMantineColorScheme 来选择颜色;请使用令牌。编辑器使用 useEditorTheme();只有没有 CSS 的 JS 使用场景(ECharts、Leaflet)才读取 useComputedColorScheme。
  11. 状态使用共享组件。 加载时使用 EmptyStateLoader,区域加载失败时使用 InlineErrorState,页面加载失败时使用 SuboptimalState,表示「此处为空」时使用 <Paper variant="dotted">。
  12. 编写 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:用于悬停时的轮廓

样式最佳实践

核心原则:优先使用主题

**目标是尽可能使用主题默认值。**样式覆盖应当是例外,而非惯例。

样式层级
  1. **最佳:**不使用自定义样式(使用主题默认值和变体)
  2. **扩展主题:**对于重复使用的样式,在 src/theme/components/<Component>.module.css 中添加变体或规则(并在 src/theme/components/index.ts 中注册)
  3. **组件属性:**简单覆盖(1–3 个属性,例如 mt="xl" w={240})
  4. **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

评论

支持 Markdown