Skip to content

Commit d5f2eda

Browse files
committed
feat: harden web ui system
1 parent 438022c commit d5f2eda

82 files changed

Lines changed: 3903 additions & 1629 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.migration/avatar.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# avatar
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated the unchanged anatomy to Base UI.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/avatar.tsx`: replaced Radix Root, Image, and Fallback with `@base-ui/react/avatar` while retaining all CNode classes and public wrapper names. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for this component.
8+
9+
## Left alone
10+
11+
Avatar consumers and image URL helpers required no API changes. Their profile and identity rendering remains domain-owned.
12+
13+
## Behavior changes
14+
15+
None. Image loading and fallback rendering retain the same observable behavior.
16+
17+
## Verify by hand
18+
19+
Open the header user menu, a topic author card, and a profile with both valid and missing avatar URLs. Confirm images fill the circle and initials appear after failures.

.migration/button.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# button
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated to Base UI while preserving CNode classes and link semantics.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/button.tsx`: replaced Radix Slot and `asChild` with `@base-ui/react/button` and its `render` contract. All Button link consumers now render one anchor without nested interactive elements. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for the wrapper and migrated Button consumers.
8+
9+
## Left alone
10+
11+
Sheet and DropdownMenu trigger/item composition remains on Radix for tasks 1.5 and 1.6. Their `asChild` sites are unrelated to Button's public API migration.
12+
13+
## Behavior changes
14+
15+
None. Rendered links retain link roles, destinations, and existing branded Button classes.
16+
17+
## Verify by hand
18+
19+
Open the header, footer, topic page, settings page, and admin lists. Tab to each Button-styled link, confirm the visible focus ring, activate it with Enter, and verify the expected destination with no nested button in the DOM.

.migration/checkbox.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# checkbox
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated to Base UI and mapped checked state attributes.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/checkbox.tsx`: replaced Radix Root and Indicator with `@base-ui/react/checkbox`; mapped `data-state=checked` styles to `data-checked` and disabled styles to `data-disabled`. Existing controlled consumers and FormControl composition remain source-compatible. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for this component.
8+
9+
## Left alone
10+
11+
No consumer currently uses an indeterminate state, so no synthetic compatibility mapping was added.
12+
13+
## Behavior changes
14+
15+
The visible control is now Base UI's span-based checkbox with a hidden native input rather than Radix's button-based root. Accessible checkbox semantics and native form submission are preserved.
16+
17+
## Verify by hand
18+
19+
Toggle notification preferences and admin table selections with pointer and Space. Submit a named checkbox in a form and confirm only its checked value is included.

.migration/dialog.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# dialog
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated Dialog and all controlled consumers to Base UI.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/dialog.tsx`: replaced Radix Overlay/Content with Base UI Backdrop/Viewport/Popup, preserved CNode styling, exposed Popup `initialFocus`/`finalFocus`, and added configurable viewport positioning. Controlled consumers in public, topic, user, settings, command, and admin surfaces now return focus to their actual opener and call `eventDetails.cancel()` when a pending request denies closing. `apps/web/tests/PrimitiveMigrationBaseline.test.tsx` verifies trigger focus return and pending Escape cancellation. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for Dialog files.
8+
9+
## Left alone
10+
11+
Destructive confirmations remain general Dialogs until the dedicated AlertDialog tasks. Tooltip and DropdownMenu remain in their own migration batch.
12+
13+
## Behavior changes
14+
15+
Overlay DOM now uses Base UI's Backdrop, Viewport, and Popup. CommandPalette positioning is owned by the Viewport rather than absolute Popup offsets. Business actions, routes, and permissions are unchanged.
16+
17+
## Verify by hand
18+
19+
Open CommandPalette by button and Ctrl/Cmd+K, close with Escape, and confirm focus returns to the opener. Start a destructive request, attempt Escape/outside close while pending, then cancel after completion and verify focus restoration.

