mcpField
C
css-architecture

css-architecture

github.com/thinkoodle设计与 UI7176 次安装8 周前更新
代码仓库

关于 css-architecture

面向 Rails 应用的 CSS 架构技能,指导开发者组织可维护的样式表。通过设计令牌、组件样式和 CSS 层管理级联,并以现代 CSS 实现暗色模式。

主要功能

  1. 01按组件拆分样式表
  2. 02使用设计令牌统一样式值
  3. 03通过 @layer 管理级联
  4. 04使用 light-dark() 支持暗色模式
  5. 05提供第三方 CSS 集成指导

适用场景

  1. 01为 Rails 应用新建或重组 CSS
  2. 02编写可复用的组件样式
  3. 03实现暗色模式与设计令牌
  4. 04排查级联和优先级问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill css-architecture

安装后在 Claude Code 中输入/css-architecture确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
components.md
dark-mode.md
design-tokens.md
layers.md
responsive.md
utilities.md

详细介绍

Rails CSS 架构专家

使用现代 CSS 特性为 Rails 应用编写易于维护、可扩展的 CSS,包括自定义属性、@layer、light-dark() 和语义设计令牌。无需依赖框架。

理念

核心原则:

  1. 组件管理自己的样式 — 每个组件对应一个 CSS 文件,绝不在页面样式表中为组件设置样式
  2. 设计令牌优于魔法数字 — 所有值都来自 _global.css 自定义属性
  3. 层控制层叠 — 使用 @layer,而不是特异性技巧或 !important
  4. 深色模式轻松实现 — color-scheme + light-dark() 无需额外工作即可实现深色模式
  5. 工具类是调味料,不是主菜 — 仅用于偶尔的调整,不要用作主要样式

架构金字塔:

      /\       Utilities (few — spacing/text tweaks)
     /  \
    /____\     Components (many — reusable UI pieces)
   /      \
  /________\   Base (element defaults, resets)
 /          \
/____________\ Tokens (_global.css — design system foundation)

何时使用此技能

  • 在 Rails 应用中创建或重组 CSS 文件结构
  • 编写新的组件样式表
  • 实现深色模式支持
  • 定义或扩展设计令牌
  • 调试层叠/特异性问题
  • 从 Tailwind 或其他框架迁移到原生 CSS
  • 设置 CSS 层
  • 创建工具类
  • 集成第三方 CSS 库

说明

第 1 步:了解文件结构

使用此架构的每个 Rails 应用都有以下结构:

app/assets/stylesheets/
├── _global.css              # Design tokens: colors, spacing, typography, radii
├── application.css          # Entry point — may just import everything
├── reset.css                # CSS reset / normalization
├── base.css                 # Element defaults (no classes)
├── utilities.css            # Single-purpose utility classes
└── components/
    ├── app-layout.css       # Main layout shell
    ├── forms.css            # Form elements + buttons
    ├── cards.css            # Card component
    ├── badges.css           # Badge variants
    ├── alerts.css           # Alerts/flash messages
    ├── tables.css           # Table styles
    └── [feature].css        # Feature-specific components

务必先检查现有结构:

# See what exists
find app/assets/stylesheets -name "*.css" | sort

# Check for existing tokens
cat app/assets/stylesheets/_global.css

# Check layer declarations
rg "@layer" app/assets/stylesheets/

# Check for existing components
ls app/assets/stylesheets/components/

遵循现有项目约定 — 保持一致比追求“理想”模式更重要。

第 2 步:设置 CSS 层

在 _global.css(或 application.css)顶部声明一次各层:

/* _global.css — FIRST LINE */
@layer reset, base, components, utilities;

层级优先级(从低到高):

  1. reset — 浏览器规范化
  2. base — 元素默认样式(h1、a、input)
  3. components — UI 组件(.card、.btn、.badge)
  4. utilities — 覆盖辅助类(.mt-4、.hidden)— 优先级始终最高

每条 CSS 规则都放在对应的层中:

/* base.css */
@layer base {
  body { font-family: var(--font-sans); color: var(--color-ink); }
  a { color: var(--color-link); }
}

/* components/cards.css */
@layer components {
  .card { background: var(--color-surface); border: 1px solid var(--color-border); }
}

/* utilities.css */
@layer utilities {
  .mt-4 { margin-top: var(--space-4); }
}

层为何重要: 没有 @layer,你就得通过嵌套、!important 或加载顺序技巧来对抗特异性。层可以消除所有这些问题。无论特异性如何,utilities 层中的工具类都优先于任何组件规则。

第 3 步:定义设计令牌

所有设计值都以自定义属性的形式存放在 _global.css 的 :root 中:

