# 主题

## 使用方法

`Theme` 本身就是主题提供者。它把 `defineTheme()` 返回的主题写到子树的 CSS 变量上，子组件用 `useTheme()` 读取。没有另外一个 Provider 组件。

token 名可以用原版的 `--color-accent`，也可以用这个包的 `--astryx-color-accent`。`[浅色, 深色]` 会写成 `light-dark()`。`mode` 默认是 `system`，也可以是 `light` 或 `dark`。最外层的 `Theme` 会把 `data-astryx-theme` 和 `data-astryx-color-mode` 同步到 `document.documentElement`，嵌套的不会。

主题对象上的 `syntax` 给这棵子树里的代码上色。更近的 `SyntaxTheme` 优先。从 `@astryx-vue/core/Theme` 引入。

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

const ocean = defineTheme({
  name: 'ocean',
  tokens: {
    '--color-accent': ['#0077B6', '#48CAE4'],
    '--color-background-surface': ['#F0F8FF', '#0A1628'],
    '--color-text-primary': ['#0A1317', '#FFFFFF'],
    '--radius-container': '16px',
  },
})
</script>

<template>
  <Theme :theme="ocean">
    <p>Ocean</p>
  </Theme>
</template>
```

`neutralTheme` 是不覆盖 token 的主题。颜色仍然来自 `@astryx-vue/themes/neutral.css`。七套现成主题也仍是手写 CSS，不会改成下面这套生成结果。

## 从几个数生成

`color`、`typography`、`radius`、`motion` 会算出一批 token，写进主题对象。同名的 `tokens` 优先。`var(--color-accent)` 这种引用会改成带 `--astryx-` 前缀的名字。

`color.accent` 是一个十六进制颜色，也可以写成 `[浅色, 深色]`。调色板按 HCT 来：色相和彩度取自种子色，明度走固定阶梯，再配成浅色和深色。不写 accent、只写 `neutralStyle` 时，强调色保持原样，只重算中性色。`--color-on-accent` 按种子色算好对比色；弱化色、文字强调色和图标强调色则引用 `--color-accent`，所以后面改强调色时它们会跟着走。

字号用 `typography.scale` 的 `base` 和 `ratio`，按 `base × ratio` 的步长取整。圆角用 `radius` 的 `base` 和 `multiplier`，`none` 和 `full` 不动。时长用 `motion` 的 `fast`、`medium`、`slow` 和 `ratio`。最小档是基准乘以 ratio，最大档是基准除以 ratio，再取整到 5 毫秒。

`components` 按换肤挂点覆盖样式，挂点写成 `组件名-部位`，比如 `button`、`card-header`。`base` 作用在挂点本身，`variant:secondary` 作用在 `[data-variant="secondary"]` 上。`Theme` 把编译结果放进 `astryx.overrides` 层，只作用在自己的 `data-astryx-theme` 子树里，遇到嵌套的主题就停下。`localTokens`、`adaptations`、已删除的旧挂点名和 `extends` 的细节，见 [扩展与自定义主题](/themes/extending)。语义图标表仍然是 `icons`，按名字换成 Iconify id。

```ts
const brand = defineTheme({
  name: 'brand',
  color: { accent: '#0064E0' },
  typography: { scale: { base: 14, ratio: 1.2 } },
  radius: { base: 4, multiplier: 1 },
  motion: { fast: 125, medium: 300, slow: 700, ratio: 0.75 },
})
```

只要 token、不要组件时，也可以直接调用 `expandColorScale`、`expandTypeScale`、`expandRadiusScale`、`expandMotionScale`。它们返回的还是不带前缀的名字，交给 `defineTheme()` 的 `tokens` 即可。

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | token 名带上 `--`，例如 `--color-accent` 或 `--astryx-color-accent`。 |
| 推荐 | 明暗两套颜色不同时，写成 `[浅色, 深色]`。 |
| 推荐 | 某一块区域要换一套主题时，再嵌一层 `Theme`。 |
| 避免 | 用 `Theme` 去翻转一张图片上的文字。那是 `MediaTheme` 的事。 |

## 示例

### Theme — Apply Theme

用 Theme 包住一块子树，这块区域里的组件都改用这套主题。

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

const forestTheme = defineTheme({
  name: 'forest-docs',
  tokens: {
    '--color-accent': ['#15803D', '#86EFAC'],
    '--color-background-card': ['#FFFFFF', '#0F3D24'],
    '--color-text-primary': ['#052E16', '#DCFCE7'],
    '--color-text-secondary': ['#166534', '#BBF7D0'],
    '--color-border': ['#BBF7D0', '#15803D66'],
  },
})
</script>

<template>
  <Section variant="muted" :padding="4" :max-width="420">
    <Theme :theme="forestTheme">
      <Card :padding="4" width="100%">
        <Stack direction="vertical" :gap="3">
          <Heading :level="4">
            Forest workspace
          </Heading>
          <Text type="body" color="secondary">
            Wrap any subtree to apply a theme locally.
          </Text>
          <Button label="Create project" variant="primary" />
        </Stack>
      </Card>
    </Theme>
  </Section>
</template>
```

