# Web Components —— `<tvu-*>` 自定义元素用法

> AUTO-GENERATED by `scripts/generate-react-bindings.mjs` — ⛔ 不要手改。
> 真源：`src/web-components/components.config.ts`（与 npm 包的 `./web-components` 出口同源）。

## 0. 什么时候读这一份

**做可交互预览 / HTML 原型时** —— 你要的是能在页面里真正渲染出来的 TVU 组件。

- 画设计稿 / mockup → 走 `01` 组件目录 + `02` 组件语义 + `03` 交付规范，不是这一份。
- 生成 Vue 代码 → 走 `08` 安装用法 + `02` canonical 组件与 props，也不是这一份。
- **预览里要出现真实组件** → 这一份。

## 1. 最小骨架（可直接抄）

```html
<link rel="stylesheet" href="./tokens/variables.css">
<script src="./assets/tvu-web-components.js"></script>

<tvu-button size="M" fill="filling">Save</tvu-button>
```

- 脚本**加载即自动注册**全部元素 —— 预览页一行 JS 都不用写。
- token 由 `variables.css` 挂在 `:root`，会穿过 shadow DOM 边界继承进组件内部；
  少了它组件会渲染成无主题色的样子。

## 2. ⛔ 属性名一律 kebab-case

组件内部的属性名是 camelCase（`fixedWidth`），**HTML attribute 必须写 kebab-case**（`fixed-width`）。

🔴 **写错不会报错，只会静默失效** —— HTML attribute 名大小写不敏感，`fixedWidth` 到不了那个属性，
组件按默认值渲染。截图上看不出来。⇒ 下表「属性」列给的**已经是可以直接写进 HTML 的形式**，照抄即可。

## 3. 元素清单

共 37 个元素。

### `<tvu-button>`

Vue 侧对应组件：`Button`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `color` | `'gray 1' \| 'green' \| 'orange' \| 'red' \| 'blue'` |
| `fixed-width` | `'no' \| 'yes'` <br>（内部属性名 `fixedWidth`） |
| `icon` | `'left' \| 'light' \| 'loading' \| 'loading button' \| 'no' \| 'right'` |
| `radius` | `'round' \| 'square'` |
| `size` | `'XS' \| 'S' \| 'M' \| 'L'` |
| `status` | `'default' \| 'disable' \| 'hover' \| 'loading'` |
| `fill` | `'filling' \| 'ghost' \| 'rimless'` |
| `theme` | `'dark' \| 'light'` |

- **事件**：`click`（用 `addEventListener` 监听）
- **默认插槽**：直接写子内容即可。

### `<tvu-input>`

Vue 侧对应组件：`Input`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `placeholder` | `string` |
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `status` | `'Placeholder' \| 'Value' \| 'Filled'` |
| `enable` | `'on' \| 'off'` |
| `ux` | `'default' \| 'error' \| 'click' \| 'hover'` |
| `size` | `'M' \| 'L' \| 'XL'` |
| `feature` | `'no' \| 'yes' \| 'text count'` |
| `readonly` | `boolean` |

