首页
/ Multica 移动端 RNR 迁移实践:shadcn 式组件体系、CSS 变量暗色主题与三阶段落地方案

Multica 移动端 RNR 迁移实践:shadcn 式组件体系、CSS 变量暗色主题与三阶段落地方案

2026-09-05 10:28:23作者:曹令琨Iris

本篇基于 Multica 移动端的迁移设计文档 rnr-migration.md,完整拆解一次"手写 React Native 组件 → react-native-reusables(RNR)+ 类级暗色模式"的迁移工程:为什么选 RNR 而不是 Tamagui/Gluestack,如何用"iOS 原生 > RNR > 讨论"瀑布收敛 18 个手写 Sheet,如何用 CSS 变量 + darkMode: 'class' 搭出 Light / Dark / System 三档主题切换,以及三阶段(Phase 0/1/2/3)的推进节奏与已验证的源码实现。读完你可以复用到自己的 NativeWind 4 + Expo SDK 55 项目中,并了解每个设计决策背后的源码依据。

1. 背景:迁移要解决的三个问题

文档开篇给出了迁移前的基线状态(注意:这描述的是 Phase 1 之前的状态,主题部分如今已修复,组件部分仍是待办):

  • apps/mobile/components/ui/ 下有 21 个手写组件,约 1,379 行,全部基于裸 <View/Text/Pressable/Modal> 构建;
  • apps/mobile/components/ 下有 18 个手写 sheet/modal 文件,全部复制同一种形态(Modal transparent fade + 手绘背景遮罩)。移动端基线文档 CLAUDE.md 的 Lesson 6 早已记录这种模式对多数内容形态是错的,并由此衍生出一串 bug:键盘挤压、maxHeight 截断 FlatList、Modal 内部 useSafeAreaInsets 返回 0;
  • 没有任何暗色/亮色主题基础设施:tailwind.config.js 用硬编码 hex 值,global.css 只有三条 @tailwind 指令,没有 CSS 变量、没有 darkMode、没有主题切换器。

移动端基线(CLAUDE.md)自 SDK 55 bootstrap 起就把 RNR 定位为 shadcn 的移动端等价物,但 RNR 从未真正安装。本文档记录了"为什么迁移到 RNR、评估过哪些替代方案、迁移如何分阶段推进"三件事,面向所有会动到 apps/mobile/components/ui/ 或给移动端加新 UI 的开发者。

2. 备选方案评估

评估维度有四项:(a) 与现有技术栈的契合度(NativeWind 4、Tailwind 3.4、Expo SDK 55、React 19);(b) 所有权模型(锁定 vs 复制粘贴);(c) 打包/编译开销;(d) 无障碍基线。

栈契合度 所有权 构建成本 无障碍 结论
react-native-reusables (RNR) NativeWind 4 + RN-Primitives + CVA —— 与本项目完全一致 复制粘贴,代码归你 零(只是 CSS + JSX) RN-Primitives(焦点管理、ARIA 等价物) 入选
Tamagui 自带编译器 + 自带 styled API,非 NativeWind 库锁定 Babel/bundler 配置 + 学习成本 内置 否决 —— 核心价值是 web+native 同代码库(移动端独立,用不上)
Gluestack UI v2 自带 styled API;可以配合 NativeWind 用但非原生路径 可复制粘贴 @react-native-aria(强) 否决 —— 已有 NativeWind 在跑,换样式体系没有收益
NativeBase / RN Paper / RN UI Lib 成熟但库绑定 锁定 视情况 否决 —— 长生命周期安装型 App 需要不依赖上游发版就能打组件补丁的能力,复制粘贴哲学很关键

RNR 的"组件归你所有"模型还与桌面/Web 端的模式(packages/ui/components/ui/ 也是 shadcn 复制粘贴)保持一致——整个代码库一种心智模型。

3. 决策:RNR 及其治理承诺

采用 RNR 时,文档立下了若干承诺。前两条是原则,约束迁移期每个 PR 以及之后的每个 UI 决策:

3.1 默认值优先(Defaults first)

使用任何 RNR 组件时,接受它的默认 variant、默认尺寸、默认间距、默认调色板。除非有明确的产品需求,不要加包装层、不要做"改进版"默认值、不要造 variant="multicaCustom" 之类的样式。手写遗留代码恰恰就是因为有人去要了标准原语的"稍微改进版"——在 RNR 底下重演这个模式等于白迁。