### Theme — Nested Theme

嵌套的 Theme 只改里面那一块，外面的主题保持不变。

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

const warmTheme = defineTheme({
  name: 'warm-docs',
  tokens: {
    '--color-accent': ['#D97706', '#FBBF24'],
    '--color-background-surface': ['#FFF7ED', '#1F1300'],
    '--color-background-card': ['#FFFBEB', '#2A1A05'],
    '--color-text-primary': ['#3B2F15', '#FEF3C7'],
    '--color-text-secondary': ['#92400E', '#FCD34D'],
    '--color-border': ['#FED7AA', '#92400E66'],
    '--radius-container': '20px',
  },
})

const forestTheme = defineTheme({
  name: 'forest-docs',
  tokens: {
    '--color-accent': ['#15803D', '#86EFAC'],
    '--color-background-surface': ['#F0FDF4', '#052E16'],
    '--color-background-card': ['#FFFFFF', '#0F3D24'],
    '--color-text-primary': ['#052E16', '#DCFCE7'],
    '--color-text-secondary': ['#166534', '#BBF7D0'],
    '--color-border': ['#BBF7D0', '#15803D66'],
    '--radius-container': '8px',
  },
})
</script>

<template>
  <Section variant="muted" :padding="4" :max-width="460">
    <Theme :theme="warmTheme">
      <Card :padding="4" width="100%">
        <Stack direction="vertical" :gap="4">
          <Stack direction="vertical" :gap="2">
            <Heading :level="4">
              Outer Warm theme
            </Heading>
            <Text type="body" color="secondary">
              The parent section uses Warm.
            </Text>
          </Stack>
          <Theme :theme="forestTheme">
            <Card :padding="3" width="100%">
              <Stack direction="vertical" :gap="2">
                <Text type="body" weight="bold">
                  Nested Forest section
                </Text>
                <Button label="Nested action" variant="primary" size="sm" />
              </Stack>
            </Card>
          </Theme>
        </Stack>
      </Card>
    </Theme>
  </Section>
