# smart-ui 深浅色主题适配指南

> 参考：[ConfigProvider 全局配置](https://developer.tuya.com/material/smartui?comId=theme) · [ConfigProvider 最佳实践](https://github.com/Tuya-Community/miniapp-smart-ui/wiki/ConfigProvider-%E7%BB%84%E4%BB%B6%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5)

---

## 1. 主题机制概述

涂鸦 APP 会向小程序注入一组**全局 CSS 变量**（`--app-B1` ~ `--app-B6`、`--app-M1` ~ `--app-M5` 等）。当 APP 从浅色切换到深色后，这些变量的值也会随之变化。

SmartUI 的组件样式正是引用了这些 APP 全局变量作为默认值，例如：

```css
.smart-button--default {
  color: var(--button-default-color, var(--app-B1-N1, rgba(0, 0, 0, 1)));
}
```

`--button-default-color` 未定义时，回退到 `--app-B1-N1`——当 APP 是深色主题时，该值为白色；浅色主题时为黑色。因此 **SmartUI 组件默认跟随 APP 主题自动切换**，大多数场景无需手动适配。

运行时可通过 `ty.getThemeInfo()` 或 `usePanelConfig` API 获取当前主题变量。

---

## 2. 主题设置方式（三选一）

### 2.1 global.config.ts 配置 darkmode（全局）

在 `global.config.ts` 的 `tuya` 对象中设置 `darkmode`，让整个小程序应用 APP 的对应主题变量：

```ts
// global.config.ts
import { GlobalConfig } from '@ray-js/types';

export const tuya = {
  darkmode: 'dark', // 'light' | 'dark'
  window: {
    backgroundColor: '--app-B1',
    navigationBarBackgroundColor: '--app-B2',
    navigationBarTextStyle: '--app-B2-N1',
  },
};

const globalConfig: GlobalConfig = { basename: '' };
export default globalConfig;
```

设置 `darkmode: 'dark'` 后，所有 `:root[theme='dark']` 内的样式也会生效：

```less
:root {
  &[theme='dark'] {
    --button-default-background-color: #1a1a1a;
  }
}
```

### 2.2 ConfigProvider theme 属性（组件树级别，v2.8.0+）

`ConfigProvider` 的 `theme` 属性会依据**涂鸦 APP 官方的深/浅色变量值**，强制覆盖其子树的公共颜色变量。适用于需要跨 OEM APP 保持一致主题的场景。

```tsx
import { ConfigProvider, Cell, CellGroup } from '@ray-js/smart-ui';

<ConfigProvider theme="dark">
  <CellGroup>
    <Cell title="标题" value="内容" />
    <Cell title="标题" value="内容" label="描述" isLink />
  </CellGroup>
</ConfigProvider>
```

典型用法——在 `composeLayout.tsx` 中全局包裹：

```tsx
import { ConfigProvider } from '@ray-js/smart-ui';

const composeLayout = (SubComp: React.ComponentType<any>) => {
  return class PanelComponent extends Component<Props, State> {
    render() {
      return (
        <Provider store={store}>
          <ConfigProvider theme="dark">
            <SubComp {...this.props} />
          </ConfigProvider>
        </Provider>
      );
    }
  };
};
```

**何时使用 ConfigProvider theme**：部分 OEM APP 即使在浅色模式下也使用偏深色的主题变量，导致设置 `darkmode: 'light'` 后 SmartUI 组件仍然偏深。此时用 `<ConfigProvider theme="light">` 可强制使用涂鸦 APP 官方浅色变量值，确保一致性。

### 2.3 ConfigProvider themeVars 属性（精细定制）

通过 `themeVars` 可直接覆盖任意组件级 CSS 变量（camelCase 形式）：

```tsx
<ConfigProvider
  themeVars={{
    buttonPrimaryBackgroundColor: '#0D84FF',
    buttonPrimaryBorderColor: '#0D84FF',
    cellTextColor: '#ffffff',
    cellBackgroundColor: 'rgba(255, 255, 255, 0.1)',
  }}
>
  {children}
</ConfigProvider>
```

| ConfigProvider Props | 说明 | 类型 | 默认值 |
|-----|------|------|--------|
| `theme` (v2.8.0) | 主题模式，强制应用官方深/浅色变量 | `'light' \| 'dark'` | - |
| `themeVars` | 自定义主题变量（camelCase），覆盖组件级 CSS 变量 | `object` | - |

---

## 3. APP 全局 CSS 变量体系

APP 注入的 CSS 变量遵循 `--app-{区域}{-N层级}` 命名规则：

### 3.1 区域变量（B1~B6）

| 变量前缀 | 用途 | 浅色典型值 | 深色典型值 |
|----------|------|-----------|-----------|
| `--app-B1` | 页面背景 | `rgba(246, 247, 251, 1)` | `rgba(0, 0, 0, 1)` |
| `--app-B2` | 顶部导航栏 | `rgba(255, 255, 255, 1)` | `rgba(26, 26, 26, 1)` |
| `--app-B3` | 卡片 | `rgba(255, 255, 255, 1)` | `rgba(26, 26, 26, 1)` |
| `--app-B4` | 弹窗 | `rgba(255, 255, 255, 1)` | `rgba(26, 26, 26, 1)` |
| `--app-B5` | 底部导航栏 | `rgba(255, 255, 255, 1)` | `rgba(26, 26, 26, 1)` |
| `--app-B6` | 列表 | `rgba(255, 255, 255, 1)` | `rgba(25.5, 25.5, 25.5, 1)` |

### 3.2 字体/图标层级（N1~N9）

每个区域变量都有 N1~N9 子变量控制文字/图标颜色，以 B1 为例：

| 变量 | 用途 | 浅色值 | 深色值 |
|------|------|--------|--------|
| `--app-B1-N1` | 主文字 | `rgba(0,0,0,0.9)` | `rgba(255,255,255,1)` |
| `--app-B1-N2` | 副文字 | `rgba(0,0,0,0.7)` | `rgba(255,255,255,0.7)` |
| `--app-B1-N3` | 辅助文字 | `rgba(0,0,0,0.5)` | `rgba(255,255,255,0.5)` |
| `--app-B1-N4` | 占位/禁用 | `rgba(0,0,0,0.3)` | `rgba(255,255,255,0.3)` |
| `--app-B1-N6` | 分割线 | `rgba(0,0,0,0.2)` | `rgba(255,255,255,0.2)` |
| `--app-B1-N7` | 浅分割/背景 | `rgba(0,0,0,0.1)` | `rgba(255,255,255,0.1)` |
| `--app-B1-N9` | 极浅背景 | `rgba(0,0,0,0.03)` | `rgba(255,255,255,0.1)` |

B2~B6 具有相同的 N1~N9 层级结构。

### 3.3 功能色（M1~M5）

| 变量前缀 | 用途 |
|----------|------|
| `--app-M1` | 品牌色、按钮 |
| `--app-M2` | 强警告、错误提示 |
| `--app-M3` | 成功、开关、推荐 |
| `--app-M4` | 正向引导、链接 |
| `--app-M5` | 轻度警告 |

### 3.4 SmartUI 公共变量

| 变量 | 用途 |
|------|------|
| `--smart-ui-overlay` | 遮罩层 |
| `--smart-ui-dialog-background` | 弹窗背景色 |
| `--smart-ui-bottom-sheet-dragger-node-background` | BottomSheet 拖拽条 |
| `--smart-ui-border-image` | 渐变边框 |

---

## 4. CSS 中使用 APP 变量

### 4.1 直接引用（推荐）

在任意 CSS/Less 文件中直接使用 APP 变量，组件会自动跟随 APP 主题：

```less
.myBox {
  background-color: var(--app-B3);
  .title {
    color: var(--app-B3-N1, rgba(0, 0, 0, 0.9));
  }
  .subtitle {
    color: var(--app-B3-N3, rgba(0, 0, 0, 0.5));
  }
}
```

### 4.2 按主题分别书写样式

不跟随 APP 变量、自行区分深浅色：

```less
:root {
  .valueText {
    color: black;
  }
  &[theme='dark'] {
    .valueText {
      color: white;
    }
  }
}
```

---

## 5. 组件级 CSS 变量覆盖

SmartUI 每个组件都暴露了细粒度的 CSS 变量。当需要**局部**调整时，可在 CSS 中覆盖，也可通过 `ConfigProvider themeVars` 全局覆盖。

### ⚠️ 样式覆盖三属性的作用范围

SmartUI 组件统一提供 `className`、`customClass`、`customStyle` 三个属性，**作用范围不同**：

| 属性 | 挂载位置 | CSS 变量是否能穿透到内部节点 | 适用场景 |
|------|---------|---------------------------|---------|
| `className` | **外层包装标签**（如 `<nav-bar-index-404ec1>`） | ❌ 不能 — 外层标签与内部节点存在组件边界 | 控制组件外部的布局（margin、display 等） |
| `customClass` | **内部根节点**（如 `.smart-nav-bar`） | ✅ 能 — CSS 变量从根节点向下级联 | **覆盖组件 CSS 变量的首选方式** |
| `customStyle` | **内部根节点**（行内样式） | ✅ 能 — 行内变量同样级联 | 少量动态样式或一次性覆盖 |

**常见错误**：将 CSS 变量写在 `className` 对应的样式类中，结果变量无法穿透到组件内部，组件文字颜色等不生效。

```tsx
// ❌ 错误：className 的 CSS 变量不会穿透到组件内部
<NavBar className={styles.myNav} />
// .myNav { --nav-bar-home-text-color: #fff; }  ← 不生效

// ✅ 正确：customClass 挂载在内部根节点，CSS 变量可以级联
<NavBar customClass={styles.myNav} />
// .myNav { --nav-bar-home-text-color: #fff; }  ← 生效
```

> **此规则适用于所有 SmartUI 组件**（NavBar、Tabs、Cell、Button 等）。需要通过 CSS 变量覆盖内部样式时，一律使用 `customClass`，不要使用 `className`。

### 5.1 NavBar

```tsx
<NavBar
  title="页面标题"
  leftText="取消"
  border={false}
  background="var(--app-B2)"
  safeAreaInsetTop
  customStyle={{
    '--nav-bar-title-text-color': 'var(--app-B2-N1)',
    '--nav-bar-text-color': 'var(--app-B2-N1)',
  } as React.CSSProperties}
/>
```

#### 完整 CSS 变量表

| 变量 | 作用 | 默认值 |
|------|------|--------|
| `--nav-bar-height` | 导航栏高度 | `var(--app-device-navbar-height, 46px)` |
| `--nav-bar-background-color` | 背景色 | `var(--app-B2, #ffffff)` |
| `--nav-bar-title-text-color` | 标题文字颜色（`title` prop） | `var(--app-B2-N1)` |
| `--nav-bar-title-font-size` | 标题文字大小 | `17px` |
| `--nav-bar-title-font-weight` | 标题字重 | `600` |
| `--nav-bar-text-color` | 左右侧文案颜色（`leftText`/`rightText`） | `var(--app-B2-N2)` |
| `--nav-bar-text-font-size` | 侧边文字大小 | `17px`（v2.7.3） |
| `--nav-bar-text-font-weight` | 侧边文字字重 | `normal`（v2.7.3） |
| `--nav-bar-home-text-color` | **首页文字颜色**（`leftTextType="home"` 时） | `var(--app-B2-N1)` |
| `--nav-bar-home-font-size` | 首页文字大小 | `22px` |
| `--nav-bar-home-font-weight` | 首页字重 | `600` |
| `--nav-bar-arrow-color` | 返回箭头颜色 | `var(--app-B2-N1)` |
| `--nav-bar-icon-color` | 图标颜色 | `var(--app-B2-N1)` |
| `--nav-bar-right-text-color` | 右侧文字颜色（v2.5.1） | `var(--app-B2-N1)` |

#### ⚠️ `leftTextType` 与 CSS 变量的对应关系

NavBar 的左侧文字支持多种类型，**不同类型使用不同的 CSS 变量**：

| `leftTextType` 值 | 效果 | 控制颜色的 CSS 变量 |
|-------------------|------|-------------------|
| 不设置（默认） | 普通左侧文字 | `--nav-bar-text-color` |
| `"home"` | 首页样式（左对齐、大字加粗） | **`--nav-bar-home-text-color`** |
| `"title"` | 左侧标题样式 | `--nav-bar-text-color` |

**常见错误**：使用 `leftTextType="home"` 时，设置 `--nav-bar-text-color` 无效，必须设置 `--nav-bar-home-text-color`。

```tsx
// ❌ 错误：home 类型不读取 --nav-bar-text-color
<NavBar
  leftText="设备名称"
  leftTextType="home"
  customClass={styles.darkNav}
/>
// .darkNav { --nav-bar-text-color: #fff; }  ← 对 home 类型无效

// ✅ 正确：使用 --nav-bar-home-text-color
// .darkNav { --nav-bar-home-text-color: #fff; }  ← 生效
```

#### NavBar 深色背景完整示例

```tsx
// 二级页面（使用 title）
<NavBar
  title="编辑情景"
  leftArrow
  background="transparent"
  customClass={styles.darkNav}
/>

// 首页（使用 leftTextType="home"）
<NavBar
  leftText="设备名称"
  leftTextType="home"
  background="transparent"
  customClass={styles.darkNavHome}
/>
```

```less
.darkNav {
  --nav-bar-background-color: transparent;
  --nav-bar-title-text-color: #ffffff;
  --nav-bar-text-color: #ffffff;
  --nav-bar-arrow-color: #ffffff;
  --nav-bar-icon-color: #ffffff;
}

.darkNavHome {
  --nav-bar-background-color: transparent;
  --nav-bar-home-text-color: #ffffff;
  --nav-bar-icon-color: #ffffff;
}
```

### 5.2 Cell

Cell 组件使用 `customClass` + CSS 变量适配（部分版本 Cell 不支持 `customStyle`）：

```tsx
<Cell
  title="选项"
  isLink
  border={false}
  customClass={styles.darkCell}
/>
```

```less
.darkCell {
  background-color: var(--app-B3) !important;
  --cell-text-color: var(--app-B3-N1);
  --cell-right-icon-color: var(--app-B3-N1);
  --cell-value-color: var(--app-B3-N2);
  --cell-label-color: var(--app-B3-N3);
}
```

### 5.3 Button

```tsx
{/* 主色按钮 */}
<Button round block color="var(--app-M1)">
  保存
</Button>

{/* 朴素按钮 - 自定义背景 */}
<Button
  round
  block
  plain
  color="#00CE52"
  customStyle={{
    backgroundColor: 'rgba(0, 206, 82, 0.1)',
    borderColor: 'transparent',
  }}
>
  预览
</Button>
```

Button 组件关键 CSS 变量：

| 变量 | 作用 |
|------|------|
| `--button-default-color` | 默认按钮文字色 |
| `--button-default-background-color` | 默认按钮背景色 |
| `--button-primary-background-color` | 主按钮背景色 |
| `--button-primary-color` | 主按钮文字色 |
| `--button-default-height` | 按钮高度（默认 48px） |

### 5.4 Tabs

设计稿中的**分段选择器**（Segmented Control）可使用 SmartUI `Tabs` 组件 `type="card"` 模式实现。通过 CSS 变量自定义卡片样式：

```tsx
<Tabs
  active={activeCategory}
  onChange={onCategoryChange}
  type="card"
  customClass={styles.darkTabs}
>
  <Tab title="风景" name="landscape" />
  <Tab title="生活" name="life" />
  <Tab title="节日" name="festival" />
  <Tab title="心情" name="mood" />
</Tabs>
```

```less
.darkTabs {
  --tabs-card-background-color: rgba(255, 255, 255, 0.1);
  --tabs-card-active-background-color: #1082FE;
  --tabs-card-text-color: rgba(255, 255, 255, 0.5);
  --tabs-card-text-active-color: #ffffff;
  --tabs-card-border-radius: 24rpx;
  --tabs-card-active-border-radius: 18rpx;
  --tabs-card-height: 64rpx;
  --tabs-card-padding: 8rpx;
  --tabs-background-color: transparent;
}
```

| 变量 | 作用 | 默认值 |
|------|------|--------|
| `--tabs-background-color` | 背景色 | `var(--app-B3, #fff)` |
| `--tabs-default-color` | 默认文字颜色 | `var(--app-M4)` |
| `--tabs-line-height` | line 模式文字行高 | `32px` |
| `--tabs-bottom-bar-height` | line 模式底部滑块高度 | `3px` |
| `--tabs-bottom-bar-color` | line 模式底部滑块颜色 | `var(--tabs-default-color)` |
| `--tabs-card-background-color` | card 模式背景色 | `var(--app-B6-N9)` |
| `--tabs-card-text-color` | card 模式文字颜色 | `var(--app-B6-N3)` |
| `--tabs-card-text-active-color` | card 模式选中文字颜色 | `var(--app-B6-N1)` |
| `--tabs-card-height` | card 模式高度 | `32px` |
| `--tabs-card-border-radius` | card 模式外层圆角 | `8px` |
| `--tabs-card-active-border-radius` | card 模式选中滑块圆角 | `6px` |
| `--tabs-card-active-background-color` | card 模式选中滑块背景色 | `var(--app-B3, #fff)` |
| `--tabs-card-padding` | card 模式内边距 | `2px` |

### 5.5 Slider

Slider 内部使用 `@ray-js/components-ty-slider`，支持 camelCase API：

```tsx
<Slider
  value={speedValue}
  min={0}
  max={100}
  step={1}
  maxTrackHeight="12px"
  minTrackHeight="12px"
  maxTrackRadius="6px"
  minTrackRadius="6px"
  maxTrackColor="var(--app-B1-N7)"
  minTrackColor="var(--app-M1)"
  thumbWidth={24}
  thumbHeight={24}
  thumbColor="#ffffff"
  thumbBoxShadowStyle="0px 4px 6px rgba(0, 0, 0, 0.15)"
  onAfterChange={(v: number) => setSpeedValue(v)}
/>
```

### 5.5 Field

| 变量 | 作用 | 默认值 |
|------|------|--------|
| `--field-label-color` | label 颜色 | `var(--app-B6-N1)` |
| `--field-input-text-color` | 输入文字颜色 | `var(--app-B6-N1)` |
| `--field-placeholder-text-color` | placeholder 颜色 | `var(--app-B6-N4)` |
| `--field-subtitle-text-color` | 副标题颜色 | `var(--app-B6-N3)` |

---

## 6. 动态主题（运行时切换）

通过 state 控制 ConfigProvider 的 `theme` 属性实现运行时切换：

```tsx
import { ConfigProvider, Cell, CellGroup } from '@ray-js/smart-ui';
import { useState } from 'react';

export default function Demo() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  return (
    <ConfigProvider theme={theme}>
      <Button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </Button>
      <CellGroup>
        <Cell title="标题" value="内容" />
      </CellGroup>
    </ConfigProvider>
  );
}
```

---

## 7. 自定义主题 Provider 模式

对于完全自定义的深色面板，可结合 `themeVars` 和业务配色统一管理：

```tsx
import { ConfigProvider } from '@ray-js/smart-ui';

interface ThemeConfig {
  themeData: {
    fontColor: string;
    background: string;
    themeColor: string;
    borderColor: string;
  };
  type: 'light' | 'dark';
}

export const GlobalThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const themeConfig: ThemeConfig = {
    themeData: {
      fontColor: '#fff',
      themeColor: '#39A9FF',
      background: '#1E2436',
      borderColor: 'rgba(255, 255, 255, 0.1)',
    },
    type: 'dark',
  };

  return (
    <ConfigProvider
      theme={themeConfig.type}
      themeVars={{
        cellTextColor: themeConfig.themeData.fontColor,
        cellBackgroundColor: themeConfig.themeData.background,
      }}
    >
      <View style={{ background: themeConfig.themeData.background }}>
        {children}
      </View>
    </ConfigProvider>
  );
};
```

---

## 8. 等宽双按钮布局

Button 组件的 `customStyle.flex` 无法可靠参与 Flex 布局。使用 **View 包裹**：

```tsx
<View className={styles.bottomBar}>
  <View className={styles.btnWrap}>
    <Button round block plain color="#00CE52">预览</Button>
  </View>
  <View className={styles.btnWrap}>
    <Button round block color="#0D84FF">保存</Button>
  </View>
</View>
```

```less
.bottomBar {
  display: flex;
  flex-direction: row;
  gap: 16rpx;
  padding: 32rpx 24rpx;
  padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
}
.btnWrap {
  flex: 1;
}
```

---

## 9. Button 内嵌 SVG 图标

Button 的 `icon` 属性使用 CSS mask 渲染，复杂 SVG 容易变成纯色方块。推荐使用 `@ray-js/svg`：

```tsx
import Svg from '@ray-js/svg';

function PreviewIcon() {
  return (
    <Svg width="16px" height="16px" viewBox="0 0 16 16">
      <path d="M2 3.5A1.5..." fill="#00CE52" />
    </Svg>
  );
}

<Button round block plain color="#00CE52">
  <View className={styles.btnContent}>
    <PreviewIcon />
    <Text>预览</Text>
  </View>
</Button>
```

| 方案 | 适用场景 | 限制 |
|------|---------|------|
| Button `icon` prop + data URI | 简单单色图标 | CSS mask 渲染，复杂 SVG 变方块 |
| CSS `background-image` data URI | 纯装饰图标 | 不支持交互，颜色固定 |
| **`@ray-js/svg` Svg 组件** | 所有场景 | 需安装 npm 包，原生 SVG 渲染最清晰 |

---

## 10. 适配决策流程

```
设计稿是否有深色背景？
├─ 否 → 使用默认主题，SmartUI 组件自动跟随 APP
├─ 是 → 整个面板都是深色？
│   ├─ 是 → 方案 A: global.config.ts 设置 darkmode: 'dark'
│   │        或 方案 B: composeLayout 中 <ConfigProvider theme="dark">
│   └─ 否（局部深色） → 局部包裹 <ConfigProvider theme="dark">
│        或使用 customClass + CSS 变量覆盖
└─ 需要跨 OEM 保持一致？
    └─ 是 → 必须使用 <ConfigProvider theme="light|dark"> 强制覆盖
```

---

## 11. 适配检查清单

- [ ] 确认主题设置方式：`darkmode` / `ConfigProvider theme` / `themeVars`
- [ ] 使用 APP 变量（`var(--app-B1-N1)` 等）而非硬编码颜色值
- [ ] NavBar 通过 `background` + CSS 变量适配
- [ ] Cell 使用 `customClass`（非 customStyle）+ CSS 变量
- [ ] Slider 使用 camelCase API（maxTrackColor 等），颜色引用 APP 变量
- [ ] Button 用 View 包裹实现 flex 等宽布局
- [ ] SVG 图标用 `@ray-js/svg` 渲染
- [ ] 自定义样式中的颜色值使用 `var(--app-Bx-Ny)` 确保主题跟随