.migration/dropdown-menu.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# dropdown-menu
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated all menu wrappers and consumers to Base UI Menu.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/dropdown-menu.tsx`: replaced Radix DropdownMenu with Base UI Menu, rebuilt popup positioning with Portal > Positioner > Popup, migrated submenu, group labels, indicators and state attributes, and added `DropdownMenuLinkItem`. `apps/web/app/components/Layout.tsx`, `apps/web/app/routes/user.$name.tsx`, and `apps/web/app/routes/admin/users.tsx` now use `render`, LinkItem and `onClick`. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for all migrated files.
8+
9+
## Left alone
10+
11+
Menu-owned role and governance actions retain their existing API calls and confirmation dialogs. Sonner remains an intentional non-Radix feedback dependency.
12+
13+
## Behavior changes
14+
15+
Base UI Menu is configured with `loopFocus={false}` and explicit `closeOnClick` for links, checkbox items and radio items to preserve observed Radix behavior.
16+
17+
## Verify by hand
18+
19+
Open the header and user-management menus by pointer and keyboard. Navigate with arrows, activate links and actions with Enter/Space, confirm disabled items do nothing, submenu focus moves correctly, and focus returns to the trigger after close.

.migration/form.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# form
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated FormControl composition from Radix Slot to Base UI `useRender`.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/form.tsx`: FormControl now merges field IDs, descriptions, and invalid state through the Base UI `render` contract. Sign-in, sign-up, and settings consumers pass their actual controls through `render`. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for the wrapper and migrated consumers.
8+
9+
## Left alone
10+
11+
React Hook Form remains the form-state owner. Checkbox itself remains Radix until task 1.4, but FormControl already composes it without Slot.
12+
13+
## Behavior changes
14+
15+
None. Existing IDs, label associations, `aria-describedby`, and `aria-invalid` values are applied to the actual form control.
16+
17+
## Verify by hand
18+
19+
Submit invalid sign-in and sign-up forms, inspect the focused field and its accessible description, then edit settings inputs and checkboxes to confirm labels and validation remain connected.