- **绑定值**：DOM 属性 `modelValue`（`string`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-form-item>`

Vue 侧对应组件：`FormItem`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `label` | `string` |
| `required` | `boolean` |
| `error` | `string` |
| `hint` | `string` |
| `label-width` | `'120 px' \| '200 px' \| 'Dynamic'` <br>（内部属性名 `labelWidth`） |
| `layout` | `'1 line' \| '1 line & Right' \| '2 lines'` |
| `status` | `'Error' \| 'Normal'` |
| `theme` | `'Dark' \| 'Light'` |
| `type` | `'Label & checkbox' \| 'Label & Input' \| 'Label & Radio' \| 'Label & Selector' \| 'Label & Switch' \| 'Label & Textarea'` |
| `prop` | `string` |
| `rules` | `unknown` |

- **默认插槽**：直接写子内容即可。
- **具名插槽**：`<div slot="label">`

### `<tvu-form>`

Vue 侧对应组件：`Form`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `model` | `Record<string, unknown>` |
| `rules` | `unknown` |
| `label-width` | `'120 px' \| '200 px' \| 'Dynamic'` <br>（内部属性名 `labelWidth`） |
| `layout` | `'1 line' \| '1 line & Right' \| '2 lines'` |
| `disabled` | `boolean` |

- **默认插槽**：直接写子内容即可。

### `<tvu-badge>`

Vue 侧对应组件：`Badge`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `color` | `'Neutral' \| 'Blue' \| 'Green' \| 'Orange' \| 'Red'` |
| `fill` | `'Filled' \| 'Line'` |
| `type` | `'Circle' \| 'Rectangle'` |

- **默认插槽**：直接写子内容即可。

### `<tvu-pill-status>`

Vue 侧对应组件：`PillStatus`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `status` | `'On-air' \| 'Preview' \| 'Analyzing' \| 'Inactive'` |
| `active` | `boolean` |
| `count` | `number \| string` |

- **默认插槽**：直接写子内容即可。
- **具名插槽**：`<div slot="count">`

### `<tvu-progress>`

Vue 侧对应组件：`Progress`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `size` | `'M' \| 'S'` |
| `status` | `'default' \| 'error' \| 'success' \| 'warning'` |
| `theme` | `'dark' \| 'light'` |
| `value` | `number` |
| `show-label` | `boolean` <br>（内部属性名 `showLabel`） |


### `<tvu-rating>`

Vue 侧对应组件：`Rating`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `readonly` | `boolean` |

- **绑定值**：DOM 属性 `value`（`'1' | '2' | '3' | '4' | '5'`），变化时派发 `update:value`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-breadcrumb-item>`

Vue 侧对应组件：`BreadcrumbItem`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `href` | `string` |
| `show-separator` | `boolean` <br>（内部属性名 `showSeparator`） |
| `separator-type` | `'arrow' \| 'slash'` <br>（内部属性名 `separatorType`） |
| `state` | `'current' \| 'Default' \| 'disabled' \| 'hover'` |

- **默认插槽**：直接写子内容即可。

### `<tvu-top-bar>`

Vue 侧对应组件：`TopBar`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `tag` | `'After Login' \| 'Before Login'` |
| `title` | `string` |
| `show-menu` | `boolean` <br>（内部属性名 `showMenu`） |
| `show-search-box` | `boolean` <br>（内部属性名 `showSearchBox`） |

- **具名插槽**：`<div slot="logo">` · `<div slot="left">` · `<div slot="search">` · `<div slot="menu">` · `<div slot="right-content">`

### `<tvu-switch>`

Vue 侧对应组件：`Switch`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `enable` | `'yes' \| 'no'` |
| `loading` | `'yes' \| 'no'` |

- **绑定值**：DOM 属性 `status`（`'off' | 'on' | 'live'`），变化时派发 `update:status`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-check-box>`

Vue 侧对应组件：`CheckBox`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `enable` | `'yes' \| 'no'` |
| `readonly` | `boolean` |

- **绑定值**：DOM 属性 `status`（`'off' | 'on' | 'some'`），变化时派发 `update:status`。⚠️ 这个要用 JS 设，不是 HTML attribute。
- **默认插槽**：直接写子内容即可。

### `<tvu-radio>`

Vue 侧对应组件：`Radio`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `enable` | `'yes' \| 'no'` |

- **绑定值**：DOM 属性 `status`（`'off' | 'on'`），变化时派发 `update:status`。⚠️ 这个要用 JS 设，不是 HTML attribute。
- **默认插槽**：直接写子内容即可。

### `<tvu-pill-counter>`

Vue 侧对应组件：`PillCounter`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `kind` | `'success' \| 'warning' \| 'info' \| 'neutral'` |

- **默认插槽**：直接写子内容即可。

### `<tvu-breadcrumb>`

Vue 侧对应组件：`Breadcrumb`。

无属性。

- **默认插槽**：直接写子内容即可。

### `<tvu-input-box-filled>`

Vue 侧对应组件：`InputBoxFilled`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `placeholder` | `string` |
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `status` | `'Placeholder' \| 'Value'` |
| `enable` | `'on' \| 'off'` |
| `ux` | `'default' \| 'error' \| 'click' \| 'hover'` |
| `size` | `'M' \| 'L' \| 'XL'` |
| `feature` | `'no' \| 'yes' \| 'text count'` |
| `readonly` | `boolean` |

- **绑定值**：DOM 属性 `modelValue`（`string`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-input-number>`