</template>
```

### Theme — Switch Themes

用状态切换传给 Theme 的主题对象，下面的外观跟着换。

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { Heading } from '@astryx-vue/core/Heading'
import { Section } from '@astryx-vue/core/Section'
import { Selector } from '@astryx-vue/core/Selector'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { defineTheme, Theme } from '@astryx-vue/core/Theme'
import { computed, ref } from 'vue'

const warmTheme = defineTheme({
  name: 'warm-docs',
  tokens: {
    '--color-accent': ['#D97706', '#FBBF24'],
    '--color-background-surface': ['#FFF7ED', '#1F1300'],
    '--color-background-card': ['#FFFBEB', '#2A1A05'],
    '--color-text-primary': ['#3B2F15', '#FEF3C7'],
    '--color-text-secondary': ['#92400E', '#FCD34D'],
    '--color-border': ['#FED7AA', '#92400E66'],
    '--radius-container': '20px',
  },
})

const forestTheme = defineTheme({
  name: 'forest-docs',
  tokens: {
    '--color-accent': ['#15803D', '#86EFAC'],
    '--color-background-surface': ['#F0FDF4', '#052E16'],
    '--color-background-card': ['#FFFFFF', '#0F3D24'],
    '--color-text-primary': ['#052E16', '#DCFCE7'],
    '--color-text-secondary': ['#166534', '#BBF7D0'],
    '--color-border': ['#BBF7D0', '#15803D66'],
    '--radius-container': '8px',
  },
})

const themes = {
  Warm: warmTheme,
  Forest: forestTheme,
}

const themeName = ref<'Warm' | 'Forest'>('Warm')
const theme = computed(() => themes[themeName.value])
const options = [
  { value: 'Warm', label: 'Warm' },
  { value: 'Forest', label: 'Forest' },
]
</script>

<template>
  <Section variant="muted" :padding="4" :max-width="420">
    <Stack direction="vertical" :gap="3">
      <Selector v-model="themeName" label="Theme" :options="options" />
      <Theme :theme="theme">
        <Card :padding="4" width="100%">
          <Stack direction="vertical" :gap="3">
            <Heading :level="4">
              {{ themeName }} preview
            </Heading>
            <Text type="body" color="secondary">
              Switching the theme object updates all tokens and component styles below the provider.
            </Text>
            <Button label="Save changes" variant="primary" />
          </Stack>
        </Card>
      </Theme>
    </Stack>
  </Section>
</template>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `theme` | `ThemeDefinition` | — | `defineTheme()` 返回的主题。 |
| `mode` | `'light' \| 'dark' \| 'system'` | `'system'` | 颜色模式。`system` 跟随页面和系统。 |

## 插槽

| 名称 | 说明 |
| --- | --- |
| `default` | 继承这套主题的内容。 |

## defineTheme

| 字段 | 说明 |
| --- | --- |
| <code>name</code> | 主题名，写到 <code>data-astryx-theme</code>。 |
| <code>tokens</code> | 语义 token。二元组先写浅色，再写深色。同名时盖过生成结果。 |
| <code>localTokens</code> | 只有这一套主题用的值，键是完整的 CSS 变量名，原样保存。不能用共享 token 的名字。 |
| <code>color</code> | 用强调色生成调色板。accent 可省略，只改中性色。 |
| <code>typography</code> | 字体，以及从 base、ratio 生成的字号。 |
| <code>radius</code> | 从 base 和 multiplier 生成圆角。 |
| <code>motion</code> | 从 fast、medium、slow 和 ratio 生成时长。 |
| <code>components</code> | 按换肤挂点覆盖样式，挂点写成 <code>组件名-部位</code>。编译结果在主题对象的 <code>css</code> 上。 |
| <code>adaptations</code> | 按屏幕宽度、指针、对比度或减弱动效生效的规则，按顺序排，后面满足的赢。 |
| <code>extends</code> | 另一份 <code>defineTheme()</code> 主题。它算好的东西全部带过来，当前这份的值优先。 |
| <code>syntax</code> | 子树里代码的语法色。更近的 <code>SyntaxTheme</code> 优先。 |
| <code>onDark</code> | 深色 <code>MediaTheme</code> 表面上的额外 token 和组件样式。 |
| <code>onLight</code> | 浅色 <code>MediaTheme</code> 表面上的额外 token 和组件样式。 |
| <code>icons</code> | 语义图标名到 Iconify id。按名字覆盖，和被扩展的主题浅合并。 |
| <code>indicators</code> | 指示器图形，按 check、checkbox、radio 替换。一处替换，所有使用处一起变。 |

## useTheme

`useTheme()` 读取最近的主题。不在 `Theme` 里时，名字是 `neutral`，`token()` 读文档上的 CSS 变量。读到的是主题本身的值，环境规则只在 CSS 里生效。返回的对象和 `token` 函数只创建一次，`tokens` 只在主题或明暗模式变了的时候才变。

| 字段 | 说明 |
| --- | --- |
| <code>name</code> | 最近的主题名。 |
| <code>mode</code> | 解析后的 <code>light</code> 或 <code>dark</code>。<code>system</code> 会被解析掉。 |
| <code>token(name)</code> | 当前模式下的一个 token。局部 token 按原名读。没人设置过的图表颜色返回默认值。 |
| <code>tokens</code> | 最近这份主题声明过的 token 和局部 token，按当前模式解析。 |

## 模板引用

组件暴露 `element`，类型是 `HTMLElement | null`。

## 原生属性

根节点是 `display: contents` 的 `div`。`class` 和 `style` 会写到它上面。其他属性不转发。