具体推论:

  • Phase 1 原样使用 shadcn 默认 neutral 调色板(light + dark)。Multica 自有自定义 token(brandsuccesswarninginfoprioritycode-surface)追加进去,但暗色值不在有实证需求之前预先创作——它们先复制亮色值,直到真正使用它们的页面在暗色下出问题为止。
  • Phase 2/3:npx @rnr/cli add <component> 写出文件后,不要立刻调它的样式;API 有差异时改调用方。
  • Tier C"基础升级"指的是把裸 <Text> 换成 RNR 的 Text、把内联条件判断换成 cva —— 指重新设计或添加组件原本没有的 variant。

3.2 iOS 原生 > RNR > 讨论

新增任何交互时走这条瀑布,命中即停:

  1. iOS / RN 有原生 API? 直接用,不要用 Modal 包一层去模拟;
  2. RNR 有对应组件? npx @rnr/cli add <name>,用默认 variant;
  3. 都没有。 停下来问用户,不要闷头手写。

第 4 节 Tier B 的修订分类就依赖这个"原生 API 层"——很多既有 sheet 换成 ActionSheetIOS / Alert.prompt / 原生日期选择器后会整个消失。

场景 原生 API
文本输入提示(单字段) Alert.prompt(title, msg, callback)
确认 / 破坏性操作提示 Alert.alert
N 选 1 动作表 ActionSheetIOS.showActionSheetWithOptions
日期 / 时间选择 @react-native-community/datetimepicker(已安装)
图片 / 相机 expo-image-picker(已安装)
文档 expo-document-picker(已安装)
分享 react-nativeShare.share
触感反馈(确认 / 错误 / 选中) expo-haptics(已安装)

3.3 主题方案

类级暗色模式(darkMode: 'class')+ CSS 变量,应用内 Settings → Appearance 提供 light / dark / system 三档选择,选择持久化在 expo-secure-store。RNR 的默认 Tailwind 配置就是 class 模式,与"需要显式用户设置"的需求匹配(纯 media-query 方案不允许用户覆盖 OS 偏好)。

3.4 组件三层分类

不做一刀切全量迁移。部分遗留组件是领域 UI 而非通用原语,留在原地,但使用 RNR 的地基(Text 组件、语义化 token、CVA variant)。

3.5 硬规则(写入 CLAUDE.md)

新组件要么来自 RNR,要么先走原生 API 层。 迁移删除手写遗留 variant,这条规则防止再次积累。这一规则如今已固化在 CLAUDE.md 的 "UI components & theming" 章节,作为迁移完成后依然有效的持久规则。

4. 组件清单与三层分类

Tier A —— RNR 直接替换(迁移对象)

通用原语中 RNR 有近乎同形等价物的,直接用 npx @react-native-reusables/cli@latest add <name> 的输出替换手写文件,然后清扫调用方:

现有文件 行数 RNR 组件 备注
components/ui/button.tsx 63 button Phase 2 的 canary
components/ui/input.tsx 9 input 平凡
components/ui/text-field.tsx 34 input + label 用 RNR 组合
components/ui/card.tsx 36 card 可直接替换
components/ui/text.tsx 18 text 到处都在用,要小心做(清扫所有 import)
components/ui/autosize-textarea.tsx 89 textarea 需核对一致性 —— 自动增高行为可能要基于 RNR textarea 重实现
components/ui/otp-input.tsx 68 (RNR 目前无等价物) 继续用 input-otp-native,移入 Tier B
components/ui/modal-close-button.tsx 25 平凡 —— sheet 迁移后并入 Dialog 关闭模式

合计约 270 行待替换,含调用方清扫约 4 小时工作量。从当前仓库结构看,components/ui/ 目录在文档撰写之后已经出现了 collapsibledropdown-menuradio-groupseparatorskeletonswitchtabs 等文件,可以推断 Tier A 之后的组件引入已按 RNR 模式在推进,但按文档 Status 行,Phase 2/3 的收尾工作仍以文档记录为准。

Tier B —— Sheets / Modals(套用 §3.2 瀑布)

这一层收益最大(CLAUDE.md Lesson 6 已给重复出现的 sheet bug 建了档)。按"iOS 原生 > RNR > 讨论"瀑布,18 个 sheet 三分:

B.1 —— 原生 API 替换(文件直接删)