Vue 侧对应组件：`InputNumber`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `type` | `'Default' \| 'Only Add' \| 'Only Reduce' \| 'Readonly'` |
| `min` | `number` |
| `max` | `number` |
| `step` | `number` |
| `disabled` | `boolean` |

- **绑定值**：DOM 属性 `modelValue`（`number`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-slider>`

Vue 侧对应组件：`Slider`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `size` | `'M' \| 'S'` |
| `theme` | `'dark' \| 'light'` |
| `min` | `number` |
| `max` | `number` |
| `step` | `number` |
| `disabled` | `boolean` |
| `show-value` | `boolean` <br>（内部属性名 `showValue`） |

- **绑定值**：DOM 属性 `modelValue`（`number`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-pagination>`

Vue 侧对应组件：`Pagination`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `type` | `'Classic' \| 'Simple' \| 'Small'` |
| `total` | `number` |
| `page-size` | `number` <br>（内部属性名 `pageSize`） |

- **绑定值**：DOM 属性 `modelValue`（`number`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。
- **事件**：`update:pageSize`（用 `addEventListener` 监听）

### `<tvu-tab>`

Vue 侧对应组件：`Tab`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `fill` | `'Line' \| 'Text' \| 'Filled' \| 'line' \| 'text' \| 'filled'` |
| `color` | `'White' \| 'Green' \| 'white' \| 'green'` |

- **绑定值**：DOM 属性 `modelValue`（`string | number`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。
- **默认插槽**：直接写子内容即可。

### `<tvu-tab-list>`

Vue 侧对应组件：`TabList`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `items` | `{ label: string; value: string \| number; disabled?: boolean }[]` |
| `fill` | `'Line' \| 'Text' \| 'Filled' \| 'line' \| 'text' \| 'filled'` |

- **绑定值**：DOM 属性 `modelValue`（`string | number`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。
- **默认插槽**：直接写子内容即可。

### `<tvu-tab-item>`

Vue 侧对应组件：`TabItem`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `disabled` | `boolean` |
| `state` | `'Normal' \| 'Active' \| 'normal' \| 'active'` |
| `color` | `'White' \| 'Green' \| 'white' \| 'green'` |
| `fill` | `'Line' \| 'Text' \| 'Filled' \| 'line' \| 'text' \| 'filled'` |
| `value` | `string \| number` |

- **默认插槽**：直接写子内容即可。

### `<tvu-steps>`

Vue 侧对应组件：`Steps`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `active` | `number` |
| `current` | `number` |
| `direction` | `'horizontal' \| 'vertical'` |
| `type` | `'number' \| 'icon'` |

- **默认插槽**：直接写子内容即可。

### `<tvu-step-item>`

Vue 侧对应组件：`StepItem`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `current` | `number` |
| `description` | `string` |
| `direction` | `'horizontal' \| 'vertical'` |
| `icon-name` | `string` <br>（内部属性名 `iconName`） |
| `index` | `number` |
| `show-leading-connector` | `boolean` <br>（内部属性名 `showLeadingConnector`） |
| `show-trailing-connector` | `boolean` <br>（内部属性名 `showTrailingConnector`） |
| `state` | `'pending' \| 'active' \| 'completed'` |
| `type` | `'number' \| 'icon'` |
| `title` | `string` |


### `<tvu-popup-box>`

Vue 侧对应组件：`PopupBox`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `theme` | `'dark' \| 'light'` |
| `title` | `string` |
| `width` | `string \| number` |
| `closable` | `boolean` |
| `close-on-backdrop` | `boolean` <br>（内部属性名 `closeOnBackdrop`） |
| `close-on-escape` | `boolean` <br>（内部属性名 `closeOnEscape`） |
| `show-footer` | `boolean` <br>（内部属性名 `showFooter`） |
| `cancel-text` | `string` <br>（内部属性名 `cancelText`） |
| `confirm-text` | `string` <br>（内部属性名 `confirmText`） |