:root {
  /* Enable dark mode detection */
  color-scheme: light dark;

  /* === Raw Palette (OKLCH for perceptual uniformity) === */
  --color-zinc-50:  oklch(0.985 0 0);
  --color-zinc-100: oklch(0.967 0.001 286.375);
  --color-zinc-200: oklch(0.92 0.004 286.32);
  --color-zinc-400: oklch(0.707 0.022 261);
  --color-zinc-500: oklch(0.552 0.016 285.938);
  --color-zinc-700: oklch(0.37 0.013 285.805);
  --color-zinc-800: oklch(0.274 0.006 286.033);
  --color-zinc-900: oklch(0.21 0.006 285.885);
  --color-zinc-950: oklch(0.141 0.005 285.823);

  /* === Semantic Color Tokens === */
  /* Surfaces */
  --color-canvas: light-dark(var(--color-zinc-50), var(--color-zinc-900));
  --color-surface: light-dark(white, var(--color-zinc-800));
  --color-surface-raised: light-dark(white, var(--color-zinc-700));
  --color-surface-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));

  /* Text */
  --color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
  --color-ink-muted: light-dark(var(--color-zinc-500), var(--color-zinc-400));
  --color-ink-subtle: light-dark(var(--color-zinc-400), var(--color-zinc-500));

  /* Borders */
  --color-border: light-dark(var(--color-zinc-200), var(--color-zinc-700));
  --color-border-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));
  --color-border-strong: light-dark(var(--color-zinc-400), var(--color-zinc-600));

  /* Interactive */
  --color-primary: light-dark(var(--color-blue-600), var(--color-blue-500));
  --color-primary-hover: light-dark(var(--color-blue-700), var(--color-blue-400));
  --color-link: light-dark(var(--color-blue-600), var(--color-blue-400));

  /* State */
  --color-positive: light-dark(var(--color-green-600), var(--color-green-500));
  --color-positive-canvas: light-dark(var(--color-green-50), var(--color-green-950));
  --color-negative: light-dark(var(--color-red-600), var(--color-red-500));
  --color-negative-canvas: light-dark(var(--color-red-50), var(--color-red-950));

  /* === Spacing Scale === */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;

  /* === Typography === */
  --font-sans: system-ui, -apple-system, sans-serif;
  --font-mono: ui-monospace, "SF Mono", monospace;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --font-medium: 500;
  --font-semibold: 600;
  --font-bold: 700;

  /* === Radii === */
  --radius-sm: 0.25rem;
  --radius-md: 0.375rem;
  --radius-lg: 0.5rem;
  --radius-xl: 0.75rem;
  --radius-full: 9999px;

  /* === Transitions === */
  --duration-fast: 150ms;
  --duration-normal: 250ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

规则:组件绝不使用原始值。始终引用令牌。

/* GOOD */
.card { padding: var(--space-6); background: var(--color-surface); }

/* BAD — hardcoded values */
.card { padding: 1.5rem; background: white; }
第 4 步:编写组件样式

每个组件在 components/ 中都有自己的文件。组件只能使用令牌。

组件结构:

/* components/cards.css */
@layer components {
  /* Base */
  .card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
  }

  /* Structural subcomponents */
  .card-header { padding-bottom: var(--space-4); border-bottom: 1px solid var(--color-border-muted); }
  .card-body { padding-top: var(--space-4); }
  .card-footer { padding-top: var(--space-4); border-top: 1px solid var(--color-border-muted); }

  /* Variants */
  .card-sm { padding: var(--space-4); }
  .card-lg { padding: var(--space-8); }

  /* States */
  .card-interactive { transition: border-color var(--duration-fast) var(--ease-out); }
  .card-interactive:hover { border-color: var(--color-border-strong); }
}

命名约定:

  • 基础:.card、.btn、.badge
  • 子组件:.card-header、.card-body
  • 变体:.card-sm、.btn-primary
  • 状态:.is-loading、.is-active

在以下情况下创建新的组件文件:

  • 某个模式使用了 3 次或更多
  • 具有相关子类(.foo、.foo-header、.foo-item)
  • 具有变体(.foo-primary、.foo-small)

以下情况保留在页面样式表中:

  • 确实仅限于特定页面(只使用一次)
  • 简单布局(网格、弹性容器)
第 5 步:使用 light-dark() 实现深色模式

遵循令牌系统即可自动支持深色模式。方法如下:

  1. 在 :root 上设置 color-scheme: light dark — 浏览器会遵循操作系统偏好
  2. 使用 light-dark() 定义语义令牌 — 第一个值表示浅色模式,第二个值表示深色模式
  3. 组件引用语义令牌 — 样式会自动适配
:root {
  color-scheme: light dark;
  --color-surface: light-dark(white, var(--color-zinc-800));
  --color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
}

/* This component supports dark mode with ZERO extra work */
.card {
  background: var(--color-surface);
  color: var(--color-ink);
}

可选的手动切换:

html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"]  { color-scheme: dark; }
function toggleTheme() {
  const current = document.documentElement.dataset.theme;
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.dataset.theme = next;
  localStorage.setItem('theme', next);
}

防止闪烁 — 将以下内容放入 <head>:

<script>
  const theme = localStorage.getItem('theme');
  if (theme) document.documentElement.dataset.theme = theme;
</script>