现有 sheet 替换为 原因
components/issue/comment-action-sheet.tsx ActionSheetIOS.showActionSheetWithOptions N 选 1 动作菜单 —— 正是 ActionSheetIOS 的用途。推荐作 Phase 3 起手(可见的删除、无样式问题)
components/issue/pickers/due-date-picker-sheet.tsx @react-native-community/datetimepicker 内联选择器 日期选择 —— 原生 API 已安装

B.2 —— 改为 formSheet 路由(已完成)

原计划是把每个 picker-sheet 换成 RNR Select。mobile-sheet-rollout PR 系列最终收敛到了不同形态:每个原 picker-sheet 都拆成 components/<domain>/pickers/ 下的纯 <XxxPickerBody> 组件,嵌入 Expo Router formSheet 路由 app/(app)/[workspace]/<context>/picker/<field>.tsx。这拿到了 iOS UISheetPresentationController 的原生 chrome(grabber + detents + 弹簧拖拽关闭),同时省掉了 RNR Select 仍需要的每个调用点状态与可见性 prop 的周折。本行文件已全部删除,无后续动作。

B.3 —— 真正需要自定义内容 sheet(RNR Dialog pageSheet)

现有 sheet 为什么保留为 Dialog
components/issue/issue-filter-sheet.tsx 一个 sheet 里多个控件(筛选表单),不是列表选择
components/issue/runs-sheet.tsx 运行历史(行 + 操作),不是 N 选 1
components/chat/session-sheet.tsx 待定 —— 到时重新审视,可能归 B.2
components/chat/agent-picker-sheet.tsx 大概率 B.2(Select)—— 待重审
components/project/add-resource-sheet.tsx 待定 —— 取决于单选还是小表单

B.4 —— RNR 没有的

空。唯一一项 components/issue/emoji-picker-sheet.tsx 已通过采纳 rn-emoji-keyboard 解决,评论表情反应流程迁移到 formSheet 路由 app/(app)/[workspace]/issue/[id]/comment/[commentId]/emoji-picker.tsx。移动端现在在每条评论动作表的"更多表情"溢出项后提供完整表情集,与 Web 对齐。

执行规则

  • 不要批量替换 sheet-shell.tsx。它被 18 个文件引用;原子替换 = 18 处同时坏掉。按 CLAUDE.md Lesson 6:一个 sheet 一个 PR,一个 PR 一次验证。
  • B.1 是删除类 —— 应当先做,因为每个都只是删文件、零替换代码(从父组件调 ActionSheetIOS 即可)。复合收益:代码更少 + 符合"默认值优先" + 符合"iOS 原生优先"。
  • B.2 简化调用点,但引入的组件是新代码,排在 B.1 之后。
  • B.3 保留结构复杂度,迁移主要是 Modal 换 RNR Dialog。视觉变化最小,但 bug 修复最大(拖拽关闭、焦点管理、安全区都由 RNR 处理)。

Tier C —— 领域 UI(保留,升级地基)

