# 图标

## 使用方法

`Icon` 按名字画一个图标。名字来自 Iconify 的集合，不需要为每个图形单独 import 一个组件。

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

<template>
  <Icon name="carbon:settings" />
</template>
```

写法是 `集合:图标`，和 Iconify 的 id 相同。集合注册之后，`carbon-settings` 也指向 Carbon 的 settings。集合名本身带连字符时请保留冒号，例如 `material-symbols:home`，避免被拆错。

`@astryx-vue/core` 不打包这些图形。装你要用的集合，再用 Vite 插件只把源码里写到的图标打进去：

```bash
pnpm add -D @iconify-json/carbon
```

```ts
import { astryxIcons } from '@astryx-vue/core/icons'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue(), astryxIcons()],
})
```

`astryxIcons()` 会读取项目依赖里的 `@iconify-json/*`。源码里的 `name="carbon:settings"` 会变成一个很小的模块，只注册这一枚图标，Carbon 的其余部分不会进包。再换一套也一样：安装 `@iconify-json/lucide`，然后写 `name="lucide:settings"`。不需要装 UnoCSS。

插件只能看到写在源码里的名字。名字要到运行时才拼出来时，自己注册整套集合，这一整套都会进包：

```ts
import { addCollection } from '@astryx-vue/core/Icon'
import carbon from '@iconify-json/carbon/icons.json'

addCollection(carbon)
```

在图标渲染之前调用，一般放在应用入口。`addIcon('carbon:settings', data)` 用来注册单独的一枚。

不传 `size` 时，图标是 `1em` 见方，颜色用 `currentColor`，跟周围的文字走。[Button](/components/button) 的 `icon` 和 `end` 槽、[IconButton](/components/icon-button)，以及输入框开头和结尾的槽，盒子尺寸已经定好。把 `Icon` 放进去，不要再设 `size`。

```vue
<Button label="设置">
  <template #icon>
    <Icon name="carbon:settings" />
  </template>
</Button>
```

装饰性图标默认 `aria-hidden`。只有图标自己承载含义、旁边又没有文字时，才设置 `label`。父级按钮或链接已经有名字时不要再设。

`icon` 仍然可以收一个 CSS 类名，或你自己的组件。导航上的 `iconClass` 也还是类名。这些用法同样不要求 UnoCSS。

## 语义名

组件内部说的是用途，不是某一套图形的文件名。搜索、关闭、勾选、向下的箭头，都是这种名字。默认指向 Lucide 里做同一件事的图标。那一套还没注册时，页面上仍是内置字形，不会空出一块。

换一整套时，在主题里把语义名指到 Iconify 的 id。子主题只写要改的名字，其余沿用上一层。直接指定某一枚图标仍然用 `name`，它优先于语义名。

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

<template>
  <Theme
    :theme="defineTheme({
      name: 'withCarbon',
      icons: { search: 'carbon:search', close: 'carbon:close' },
    })"
  >
    <slot />
  </Theme>
</template>
```

完整名单在属性页。读取当前 id 用 `useSemanticIcon`，它和 `Icon` 从同一个入口导出。

### 结构

- **符号**: 为所选图标画出的视觉符号。

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | 同一块界面里用同一套集合。线框和实心混在一行里，会显得没画完。 |
| 推荐 | 放进按钮或输入框的槽时不要再传 `size`。槽已经定好盒子，`1em` 会铺满它。 |
| 推荐 | 图标单独出现、旁边没有说明它是什么的文字时，设置 `label`。 |
| 推荐 | 只安装用到的集合。插件打包的是你写到的图标，不是整套。 |
| 避免 | 每个图标 import 一个组件。对外的用法是 `name`，查找交给插件。 |
| 避免 | 在已经有名字的按钮或链接里再给图标设 `label`。辅助技术会把名字读两遍。 |
| 避免 | 运行时拼接名字，却不调用 `addCollection` 或 `addIcon`。插件看不到这个名字，图标会是空的。 |

## 示例

### 基础

上面是 md 尺寸的 carbon:search。下面两行没有设 size，settings 跟在正文和大号文字后面，字号变了，图标跟着变。

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <VStack :gap="4" h-align="start">
    <Icon name="carbon:search" color="primary" size="md" />
    <Text>
      <Icon name="carbon:settings" /> Settings
    </Text>
    <Text size="lg">
      <Icon name="carbon:settings" /> Settings
    </Text>
  </VStack>
</template>
```

### 尺寸

以 16px 为根字号时，xsm 是 12px，sm 是 16px，md 是 20px，lg 是 24px。

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <HStack :gap="4">
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="xsm" />
      <Text type="supporting">
        xsm
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="sm" />
      <Text type="supporting">
        sm
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="md" />
      <Text type="supporting">
        md
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="lg" />
      <Text type="supporting">
        lg
      </Text>
    </VStack>
  </HStack>
</template>
```

### 语义色

语义色。primary、secondary、tertiary、disabled 跟着文字的层次走。accent、success、error、warning 本身带有含义。

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <HStack :gap="4" wrap="wrap">
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" color="primary" />
      <Text type="supporting">
        primary
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:menu" color="secondary" />
      <Text type="supporting">
        secondary
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:information" color="tertiary" />
      <Text type="supporting">
        tertiary
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:time" color="disabled" />
      <Text type="supporting">
        disabled
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:calendar" color="accent" />
      <Text type="supporting">
        accent
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:checkmark-filled" color="success" />
      <Text type="supporting">
        success
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:error" color="error" />
      <Text type="supporting">
        error
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:warning" color="warning" />
      <Text type="supporting">
        warning
      </Text>
    </VStack>
  </HStack>
</template>
```

### 色板

用来分组的色板。这些颜色不表示状态。

```vue
<script setup lang="ts">
import type { IconColor } from '@astryx-vue/core/Icon'
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const colors = [
  'blue',
  'red',
  'green',
  'gray',
  'cyan',
  'teal',
  'yellow',
  'orange',
  'pink',
  'purple',
] as const satisfies readonly IconColor[]
</script>

<template>
  <HStack :gap="4" wrap="wrap">
    <VStack v-for="color in colors" :key="color" :gap="1" h-align="center">
      <Icon name="carbon:search" :color="color" />
      <Text type="supporting">
        {{ color }}
      </Text>
    </VStack>
  </HStack>
</template>
```

### 状态

状态行同时有图标和文字。文字已经说明发生了什么，图标是装饰，不需要 label。

```vue
<script setup lang="ts">
import type { IconColor } from '@astryx-vue/core/Icon'
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const statuses: { name: string, color: IconColor, label: string }[] = [
  { name: 'carbon:checkmark-filled', color: 'success', label: 'Deployed successfully' },
  { name: 'carbon:warning', color: 'warning', label: 'Build has warnings' },
  { name: 'carbon:error', color: 'error', label: 'Pipeline failed' },
  { name: 'carbon:information', color: 'accent', label: 'New version available' },
]
</script>

<template>
  <VStack :gap="3">
    <HStack v-for="status in statuses" :key="status.label" align="center" :gap="2">
      <Icon :name="status.name" :color="status.color" size="sm" />
      <Text type="body">
        {{ status.label }}
      </Text>
    </HStack>
  </VStack>
</template>
```

### 在按钮里

放在 Button 的前置槽和后置槽、IconButton，以及输入框的开头和结尾。都没有设 size，盒子由槽决定。

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { TextInput } from '@astryx-vue/core/TextInput'
import { ref } from 'vue'

const email = ref('')
</script>

<template>
  <VStack :gap="4" h-align="start">
    <HStack :gap="3" wrap="wrap">
      <Button label="Settings" variant="primary">
        <template #icon>
          <Icon name="carbon:settings" />
        </template>
      </Button>
      <Button label="Continue" variant="secondary">
        <template #end>
          <Icon name="carbon:chevron-right" />
        </template>
      </Button>
      <IconButton label="Search">
        <Icon name="carbon:search" />
      </IconButton>
    </HStack>
    <div style="width: 280px; max-width: 100%">
      <TextInput v-model="email" label="Email" placeholder="ada@example.com">
        <template #icon>
          <Icon name="carbon:email" />
        </template>
        <template #end>
          <Icon name="carbon:view" />
        </template>
      </TextInput>
    </div>
  </VStack>
</template>
```

### 随主题换图形

同一组语义名。第一行是内置字形。后两行把搜索、关闭、勾选和向下箭头换成 Carbon 和 Heroicons。

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

const names = ['search', 'close', 'check', 'chevronDown'] as const

const carbon = defineTheme({
  name: 'withCarbon',
  icons: {
    search: 'carbon:search',
    close: 'carbon:close',
    check: 'carbon:checkmark',
    chevronDown: 'carbon:chevron-down',
  },
})

const hero = defineTheme({
  name: 'hero-icons',
  icons: {
    search: 'heroicons:magnifying-glass-20-solid',
    close: 'heroicons:x-mark-20-solid',
    check: 'heroicons:check-20-solid',
    chevronDown: 'heroicons:chevron-down-20-solid',
  },
})
</script>

<template>
  <div class="icon-theme-board">
    <div class="icon-theme-row">
      <Icon v-for="name in names" :key="name" :icon="name" size="lg" />
      <span>Built in</span>
    </div>
    <Theme :theme="carbon">
      <div class="icon-theme-row">
        <Icon v-for="name in names" :key="name" :icon="name" size="lg" />
        <span>Carbon</span>
      </div>
    </Theme>
    <Theme :theme="hero">
      <div class="icon-theme-row">
        <Icon v-for="name in names" :key="name" :icon="name" size="lg" />
        <span>Heroicons</span>
      </div>
    </Theme>
  </div>
</template>
```

## 无障碍

- 不传 `label` 时，根元素是 `aria-hidden="true"`，里面的 SVG 也隐藏。
- `label` 非空时，根元素变为 `role="img"` 并带上 `aria-label`，同时去掉 `aria-hidden`。没有相邻文字的状态图标才需要它。
- 空字符串的 `label` 仍按装饰处理。
- 父级按钮或链接已经有名字时，不要再设 `label`。
- 图形使用 `currentColor`。只在元素上写死一个十六进制颜色，不会跟着主题走；要换色就用 `color`。

## 属性

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `name` | `string` | — | Iconify 的 id，格式是 `集合:图标`。carbon 集合注册之后，`carbon-settings` 与 `carbon:settings` 相同。集合名里带连字符时请保留冒号。 |
| `icon` | `string \| Component` | — | 语义名、CSS 图标类，或你自己的组件。语义名由最近的主题换成 Iconify id；该图标尚未注册时用内置字形。设置了 `name` 时忽略它。 |
| `color` | `'inherit' \| 'primary' \| 'secondary' \| 'tertiary' \| 'disabled' \| 'accent' \| 'success' \| 'error' \| 'warning' \| 'blue' \| 'cyan' \| 'green' \| 'gray' \| 'orange' \| 'pink' \| 'purple' \| 'red' \| 'teal' \| 'yellow'` | `'inherit'` | 决定图标里 `currentColor` 解析成哪种颜色。`inherit` 保持周围文字的颜色。 |
| `size` | `'xsm' \| 'sm' \| 'md' \| 'lg'` | — | 根字号 16px 时分别是 12px、16px、20px、24px。不传时，`name` 图标是 `1em`。其他图标在没有父级尺寸时回退到 `md`。 |
| `label` | `string` | — | 有含义的图标的无障碍名称。会设置 `role="img"` 和 `aria-label`。装饰性图标省略它，保持 `aria-hidden="true"`。空字符串仍按装饰处理。 |

## 插槽

- 默认插槽：内联 SVG。在既没有 <code>name</code> 也没有 <code>icon</code> 时使用。

## 注册

| 名称 | 来自 | 说明 |
| --- | --- | --- |
| `astryxIcons()` | `@astryx-vue/core/icons` | Vite 插件。扫描静态名字，只从已安装的 `@iconify-json/*` 打包这些图标。用 `collections` 限制读取哪些前缀。 |
| `addIcon(name, data)` | `@astryx-vue/core/Icon` | 注册一枚图标。`name` 是 `集合:图标`。`data.body` 是 SVG 内部标记。 |
| `addCollection(collection)` | `@astryx-vue/core/Icon` | 注册一份 `@iconify-json` 文档。整套集合都会进入包。 |

## 语义名

`defineTheme({ icons })` 按名字覆盖。和被扩展的主题浅合并，当前这份优先。没写到的名字保持默认 id。集合要已经注册，否则仍画内置字形。

| 名字 | 默认 Iconify id |
| --- | --- |
| `close` | `lucide:x` |
| `chevronDown` | `lucide:chevron-down` |
| `chevronLeft` | `lucide:chevron-left` |
| `chevronRight` | `lucide:chevron-right` |
| `chevronsLeft` | `lucide:chevrons-left` |
| `chevronsRight` | `lucide:chevrons-right` |
| `check` | `lucide:check` |
| `success` | `lucide:circle-check` |
| `error` | `lucide:circle-x` |
| `warning` | `lucide:triangle-alert` |
| `info` | `lucide:info` |
| `calendar` | `lucide:calendar` |
| `clock` | `lucide:clock` |
| `externalLink` | `lucide:external-link` |
| `menu` | `lucide:menu` |
| `moreHorizontal` | `lucide:ellipsis` |
| `search` | `lucide:search` |
| `upload` | `lucide:upload` |
| `arrowUp` | `lucide:arrow-up` |
| `arrowDown` | `lucide:arrow-down` |
| `arrowsUpDown` | `lucide:arrow-up-down` |
| `funnel` | `lucide:funnel` |
| `eyeSlash` | `lucide:eye-off` |
| `viewColumns` | `lucide:columns-3` |
| `copy` | `lucide:copy` |
| `checkDouble` | `lucide:check-check` |
| `wrench` | `lucide:wrench` |
| `stop` | `lucide:square` |
| `microphone` | `lucide:mic` |
| `image` | `lucide:image` |

表里这些是**共享名字**，所有组件共用一套。`IconRegistry` 类型表示「每个共享名字都画了」的完整注册表，`upload` 也在必填之列。自己写一整套主题图标时，要给 `upload` 单独配一个上传图形，不能拿 `arrowUp` 顶替。换 `upload` 不会影响表格排序箭头或发送按钮用的 `arrowUp`。

只有某一个组件自己用的图标，不进共享名字，而是用带命名空间的键，写成 `组件:用途`，类型是 `NamespacedIconName`。现在有一个：

| 键 | 用在哪里 |
| --- | --- |
| `numberInput:stepperDown` | NumberInput 的加减按钮。向下的小箭头，加号那一格把它翻过来用。 |

主题用同一个 `icons` 覆盖它：`defineTheme({ icons: { 'numberInput:stepperDown': 'carbon:chevron-down' } })`。不覆盖时用内置字形。加新的命名空间键不会让 `IconRegistry` 多出必填项。

`useSemanticIcon(name)` 从 `@astryx-vue/core/Icon` 引入，也从包根导出。未知字符串、主题也没有覆盖时，返回空，调用方可以继续把它当 CSS 类。

## 主题

颜色走图标令牌。主题在根上覆盖对应变量即可。

```css
.my-theme {
  --astryx-icon-accent-foreground: #0074e2;
}
```

- `--astryx-icon-primary-foreground`
- `--astryx-icon-secondary-foreground`
- `--astryx-icon-disabled-foreground`
- `--astryx-icon-accent-foreground`
- `--astryx-icon-success-foreground`
- `--astryx-icon-error-foreground`
- `--astryx-icon-warning-foreground`
- `--astryx-icon-blue-foreground`，cyan、green、gray、orange、pink、purple、red、teal、yellow 同样是这个形式。

## 无障碍

根元素是 `<span>`。没有 `label` 时为 `aria-hidden="true"`。`label` 非空时为 `role="img"`，这段文字就是无障碍名称。内部 SVG 始终 `aria-hidden`。

`class` 和 `style` 会落到根元素上。`name` 不会：它用来选择图标。