.migration/label.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# label
2+
3+
2026-08-02, engine strategy for legacy `new-york`; replaced the Radix primitive with native label semantics.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/label.tsx`: now renders a native `<label>` while preserving all existing classes and ref/HTML prop support. `apps/web/app/components/ui/form.tsx` now types FormLabel against the native element. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for both files.
8+
9+
## Left alone
10+
11+
Route-level label text and field ownership remain in their existing forms; broader autocomplete and field audit work belongs to task 3.4.
12+
13+
## Behavior changes
14+
15+
None. `htmlFor` and click-to-focus behavior remain native and unchanged.
16+
17+
## Verify by hand
18+
19+
Open sign-in, sign-up, and settings forms. Click each visible label and confirm focus moves to the associated input, textarea, or checkbox.

.migration/registry-review.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Base registry 组件审查
2+
3+
审查日期:2026-08-02。仅执行预览,未应用 registry 输出、未安装依赖。
4+
5+
## 环境与方法
6+
7+
- 工作目录:`apps/web`
8+
- 固定 CLI:`pnpm exec shadcn --version` -> `4.16.1`
9+
- `pnpm exec shadcn info --json`:React Router、Tailwind CSS v4、`style: new-york`,但 legacy 配置被解析为 `base: radix`
10+
- `base-new-york` registry 不存在;为避免普通组件名回落到 Radix,本次显式审查 `https://ui.shadcn.com/r/styles/base-nova/<component>.json`
11+
- 每项均运行 `pnpm exec shadcn add "<URL>" --dry-run``pnpm exec shadcn add "<URL>" --diff``command` 的完整 diff 只显示 5/6 个文件,因此另运行 `--diff app/components/ui/command.tsx`
12+
- CNode 保留基线:语义 token、`bg-card``rounded-xl/2xl``border-cnode-green/*``shadow-card/floating/brand`、绿色 hover、统一 `h-9` 与 focus ring、中文无障碍文案,以及既有 Base UI `render`、overlay focus/Viewport 契约。
13+
14+
## select
15+
16+
```bash
17+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/select.json" --dry-run
18+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/select.json" --diff
19+
```
20+
21+
- 结果:仅创建 `app/components/ui/select.tsx`;CLI 未提出新增包。源码使用现有 `@base-ui/react/select``lucide-react``~/lib/utils`,无 Radix、无无关覆盖。
22+
- 必须保留/合并:将 registry 的 `h-8/h-7``rounded-lg`、透明背景、通用 `shadow-md/ring` 调整到 CNode 的 `h-9``rounded-xl``bg-card`、品牌边框/阴影与绿色 hover;保留 Base UI `Positioner/Popup``data-open/data-closed` 和错误/禁用语义。
23+
- 结论:可作为手工品牌化基线,不可原样应用。
24+
25+
## native-select
26+
27+
```bash
28+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/native-select.json" --dry-run
29+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/native-select.json" --diff
30+
```
31+
32+
- 结果:仅创建 `app/components/ui/native-select.tsx`;CLI 未提出新增包。源码仅使用原生 `select`、现有 `lucide-react` 和 utils,无 Radix、无无关覆盖。
33+
- 必须保留/合并:默认 `w-fit h-8 rounded-lg bg-transparent` 与 CNode 表单的 `w-full h-9 rounded-xl bg-card shadow-sm`、绿色 hover 边框不同;后台紧凑 `sm` 密度应显式保留,`Canvas/CanvasText` 原生 option 配色可保留。
34+
- 结论:可手工品牌化引入。
35+
36+
## textarea
37+
38+
```bash
39+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/textarea.json" --dry-run
40+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/textarea.json" --diff
41+
```
42+
43+
- 结果:仅创建 `app/components/ui/textarea.tsx`;CLI 未提出新增包,仅使用 React 与 utils,无 Radix、无无关覆盖。
44+
- 必须保留/合并:registry 为 `rounded-lg bg-transparent`、无品牌 hover/阴影;应与 CNode Input 的 `rounded-xl bg-card shadow-sm hover:border-cnode-green/35 focus-visible:ring-2` 对齐,并按场景保留合理最小高度与 resize 行为。
45+
- 结论:可手工品牌化引入。
46+
47+
## alert-dialog
48+
49+
```bash
50+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/alert-dialog.json" --dry-run
51+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/alert-dialog.json" --diff
52+
```
53+
54+
- 结果:创建 `app/components/ui/alert-dialog.tsx`,同时覆盖 `app/components/ui/button.tsx`;CLI 提出新增 `radix-ui`。AlertDialog 本体使用 `@base-ui/react/alert-dialog`,但嵌套 `button` 依赖按 legacy 配置解析为 Radix Slot/`asChild`
55+
- 拒绝项:重新引入 Radix;覆盖已经迁移到 Base UI 的 Button;删除 `inverse` variant 和 CNode 圆角、颜色、阴影、active/focus 差异。
56+
- 必须保留/合并:现有 Dialog 的 `bg-cnode-ink/70`、品牌边框、`rounded-2xl``shadow-floating``100dvh` 可滚动 Viewport、safe-area/final-focus/pending close 契约;取消按钮作为安全默认焦点,只有最终确认使用 destructive。
57+
- 结论:拒绝直接应用,后续只手工摘取 Base AlertDialog anatomy。
58+
59+
## alert
60+
61+
```bash
62+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/alert.json" --dry-run
63+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/alert.json" --diff
64+
```
65+
66+
- 结果:仅创建 `app/components/ui/alert.tsx`;CLI 未提出新增包,使用现有 CVA 和 utils,无 Radix、无无关覆盖。
67+
- 必须保留/合并:registry 的紧凑 `rounded-lg border bg-card` 需映射 CNode 的 `rounded-xl`、品牌边框/状态 surface;不能让所有信息都固定为打断式 `role="alert"`,普通说明、pending/success 应允许 `status`,错误才使用 `alert`
68+
- 结论:结构可用,但需品牌化并修正播报接口后引入。
69+
70+
## pagination
71+
72+
```bash
73+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/pagination.json" --dry-run
74+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/pagination.json" --diff
75+
```
76+
77+
- 结果:创建 `app/components/ui/pagination.tsx`,同时覆盖 `app/components/ui/button.tsx`;CLI 提出新增 `radix-ui`
78+
- 拒绝项:嵌套 Button 回落到 Radix Slot/`asChild` 并覆盖品牌 Button。生成的 Pagination 又向 Button 传入 Base API `nativeButton={false}``render`,与同次预览生成的 Radix Button 接口不一致。
79+
- 必须保留/合并:领域 `app/components/Pagination.tsx` 继续拥有 URL/query 生成;UI primitive 保留中文“上一页/下一页”、`aria-current="page"`、不可用状态、窄屏无溢出,以及 CNode `rounded-xl``bg-card`、品牌 active/hover 样式。
80+
- 结论:拒绝直接应用;手工组合现有 Base Button,禁止覆盖领域 Pagination。
81+
82+
## empty
83+
84+
```bash
85+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/empty.json" --dry-run
86+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/empty.json" --diff
87+
```
88+
89+
- 结果:仅创建 `app/components/ui/empty.tsx`;CLI 未提出新增包,使用现有 CVA 和 utils,无 Radix、无无关覆盖。
90+
- 必须保留/合并:registry 的中性 `rounded-xl`、小号 muted icon 未体现 CNode 绿色 surface/边框;按页面保留品牌 Empty 容器、中文标题/说明及清除筛选、返回列表或创建入口。registry 根节点只有 `border-dashed` 而无 `border`,不能假定会显示边框。
91+
- 结论:可作为组合结构,需品牌化后引入。
92+
93+
## command
94+
95+
```bash
96+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/command.json" --dry-run
97+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/command.json" --diff
98+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/command.json" --diff app/components/ui/command.tsx
99+
```
100+
101+
- 结果:创建 `textarea.tsx``input-group.tsx``command.tsx`;覆盖 `dialog.tsx``button.tsx``input.tsx`;CLI 提出新增 `cmdk``radix-ui`
102+
- 拒绝项:Dialog 被改回 Radix `data-state/asChild`,丢失 Base Backdrop/Viewport/Popup、focus restore 和 pending close;Button 回到 Radix Slot;Input 丢失 CNode 圆角、card 背景、绿色 hover 和阴影。以上均为任务外覆盖,且 `radix-ui` 不允许重新引入。
103+
- 必须保留/合并:`cmdk` 是允许保留的非 Radix command 引擎,但应单独评估安装;保留现有 CommandPalette 的中文标题/说明/关闭文案、`border-cnode-green/20``shadow-floating`、权限过滤和 final focus。补齐方向键/Enter/Escape、空结果、移动端 `100dvh`/safe-area/overscroll,不直接采用默认英文文案和 `max-h-72`
104+
- 结论:拒绝整个 registry 变更集;后续仅手工摘取 `command.tsx`/必要 InputGroup 结构并复用现有 Base Dialog、Button、Input、Textarea。
105+
106+
## radio-group
107+
108+
```bash
109+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/radio-group.json" --dry-run
110+
pnpm exec shadcn add "https://ui.shadcn.com/r/styles/base-nova/radio-group.json" --diff
111+
```
112+
113+
- 结果:仅创建 `app/components/ui/radio-group.tsx`;CLI 未提出新增包。源码使用现有 `@base-ui/react/radio``@base-ui/react/radio-group` 和 utils,无 Radix、无无关覆盖。
114+
- 必须保留/合并:保留 Base `data-checked`、disabled、invalid 与方向键行为;focus、边框和选中颜色需与 CNode token 对齐。组名和每项可见 Label 由消费方提供,不能只渲染无名称圆点。
115+
- 结论:可手工品牌化引入。
116+
117+
## 总结与阻塞
118+
119+
- 可进入后续手工品牌化:`select``native-select``textarea``alert``empty``radio-group`
120+
- 必须拒绝当前完整输出:`alert-dialog``pagination``command`,因为会新增 `radix-ui` 并覆盖无关或已迁移的品牌组件。
121+
- 阻塞根因:legacy `components.json` 被 CLI 解析为 `base: radix`;即使顶层使用显式 `base-nova` URL,未限定地址的嵌套 registry dependency 仍按 Radix style 解析。后续实现必须逐文件手工合并,不能直接运行无预览的 `add`
122+
- `command` 另需决定是否在后续实现任务中显式加入 `cmdk`;本次未安装任何依赖。