这些不是通用 shadcn 组件 —— RNR 没有"优先级图标"或"参与者头像"的等价物。它们留在 components/ui/ 以兼容现有 import,但内部构建块必须迁到 RNR 地基:用 RNR 的 Text 替代裸 <Text>,用 cva 做 variant 表(而非内联条件),用语义化 token(text-foreground,永不用 #1f1f23):

  • actor-avatar.tsx(158 行)—— 底层用 RNR 的 avatar 原语,但业务逻辑保留
  • app-header-actions.tsx(51)、avatar-stack.tsx(97)、presence-dot.tsx(44)
  • priority-icon.tsx(80)、project-icon.tsx(49)、project-priority-icon.tsx(71)、project-status-icon.tsx(130)、status-icon.tsx(163)
  • pulse-dot.tsx(52)、screen-header.tsx(37)

Tier C 不排期 —— 某个 bug 或功能触碰到文件时,顺手在该 PR 里升级地基,机会主义而非排期驱动。

5. 主题架构:CSS 变量 + 类级暗色模式

目标:Settings → Appearance 提供 Light / Dark / System 三档,选择持久化在 expo-secure-store(键 theme-preference,值 light / dark / system)。

5.1 分层结构

global.css                  CSS 变量定义于 :root 与 .dark:root(颜色的唯一事实来源)
tailwind.config.js          darkMode: 'class' + 工具类映射到 hsl(var(--...))
lib/theme.ts                CSS 变量的 TypeScript 镜像,导出给 React
                              Navigation 用的 NAV_THEME
lib/use-color-scheme.ts     包装 NativeWind 的 useColorScheme +
                              expo-secure-store 持久化
app/_layout.tsx             启动时读取持久化偏好,调 setColorScheme(...),
                              用 ThemeProvider(NAV_THEME[...]) 包 Stack,
                              为 RNR dialog/popover 挂载 <PortalHost />

5.2 各层实现细节(源码佐证)

global.css:Phase 1 之前 tailwind.config.js 里约 20 个语义化 token 是硬编码 hex。现在它们成了 global.css:root(亮色)与 .dark:root(暗色)下的 CSS 变量。亮色基础值即 shadcn neutral 默认(如 --background: 0 0% 100%--primary: 0 0% 9%),Multica 自定义 token 追加其后:

/* Multica custom tokens */
--brand: 225 71% 58%;
--brand-foreground: 0 0% 98%;
--success: 142 71% 45%;
--warning: 48 89% 47%;
--info: 217 91% 60%;
--priority: 25 95% 53%;
--code-surface: 240 4% 92%;

实际实现比文档更进一步,补了一套 5 级表面高程阶梯(--surface-1 / --surface-2),按 Refactoring UI"≥5% L 差异为可感知阈值"校准:亮色下 L100 页面底 → L98 surface-1(评论气泡)→ L96.1 shadcn secondary → L90 surface-2(气泡内嵌代码块)→ L84 border;暗色下明度随高程升高(阴影在暗色下不成立,色调抬升才是高程线索),--code-surface 是自定义 token 中唯一需要真实暗色值的例外(240 4% 18%——亮色值近白,暗色下会把代码块衬得比页面还亮)。

tailwind.config.jsdarkMode: "class",所有颜色工具类映射为 hsl(var(--background)) 形式,borderWidth 注册了 hairline: hairlineWidth()(来自 nativewind/theme),插件区注册 tailwindcss-animate,并保留移动端的 borderRadius 覆盖:

module.exports = {
  darkMode: "class",
  content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: {
    extend: {
      colors: {
        background: "hsl(var(--background))",
        primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
        // ... 以及 brand / success / warning / code-surface 等自定义 token
      },
      borderWidth: { hairline: hairlineWidth() },
    },
  },
  plugins: [require("tailwindcss-animate")],
};

lib/theme.ts:CSS 变量的 TS 镜像。THEME 是原始 token 对象,供内联样式、动画等 Tailwind class 够不到的场景;NAV_THEME 是给 @react-navigation/nativeThemeProvider 用的主题(header、modal、返回键跟随明暗)。

lib/use-color-scheme.ts:包装 NativeWind 的 useColorScheme(),加上 expo-secure-store 持久化(键 theme-preference)。暴露 { colorScheme, isDarkColorScheme, setPreference }。文档没有细说但源码里写明的一个取舍:首挂载时是异步读取保存的偏好,读取完成前 NativeWind 默认行为(跟随 OS)生效——意味着选了 Dark 的用户在亮色 OS 上冷启动可能短暂闪一下亮色;可接受,因为 secure-store 没有同步后端。

app/_layout.tsx:实际接线处。RootLayout 调用 useColorScheme() 拿到 colorScheme / isDarkColorScheme,用 ThemeProvider value={NAV_THEME[colorScheme]} 包住 StackStatusBar 颜色随模式翻转,<PortalHost />@rn-primitives/portal)挂在 provider 子树末尾供 RNR dialog/popover 使用。

components.json:标准 RNR/shadcn 配置 —— style: "new-york"baseColor: "neutral"cssVariables: true,别名指向 @/components@/lib/utils

metro.config.jswithNativeWind(config, { input: "./global.css", inlineRem: 16 }) —— inlineRem: 16 是 Phase 1 清单里的关键项,保证 CSS 变量按 16px 基准编译。

5.3 为什么是 class 模式而不是 media-query 模式

media-query 模式(@media (prefers-color-scheme: dark))是更简单的默认,但应用无法覆盖它。Settings → Appearance 需要覆盖 OS 偏好,必须 class 模式。代价是启动时一次 setColorScheme() 调用来应用保存的偏好,首次绘制前一次性付清。

5.4 system 选项

用户选 system 时调用 setColorScheme('system')(NativeWind v4 原生支持),框架通过 Appearance.addChangeListener 跟随 OS,无需自己订阅,isDarkColorScheme 响应式更新。

5.5 暗色调色板策略

旧配置里不存在暗色值,必须创作。两个选项:

  1. 直接用 shadcn neutral-base 暗色调色板(RNR 默认安装的 --background: 0 0% 3.9% 等)—— 最快,立刻有能跑的暗色模式,品牌对齐可能要二遍;
  2. 手工创作暗色对齐 Web/桌面暗色主题 —— 慢,但与桌面视觉一致。

