# 扩展与自定义主题

`@astryx-vue/themes` 开箱自带了 7 套主题：Neutral（中性默认）、Butter（黄油暖金）、Chocolate（巧克力暖棕）、Gothic（哥特深邃）、Matcha（抹茶淡绿）、Stone（石石板岩）与 Y2K（复古千禧）。

每一套主题本质上都是一个已经写好的 CSS 文件（例如 `butter.css`）。包内不包含复杂的运行时主题注册或状态管理逻辑，因此运行性能极高、对打包工具完全透明。

## 场景一：只修改个别变量（最常用的做法）

如果你只想微调某个颜色或把所有圆角稍微改大一点，只需要在你的项目样式里覆盖对应的 CSS 变量即可。

**推荐做法：** 将覆盖代码写在 `@layer astryx.overrides` 里面，这样既能稳定覆盖默认主题，又绝对不会破坏组件原有的布局骨架：

```css
/* 确保这段样式在引入 @astryx-vue/themes/neutral.css 之后加载 */
@layer astryx.overrides {
  :root {
    /* 把主文本色调深一点 */
    --astryx-color-text-primary: light-dark(#1a1a1a, #f5f5f5);
    /* 把全站组件的基础圆角改成 8px */
    --astryx-radius-element: 0.5rem;
  }
}
```

## 场景二：在页面局部区域套用计算主题

如果不是全站修改，而是希望某个特定的弹窗、侧边栏或卡片拥有一套独立生成的品牌色，可以使用 Vue 组件 [`Theme`](/components/theme)，搭配 `defineTheme()` 工具函数。`color`、`typography`、`radius`、`motion` 这几项只要给几个数，就能算出一整套变量：

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { defineTheme, Theme } from '@astryx-vue/core/Theme'

const brand = defineTheme({
  name: 'brand',
  // 一个颜色；也可以写 [浅色种子, 深色种子]，深浅两套各自从自己的种子算
  color: { accent: ['#0064E0', '#48CAE4'] },
  typography: { scale: { base: 14, ratio: 1.2 } },
  radius: { base: 4, multiplier: 1.5 },
})
</script>

<template>
  <!-- 外部维持默认主题 -->
  <Button label="默认风格按钮" />

  <!-- 局部子树应用新主题 -->
  <Theme :theme="brand">
    <Button variant="primary" label="品牌风格按钮" />
  </Theme>
</template>
```

`Theme` 会把这组 CSS 变量写到子树的根节点上，并且会重新绑定文字、按钮等别名，防止它们停留在外层已经算好的颜色上。

### 覆盖主题的变量

`Theme` 会把变量写进外层元素里面的一个 `<style>` 里。这条规则的选择器是 `:scope.astryx-theme`，权重相当于两个 class，而且这个 `<style>` 在文档里排在你的样式表后面。所以，如果你的覆盖写在 `@layer astryx.overrides` 里，只用一个或两个 class 去选外层元素，是压不过它的。可以这样选：

- 把覆盖写在没放进任何 `@layer` 的普通样式里。不分层的样式总是赢过分层里的，所以一定能赢。这是最省事的办法。
- 如果一定要写在 `@layer astryx.overrides` 里，可以在外层元素上叠至少 3 个 class（`.wrapper.wrapper.wrapper`）；也可以在 `Theme` 外面自己再包一个带 id 的元素，用 `#brand .wrapper` 这样的写法去选。`Theme` 不会把 `id` 写到它的外层元素上，所以单写 `#id` 是选不中的。

适配出来的变量也一样，不管条件有没有成立。直接写在外层元素 `style` 属性里的值同样会赢。

主题名要唯一。两个 `name` 相同的 `Theme` 共用一份样式。如果它们的值不一样，后渲染的那个会对两边都生效。不同的主题请起不同的名字。

## 场景三：只让某个组件与众不同

### 用 `components` 给组件的某个部位换样式

`components` 里的每个键叫「换肤挂点」（theme target），写法是 `组件名-部位`，全部小写、用短横线连起来，例如 `button`、`card-header`、`radio-indicator`、`progress-bar-fill`。每个挂点就是元素上一个固定的 `astryx-*` 类名。

挂点下面的键用来挑选状态：

- `base`：这个部位本身。
- `variant:primary`：`variant` 等于 `primary` 的时候，对应元素上的 `[data-variant="primary"]`。
- `checked` 这种单个词：对应 `[data-checked="checked"]`。
- 用 `+` 把两个条件连起来，两个都满足才生效。

```ts
const brand = defineTheme({
  name: 'brand',
  components: {
    'button': { 'variant:primary': { borderRadius: '999px' } },
    'card-header': { base: { color: 'var(--brand-ink)' } },
  },
})
```

几点细节：

- `:hover` 只对能点的控件生效，禁用的按钮不会因为鼠标悬停而变色。
- `card`、`dialog`、`bottom-sheet`、`dropdown-menu`、`context-menu`、`segmented-control` 这几个组件，主题里改了 `borderRadius` 或 `padding`，里面跟着它走的部位也会一起变，比如卡片头部的圆角。`chat` 的 `borderRadius` 和 `padding` 会写到输入区的圆角和内边距上，`progress-bar-mark` 的 `width` 和 `height` 会写到那根刻度线上。

### 旧写法已经不再支持

