Skip to content

Commit 40c0434

Browse files
committed
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
1 parent a15bad4 commit 40c0434

23 files changed

Lines changed: 3716 additions & 115 deletions

apps/admin/src/api/drafts.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import type {
88

99
import { request } from '~/utils/request'
1010

11+
export type DraftSortOrder = 'asc' | 'desc'
12+
1113
export interface GetDraftsParams {
1214
page?: number
1315
size?: number
1416
refType?: DraftRefType
1517
hasRef?: boolean
16-
sortBy?: string
17-
sortOrder?: 1 | -1
18+
sort_by?: string
19+
sort_order?: DraftSortOrder
1820
}
1921

2022
export interface CreateDraftData {

apps/admin/src/api/notes.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@ import type { NoteModel } from '~/models/note'
33

44
import { request } from '~/utils/request'
55

6+
export type NoteSortKey =
7+
| 'title'
8+
| 'createdAt'
9+
| 'modifiedAt'
10+
| 'weather'
11+
| 'mood'
12+
export type SortOrder = 'asc' | 'desc'
13+
614
export interface GetNotesParams {
715
page?: number
816
size?: number
9-
sortBy?: string
10-
sortOrder?: number
17+
sort_by?: NoteSortKey
18+
sort_order?: SortOrder
19+
/**
20+
* @deprecated backend dropped db_query in v12.10.x pager refactor; param is silently ignored
21+
*/
1122
db_query?: Record<string, boolean>
1223
}
1324

apps/admin/src/api/posts.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import type { PostModel } from '~/models/post'
33

44
import { request } from '~/utils/request'
55

6+
export type PostSortKey = 'createdAt' | 'modifiedAt' | 'pinAt'
7+
export type SortOrder = 'asc' | 'desc'
8+
69
export interface GetPostsParams {
710
page?: number
811
size?: number
9-
sortBy?: string
10-
sortOrder?: number
12+
sort_by?: PostSortKey
13+
sort_order?: SortOrder
1114
categoryIds?: string[]
1215
}
1316

apps/admin/src/views/extra-features/webhook/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,13 @@ export default defineComponent({
160160
data: submitData,
161161
})
162162
} else {
163-
createMutation.mutate(data as any)
163+
// Server requires `secret` as a string; admin form leaves it optional.
164+
// Default to "" when the user did not provide one so creation succeeds.
165+
const createPayload: Partial<WebhookModel> = {
166+
...data,
167+
secret: data.secret ?? '',
168+
}
169+
createMutation.mutate(createPayload as any)
164170
}
165171
}
166172