- **绑定值**：DOM 属性 `visible`（`boolean`），变化时派发 `update:visible`。⚠️ 这个要用 JS 设，不是 HTML attribute。
- **事件**：`close` · `cancel` · `confirm`（用 `addEventListener` 监听）
- **默认插槽**：直接写子内容即可。
- **具名插槽**：`<div slot="footer">`

### `<tvu-select-box-line>`

Vue 侧对应组件：`SelectBoxLine`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `placeholder` | `string` |
| `options` | `{ label: string; value: string \| number }[]` |
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `status` | `'Placeholder' \| 'Value' \| 'multi select'` |
| `enable` | `'on' \| 'off'` |
| `ux` | `'default' \| 'error' \| 'hover' \| 'click' \| 'editable'` |
| `size` | `'M' \| 'L'` |
| `feature` | `'default' \| 'time' \| 'date'` |
| `multiple` | `boolean` |
| `editable` | `boolean` |

- **绑定值**：DOM 属性 `modelValue`（`string | number | Array<string | number>`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-select-box-filled>`

Vue 侧对应组件：`SelectBoxFilled`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `placeholder` | `string` |
| `options` | `{ label: string; value: string \| number }[]` |
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `status` | `'Placeholder' \| 'Value' \| 'multi select'` |
| `enable` | `'on' \| 'off'` |
| `ux` | `'default' \| 'error' \| 'hover' \| 'click' \| 'editable'` |
| `size` | `'M' \| 'L'` |
| `feature` | `'default' \| 'time' \| 'date'` |
| `multiple` | `boolean` |
| `editable` | `boolean` |

- **绑定值**：DOM 属性 `modelValue`（`string | number | Array<string | number>`），变化时派发 `update:modelValue`。⚠️ 这个要用 JS 设，不是 HTML attribute。

### `<tvu-drop-down-list-select>`

Vue 侧对应组件：`DropDownListSelect`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `dark-theme` | `'on' \| 'off'` <br>（内部属性名 `darkTheme`） |
| `type` | `'Radio' \| 'Multi' \| 'Operation List' \| 'Sort By'` |
| `items` | `{ label: string; value: string; checked: boolean; active: boolean; disabled: boolean }[]` |
| `listbox-id` | `string` <br>（内部属性名 `listboxId`） |

- **事件**：`select` · `close`（用 `addEventListener` 监听）

### `<tvu-tooltip>`

Vue 侧对应组件：`Tooltip`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `dark-theme` | `'off' \| 'on'` <br>（内部属性名 `darkTheme`） |
| `pointing` | `'Center down' \| 'Center up' \| 'left down' \| 'left up' \| 'right down' \| 'right up'` |
| `content` | `string` |
| `disabled` | `boolean` |
| `open` | `boolean` |

- **默认插槽**：直接写子内容即可。
- **具名插槽**：`<div slot="content">`

### `<tvu-notification>`

Vue 侧对应组件：`Notification`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `form` | `'dialog' \| 'alert' \| 'pop confirm' \| 'slide'` |
| `type` | `'warning' \| 'danger' \| 'error' \| 'info' \| 'success' \| 'default'` |
| `theme` | `'dark' \| 'light'` |
| `title` | `string` |
| `description` | `string` |
| `closable` | `boolean` |
| `cancel-text` | `string` <br>（内部属性名 `cancelText`） |
| `confirm-text` | `string` <br>（内部属性名 `confirmText`） |
| `ok-text` | `string` <br>（内部属性名 `okText`） |

- **事件**：`close` · `cancel` · `confirm`（用 `addEventListener` 监听）

### `<tvu-message>`

Vue 侧对应组件：`Message`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `status` | `'success' \| 'info' \| 'error' \| 'warning'` |
| `size` | `'M' \| 'L'` |
| `closable` | `boolean` |


### `<tvu-table>`

Vue 侧对应组件：`Table`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `align` | `'Center' \| 'Left' \| 'Right'` |
| `type` | `'Header' \| 'Tbody'` |
| `columns` | `readonly { key: string; title: string; align?: 'left' \| 'center' \| 'right'; width?: string \| number; showLeftIcon?: boolean; leftIconName?: string; showRightIcon?: boolean; rightIconName?: string; sortOrder?: 'asc' \| 'desc' \| null; cell?: { kind: 'text' } \| { kind: 'pill'; map?: Record<string, { status?: 'On-air' \| 'Preview' \| 'Analyzing' \| 'Inactive'; active?: boolean; label?: string }> } \| { kind: 'button'; label?: string; actionKey?: string; color?: 'gray 1' \| 'green' \| 'orange' \| 'red' \| 'blue'; fill?: 'filling' \| 'ghost' \| 'rimless'; size?: 'L' \| 'M' \| 'S' \| 'XS'; iconName?: string } \| { kind: 'actions'; items: { label?: string; actionKey: string; color?: 'gray 1' \| 'green' \| 'orange' \| 'red' \| 'blue'; fill?: 'filling' \| 'ghost' \| 'rimless'; size?: 'L' \| 'M' \| 'S' \| 'XS'; iconName?: string }[] } \| { kind: 'link'; href?: string; hrefKey?: string; label?: string; actionKey?: string } \| { kind: 'icon'; name?: string; map?: Record<string, string> } }[]` |
| `data` | `Record<string, unknown>[]` |
| `striped` | `boolean` |
| `loading` | `boolean` |
| `row-key` | `string` <br>（内部属性名 `rowKey`） |
| `selected-keys` | `(string \| number)[]` <br>（内部属性名 `selectedKeys`） |

- **具名插槽**：`<div slot="empty">`

### `<tvu-chart>`

Vue 侧对应组件：`Chart`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `type` | `'pie' \| 'donut' \| 'line' \| 'bar' \| 'bar-horizontal' \| 'line-bar'` |
| `datasets` | `{ label: string; data: number[]; type?: 'line' \| 'bar'; color?: string }[]` |
| `labels` | `string[]` |
| `height` | `number` |
| `width` | `number` |


### `<tvu-logo>`

Vue 侧对应组件：`Logo`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `type` | `'tvu' \| 'ts'` |
| `size` | `number` |


### `<tvu-menu-list>`

Vue 侧对应组件：`MenuList`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `items` | `{ label: string; value?: string \| number; icon?: string; badge?: string \| number \| { label?: string \| number; color?: 'Neutral' \| 'Blue' \| 'Green' \| 'Orange' \| 'Red'; fill?: 'Filled' \| 'Line'; type?: 'Circle' \| 'Rectangle' }; active?: boolean; disabled?: boolean }[]` |
| `orientation` | `'horizontal' \| 'vertical'` |

- **事件**：`select`（用 `addEventListener` 监听）

### `<tvu-user-menu>`

Vue 侧对应组件：`UserMenu`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `name` | `string` |
| `email` | `string` |
| `role` | `string` |
| `image` | `string` |
| `color` | `string` |
| `actions` | `{ label: string; value?: string \| number; icon?: string; active?: boolean; disabled?: boolean }[]` |
| `languages` | `{ label: string; value: string }[]` |
| `lang` | `string` |

- **事件**：`action` · `language-change` · `sign-out`（用 `addEventListener` 监听）
- **具名插槽**：`<div slot="panel-top">` · `<div slot="badge">`

### `<tvu-icon>`

Vue 侧对应组件：`Icon`。

| 属性（HTML 写法） | 取值 |
|---|---|
| `name` | `string` |
| `size` | `number` |
| `color` | `string` |


## 4. ⛔ 反模式

- ⛔ **别用 `<button class="tvu-…">` 自己拼**。用了运行时就用真元素；自拼的东西拿不到
  组件的状态机、a11y 属性和主题响应。
- ⛔ **别硬编码颜色 / 间距**。值查 `tokens/variables.css`（或 `tokens/tokens.dtcg.json`），
  写 `var(--brand)` 而不是 `#2fb54e` —— 硬编码在主题切换时就错了。
- ⛔ **别手拼组件已经自带的能力**（分页器的页码、下拉的选中态、Tooltip 的浮层定位…）。
  先查 `reference/composition.json` 的 `built_in_features` 与 `do_not_hand_compose`。
- ⛔ **别照 Vue 的 prop 名写 attribute**（见 §2）。

## 5. 组件怎么嵌套

`reference/composition.json` 里有全部 canonical 组件的 `contains` / `contained_by` 关系图，
以及 `do_not_hand_compose` 反模式清单。页面级合成前先查它，别凭直觉套。