Phase 1 选了选项 1换取速度。现在基础设施已验证,后续可以按桌面的 tokens.css 暗色主题做校准(但注意:桌面用 oklch / Tailwind v4,移动端用 hsl / Tailwind v3.4,跨版本共享不现实,分歧是刻意保留的基线)。

6. 三阶段推进计划

Phase 0 —— 研究与文档(本文档)✅

通读 RNR 安装与定制文档、检查移动端现状(tailwind.config.jsglobal.cssapp/_layout.tsxmetro.config.jsbabel.config.js)、更新 CLAUDE.md 的 UI 与主题规则、写下本文档,最后过用户验证关卡。

Phase 1 —— 基础基础设施 ✅ 已完成

目标:装上 RNR 脚手架但不碰任何一个现有组件。验证标准 = App 构建运行与之前完全一致,设置里的主题切换器端到端可用。Phase 1 的 10 项清单全部交付:

  1. 依赖(按 RNR 手动安装第 3 步):npx expo install tailwindcss-animate class-variance-authority clsx tailwind-merge @rn-primitives/portalcva/clsx/tailwind-merge/@rn-primitives/slot 确认已存在,新增的只有 tailwindcss-animate@rn-primitives/portal
  2. metro.config.jsinlineRem: 16
  3. 重写 global.css:root + .dark:root 双调色板,含 Multica 自定义 token);
  4. 重写 tailwind.config.jshsl(var(--...)) 映射、darkMode: 'class'tailwindcss-animate 插件、hairlineWidth() 边框宽度,保留移动端专属覆盖);
  5. 新建 lib/theme.ts(TS 镜像 + NAV_THEME);
  6. 新建 lib/use-color-scheme.ts(持久化包装);
  7. 新建 components.json(标准 RNR 配置);
  8. 更新 app/_layout.tsx(启动时读持久化偏好、首绘前 setColorScheme(...)ThemeProvider(NAV_THEME[...])Stack、provider 末尾挂 <PortalHost />);
  9. Settings → Appearance 选择器:三行(Light / Dark / System)调 setPreference + 持久化,UI 复用既有行模式,无新依赖;
  10. 验证:构建通过、亮色下所有既有页面渲染不变、切 Dark 背景/文字翻转、切 System 跟随模拟器 OS 外观、杀进程重开偏好仍在。

Phase 1 不替换任何现有组件。 按钮、输入、sheet 仍是手写版。暗色模式之所以对既有 hex 派生的语义 token "能用",是因为只是把它们改道经过 CSS 变量 —— 同一个 bg-background class 现在解析到有一个值变两个值的变量。