深色模式规则:

  • 阴影在深色模式下效果不佳 — 优先使用边框
  • 使用 color-mix() 创建微妙的着色背景
  • 状态颜色需要同时提供前景和画布变体
  • 绝不使用原始白色/黑色 — 始终使用语义令牌
第 6 步:编写工具类

工具类放在 utilities.css 的 @layer utilities 中。每个工具类只负责一项功能,且不可变:

@layer utilities {
  /* Spacing */
  .mt-2 { margin-top: var(--space-2); }
  .mt-4 { margin-top: var(--space-4); }
  .mt-6 { margin-top: var(--space-6); }
  .mb-4 { margin-bottom: var(--space-4); }
  .p-4 { padding: var(--space-4); }
  .gap-4 { gap: var(--space-4); }

  /* Text */
  .text-sm { font-size: var(--text-sm); }
  .text-muted { color: var(--color-ink-muted); }
  .font-medium { font-weight: var(--font-medium); }

  /* Layout */
  .flex { display: flex; }
  .grid { display: grid; }
  .hidden { display: none; }
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
}

使用工具类进行偶尔的调整:

<%# Good — utility for spacing adjustment %>
<div class="card mt-6">

<%# Bad — utilities replacing component styles %>
<div class="p-6 bg-white border rounded-lg shadow">
第 7 步:页面样式表应保持精简

页面专属样式应尽量精简——只负责布局:

/* pages/dashboard.css */
@layer components {
  .dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-6);
  }
}

页面由现有组件组合而成:

<div class="dashboard-grid">
  <div class="card">...</div>
  <div class="card">...</div>
  <div class="card mt-4">...</div>  <%# Component + utility %>
</div>

切勿在页面样式表中设置组件样式:

/* BAD — page overriding component */
.dashboard .card { padding: 2rem; }

/* GOOD — create a variant in the component file */
.card-lg { padding: var(--space-8); }
第 8 步:集成第三方 CSS

集成外部库时,将其变量映射到你的设计令牌:

:root {
  --lexxy-color-ink: var(--color-ink);
  --lexxy-color-canvas: var(--color-surface);
  --lexxy-color-selected: light-dark(oklch(0.92 0.026 254), oklch(0.3 0.05 254));
}

加载顺序很重要——库的 CSS 应在你的覆盖样式之前加载:

<%= stylesheet_link_tag "lexxy" %>
<%= stylesheet_link_tag :app %>

反模式

不要应该这样做
硬编码颜色(white、#333)使用语义化令牌(var(--color-surface))
使用 !important使用 @layer 控制层叠
深层嵌套(.page .section .card .title)使用扁平、明确的类名(.card-title)
使用通用名称(.container、.title)使用带前缀的名称(.page-container、.card-title)
在页面 CSS 中设置组件样式创建组件变体
在 ERB 中使用内联样式使用工具类或组件类
在深色模式中使用 box-shadow 表示层级使用边框
使用魔法数字(padding: 23px)使用间距令牌(var(--space-6))
使用媒体查询实现深色模式配合 color-scheme 使用 light-dark()
在视图中使用框架专属类使用你自己的语义化类

快速参考

命名约定
.component           → .card, .btn, .badge
.component-sub       → .card-header, .card-body
.component-variant   → .card-sm, .btn-primary
.component-state     → .card-interactive (or .is-loading for global states)
.page-element        → .dashboard-grid, .settings-sidebar
响应式模式
/* Mobile-first with container queries where possible */
.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

@media (min-width: 768px) {
  .dashboard-grid {
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-6);
  }
}
浏览器支持

light-dark() 需要 Chrome 123+、Firefox 120+、Safari 17.5+。

旧版浏览器的回退方案:

@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: var(--color-zinc-800);
    --color-ink: var(--color-zinc-50);
  }
}
新组件检查清单
  • 已创建 components/[name].css
  • 所有规则均包裹在 @layer components { } 中
  • 所有值均引用设计令牌(没有魔法数字)
  • 使用语义化颜色令牌(而非原始调色板值)
  • 类名明确具体,不会与其他类冲突
  • 子组件遵循 .component-sub 命名规则
  • 深色模式自动生效(已验证)
  • 如有需要,已在 application.css 中导入
设计令牌检查清单
  • 已将新令牌添加到 _global.css 的 :root 下
  • 颜色令牌使用 light-dark() 支持两种模式
  • 原始调色板值使用 OKLCH
  • 间距遵循现有比例
  • 令牌名称具有语义性(例如 --color-surface,而非 --color-white)

有关详细模式和示例,请参阅 references/ 目录:

  • references/design-tokens.md — 完整的颜色调色板(OKLCH)、间距、字体排版和命名约定
  • references/components.md — 按钮、卡片、徽章、警告框、表单、表格模式,以及 Rails 集成
  • references/dark-mode.md — light-dark() 深入解析、手动切换、阴影、color-mix()
  • references/responsive.md — 移动优先断点、容器查询、流式字体排版、布局模式
  • references/layers.md — @layer 层叠控制、反模式、第三方 CSS
  • references/utilities.md — 完整的推荐工具类集合

评论

支持 Markdown