revert(tasks-ui): drop the shadcn/Tailwind layer and return to the plugin's own UI system

shadcn's defaults are built for a standalone app: 14px body, 36px controls,
solid primary fills and elevation read as a foreign surface next to this
plugin's own tabs (files, changes, side chat, settings), which are 12/11/10px
type, 28px controls, hairline borders and flat panels. Retuning the layer did
not converge, so the layer goes.

- Removed: src/client/ui/** (21 vendored components), components.json,
  scripts/ui-css.mjs, the Tailwind/PostCSS compile step in tsdown, and the deps
  tailwindcss / @tailwindcss/postcss / postcss / radix-ui /
  class-variance-authority / tailwind-merge. (diff and simple-icons stay: they
  patch undeclared bare imports of the host primitives, unrelated to shadcn.)
- The seven page surfaces are back on the plugin's own base: *.module.css with
  --dsw-* tokens plus the host primitives (Button/Menu/Modal/Pill/Tag/Switch/
  Input/Tooltip/StateDot/MarkdownText). Behaviour, data-* hooks, aria labels,
  i18n keys and the AnchoredPopover geometry/drag/dismiss contract are
  unchanged; the non-styling fixes landed during the shadcn era (model id
  dedupe, task-window CAS, drawer arming, popover dragging, the chunk packaging
  glob) were re-applied and are covered by the existing specs.
- The tasks page returns to the core bundle: `tasks` left CHUNK_NAMES /
  CHUNKS / ChunkName, src/client/chunks/tasks.tsx is gone and tabs.tsx imports
  SubagentView directly.
- The reverted stylesheet was retuned to the plugin's measured vocabulary
  (12/11/10px, 28px controls, border-l2 with border-l1 hairlines, hover fills,
  no static shadows, --dsw-shadow-lv2/3 on floating layers only) and verified
  against a 70-assertion real-browser contract; the harness's own two
  measurement defects were fixed with a falsification test proving the
  assertions still catch injected regressions (56/70, 14 FAIL).
- Tests: ui-foundation / ui-bundle / ui-shadows (the shadcn guards) deleted,
  theme.spec scoped back to the plugin's own stylesheets, bundle-route and
  manifest-consistency chunk lists back to four. Docs: AGENTS.md and the plugin
  guide drop the Tailwind/shadcn guidance; the design doc marks those sections
  superseded and records why the layer was removed.

Verified: typecheck/lint clean; 133 files / 1410 tests; core bundle 987.3 KiB
with no radix-ui/--tw-/tailwind strings and no client-tasks.js; mount e2e 7/7;
20 real-browser screenshots at 360/720 for graph, tree, board, task window,
drawer (including armed), both popovers and the 77-node fixture.
This commit is contained in:
Menghuan1918
2026-09-27 22:18:19 +08:00
parent e500a3d2b9
commit a71d158f9a
47 changed files with 2373 additions and 4107 deletions
-26
View File
@@ -1004,32 +1004,6 @@ interface SettingsDescriptor {
- 类名是 CSS Modules 哈希,**不是契约**;精确命中用 `[data-dsh-better-sidebar]` + 子串类名(`[class*='panel']`)或 DOM 结构。
- 改动本契约必须同步本文档、设计文档与 `tests/theme.spec.ts`。
### 12.3 在插件里用 Tailwind / shadcn/ui
本仓库自己的任务管理页(v0.20 起)就是这套姿势的参考实现:组件源码 vendoring 在 `src/client/ui/`,样式由 Tailwind v4 编译,颜色全部经 shadcn 语义令牌桥接到 `--dsw-*`。你的插件照做即可自动兼容全部皮肤;**接入的难点只有三条,其余都是常规 Tailwind**。
**① 只引 `theme` + `utilities`,绝不引 preflight。** 插件的 CSS 最终是全局 `<style data-plugin>`(没有 shadow DOM),`@import "tailwindcss"` 连带的 preflight 会重置**整张 DSH 宿主页面**(会话正文、原生右侧栏、设置页)。唯一合法的入口形状:
```css
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@source "../**/*.tsx"; /* 显式限定扫描面,否则 Tailwind 会连 lib/、node_modules/ 一起扫 */
```
| 现象 | 原因 |
|---|---|
| 自己的 `.rounded-md` 被宿主样式压过 | 宿主样式表是**无层级(unlayered)**的,无层级声明优先于任何 `@layer` 内的声明(与特异性无关)。补偿方式是把重置放进层里并抬到**类特异性**、挂在页面自己的作用域根类上(本仓库是 `.dsw-tasks`:`color` / `:where(...)` 的 `box-sizing` 与 `font: inherit` / `focus-visible` 环),而不是去动宿主 |
| 深浅主题下颜色不翻 | DSH 的主题翻转是给 `<body>` 打 `data-ds-dark-theme`,不是加 `.dark` 类。若确需 `dark:` 变体,先定义 `@custom-variant dark (&:where([data-ds-dark-theme], [data-ds-dark-theme] *))`——但**令牌本身就是翻转的**,绝大多数情况下写 `dark:` 是双重记账,不要写 |
**② 令牌桥接到 `--dsw-*`,颜色零字面量。** 在 `:root` 里把 shadcn 语义名接上 DSH 令牌(`--background: var(--dsw-alias-bg-base)`、`--card: var(--dsw-alias-bg-layer-1)`、`--popover: var(--dsw-alias-bg-layer-2)`、`--border: var(--dsw-alias-border-l4)`、`--primary: var(--dsw-alias-state-business-primary)`、`--muted-foreground: var(--dsw-alias-label-secondary)`、`--destructive: var(--dsw-alias-state-error-primary)` …),再用 `@theme inline { --color-background: var(--background); … }` 接成 Tailwind 工具类。`inline` 保证产物里留着这层间接,宿主皮肤改令牌能立刻生效。**禁止** `#hex` / `rgb()` / `oklch()` 字面量,也**禁止** Tailwind 默认调色板类(`bg-blue-500` / `text-white`)——后者的值是 Tailwind 自己的主题,不是 DSH 的。完整映射表见[任务页设计文档](./plans/2026-09-14-tasks-graph-workflow-teams-design.md)的「shadcn 令牌 → `--dsw-*` 映射表」。
**③ 静态面板零阴影。** 阴影是**层级**手段,不是装饰:卡片、节点、行、面板互相叠加的投影是「节点一多就辣眼睛」的直接原因。静态面板的层级改用 **1px hairline(`border-border`)+ 表面阶梯(`background` → `card`/layer-1 → `popover`/layer-2)+ 三档墨色**承载;`shadow-*` 只允许出现在真正浮动、由自己锚定几何定位的层(popover / dropdown-menu / tooltip / 可拖动窗口)。上游 shadcn 组件默认给 card / input / textarea / toggle / toggle-group 都带 `shadow-xs`/`shadow-sm`,vendoring 时要逐处删掉。
**另外两条与皮肤无关但同样会遇到**:
- **图标用宿主图标,不引 `lucide-react`**:`@deepseek-ai/dsh-client-ui-primitives` 的 `IconXxx` 是平台模块(`size` / `className` 两个 props,颜色靠 `currentColor`),宿主图标渲染的就是 `svg`,所以 shadcn 组件里的 `[&_svg]` / `[&_svg:not([class*='size-'])]:size-4` 选择器照旧成立。引 lucide 会把整套图标内联进你的 bundle。
- **vendoring 而不是新增重依赖**:`npx shadcn add <component>` 把源码落进你自己的目录(本仓库 `components.json` 的 aliases 全部指向 `src/client/ui`),依赖只增加 `radix-ui` / `class-variance-authority` / `clsx` / `tailwind-merge` 这四个可 tree-shake 的包;`tailwindcss` 与 `@tailwindcss/postcss` 只进 devDependencies(运行期产物是编译后的 CSS)。落盘后**逐文件复核**:CLI 默认会写 `import { cn } from "cn"`(npm 上真有一个同名包)、带回 `dark:` 颜色覆写、带回被删的 `shadow-xs` / `text-white` 与未使用的变体。升级上游组件用 `npx shadcn@latest add <component> --diff`,只取结构性更新再重新施加自己的删改,**不要**用 `--overwrite`。
---
## 13. 完整最小示例
@@ -147,7 +147,9 @@ PR:[#680](https://github.com/omdsh-dev/DSH-better-sidebar/pull/680)(分支 `
未在本机自动化验证、留给用户实机确认的一项:3384 桌面应用**重启后**的 UI 级复看(新 bundle 已装进 `~/.dsh/profiles/web`,与仓库构建产物 SHA-256 一致)。
## shadcn/ui 迁移(2026-09-17,视觉基座替换)
## shadcn/ui 迁移(2026-09-17,视觉基座替换)—— 已废弃
> **已废弃(2026-09-21 整体回退)**:本节及其后全部子节(依赖与实际版本 / Tailwind 接入方式 / 令牌映射表 / vendored 组件清单 / 阴影策略 / 体积与 mount 结果 / 真机排障 / stock 风格回调 / 升级路径与本地适配清单 / 页面重组)记录的是 2026-09-17–09-21 期间任务页的 shadcn/ui 视觉基座。该基座已整体回退到插件自有体系,回退根因见文末「回退:任务页回到插件自有体系(2026-09-21,shadcn 层废弃)」。保留全文只为留下排障现场与决策证据——**其中描述的接入方式不再是本仓库或消费插件的建议做法**。
三轮返工把**行为**收敛到位(统一的详情窗口、常驻任务板、可拖动浮窗),但**视觉基座**仍是自建:
@@ -397,3 +399,45 @@ npx -y shadcn@latest add button card badge separator input textarea tooltip popo
2. **`<Spinner size={12} />` 类型合法但静默失效**:刷新后的 Spinner 变成 `React.ComponentProps<"span">`,`size` 是合法 HTML 属性 → typecheck 全绿而图标按默认 `size-4` 渲染。改用 `className="size-3"`。**教训:组件 API 变更中"仍然类型合法但语义丢失"的那类最危险,只有看渲染结果才能发现。**
**已知未修(记录在案)**:抽屉行在 ~420px 以下的窄宽度里会横向溢出(Radix ScrollArea 的 table 视口按 min-content 计算 + 行内 `nowrap` 的 mono 标题),修复需要重新决定 ScrollArea/行的最小宽度策略;宽面板(用户实机)不受影响。
---
## 回退:任务页回到插件自有体系(2026-09-21,shadcn 层废弃)
任务页 2026-09-17 换成 shadcn/ui + Tailwind v4 视觉基座,09-20 又按「对齐上游 stock」回调一轮;09-21 判定该基座与本插件的体系不可调和,整体回退。
**改动面**:8 个页面文件(`TasksGraph` / `TasksTree` / `TaskWindow` / `TeamBoard` / `JobsDrawer` / `TasksPopovers` / `SubagentView` / `tasks-shared`)以 `12b4716` 的原生实现(CSS Modules + 宿主 `@deepseek-ai/dsh-client-ui-primitives`)为基线重建,`src/client/ui/**`(23 个文件)、`components.json`、`scripts/ui-css.mjs` 与其 `ui:css` 脚本删除;`radix-ui` / `class-variance-authority` / `tailwind-merge` / `tailwindcss` / `@tailwindcss/postcss` / `postcss` 六个依赖下线;`tasks` 懒加载 chunk 与 `CHUNK_NAMES` 的 `tasks` 项一并取消(回到 `terminal` / `editor` / `mermaid` / `locale` 四项)。**回退只换视觉基座:09-17–09-21 期间的行为修复逐条重新落地**(见下「保留的行为修复」)。
### 回退根因
1. **默认尺度与插件体系冲突(最直接的一条)**:stock shadcn 的正文是 `text-sm`(14px)、meta `text-xs`(12px)、控件按 `h-9`(36px) 起;本插件的语言是正文 12px / meta 11px / 微标 10px、控件 28px(`sidebar.module.css` 的图标按钮即为 28px 方)。要贴合就得把 stock 的每个尺寸逐处改回去——那等于不用 shadcn,只是多背一层工具类。09-20 的「stock 回调」正是这条冲突的产物:字号一放开,`GRAPH_NODE_W` 132px 的图节点标题只剩约 7 个汉字(“图形重布重写”被截成“图…”),两轮下来只能把宽度 132→150→176 并改两行标题救场。
2. **层级语义冲突**:shadcn 给静态组件默认带投影(card `shadow-sm`,input / textarea / toggle / button-outline `shadow-xs`);本插件的语言是**静态面板无阴影**(层级 = 1px `--dsw-alias-border-l2` + hairline `--dsw-alias-border-l1` + 表面阶梯 + 墨色三档),只有浮层用 `--dsw-shadow-lv2/3`。第一轮把静态阴影全删了,stock 回调又把它们恢复成 stock——两次改的都是同一份 vendored 源码,每次跟上游 `--diff` 都要重新施加。
3. **彩色语义是第二套词汇表**:shadcn 的 `primary` / `secondary` / `muted` / `accent` / `destructive` 与 DSH 的 `--dsw-alias-state-*` / `--dsw-alias-interactive-bg-hover` 并非一一对应,任务状态(进行中 / 阻塞 / 完成 / 错误)还要再自造 `--success` / `--warning` 两个非 stock 令牌,整张映射表 22 条必须逐条维护:`--border` 先桥到 `--dsw-alias-border-l4`、09-20 又改成 `l2`,只因为 shadcn 的 1px 边框语义变了——而 `l2`(1px 边框)/ `l1`(hairline)的分工本来就是本插件自己的契约,不必经过第三层命名。
4. **不引 preflight 就得自己补一套**:插件注入的是全局 `<style data-plugin>`(无 shadow DOM),引 Tailwind 完整入口会重置整张宿主页面,所以只能引 `theme` + `utilities`;表单控件因此保留 UA 外观,真机反馈「任务页看起来完全没有 CSS,边框非常模糊」的根因即在此。补救是在 `theme.css` 写限定 `.dsw-tasks` 作用域的 preflight 子集 + 显式 `@layer theme, base, components, utilities;` 层序声明 + 页面根类。**这整套补偿只为承载 Tailwind 而存在**,原生 CSS Modules 不需要。
5. **架构代价**:radix 浮层栈 + `tailwind-merge` 把核心包撑到 1452623 bytes(+457.6 KiB,超 +250 KiB 预算),处置是把任务页整体下沉为懒加载 chunk(`lib/client-tasks.js` 706.7 KiB,`CHUNK_NAMES` 加 `tasks`、`package.json#files` 补 glob)。原生实现下这些负载不存在,`tasks` chunk 随回退下线。
6. **vendored 源码的持续负担**:每个 vendored 组件的本地适配(`cn` 改 `./utils`、图标换宿主 `IconXxx`、删 `dark:` 覆写、`text-white` → 令牌、React 18 的 `forwardRef`)都要跟着上游逐次重放;`popover.tsx` / `skeleton.tsx` 自始至终没有消费者。
### 结论(写给消费插件)
**嵌入式插件沿用宿主体系**:样式用 CSS Modules + `--dsw-*` 令牌(参考 `src/client/sidebar.module.css` / `src/client/changes/changes.module.css` / `src/client/SideChatView.module.css`),控件与图标用宿主 `@deepseek-ai/dsh-client-ui-primitives` 的 `Button` / `Menu` / `Modal` / `Pill` / `Tag` / `Switch` / `Input` / `Tooltip` / `StateDot` / `MarkdownText` / `DisclosureRow` 与 `IconXxx`;不引 Tailwind / radix / CVA / lucide,不自绘 svg(连线几何除外)。插件样式表是**全局作用域**的,任何自带 reset 或工具类的方案都要额外处理与宿主无层级样式表的优先级关系——这是本插件选择自有体系的直接原因。
### 保留的行为修复(与视觉基座无关,已在原生实现上重新落地)
| 修复 | 位置 | 守护 |
|---|---|---|
| 节点 id 去重:workflow member 的 `childId` 不在 run 发起者 catalog 里、但存在于树中别处时不再合成同 id 的第二张卡片;返回前兜底去重 | `src/client/tasks-model.ts` | `tests/tasks-model.spec.ts` |
| 任务窗口 CAS:编辑 / 改派携带 `expectedRevision`,冲突不吞 | `src/client/TaskWindow.tsx` | `tests/tasks-page.spec.tsx` |
| 后台任务终止两击确认(首击 arm、二击才发 `jobs.kill`) | `src/client/JobsDrawer.tsx` | `tests/subagent-jobs-view.spec.tsx` |
| 后台任务抽屉 ≥8 代理自动折叠,条形按钮 `aria-expanded` 可展开 | `src/client/JobsDrawer.tsx` | `tests/tasks-page.spec.tsx` |
| 输出浮窗可拖动;稳定 `data-*` 钩子(`data-graph-node` / `data-graph-controls`)不变 | `AnchoredPopover.tsx` / `TasksGraph.tsx` | `tests/tasks-page.spec.tsx` |
### 测试与文档清扫
- 删除三个守 shadcn 层的 spec:`tests/ui-foundation.spec.ts`(Tailwind 入口 / 令牌桥 / 层序 / 作用域 reset)、`tests/ui-bundle.spec.ts`(核心包与 chunk 的体积与产物断言)、`tests/ui-shadows.spec.ts`(静态面板阴影白名单)。
- `tests/theme.spec.ts` 去掉指向 `src/client/ui/**` 与令牌桥的一节,保留「图标模块零颜色字面量」与「每条 `color:` 解析到主题令牌」,后者从 tab 图标单表扩展到 `src/client/**/*.module.css` 全量(`color-mix()` / `var()` 链同样按令牌逐个校验,`inherit` / `currentcolor` / `transparent` 视为「不自己上色」)。
- `tests/bundle-route.spec.ts` 的 `CHUNK_NAMES` 期望回到 4 项;`tests/manifest-consistency.spec.ts` 的「每个 chunk 都被 `package.json#files` 覆盖」断言保留,列表继续从 `CHUNK_NAMES` 派生(不手抄)。
- `README.md` 两处「视觉基座为 shadcn/ui + Tailwind v4」描述删除;`docs/external-plugin-guide.md` 删除 §12.3「在插件里用 Tailwind / shadcn/ui」整节(含 preflight 规避、令牌桥、静态面板零阴影三条)。
### 验收(回退后,2026-09-21)
`pnpm typecheck` ✅ 0 错误;`pnpm lint` ✅ 0 错误;`pnpm vitest run` **133 文件通过 / 1410 用例通过 / 9 skipped / 0 失败**(回退前:4 个文件失败,其中 `theme.spec.ts`、`ui-foundation.spec.ts` 在收集期就因 `src/client/ui/theme.css` 缺失报 ENOENT,`ui-shadows.spec.ts` 2 例、`bundle-route.spec.ts` 1 例断言失败)。
@@ -47,6 +47,8 @@ peer 范围、`dsh.client.inject`、chunk externals 白名单、`SIDEBAR_SERVICE
- `pnpm build` → 成功;`ui-bundle` 守卫仍绿(核心包 835.6 KiB、chunk 709.8 KiB、无 preflight/oklch/lucide)。
- `pnpm test:mount`(`DSH_CMD="npx -y --package @deepseek-ai/dsh@0.1.6-alpha.2 dsh"`,真实挂载 + 无头 tab 全扫)→ **7/7**;`PERF_SUMMARY mount=480ms longtasks=2/221ms`(预算 ≤8);资源清单里 `/sidebar/bundle/tasks.js` **真实下载**(`decodedBodySize 726905`),懒加载路径在 alpha 线上同样成立。
> **注(2026-09-21 回退后)**:上面两条引用的 `ui-bundle` 守卫与 `tasks` 懒加载 chunk(`/sidebar/bundle/tasks.js`)都是 shadcn/ui 视觉基座时期的产物,该基座已整体回退(见 [2026-09-14 任务页设计文档](./2026-09-14-tasks-graph-workflow-teams-design.md) 文末「回退:任务页回到插件自有体系」):`src/client/ui/**`、`ui-bundle` spec 与 `tasks` chunk 均已删除,任务页回到核心包 + 原生 CSS Modules。以上数字是当时的实测记录,原样保留。
## CI 钉版为什么留在 rc.2(含证伪过程)
第一版把挂载车道也钉到 `0.1.6-alpha.2`,CI 的 `plugin-mount` **确定性失败**(重跑同样红):`mount.e2e.ts:216` 等 `[data-dsh-better-sidebar] [title]`(插件底部工作台 tab 条)90s 不出现,`perf.e2e.ts:182` 同样卡在底部拖拽条。失败截图显示外壳正常但**没有活动会话**(会话面板停在空态占位),按 e2e 自己的注释「没有 session scope → 工作台没有 pane」推断:alpha 线在 CI 上不再自动激活 scratch profile 的种子会话。