From 40c043469a437b8525d58b676f4bb5e236f47bff Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 22 May 2026 00:36:49 +0800 Subject: [PATCH 1/2] fix(admin): smoke-test driven API alignment for v2 migration - posts/notes/drafts: switch list sort params to wire-format sort_by/sort_order (snake_case enums) to match server's createPagerSchema; previous camelCase + numeric -1/1 was rejected - manage-posts/notes list views: pass the new sort keys through - webhook create: always send `secret: ''` when the form leaves it blank; server's WebhookSchema requires `z.string()` and the admin form intentionally treats the field as optional - docs/superpowers: capture in-progress React migration specs (status, layout shell, data flow, datatable, list filter bar, recovery banner, rightpane quick edit) so they survive across machines --- apps/admin/src/api/drafts.ts | 6 +- apps/admin/src/api/notes.ts | 15 +- apps/admin/src/api/posts.ts | 7 +- .../views/extra-features/webhook/index.tsx | 8 +- apps/admin/src/views/manage-notes/list.tsx | 9 +- apps/admin/src/views/manage-posts/list.tsx | 5 +- .../2026-05-06-react-migration/00-roadmap.md | 4 +- .../01-repo-skeleton.md | 3 +- .../02-design-tokens.md | 138 ++- .../03-ui-primitives.md | 94 +- .../04-state-layer.md | 2 +- .../06-routing-auth.md | 2 +- .../07-layouts-patterns.md | 2 + .../08-form-system.md | 248 +++-- .../10-charts-misc.md | 28 +- .../2026-05-06-react-migration/STATUS.md | 67 ++ .../2026-05-09-p1-layout-shell-design.md | 446 +++++++++ .../2026-05-10-data-flow-redesign-design.md | 867 ++++++++++++++++++ .../specs/2026-05-10-datatable-design.md | 424 +++++++++ .../specs/2026-05-10-posts-list-design.md | 32 +- .../2026-05-11-list-filter-sort-bar-design.md | 579 ++++++++++++ .../2026-05-11-recovery-banner-design.md | 226 +++++ .../2026-05-11-rightpane-quickedit-design.md | 619 +++++++++++++ 23 files changed, 3716 insertions(+), 115 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-06-react-migration/STATUS.md create mode 100644 docs/superpowers/specs/2026-05-09-p1-layout-shell-design.md create mode 100644 docs/superpowers/specs/2026-05-10-data-flow-redesign-design.md create mode 100644 docs/superpowers/specs/2026-05-10-datatable-design.md create mode 100644 docs/superpowers/specs/2026-05-11-list-filter-sort-bar-design.md create mode 100644 docs/superpowers/specs/2026-05-11-recovery-banner-design.md create mode 100644 docs/superpowers/specs/2026-05-11-rightpane-quickedit-design.md diff --git a/apps/admin/src/api/drafts.ts b/apps/admin/src/api/drafts.ts index eafd942c5..c8a26ab4f 100644 --- a/apps/admin/src/api/drafts.ts +++ b/apps/admin/src/api/drafts.ts @@ -8,13 +8,15 @@ import type { import { request } from '~/utils/request' +export type DraftSortOrder = 'asc' | 'desc' + export interface GetDraftsParams { page?: number size?: number refType?: DraftRefType hasRef?: boolean - sortBy?: string - sortOrder?: 1 | -1 + sort_by?: string + sort_order?: DraftSortOrder } export interface CreateDraftData { diff --git a/apps/admin/src/api/notes.ts b/apps/admin/src/api/notes.ts index 86a39b742..b0b615b34 100644 --- a/apps/admin/src/api/notes.ts +++ b/apps/admin/src/api/notes.ts @@ -3,11 +3,22 @@ import type { NoteModel } from '~/models/note' import { request } from '~/utils/request' +export type NoteSortKey = + | 'title' + | 'createdAt' + | 'modifiedAt' + | 'weather' + | 'mood' +export type SortOrder = 'asc' | 'desc' + export interface GetNotesParams { page?: number size?: number - sortBy?: string - sortOrder?: number + sort_by?: NoteSortKey + sort_order?: SortOrder + /** + * @deprecated backend dropped db_query in v12.10.x pager refactor; param is silently ignored + */ db_query?: Record } diff --git a/apps/admin/src/api/posts.ts b/apps/admin/src/api/posts.ts index 0286c2eb3..4b80fa427 100644 --- a/apps/admin/src/api/posts.ts +++ b/apps/admin/src/api/posts.ts @@ -3,11 +3,14 @@ import type { PostModel } from '~/models/post' import { request } from '~/utils/request' +export type PostSortKey = 'createdAt' | 'modifiedAt' | 'pinAt' +export type SortOrder = 'asc' | 'desc' + export interface GetPostsParams { page?: number size?: number - sortBy?: string - sortOrder?: number + sort_by?: PostSortKey + sort_order?: SortOrder categoryIds?: string[] } diff --git a/apps/admin/src/views/extra-features/webhook/index.tsx b/apps/admin/src/views/extra-features/webhook/index.tsx index d43eb9472..cbaa314fe 100644 --- a/apps/admin/src/views/extra-features/webhook/index.tsx +++ b/apps/admin/src/views/extra-features/webhook/index.tsx @@ -160,7 +160,13 @@ export default defineComponent({ data: submitData, }) } else { - createMutation.mutate(data as any) + // Server requires `secret` as a string; admin form leaves it optional. + // Default to "" when the user did not provide one so creation succeeds. + const createPayload: Partial = { + ...data, + secret: data.secret ?? '', + } + createMutation.mutate(createPayload as any) } } diff --git a/apps/admin/src/views/manage-notes/list.tsx b/apps/admin/src/views/manage-notes/list.tsx index 49f10ad80..e28e8779a 100644 --- a/apps/admin/src/views/manage-notes/list.tsx +++ b/apps/admin/src/views/manage-notes/list.tsx @@ -14,6 +14,7 @@ import { NButton, NEllipsis, NInput, NPopconfirm, NSpace } from 'naive-ui' import { computed, defineComponent, reactive, ref, watchEffect } from 'vue' import { RouterLink } from 'vue-router' import { toast } from 'vue-sonner' +import type { NoteSortKey } from '~/api/notes' import type { NoteModel } from '~/models/note' import type { TableColumns } from 'naive-ui/lib/data-table/src/interface' import type { PropType } from 'vue' @@ -225,8 +226,12 @@ export const ManageNoteListView = defineComponent({ return notesApi.getList({ page: params.page, size: params.size, - sortBy: params.sortBy || undefined, - sortOrder: params.sortOrder || undefined, + ...(params.sortBy + ? { + sort_by: params.sortBy as NoteSortKey, + sort_order: params.sortOrder === 1 ? 'asc' : 'desc', + } + : {}), db_query: params.filters?.dbQuery, }) as Promise }, diff --git a/apps/admin/src/views/manage-posts/list.tsx b/apps/admin/src/views/manage-posts/list.tsx index 3d6735862..379e35370 100644 --- a/apps/admin/src/views/manage-posts/list.tsx +++ b/apps/admin/src/views/manage-posts/list.tsx @@ -19,6 +19,7 @@ import { } from 'vue' import { RouterLink } from 'vue-router' import { toast } from 'vue-sonner' +import type { PostSortKey } from '~/api/posts' import type { FilterOption, FilterState, @@ -206,8 +207,8 @@ export const ManagePostListView = defineComponent({ categoryIds: params.filters?.categoryIds, ...(params.sortBy ? { - sortBy: params.sortBy, - sortOrder: params.sortOrder, + sort_by: params.sortBy as PostSortKey, + sort_order: params.sortOrder === 1 ? 'asc' : 'desc', } : {}), }) diff --git a/docs/superpowers/specs/2026-05-06-react-migration/00-roadmap.md b/docs/superpowers/specs/2026-05-06-react-migration/00-roadmap.md index 0da4bcf31..97f7385db 100644 --- a/docs/superpowers/specs/2026-05-06-react-migration/00-roadmap.md +++ b/docs/superpowers/specs/2026-05-06-react-migration/00-roadmap.md @@ -20,7 +20,7 @@ docs/superpowers/specs/2026-05-06-react-migration/ ├── 05-data-layer.md ← request.ts port, TanStack Query patterns, socket.io hook ├── 06-routing-auth.md ← react-router tree, ProtectedRoute, better-auth + passkey ├── 07-layouts-patterns.md ← AppShell, Sidebar, MasterDetailLayout, header-action injection -├── 08-form-system.md ← react-hook-form + zod, ConfigForm DSL port +├── 08-form-system.md ← @tanstack/react-form + zod (Standard Schema), ConfigForm DSL port ├── 09-editors.md ← haklex direct-mount, monaco, codemirror, xterm, draft system ├── 10-charts-misc.md ← G2 hook, kbar swap, shiki/marked, diffs, excalidraw, confetti ├── 11-views-migration.md ← 21 views in batches; preconditions + acceptance per batch @@ -43,7 +43,7 @@ docs/superpowers/specs/2026-05-06-react-migration/ | UI primitives | Base UI (`@base-ui-components/react`) | headless; pairs with css.ts | | Styling | css.ts (`@vanilla-extract/css` + vite plugin) | Compile-time CSS; tokens live as TS exports | | Design system | Linear dark-canvas (per pasted DESIGN.md) | Lavender accent, four-step surface ladder, no second chromatic accent | -| Forms | `react-hook-form` + `zod` | `zod` already in source repo | +| Forms | `@tanstack/react-form` + `zod` (Standard Schema) | aligns with TanStack Query/Table; zod already in source repo; library swap landed 2026-05-10 | | Animation | `motion` | User-specified | | Icons | `lucide-react` | 1:1 swap from `lucide-vue-next` | | Toast | `sonner` | 1:1 swap from `vue-sonner` | diff --git a/docs/superpowers/specs/2026-05-06-react-migration/01-repo-skeleton.md b/docs/superpowers/specs/2026-05-06-react-migration/01-repo-skeleton.md index 0a17674d0..bcc8fdf31 100644 --- a/docs/superpowers/specs/2026-05-06-react-migration/01-repo-skeleton.md +++ b/docs/superpowers/specs/2026-05-06-react-migration/01-repo-skeleton.md @@ -143,8 +143,7 @@ admin-react/ "lucide-react": "latest", "sonner": "^1.7.0", "kbar": "^0.1.0", - "react-hook-form": "^7.53.0", - "@hookform/resolvers": "^3.9.0", + "@tanstack/react-form": "^1.29.3", "zod": "4.3.6", "ofetch": "1.5.1", "better-auth": "1.4.18", diff --git a/docs/superpowers/specs/2026-05-06-react-migration/02-design-tokens.md b/docs/superpowers/specs/2026-05-06-react-migration/02-design-tokens.md index f66b9a066..151319cfa 100644 --- a/docs/superpowers/specs/2026-05-06-react-migration/02-design-tokens.md +++ b/docs/superpowers/specs/2026-05-06-react-migration/02-design-tokens.md @@ -1,6 +1,7 @@ # 02 · Design Tokens **Date**: 2026-05-06 +**Status**: v1 calibrated (2026-05-10) **Owner spec**: [00-roadmap.md](./00-roadmap.md) **Phase**: P0 (v0) → calibrated end of P1 **Depends on**: 01 (vanilla-extract plugin wired) @@ -89,11 +90,30 @@ export const color = { ```ts // src/styles/tokens/typography.ts export const fontFamily = { - display: "'Inter', 'SF Pro Display', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif", - text: "'Inter', 'SF Pro Text', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif", + display: "'Inter Variable', 'Inter', 'SF Pro Display', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif", + text: "'Inter Variable', 'Inter', 'SF Pro Text', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif", mono: "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace", } as const +/** Atomic axes — pick one size + one weight when authoring a new component. */ +export const fontSize = { + xs: '11px', + sm: '12px', + md: '13px', + base: '14px', + lg: '16px', +} as const + +export const fontWeight = { + regular: '450', // Inter Variable axis; static fonts fall back to 400 + medium: '500', + semibold: '600', +} as const + +/** Linear icon scale: 16 row-start / 14 secondary / 12 inline-with-text. */ +export const iconSize = { lg: 16, md: 14, sm: 12 } as const + +/** Composed presets — bundle size + weight + lineHeight + color hint. */ export const typography = { displayXl: { size: '80px', weight: '600', lineHeight: '1.05', letterSpacing: '-3px' }, displayLg: { size: '56px', weight: '600', lineHeight: '1.10', letterSpacing: '-1.8px' }, @@ -108,10 +128,25 @@ export const typography = { button: { size: '14px', weight: '500', lineHeight: '1.20', letterSpacing: '0' }, eyebrow: { size: '13px', weight: '500', lineHeight: '1.30', letterSpacing: '0.4px' }, mono: { size: '13px', weight: '400', lineHeight: '1.50', letterSpacing: '0' }, + // Linear-aligned compact-list density (inbox / posts list / notes list / …). + // Only TWO sizes (13/12), only TWO weights (500/450). Hierarchy comes from + // weight + color, NOT size. line-height: normal = font-intrinsic ≈ 1.2. + listTitle: { size: fontSize.md, weight: fontWeight.medium, lineHeight: 'normal', letterSpacing: '0' }, + listMeta: { size: fontSize.md, weight: fontWeight.regular, lineHeight: 'normal', letterSpacing: '0' }, + listLabel: { size: fontSize.sm, weight: fontWeight.regular, lineHeight: 'normal', letterSpacing: '0' }, } as const ``` -**Admin context override**: most admin surfaces will live in `body` / `bodySm`; `display*` tokens exist for the rare oversized empty state, marketing-style auth screen, or onboarding hero. Keep them in the contract; use sparingly. +**Admin context override**: most admin surfaces live in `body` / `bodySm`; `display*` tokens exist for the rare oversized empty state, marketing-style auth screen, or onboarding hero. Keep them in the contract; use sparingly. + +**Compact-list density rules** (Linear inbox-aligned — applies to every read-list view: posts / notes / pages / says / recently / comments / etc.) + +- Use `listTitle` (13/500/normal/`ink`) for the row's primary text. It is the only text on a row that uses `medium` weight. +- Use `listMeta` (13/450/normal/`inkSubtle`) for everything else on the meta line — status, id, time, counts. Same metric as the title; only color differentiates it. +- `listLabel` (12/450/normal/`inkMuted`) is reserved for chip / pill bodies on coloured backgrounds. +- Never introduce a third list size. If you feel like you need 11 px or 14 px for a list element, change the **color** instead. +- Row height: `min-height: 57px` with `padding: 10px 16px` and `gap: 4px` between the title line and the meta line. This is the "B variant" used by `2026-05-10-posts-list-design.md`. +- Icon-in-text uses `iconSize.sm` (12). Row-start semantic icons use `iconSize.lg` (16). Right-of-row status / action glyphs use `iconSize.md` (14). ### spacing @@ -364,6 +399,101 @@ A short header at the top of DESIGN.md will state: ## Open questions -- **Surface ladder calibration**: the four surface step values need confirmation against linear.app computed styles. Owner: whoever picks up implementation. Resolves in P1 calibration window. +- ~~**Surface ladder calibration**~~ — resolved in v1 calibration pass below (2026-05-10). - **Light theme priority**: defer to 02b. Decide post-P1 whether light theme is part of v1 cutover or a follow-up. - **Charts theme**: `@antv/g2` and CodeMirror One Dark have their own theme APIs. Spec 09/10 own the bridge from `themeContract.color.*` into those libraries. + +--- + +## v1 calibrated · 2026-05-10 + +Calibration pass per spec §7 of [`2026-05-09-p1-layout-shell-design.md`](../2026-05-09-p1-layout-shell-design.md). Targets: color surface ladder + hairlines + canvas + active-nav background. Spacing / typography / radius / motion / zIndex unchanged. + +### color · before / after + +| Token | v0 | v1 | Rationale | +|---|---|---|---| +| `canvas` | `#010102` | `#08090b` | 纯黑过死,与 surface1 之差不可辨;微提亮且偏蓝调以拟 Linear 之深底;保 canvas → surface1 之 step。 | +| `surface1` | `#0f1011` | `#101113` | 微调 hue,使 5-lightness step 更匀;Card / panel 默认底。 | +| `surface2` | `#181a1c` | `#171a1d` | 与 surface1 之差更线性;hover / faint elevation。 | +| `surface3` | `#1f2124` | `#1f2226` | popover / dropdown / 强 hover;保留较显著之提升。 | +| `surface4` | `#26282b` | `#272b30` | 顶层 chip / floating popover;微调 hue 与 step。 | +| `hairline` | `#23252a` | `#23262b` | 标准分隔线;hue 微调贴 surface 序列。 | +| `hairlineStrong` | `#2f3137` | `#2f3239` | 强调分隔,重 emphasis 时用。 | +| `hairlineTertiary` | `#1a1c20` | `#181b1f` | 轻分隔(如卡内分组),原值过亮。 | +| `primary / primaryHover / primaryFocus` | `#5e6ad2 / #828fff / #5e69d1` | unchanged | Linear brand 精确值,不动。 | + +### sidebar 之 active 项背景 + +非 token,乃局部静态值,置于 `src/layouts/AppShell/Sidebar.css.ts`: + +| | v0 | v1 | Rationale | +|---|---|---|---| +| `itemRecipe.variants.active.true.background` | `themeContract.color.surface3` | `'#1a1c20'` | 旧值过亮喧宾,新值居 surface2 与 surface3 之间,与 sidebar 底色协而不杂。spec §7 «out of scope: 静态值可解者勿污 contract»,故不入 token。 | + +### 不动者 + +- `spacing` · `radius` · `zIndex` · `typography` · `fontFamily` · `motion` · `elevation` :v0 即定,无变。 +- `chrome` 维度 token:在 P1 layout shell 落地时已就位(`src/styles/tokens/chrome.ts`),非本次 v1 calibration 之新增。 +- 测试无依赖 hex 值者,`pnpm test` 仍绿。 + +### 影响 + +- `:root, :root.dark` 之 CSS 变量值随之而变;所有引用 `themeContract.color.*` 之处自动生效。 +- `src/styles/tokens/elevation.ts` 之 recipe 仍引用 `themeContract`,故 elevation 表象随 surface 序列同步刷新。 +- 一处静态 hex(active nav)外,无其他直引值。 + +--- + +## v2 calibrated · 2026-05-11 + +Typography pass triggered by Linear inbox-row anatomy: visual hierarchy in compact lists must come from **weight + color**, not size. Posts-list v1 first surfaced the gap (rows felt crowded, 14/500 title vs 12/400 meta read as two competing weights). Source: live-fetched computed styles from `linear.app/lobehub/reviews` of the "Ready to merge" list. Only typography & icon-size tokens shift — color / spacing / radius / motion / zIndex unchanged. + +### Linear's measured row anatomy (sample row #14597) + +| Element | size | weight | line-height | letter-spacing | color | +|---|---|---|---|---|---| +| Title | 13px | 500 | normal (≈15.6) | normal | `lch(100 0 272)` (≈ ink) | +| Status / id / time | 13px | 450 | normal | normal | `lch(61.4 1.15 272)` (≈ inkSubtle) | +| Branch / chip label | 12px | 450 | normal (≈14.4) | normal | `lch(90.35 1.15 272)` (≈ inkMuted) | +| Row icon (start) | 16 × 16 | — | — | — | `currentColor` | +| Inline meta icon | 12 × 12 | — | — | — | `currentColor` | +| Right-side status / action | 14 × 14 | — | — | — | `currentColor` | + +Row container: 889 × **57** px, `padding: 0` outer, `2px 0` wrapper, two text lines centered with ~12 px between baselines. `font-family: 'Inter Variable', 'SF Pro Display', system-ui, …`. + +### token additions · before / after + +| Symbol | v1 | v2 | Rationale | +|---|---|---|---| +| `fontFamily.text / display` | `'Inter', 'SF Pro …'` | `'Inter Variable', 'Inter', 'SF Pro …'` | 加 Inter Variable 首位,俾 weight 450 真生效;无变体字体之机退至 'Inter' 静体(450 → 400 之舍入)。 | +| `fontSize` | (无原子轴) | `xs:11 / sm:12 / md:13 / base:14 / lg:16` | Linear 之轴只 13/12 二档,但 admin 偶有 11/14/16 之需(pane title / button / right-pane title),故全表立。 | +| `fontWeight` | (无原子轴) | `regular:450 / medium:500 / semibold:600` | Linear inbox 实测之 450 而非 400,俾 meta line 较 caption 更厚;title 之 500 与之差 50,正好成轻重对比而不喧。 | +| `iconSize` | (无原子轴) | `lg:16 / md:14 / sm:12` | Linear 三档:行首语义 16,次级状态 14,inline-with-text 12。无 18/20 — 大于 16 之图标当回到 components 自决。 | +| `typography.listTitle` | (未存) | `13/500/normal/0` | 行标,唯一之 medium-weight 元素。 | +| `typography.listMeta` | (未存) | `13/450/normal/0` | 状态 / id / time / counts 之同一规格,仅以色分级。 | +| `typography.listLabel` | (未存) | `12/450/normal/0` | chip / pill 之内文,inkMuted 色以平衡彩底。 | + +### compact-list 密度之规 + +不再以「每个列表自定 size」而以 token 表所记。posts list / notes list / pages list / says / recently / comments — 凡 read-list 之视,皆遵: + +- 行 `min-height: 57px`,padding `10px 16px`,title↔meta 行间 `gap: 4px`。 +- 标题 = `typography.listTitle`,唯一 medium。 +- 元数据 = `typography.listMeta`,唯色分级(`inkSubtle` / `inkMuted` / `inkTertiary`)。 +- 不引第三号字。如须变化,换色,毋换号。 +- icon 三档严守 `iconSize.{lg|md|sm}`。 + +### 不动者 + +- `color` · `spacing` · `radius` · `zIndex` · `motion` · `elevation` · `chrome` :v1 即定,无变。 +- 既有 `typography.{displayXl..mono}` preset 全保留 — 仅追新者 `listTitle / listMeta / listLabel` 与原子轴;既有引用之处不动。 +- `body` 全局之 16px / 400 / 1.5 不变(global.css.ts 仍引 `typography.body`)。 + +### 影响 + +- `src/pages/posts/view/PostsView.css.ts` 之 row / meta / time / category 全改用 `typography.list*` + `themeContract.color.*`,row 高 56 → **57**。 +- `src/pages/posts/view/list/Row.tsx` 之 `lucide` icon size 11 → `iconSize.sm`(12)。 +- 后续 list views(spec 11 batch 3a 之 notes / pages / says / recently)必直引此处 token,毋再于 view 内立私规模。 +- 既有非 list 视面(dashboard / setup / login / 表单)之 typography preset 不动,故无视觉回归。 +- `pnpm typecheck` + `oxlint` + 122 vitest 皆绿(2026-05-11)。 diff --git a/docs/superpowers/specs/2026-05-06-react-migration/03-ui-primitives.md b/docs/superpowers/specs/2026-05-06-react-migration/03-ui-primitives.md index a26a9836c..8681c5d64 100644 --- a/docs/superpowers/specs/2026-05-06-react-migration/03-ui-primitives.md +++ b/docs/superpowers/specs/2026-05-06-react-migration/03-ui-primitives.md @@ -26,6 +26,7 @@ Defines the headless-primitive layer wrapping `@base-ui-components/react` (Base - **Motion is opt-in.** Primitives ship with sensible enter/exit animations via `motion` for overlays (Modal, Drawer, Tooltip, Popover, Toast). Buttons / inputs / cards animate on hover/focus via CSS transitions only. - **One props philosophy: `intent | size | tone`.** Intent describes *purpose* (primary, secondary, danger). Size scales padding / type. Tone is rare — reserved for cases where a primitive needs a chromatic accent (e.g. Tag). - **Class-merge via a tiny `cx` helper** in `src/utils/cx.ts`. No `clsx` dependency unless conditional logic explodes. +- **Scrolling — always through ``.** Any region that the user can scroll (modal body, dropdown popups, list panes, sidebar nav, page content, drawers, custom virtualized lists, and so on) MUST route through the `Scroll` primitive. Raw `overflow: auto` / `overflow: scroll` is banned in component and layout CSS — including `Modal.Body`, `Select.Popup`, `CommandPalette` results, `FullLayout.Body`, `TwoColLayout` panes, and `AppShell` sidebar nav, all of which were converted on 2026-05-10. **Pattern**: the parent owns sizing (`flex: 1; min-height: 0` or fixed height) and uses `display: flex` so the `` child can fill it; `` wraps a single inner element that holds the actual content + padding. Exception: the page-level `` / `` natural scroll is fine — Scroll is for in-app containers. --- @@ -206,27 +207,28 @@ export { toast } Ships in P2 (after P1 calibrates tokens). Rough order of dependence so blockers come first: -| Primitive | Base UI source | Notes | -|---|---|---| -| **Drawer** | `dialog` (positioned) | Slide-in panel; reuses Modal animation kit. Variants: `placement` ∈ {right, left, bottom, top}. | -| **Tooltip** | `tooltip` | Default delay 300 ms. Hover + focus trigger. | -| **Popover** | `popover` | Floating-UI under the hood. Used heavily in editor toolbars. | -| **Tabs** | `tabs` | Accessible roving tabindex, controlled + uncontrolled. | -| **Select** | `select` | Headless dropdown; multi-select supported via `multiple` prop. Search-in-list deferred to a `Combobox` if needed. | -| **Switch** | `switch` | On/off toggle. | -| **Checkbox** | `checkbox` | Tri-state via `indeterminate`. | -| **Radio** | `radio-group` | Group + Item pattern. | -| **Tag** | own | Plain styled ``. Variants: `tone` ∈ {neutral, primary, success, danger, warning, info}; `size` ∈ {sm, md}; `closable`. | -| **Avatar** | own | Variants: `size` ∈ {sm, md, lg, xl}; `shape` ∈ {circle, rounded}. Image fallback to initials. | -| **Badge** | own | Numeric or dot indicator on top-right of a slot. | -| **Pagination** | own | Page list + size dropdown. Internal state lifted to `useDataTableState` for tables. | -| **Skeleton** | own | Pulse animation via `motion`. Variants: `text`, `circle`, `rect`. | -| **Empty** | own | Centered empty-state composition: icon slot + title + description + optional CTA. | -| **Spinner** | own | Inline SVG; sizes match button sizes. | -| **Progress** | `progress` | Indeterminate + determinate. | -| **Ellipsis** | own | Pure CSS `text-overflow: ellipsis`. With Tooltip when overflowing — overflow detection via `useResizeObserver`. | -| **Scroll** | own | Compose-only — applies overflow + custom scrollbar styling on the children's root. No Base UI primitive needed. | -| **Space** | own | Layout-only flex/gap utility. Variants: `direction` ∈ {row, column}; `gap` ∈ spacing tokens; `align`, `justify`. | +| Primitive | Base UI source | Status | Notes | +|---|---|---|---| +| **Drawer** | `dialog` (positioned) | shipped 2026-05-10 | Slide-in panel; reuses Modal animation kit. Variants: `placement` ∈ {right, left, bottom, top}. | +| **Tooltip** | `tooltip` | shipped 2026-05-10 (P2 batch A) | Default delay 300 ms set on `` mounted in `App.tsx`. Sugar `` wraps Root + Trigger + Portal + Positioner + Popup; namespace parts also exposed. | +| **Popover** | `popover` | shipped 2026-05-10 (P2 batch A) | Namespace API (Root/Trigger/Portal/Positioner/Popup/Close/Title/Description/Backdrop). Popup variants: `padding` ∈ {none,sm,md,lg}, `width` ∈ {auto,sm,md,lg}. | +| **Tabs** | `tabs` | shipped 2026-05-10 (P2 batch A) | Variants: `variant` ∈ {underline, pill} threaded through React context so List/Tab/Indicator stay in sync. Indicator slides via Base UI's `--active-tab-*` CSS vars. | +| **Select** | `select` | shipped 2026-05-10 (P2 batch A) | Namespace API (Root/Trigger/Value/Icon/Portal/Positioner/Popup/Item). Trigger size ∈ {sm,md,lg}. Item ships an inline `ItemIndicator` ✓ check. Multi-select / Combobox deferred. | +| **Switch** | `switch` | shipped 2026-05-10 (P2 batch A) | Single `` component (size ∈ {sm,md,lg}) — Base UI emits the hidden input internally. | +| **Checkbox** | `checkbox` | shipped 2026-05-10 (P2 batch A) | Tri-state via `indeterminate`; renders a check or minus inside Indicator depending on state. Sizes {sm,md,lg}. | +| **Radio** | `radio` + `radio-group` | shipped 2026-05-10 (P2 batch B) | `Radio.Group` (orientation × size context) + `Radio.Root` + `Radio.Item` (label-wrapped sugar). Indicator scales via `data-starting-style` / `data-ending-style`. | +| **Textarea** | own | shipped 2026-05-10 (form-system needs) | Multi-line text input mirroring `Input`'s intent × size variants. `invalid` flips intent to `danger` and sets `aria-invalid`. Vertical resize only. | +| **Tag** | own | shipped 2026-05-10 (P2 batch B) | Variants: `tone` ∈ {neutral, primary, success, danger, warning, info}; `size` ∈ {sm, md}; `closable` exposes a sub-button with its own focus ring. | +| **Avatar** | `avatar` | shipped 2026-05-10 (P2 batch B) | Wraps Base UI avatar. Variants: `size` ∈ {xs, sm, md, lg, xl}; `shape` ∈ {circle, rounded}. Image → initials → `fallback` slot. | +| **Badge** | own | shipped 2026-05-10 (P2 batch B) | Numeric / dot. Anchors top-right when wrapping a child; renders standalone otherwise. `count` formats `${max}+` when over `max`. `showZero={false}` hides numeric zero. | +| **Pagination** | own | shipped 2026-05-10 (P2 batch B) | Page list + page-size Select. Caller-controlled (`page`, `pageSize`). Sibling pages collapse to ellipsis when range > 2·siblingCount + 5. Composes our `} +
+ name="name" label="名称" required> + {({ field, invalid }) => ( + + )} - - {(field) => } + name="url" label="URL" required> + {({ field, invalid }) => ( + + )} - - {(field) =>