残留风险(带入 Phase 3):硬编码 hex 的组件(如 <Ionicons color="#71717a">bg-[#fafafa])不响应主题切换。Phase 1 做过一次 #[0-9a-fA-F]{3,6} 的 grep 清扫,每个命中要么换 token,要么标 TODO(rnr-migration): 留给 Phase 3。

Phase 2 —— 首个组件 canary(未开始)

目标:在批量做 20 个之前,用"最简单且非平凡"的组件验证迁移机械流程。选定:button.tsx。理由:调用方数量高(能验证 import 清扫模式)、RNR 的 button 带 variant/size prop 与 shadcn 同形(验证 API 对等)、到处可见(视觉回归立刻明显)。

步骤:

  1. npx @react-native-reusables/cli@latest add button —— CLI 覆盖现有文件(旧文件成为 git 里的 diff 基线);
  2. 新旧 diff,记录 prop 或视觉差异(RNR variant 枚举 vs 自有的、默认尺寸差异);
  3. 清扫所有调用方,API 有差异就在同一 PR 里改调用方;
  4. 模拟器视觉 diff:逐个打开用到 button 的页面,截图与 main 对比;
  5. 亮色暗色双模式验证。

Phase 2 就是一个 PR。验收标准:所有既有按钮仍工作;亮色无回归;暗色下默认值合理。

Phase 3 —— 其余全部

顺序:

  1. 其余 Tier A 原语(input、text-field、card、text、textarea)—— 一个组件或一组一个 PR,同 canary 模式;
  2. text 特殊处理:几乎每个文件都 import 它,单独排 PR;codemod 是机械的(import { Text } from "@/components/ui/text" 已存在,背后文件变化即可);
  3. Tier B sheets —— 一个 sheet 一个 PR,顺序 B.1(原生 API 替换,删文件)→ B.2(Select 替换)→ B.3(Dialog 迁移)。首个 sheet PR = comment-action-sheet.tsxActionSheetIOS,干净的删除、且在实践中验证 §3.2 瀑布。每个 PR 后停下来重新验证;
  4. Tier C 地基升级 —— 机会主义,无排期 PR;
  5. 收尾 PR:删除不再被引用的遗留文件、清掉 Phase 1 hex 清扫留下的 TODO 注释,最终 pnpm typecheck && pnpm lint 干净。

停止规则

  • 连续 3 个 PR 引入视觉回归就暂停,重新审视 token 映射,不要硬推;
  • Tier B sheet 迁不干净(RNR dialog 不适合该场景)时,在本文档记录分歧,保留手写版并标记为"刻意例外"而非"待办"。

7. 已知陷阱(写进反射弧)

研究阶段踩过的坑,动手前先固化:

  1. darkMode: 'class' + .dark:root 是 NativeWind v4 类控制模式下唯一可用的组合。不要用标准 .dark 选择器 —— NativeWind 需要 :root 后缀才能全局应用。global.css 中的选择器就是 .dark:root
  2. setColorScheme() 来自 NativeWind,不是 React Native。react-native import 得到的是只读的 OS 值。NativeWind 版支持 setColorScheme('light' | 'dark' | 'system') 并触发重渲染。
  3. 同步陷阱lib/theme.tsglobal.css 必须互为镜像。只改一边,Tailwind class 样式化的组件看着对,但直接读 THEME 的地方(内联样式、动画、React Navigation chrome)就错了。两份文件的头部注释都写明"改一边必须改另一边,见 rnr-migration.md §5"。
  4. AbortSignal.timeout / AbortSignal.any 在 Hermes 上仍不存在(CLAUDE.md Lesson 5)。与 RNR 迁移无直接关系,但任何自己发网络请求的新组件都要手动 AbortController 模式。
  5. RNR Dialog 内部的 useSafeAreaInsets 与裸 Modal 一样不可靠。CLAUDE.md Lesson 6 的 pageSheet 陷阱仍适用 —— 在父组件读 insets,把 bottomInset 当 prop 传进去。
  6. @rn-primitives/portal<PortalHost /> 位置敏感。 必须是所有 provider 的最后一个子节点;如果挂在频繁重渲染的 provider 内,dialog 会被意外卸载。只在 app/_layout.tsx 放一次。
  7. CLI 覆盖文件无确认。 components/ui/button.tsx 存在时 add button 直接替换。对迁移是期望行为,但对 Tier C 文件误跑是灾难 —— 每次 add 后都查 git status。
  8. NativeWind 5 尚未采纳。 基线钉在 v4,RNR v1 两者都支持,本项目走 v4 安装路径。

8. 开放问题

  • 暗色品牌色:当前品牌色 #4571e0(仅亮色)。暗色等价值要定 —— 保留(深色底上对比度高)还是偏移,留给设计 pass。
  • code-surface token:亮色下是 #e8e8eb(比 secondary 深 5%)。暗色等价是"比暗色 secondary 亮 5%"。实际实现已在 .dark:root 中给出 240 4% 18%,理由注释齐全。
  • 设置页 UX:Appearance 选择器可内联(三行直接摆)也可做成打开 picker sheet 的行。v1 选内联更简单;sheet 变体等 Tier B 迁完再说。
  • 暗色 token 是否与桌面共享:桌面 packages/ui/styles/tokens.css 用 oklch(Tailwind v4),移动端 hsl(Tailwind 3.4),跨版本共享不现实,接受分歧为既有基线;移动端升级到 NativeWind 5 + Tailwind v4 时再议。

9. 小结与延伸阅读

这次迁移的方法论可以浓缩为四条:复制粘贴所有权优先于库绑定;默认值优先,拒绝"稍微改进版"原语;用"原生 > 组件库 > 讨论"瀑布消灭不必要的抽象层;基础设施先行、canary 组件验证、批量推进时设停止规则。配套文档:迁移主文档 rnr-migration.md、持久 UI/主题规则 CLAUDE.md "UI components & theming" 章节、主题实现的四个文件 global.css / tailwind.config.js / lib/theme.ts / lib/use-color-scheme.ts,以及接线入口 app/_layout.tsx

登录后查看全文
热门项目推荐
相关项目推荐