关于 styling
styling 是面向 Svelte 项目的界面样式技能,供开发者遵循仓库中的 Tailwind 与组件规范。它还说明了 flex 布局中可滚动区域的尺寸处理方式。
主要功能
- 01减少不必要的包装元素
- 02使用 Tailwind 工具类与语义令牌
- 03通过 tailwind-variants 管理组件变体
- 04用 HTML disabled 表达不可交互状态
- 05处理 flex 列布局中的滚动区域
适用场景
- 01统一 Svelte 界面的样式写法
- 02为共享组件选择合适的覆盖方式
- 03修复面板内容无法独立滚动的问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill styling安装后在 Claude Code 中输入/styling确认已加载。
详细介绍
样式指南
如果样式依赖 shadcn-svelte 结构、类合并、变体、Bits UI 组合方式或本地封装的行为,请阅读 ui-design 的组件系统参考,了解上游依据。普通 Tailwind 工具类和下方仓库本地布局规则无需外部查询。
尽量减少封装元素
避免创建不必要的封装 div。如果直接将类应用到现有语义元素也能达到相同效果,应优先采用这种方式。
推荐(直接应用)
<main class="flex-1 mx-auto max-w-7xl">
{@render children()}
</main>
避免(不必要的封装)
<main class="flex-1">
<div class="mx-auto max-w-7xl">
{@render children()}
</div>
</main>
只要样式不与元素的语义用途冲突,也不会造成布局问题,这项原则就适用于所有元素。
Tailwind 最佳实践
- 在应用中使用
@epicenter/ui/utils的cn(),在 UI 包中使用../utils.js,以便有条件地组合类 - 局部布局和状态优先使用工具类,而非自定义 CSS
- 优先使用共享比例和语义令牌工具类,而非任意方括号值和原始颜色
- 使用
tailwind-variants构建组件变体系统 - 遵循颜色的
background/foreground惯例 - 使用 CSS 变量保持主题一致性
共享基础组件的覆盖样式
为本地 @epicenter/ui 基础组件添加样式时,请参考 ui-design 的组件系统文档。共享基础组件上的 Tailwind 类通常应表达父级布局或产品状态,而不是重新定义基础组件的视觉规格。
推荐的基础组件覆盖样式:
<Item.Button size="sm" class="w-full justify-start text-left" />
值得审视的基础组件覆盖样式:
<Item.Button size="sm" class="gap-2 rounded px-2 py-1.5 text-sm hover:bg-accent/50" />
如果覆盖样式重复设置尺寸、密度、圆角、间距、内边距、排版、悬停、聚焦或过渡类,请考虑基础组件是否需要尺寸、变体或语义封装。
禁用状态:使用 HTML disabled + Tailwind 变体
当交互元素可能无法交互时(空白分区、加载状态、没有条目),应使用 HTML disabled 属性,而不是 JS 条件保护。配合 Tailwind 的 enabled: 和 group-disabled: 变体使用。
为什么使用 disabled 而不是 JS 条件保护
disabled会原生阻止点击:无需if (!hasItems) return- 可使用
:disabledCSS 伪类设置样式 - 符合无障碍语义(屏幕阅读器会播报“已变暗”或“不可用”)
- Tailwind 的
enabled:和group-disabled:变体可以顺畅组合
模式
<button
class="group enabled:cursor-pointer enabled:hover:opacity-80"
disabled={item.count === 0}
onclick={toggle}
>
{item.label} ({item.count})
<ChevronIcon class="group-disabled:invisible" />
</button>
关键变体
enabled:cursor-pointer:仅在可点击时显示指针光标enabled:hover:bg-accent/50:仅在可交互时应用悬停效果group-disabled:invisible:父元素被禁用时隐藏子元素(例如展开箭头)disabled:opacity-50:元素被禁用时降低其不透明度
反模式
<button
class="cursor-pointer hover:opacity-80"
onclick={() => { if (item.count > 0) toggle(); }}
>
JS 条件保护会让非交互元素上的 cursor-pointer 和 hover:opacity-80 继续生效。用户看到一个看起来可以点击、实际却没有反应的按钮。应使用 disabled,让浏览器和 CSS 来处理。
Flex 列滚动陷阱
当 flex 子元素使用 h-full(高度:100%),但与其他同处一个 flex 列中的兄弟元素(页眉、工具栏、页脚)共用布局时,它计算出的高度会等于父元素的完整高度:它会溢出并覆盖兄弟元素,而不是占据剩余空间。内容会被裁切,或将布局挤出视口,内部滚动区域也永远不会启用。
这是本代码库中最常见的布局错误。以下情况都可能出现:
- 需要滚动的
Resizable.Pane(paneforge)内有组件 - flex 列中有一个
ScrollArea.Root(bits-ui)或overflow-autodiv,且旁边有页眉或工具栏 - 任何需要让某个分区独立滚动的分屏或面板布局
修复方法:flex-1 min-h-0 overflow-hidden
将包含可滚动内容的 flex 子元素上的 h-full 替换为这三个工具类。它们分别解决不同的问题:
| 工具类 | 作用 | 必要原因 |
|---|---|---|
flex-1 | 占据兄弟元素之后的剩余空间 | h-full = 父元素高度的 100%,不考虑兄弟元素。flex-1 = 剩余空间。 |
min-h-0 | 允许元素缩小到内容高度以下 | Flex 项默认 min-height: auto,因此不能小于其内容的高度。 |
overflow-hidden | 建立有界高度上下文 | 没有它,带有 overflow-auto 或 ScrollArea 的子元素就没有可用于滚动的高度上限。 |
这三个都不可或缺。缺少其中任何一个,修复都会失效:
- 缺少
flex-1:元素仍为父元素高度的 100%,会溢出并覆盖兄弟元素 - 缺少
min-h-0:元素拒绝缩小,内容会把它撑得更高 - 缺少
overflow-hidden:内部滚动容器没有有界祖先,因此会扩展而不是滚动
修改前 / 修改后
<main class="flex h-full flex-col overflow-hidden">
<div class="border-b px-4 py-2">Toolbar</div>
<MyScrollableContent class="h-full" />
</main>
<main class="flex h-full flex-col overflow-hidden">
<div class="border-b px-4 py-2">Toolbar</div>
<MyScrollableContent class="flex-1 min-h-0 overflow-hidden" />
</main>
在可调整大小的窗格中(paneforge)
Paneforge 的 Pane 组件通过 flex 比例设置宽度,但不会限制高度或裁剪溢出。Pane 中的任何可滚动内容,都需要在其根元素上使用完整的 flex-1 min-h-0 overflow-hidden 链:
<Resizable.Pane defaultSize={80}>
<div class="flex flex-1 min-h-0 flex-col overflow-hidden">
<div class="border-b">Header</div>
<div class="flex-1 overflow-y-auto">
</div>
</div>
</Resizable.Pane>
使用 ScrollArea(bits-ui)
ScrollArea.Root 渲染时使用 position: relative,其视口则使用 height: 100%。这会破坏 flex 尺寸计算链:视口的百分比高度会相对于 relative 父元素计算,而该父元素在 flex 上下文中没有显式高度。内容会扩展,而不是滚动。
有两种选择:
- 优先使用普通的
overflow-y-auto,应用于带有flex-1 min-h-0的 div(更简单,始终有效) - 如果需要带样式的滚动条,将
ScrollArea.Root包装在带有flex-1 min-h-0 overflow-hidden的 div 中,为其提供有界祖先
<div class="flex-1 overflow-y-auto">
{#each items as item}
<div>{item.name}</div>
{/each}
</div>
<div class="flex-1 min-h-0 overflow-hidden">
<ScrollArea.Root class="h-full">
{#each items as item}
<div>{item.name}</div>
{/each}
</ScrollArea.Root>
</div>
经验法则
如果你在一个同一 flex 列中有兄弟元素的 flex 子元素上写了 h-full,请停下来,将其替换为 flex-1 min-h-0 overflow-hidden。h-full 模式仅在该元素是其 flex 父元素的唯一子元素时才有效。