.migration/sheet.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# sheet
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated the Dialog-derived Sheet to Base UI.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/sheet.tsx`: rebuilt Sheet with Base UI Portal, Backdrop, Viewport, and Popup while preserving side variants and public names. Added `100dvh`, internal scrolling, overscroll containment, safe-area padding, Base open/closed state selectors, and Popup `finalFocus` forwarding. `apps/web/app/components/JobFilterBar.tsx` and `apps/web/app/components/Layout.tsx` now use Trigger `render`; `JobMetaCard` supplies an explicit final-focus target. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for Sheet files.
8+
9+
## Left alone
10+
11+
Route-owned mobile navigation links and job filtering state were not redesigned. Their existing actions and URLs remain unchanged.
12+
13+
## Behavior changes
14+
15+
Sheet content now scrolls within a Base UI modal viewport with background scroll locking and safe-area spacing. Existing side placement and close controls remain available.
16+
17+
## Verify by hand
18+
19+
At 375px width, open public navigation and job filters, scroll to the final item, confirm the page behind does not move, close with button and Escape, and verify focus returns to the trigger.

.migration/tabs.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# tabs
2+
3+
2026-08-02, engine strategy for legacy `new-york`; migrated to Base UI while explicitly retaining automatic activation.
4+
5+
## Changed
6+
7+
`apps/web/app/components/ui/tabs.tsx`: replaced Radix Trigger and Content with Base UI Tab and Panel, mapped active styles to `data-active`, and set `TabsList.activateOnFocus` to `true` by default. The leftover scan `grep -n "radix-ui\|@radix-ui"` is clean for this component.
8+
9+
## Left alone
10+
11+
Existing admin tab consumers required no prop changes and retain their route/local state ownership.
12+
13+
## Behavior changes
14+
15+
None by design. Base UI defaults to manual activation, but this wrapper explicitly enables `activateOnFocus` to preserve Radix's existing arrow-key automatic activation.
16+
17+
## Verify by hand
18+
19+
Focus the first tab in admin settings and bans, press Left/Right, and confirm focus, selected state, and visible panel move together. Click each tab and verify the active styling.

0 commit comments

Comments
 (0)