apps/admin/src/views/manage-notes/list.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { NButton, NEllipsis, NInput, NPopconfirm, NSpace } from 'naive-ui'
1414
import { computed, defineComponent, reactive, ref, watchEffect } from 'vue'
1515
import { RouterLink } from 'vue-router'
1616
import { toast } from 'vue-sonner'
17+
import type { NoteSortKey } from '~/api/notes'
1718
import type { NoteModel } from '~/models/note'
1819
import type { TableColumns } from 'naive-ui/lib/data-table/src/interface'
1920
import type { PropType } from 'vue'
@@ -225,8 +226,12 @@ export const ManageNoteListView = defineComponent({
225226
return notesApi.getList({
226227
page: params.page,
227228
size: params.size,
228-
sortBy: params.sortBy || undefined,
229-
sortOrder: params.sortOrder || undefined,
229+
...(params.sortBy
230+
? {
231+
sort_by: params.sortBy as NoteSortKey,
232+
sort_order: params.sortOrder === 1 ? 'asc' : 'desc',
233+
}
234+
: {}),
230235
db_query: params.filters?.dbQuery,
231236
}) as Promise<any>
232237
},

apps/admin/src/views/manage-posts/list.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from 'vue'
2020
import { RouterLink } from 'vue-router'
2121
import { toast } from 'vue-sonner'
22+
import type { PostSortKey } from '~/api/posts'
2223
import type {
2324
FilterOption,
2425
FilterState,
@@ -206,8 +207,8 @@ export const ManagePostListView = defineComponent({
206207
categoryIds: params.filters?.categoryIds,
207208
...(params.sortBy
208209
? {
209-
sortBy: params.sortBy,
210-
sortOrder: params.sortOrder,
210+
sort_by: params.sortBy as PostSortKey,
211+
sort_order: params.sortOrder === 1 ? 'asc' : 'desc',
211212
}
212213
: {}),
213214
})

docs/superpowers/specs/2026-05-06-react-migration/00-roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ docs/superpowers/specs/2026-05-06-react-migration/
2020
├── 05-data-layer.md ← request.ts port, TanStack Query patterns, socket.io hook
2121
├── 06-routing-auth.md ← react-router tree, ProtectedRoute, better-auth + passkey
2222
├── 07-layouts-patterns.md ← AppShell, Sidebar, MasterDetailLayout, header-action injection
23-
├── 08-form-system.md ← react-hook-form + zod, ConfigForm DSL port
23+
├── 08-form-system.md ← @tanstack/react-form + zod (Standard Schema), ConfigForm DSL port
2424
├── 09-editors.md ← haklex direct-mount, monaco, codemirror, xterm, draft system
2525
├── 10-charts-misc.md ← G2 hook, kbar swap, shiki/marked, diffs, excalidraw, confetti
2626
├── 11-views-migration.md ← 21 views in batches; preconditions + acceptance per batch
@@ -43,7 +43,7 @@ docs/superpowers/specs/2026-05-06-react-migration/
4343
| UI primitives | Base UI (`@base-ui-components/react`) | headless; pairs with css.ts |
4444
| Styling | css.ts (`@vanilla-extract/css` + vite plugin) | Compile-time CSS; tokens live as TS exports |
4545
| Design system | Linear dark-canvas (per pasted DESIGN.md) | Lavender accent, four-step surface ladder, no second chromatic accent |
46-
| Forms | `react-hook-form` + `zod` | `zod` already in source repo |
46+
| Forms | `@tanstack/react-form` + `zod` (Standard Schema) | aligns with TanStack Query/Table; zod already in source repo; library swap landed 2026-05-10 |
4747
| Animation | `motion` | User-specified |
4848
| Icons | `lucide-react` | 1:1 swap from `lucide-vue-next` |
4949
| Toast | `sonner` | 1:1 swap from `vue-sonner` |

docs/superpowers/specs/2026-05-06-react-migration/01-repo-skeleton.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,7 @@ admin-react/
143143
"lucide-react": "latest",
144144
"sonner": "^1.7.0",
145145
"kbar": "^0.1.0",
146-
"react-hook-form": "^7.53.0",
147-
"@hookform/resolvers": "^3.9.0",
146+
"@tanstack/react-form": "^1.29.3",
148147
"zod": "4.3.6",
149148
"ofetch": "1.5.1",
150149
"better-auth": "1.4.18",

docs/superpowers/specs/2026-05-06-react-migration/02-design-tokens.md

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# 02 · Design Tokens
22

33
**Date**: 2026-05-06
4+
**Status**: v1 calibrated (2026-05-10)
45
**Owner spec**: [00-roadmap.md](./00-roadmap.md)
56
**Phase**: P0 (v0) → calibrated end of P1
67
**Depends on**: 01 (vanilla-extract plugin wired)
@@ -89,11 +90,30 @@ export const color = {
8990
```ts
9091
// src/styles/tokens/typography.ts
9192
export const fontFamily = {
92-
display: "'Inter', 'SF Pro Display', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif",
93-
text: "'Inter', 'SF Pro Text', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif",
93+
display: "'Inter Variable', 'Inter', 'SF Pro Display', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif",
94+
text: "'Inter Variable', 'Inter', 'SF Pro Text', -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif",
9495
mono: "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace",
9596
} as const
9697

98+
/** Atomic axes — pick one size + one weight when authoring a new component. */
99+
export const fontSize = {
100+
xs: '11px',
101+
sm: '12px',
102+
md: '13px',
103+
base: '14px',
104+
lg: '16px',
105+
} as const
106+
107+
export const fontWeight = {
108+
regular: '450', // Inter Variable axis; static fonts fall back to 400
109+
medium: '500',
110+
semibold: '600',
111+
} as const
112+
113+
/** Linear icon scale: 16 row-start / 14 secondary / 12 inline-with-text. */
114+
export const iconSize = { lg: 16, md: 14, sm: 12 } as const
115+
116+
/** Composed presets — bundle size + weight + lineHeight + color hint. */
97117
export const typography = {
98118
displayXl: { size: '80px', weight: '600', lineHeight: '1.05', letterSpacing: '-3px' },
99119
displayLg: { size: '56px', weight: '600', lineHeight: '1.10', letterSpacing: '-1.8px' },
@@ -108,10 +128,25 @@ export const typography = {
108128
button: { size: '14px', weight: '500', lineHeight: '1.20', letterSpacing: '0' },
109129
eyebrow: { size: '13px', weight: '500', lineHeight: '1.30', letterSpacing: '0.4px' },
110130
mono: { size: '13px', weight: '400', lineHeight: '1.50', letterSpacing: '0' },
131+
// Linear-aligned compact-list density (inbox / posts list / notes list / …).
132+
// Only TWO sizes (13/12), only TWO weights (500/450). Hierarchy comes from
133+
// weight + color, NOT size. line-height: normal = font-intrinsic ≈ 1.2.
134+
listTitle: { size: fontSize.md, weight: fontWeight.medium, lineHeight: 'normal', letterSpacing: '0' },
135+
listMeta: { size: fontSize.md, weight: fontWeight.regular, lineHeight: 'normal', letterSpacing: '0' },
136+
listLabel: { size: fontSize.sm, weight: fontWeight.regular, lineHeight: 'normal', letterSpacing: '0' },
111137
} as const
112138
```
113139

114-
**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.
140+
**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.
141+
142+
**Compact-list density rules** (Linear inbox-aligned — applies to every read-list view: posts / notes / pages / says / recently / comments / etc.)
143+
144+
- Use `listTitle` (13/500/normal/`ink`) for the row's primary text. It is the only text on a row that uses `medium` weight.
145+
- 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.
146+
- `listLabel` (12/450/normal/`inkMuted`) is reserved for chip / pill bodies on coloured backgrounds.
147+
- 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.
148+
- 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`.
149+
- 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).
115150

116151
### spacing
117152

@@ -364,6 +399,101 @@ A short header at the top of DESIGN.md will state:
364399

365400
## Open questions
366401

367-
- **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.
402+
- ~~**Surface ladder calibration**~~ — resolved in v1 calibration pass below (2026-05-10).
368403
- **Light theme priority**: defer to 02b. Decide post-P1 whether light theme is part of v1 cutover or a follow-up.
369404
- **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.
405+
406+
---
407+
408+
## v1 calibrated · 2026-05-10
409+
410+
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.
411+
412+
### color · before / after
413+
414+
| Token | v0 | v1 | Rationale |
415+
|---|---|---|---|
416+
| `canvas` | `#010102` | `#08090b` | 纯黑过死,与 surface1 之差不可辨;微提亮且偏蓝调以拟 Linear 之深底;保 canvas → surface1 之 step。 |
417+
| `surface1` | `#0f1011` | `#101113` | 微调 hue,使 5-lightness step 更匀;Card / panel 默认底。 |
418+
| `surface2` | `#181a1c` | `#171a1d` | 与 surface1 之差更线性;hover / faint elevation。 |
419+
| `surface3` | `#1f2124` | `#1f2226` | popover / dropdown / 强 hover;保留较显著之提升。 |
420+
| `surface4` | `#26282b` | `#272b30` | 顶层 chip / floating popover;微调 hue 与 step。 |
421+
| `hairline` | `#23252a` | `#23262b` | 标准分隔线;hue 微调贴 surface 序列。 |
422+
| `hairlineStrong` | `#2f3137` | `#2f3239` | 强调分隔,重 emphasis 时用。 |
423+
| `hairlineTertiary` | `#1a1c20` | `#181b1f` | 轻分隔(如卡内分组),原值过亮。 |
424+
| `primary / primaryHover / primaryFocus` | `#5e6ad2 / #828fff / #5e69d1` | unchanged | Linear brand 精确值,不动。 |
425+
426+
### sidebar 之 active 项背景
427+
428+
非 token,乃局部静态值,置于 `src/layouts/AppShell/Sidebar.css.ts`
429+
430+
| | v0 | v1 | Rationale |
431+
|---|---|---|---|
432+
| `itemRecipe.variants.active.true.background` | `themeContract.color.surface3` | `'#1a1c20'` | 旧值过亮喧宾,新值居 surface2 与 surface3 之间,与 sidebar 底色协而不杂。spec §7 «out of scope: 静态值可解者勿污 contract»,故不入 token。 |
433+
434+
### 不动者
435+
436+
- `spacing` · `radius` · `zIndex` · `typography` · `fontFamily` · `motion` · `elevation` :v0 即定,无变。
437+
- `chrome` 维度 token:在 P1 layout shell 落地时已就位(`src/styles/tokens/chrome.ts`),非本次 v1 calibration 之新增。
438+
- 测试无依赖 hex 值者,`pnpm test` 仍绿。
439+
440+
### 影响
441+
442+
- `:root, :root.dark` 之 CSS 变量值随之而变;所有引用 `themeContract.color.*` 之处自动生效。
443+
- `src/styles/tokens/elevation.ts` 之 recipe 仍引用 `themeContract`,故 elevation 表象随 surface 序列同步刷新。
444+
- 一处静态 hex(active nav)外,无其他直引值。
445+
446+
---
447+
448+
## v2 calibrated · 2026-05-11
449+
450+
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.
451+
452+
### Linear's measured row anatomy (sample row #14597)
453+
454+
| Element | size | weight | line-height | letter-spacing | color |
455+
|---|---|---|---|---|---|
456+
| Title | 13px | 500 | normal (≈15.6) | normal | `lch(100 0 272)` (≈ ink) |
457+
| Status / id / time | 13px | 450 | normal | normal | `lch(61.4 1.15 272)` (≈ inkSubtle) |
458+
| Branch / chip label | 12px | 450 | normal (≈14.4) | normal | `lch(90.35 1.15 272)` (≈ inkMuted) |
459+
| Row icon (start) | 16 × 16 |||| `currentColor` |
460+
| Inline meta icon | 12 × 12 |||| `currentColor` |
461+
| Right-side status / action | 14 × 14 |||| `currentColor` |
462+
463+
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, …`.
464+
465+
### token additions · before / after
466+
467+
| Symbol | v1 | v2 | Rationale |
468+
|---|---|---|---|
469+
| `fontFamily.text / display` | `'Inter', 'SF Pro …'` | `'Inter Variable', 'Inter', 'SF Pro …'` | 加 Inter Variable 首位,俾 weight 450 真生效;无变体字体之机退至 'Inter' 静体(450 → 400 之舍入)。 |
470+
| `fontSize` | (无原子轴) | `xs:11 / sm:12 / md:13 / base:14 / lg:16` | Linear 之轴只 13/12 二档,但 admin 偶有 11/14/16 之需(pane title / button / right-pane title),故全表立。 |
471+
| `fontWeight` | (无原子轴) | `regular:450 / medium:500 / semibold:600` | Linear inbox 实测之 450 而非 400,俾 meta line 较 caption 更厚;title 之 500 与之差 50,正好成轻重对比而不喧。 |
472+
| `iconSize` | (无原子轴) | `lg:16 / md:14 / sm:12` | Linear 三档:行首语义 16,次级状态 14,inline-with-text 12。无 18/20 — 大于 16 之图标当回到 components 自决。 |
473+
| `typography.listTitle` | (未存) | `13/500/normal/0` | 行标,唯一之 medium-weight 元素。 |
474+
| `typography.listMeta` | (未存) | `13/450/normal/0` | 状态 / id / time / counts 之同一规格,仅以色分级。 |
475+
| `typography.listLabel` | (未存) | `12/450/normal/0` | chip / pill 之内文,inkMuted 色以平衡彩底。 |
476+
477+
### compact-list 密度之规
478+
479+
不再以「每个列表自定 size」而以 token 表所记。posts list / notes list / pages list / says / recently / comments — 凡 read-list 之视,皆遵:
480+
481+
-`min-height: 57px`,padding `10px 16px`,title↔meta 行间 `gap: 4px`
482+
- 标题 = `typography.listTitle`,唯一 medium。
483+
- 元数据 = `typography.listMeta`,唯色分级(`inkSubtle` / `inkMuted` / `inkTertiary`)。
484+
- 不引第三号字。如须变化,换色,毋换号。
485+
- icon 三档严守 `iconSize.{lg|md|sm}`
486+
487+
### 不动者
488+
489+
- `color` · `spacing` · `radius` · `zIndex` · `motion` · `elevation` · `chrome` :v1 即定,无变。
490+
- 既有 `typography.{displayXl..mono}` preset 全保留 — 仅追新者 `listTitle / listMeta / listLabel` 与原子轴;既有引用之处不动。
491+
- `body` 全局之 16px / 400 / 1.5 不变(global.css.ts 仍引 `typography.body`)。
492+
493+
### 影响
494+
495+
- `src/pages/posts/view/PostsView.css.ts` 之 row / meta / time / category 全改用 `typography.list*` + `themeContract.color.*`,row 高 56 → **57**
496+
- `src/pages/posts/view/list/Row.tsx``lucide` icon size 11 → `iconSize.sm`(12)。
497+
- 后续 list views(spec 11 batch 3a 之 notes / pages / says / recently)必直引此处 token,毋再于 view 内立私规模。
498+
- 既有非 list 视面(dashboard / setup / login / 表单)之 typography preset 不动,故无视觉回归。
499+
- `pnpm typecheck` + `oxlint` + 122 vitest 皆绿(2026-05-11)。

0 commit comments

Comments
 (0)