上游在 0.7.0 删掉了一批旧名字，我们也跟着删了。驼峰写法（比如 `buttonIcon`）或者大写开头（比如 `Card`）会直接报错；连在一起的旧名（比如 `progressbar`）不会报错，但什么都匹配不到。请改成右边的新名字：

| 已删除 | 请改用 |
| --- | --- |
| `base-table` | `table` |
| `checkbox` | `checkbox-indicator` |
| `codeblock`、`codeblock-copy-button`、`codeblock-header`、`codeblock-title` | `code-block`、`code-block-copy-button`、`code-block-header`、`code-block-title` |
| `date-input-clear-icon`、`date-range-input-clear-icon`、`multi-selector-clear-icon`、`selector-clear-icon` | `input-clear-icon` |
| `hovercard` | `hover-card` |
| `navicon` | `nav-icon` |
| `popover-surface` | `popover` |
| `progressbar`、`progressbar-fill`、`progressbar-mark`、`progressbar-track` | `progress-bar`、`progress-bar-fill`、`progress-bar-mark`、`progress-bar-track` |
| `radio`、`radio-dot` | `radio-indicator`、`radio-indicator-dot` |
| `statusdot` | `status-dot` |
| `textarea` | `text-area` |

组件也不再输出 `.primary`、`.sm` 这种光秃秃的类名。你自己的 CSS 里如果写了 `.astryx-button.primary`，请改成 `.astryx-button[data-variant="primary"]`。

### 直接覆盖组件变量

如果你只想覆盖单个组件的颜色，也可以直接改它的专属变量。例如 `packages/themes/neutral/button.css` 里，按钮背景对应的是 `--astryx-button-primary-background`：

```css
@layer astryx.overrides {
  .special-container {
    --astryx-button-primary-background: light-dark(#4338ca, #6366f1);
  }
}
```

## 进阶：主题自己的变量、环境适配和继承

### 主题局部变量 `localTokens`

有些值只有你这一套主题用得上，比如品牌的墨水色。把它放进 `localTokens`，键就是完整的 CSS 变量名，原样保存，不会加前缀：

```ts
defineTheme({
  name: 'brand',
  localTokens: { '--brand-ink': ['#0A2540', '#E6F0FF'] },
  components: { 'card-header': { base: { color: 'var(--brand-ink)' } } },
})
```

不能拿共享的名字（比如 `--color-accent`）来当局部变量，因为组件已经在读它了。继承这套主题的子主题会一起继承局部变量，也可以改它们的值。局部变量之间不能互相引用成一个圈，否则会报错。

### 按环境调整 `adaptations`

有时候只想在某些环境下换一组值，比如「手机这类触屏、而且屏幕比较窄的时候，正文字号放大一点」。`adaptations.rules` 是一串按顺序排好的规则，每条写成 `{ when, value }`：

```ts
defineTheme({
  name: 'brand',
  typography: { scale: { base: 14, ratio: 1.2 } },
  adaptations: {
    rules: [{
      when: { pointer: 'coarse', width: { below: 'lg' } },
      value: { typography: { scale: { base: 16, ratio: 1.1736 } } },
    }],
  },
})
```

- `when` 可以写屏幕宽度范围（用断点名：`sm` 640、`md` 768、`lg` 1024、`xl` 1280、`2xl` 1536，单位都是 CSS 像素，可以用 `widthBreakpoints` 改）、`pointer`（`coarse` 粗指针，也就是手指；`fine` 细指针，也就是鼠标）、`contrast`（对比度偏好）、`motion`（是否减弱动效）。同一条 `when` 里写的条件要全部满足。
- `value` 能写的东西和主题本身一样。只写了一半的字号阶梯、圆角或动效，缺的部分会从主题自己的设置里补上。
- 规则按顺序在主题之后生效，同时满足好几条时，后面的赢。
- 这些规则只通过 CSS 生效，`useTheme().token()` 读到的始终是主题本身的值。
- 规则里的变量排在根 token 后面，选择器和根 token 一样，所以条件成立时按层叠顺序盖过根值，没有用 `!important`。想覆盖它，请看下面的「覆盖主题的变量」。

### 继承 `extends`

`extends` 会把另一套主题算好的东西全部带过来：变量、局部变量、组件样式、`onDark` 和 `onLight`、环境规则、图标和指示器。新主题自己写的值优先。继承来的环境规则会按新主题的字号阶梯重新算一遍。

### 写错的值会被丢掉

如果某个值会提前结束这条 CSS 声明，比如 `red; } body { display: none`，它会被丢掉，控制台里会有一条警告，主题里其他的值照常生效。看起来奇怪但其实合法的 CSS，比如引号里的分号、data URL，会原样保留。

## 注意事项

1. **不需要另外准备深浅两套样式表：** 因为变量本身采用 `light-dark()` 机制，覆盖时也推荐同时写下浅色和深色两个取值。
2. **多主题隔离：** 预设主题除了 Neutral 会同时挂在 `:root` 上，其他 6 套都是通过属性选择器 `[data-astryx-theme='...']` 局部隔离的，多主题并存时互不干扰。关于明暗切换的完整细节，请参阅 [深色模式配置](/guide/dark-mode)。
3. **Tailwind：** 本包不提供 Tailwind 桥接文件。样式本身就是普通的 CSS 变量和 `@layer`，Tailwind 或 UnoCSS 项目可以直接写 `bg-[var(--astryx-color-background-surface)]`，或者在自己的主题配置里做映射。
