refactor(ui): 侧栏统一成一个组件,次级页共享外壳,跳转不再整页刷新
BUG-744 / BUG-745 / BUG-746,任务书 TASK-sidebar-unify-20260916。 T1 侧栏只剩一个组件。`AppSidebar` 收编次级页那份另写的侧栏:会话操作与 账户菜单收进可选的 `controls`,不传就渲染只读模式。只读行仍是同一个 `SidebarSessionRow`、同一套 `.session-row > .session-main` 标记,只是 `.session-main` 是 `<Link>`、不渲染菜单按钮,并用 `data-readonly="true"` 去掉那一列从不使用的 44px 空位;页脚是同一个 56px `.profile-trigger`, 渲染成去 `/` 的链接。只读模式只少菜单按钮、chevron、账户菜单三样。 `app-nav-rail.tsx`、`use-nav-rail.ts` 与 `.nav-rail-*` 两段 CSS 删除。 T2 四个次级路由移进 `app/(secondary)/` 路由组,`layout.tsx` 承载 `SidebarProvider + AppSidebar(只读) + SidebarInset`。`SecondaryShell` 拆剩 46px 顶栏并改名 `SecondaryHeader`,14 处调用同步。路由组不进 URL,四个 地址与四个渲染标记均未变。 T3 `sidebar-data-cache.ts`:模块级、按账户 id 键、60 秒的内存缓存,同步读 再后台刷新,不落 localStorage。`use-session-management.ts` 的新建 / 重命名 / 删除 / 归档 / 收藏成功后失效,401 清空。 T4 三个页面项改 `<SidebarMenuLink href=…>` 客户端跳转,`persistLoginSessionReturn()` 保留在 `onClick` 里,`/login` 仍是硬跳转。顺带删掉从未被调用的死 prop `onOpenReports`;它删掉后 `page.tsx` 的 `router` 再无消费者,`useConsultationRun` 里同样解构成 `_router` 的死参数一并删。 T5 折叠状态存 localStorage 的 `sidebar_state`(不用 cookie:`/`、`/chart`、 `/ephemeris` 都是 Static,服务端读 cookie 会让三条路由掉出静态渲染)。移动端 抽屉不记。整页加载首帧仍可能闪一下,属让步顺序第 1 条,写在 BUG-746 与真机清单。 `Home()` 的 useState 36 / useRef 37 均未增长,`page.tsx` 净删 1 行。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
302ff08504
commit
d9d347236f
+42
-21
@@ -346,7 +346,7 @@ The birth-time rectification session is the consultation transcript plus a house
|
||||
- **Scrollbar:** `SidebarContent`, ordinary session `.conversation`, and the rectification house board use a quiet overlay scrollbar: transparent track, no `scrollbar-gutter`, and a 4px warm thumb mixed from `--color-ink`. The thumb stays transparent until hover or keyboard focus inside the scroller, then uses `color-mix(in srgb, var(--color-ink) 26%, transparent)`; thumb hover uses 40%. Increased contrast keeps the thumb visible; forced colors restore the system scrollbar.
|
||||
- **Motion:** Sidebar state changes are immediate on desktop, tablet, and mobile. The 44px trigger keeps one stable 18px sidebar glyph and never enters an intermediate scale or opacity state.
|
||||
- **Accessibility:** Command/Control+B shortcut outside editable controls, contextual trigger labels, 44px targets, focus return, collapsed-only tooltips, reduced-motion, reduced-transparency, and increased-contrast support.
|
||||
- **State:** session-local; reload uses breakpoint defaults rather than cookie or local-storage persistence.
|
||||
- **State:** the desktop and tablet collapse state is remembered per browser under the `sidebar_state` localStorage key, so collapsing on `/` survives the walk to `/chart` and back. Nothing is stored until someone actually toggles it; with nothing stored the breakpoint defaults stand. The mobile drawer stays session-local and is never remembered — reopening it on every navigation is not a preference anyone set. localStorage rather than the shadcn `sidebar_state` cookie because `/`, `/chart` and `/ephemeris` are static routes and a server-read cookie would opt all three out of static rendering; the stored value is therefore applied in the same post-mount effect that already applied the breakpoint default, and costs no frame that default did not already cost.
|
||||
|
||||
### Message
|
||||
|
||||
@@ -376,25 +376,46 @@ Text release is paced, not animated: the frame buffer commits at most once per a
|
||||
|
||||
### Secondary page shell
|
||||
|
||||
`/chart`, `/ephemeris` and `/reports` share the chat shell through
|
||||
`SecondaryShell`. Each used to own a full-screen layout — a `*-shell` root, a
|
||||
`*-topbar` with one 「返回对话」 link, and a `*-hero` with a page-sized h1 —
|
||||
three copies of one skeleton, none of them carrying the sidebar.
|
||||
`/chart`, `/ephemeris`, `/reports` and `/reports/[reportId]` share the chat
|
||||
shell. Each used to own a full-screen layout — a `*-shell` root, a `*-topbar`
|
||||
with one 「返回对话」 link, and a `*-hero` with a page-sized h1 — three copies of
|
||||
one skeleton, none of them carrying the sidebar.
|
||||
|
||||
- **Composition:** `SidebarProvider` → `AppNavRail` → `SidebarInset.chat-panel.secondary-panel`.
|
||||
The header is the same 46px row the chat page uses: sidebar trigger, page name,
|
||||
an optional quiet note chip, and up to two right-aligned actions. The body is
|
||||
the only thing that scrolls, and it owns the page floor (`--color-canvas-soft`).
|
||||
- **The rail is read-only.** It carries the brand, 新建对话, the three page
|
||||
actions, and one flat 「最近」 list whose rows navigate to `/?c=<uuid>`.
|
||||
Renaming, pinning, archiving, deleting and the account menu stay on `/`: they
|
||||
are backed by `Home()`'s optimistic-update and rollback layer, and lifting that
|
||||
so four routes can share it is far more than these pages need. The footer shows
|
||||
identity and balance as text, not as a menu trigger.
|
||||
- **Loading:** the rail renders its nav immediately and fills the list when
|
||||
- **Composition:** `app/(secondary)/layout.tsx` = `SidebarProvider` →
|
||||
`AppSidebar` (read-only) → `SidebarInset.chat-panel.secondary-panel`. The
|
||||
route group changes no URL; what it changes is that the four routes share
|
||||
**one** shell instance, so moving between them neither remounts the sidebar
|
||||
nor re-reads the session list. Each page contributes only `SecondaryHeader`:
|
||||
the same 46px row the chat page uses — sidebar trigger, page name, an optional
|
||||
quiet note chip, and up to two right-aligned actions. The body is the only
|
||||
thing that scrolls, and it owns the page floor (`--color-canvas-soft`).
|
||||
- **One sidebar component, two modes.** `AppSidebar` is the only sidebar in the
|
||||
product. `/` passes `controls`; the secondary layout does not, and without it
|
||||
the same markup renders read-only. There used to be a second component for
|
||||
these pages, and two components meant two shapes: its rows had no
|
||||
`.session-main`, so they lost the 44px minimum, the row padding and the 2px
|
||||
current-item marker, while `.session-row` still reserved a 44px column for a
|
||||
menu button it never rendered.
|
||||
- **What read-only drops — exactly three things.** The per-row menu trigger (and
|
||||
with it the reserved column, via `.session-row[data-readonly="true"]`), the
|
||||
footer chevron, and the account menu behind it. Everything else is the same
|
||||
element, the same class and the same size: brand row, 新建对话, the three page
|
||||
actions, the flat 「最近」 list, the 56px `.profile-trigger`. Rows are links to
|
||||
`/?c=<uuid>`; the footer is a link to `/`. Renaming, pinning, archiving and
|
||||
deleting stay on `/`, backed by `Home()`'s optimistic-update and rollback
|
||||
layer, which is far more than these four routes need.
|
||||
- **Leaving the chat is a navigation, not a reload.** 星盘 / 星历 / 我的报告 are
|
||||
`<Link>` on every page, including `/`, where they used to be a full document
|
||||
load that threw away the React tree, the session list and the account. Only
|
||||
`/login` stays a hard exit, and `persistLoginSessionReturn()` still stashes the
|
||||
`?c=` before any of them.
|
||||
- **Loading:** the sidebar renders its nav immediately and fills the list when
|
||||
`/api/sessions` returns. While that is in flight the list area carries static
|
||||
copy — never a skeleton, per the unified-loading ruling.
|
||||
- **Signed out:** the rail degrades to brand, nav, a 「登录后可以看到你的对话」
|
||||
copy — never a skeleton, per the unified-loading ruling. A list read in the
|
||||
last 60 seconds is reused from memory and shown in the first frame; the entry
|
||||
is keyed by account, never persisted, dropped on any session write on `/` and
|
||||
on any 401.
|
||||
- **Signed out:** the sidebar degrades to brand, nav, a 「登录后可以看到你的对话」
|
||||
line, and a 去登录 footer. It never renders an error state of its own.
|
||||
- **Page name:** lives in the header, so none of these pages carries an `h1` any
|
||||
more. A page's own reassurance copy (the chart page's cost-and-speed eyebrow,
|
||||
@@ -420,7 +441,7 @@ three copies of one skeleton, none of them carrying the sidebar.
|
||||
- **TOC rail:** a persistent right-hand column built from the outline's own `##`/`###` ids — no second slugger, and nothing that touches the chart-grid rehype pass. It is sticky under the report's own action bar, highlights the section in view via `IntersectionObserver`, and marks it with `aria-current="location"` plus a 2px `--color-action` bar. Placement is explicit (`grid-column: 2`) rather than DOM-ordered, so the narrow-screen drawer can stay first in the source. Below 860px the rail is gone and what remains is the collapsed 「目录」 drawer.
|
||||
- **Paper stays paper:** the rail reads in the app palette because it is chrome. `--report-paper` / `--report-rule` / `--report-accent` are the document's own, deliberately out of step with the app accent, and nothing in this surface puts them on chrome or takes chrome colour onto the page. D11, continuing D3.
|
||||
- **Surface:** page floor `--color-canvas-soft`; the Markdown article is a `--color-canvas` sheet with a hairline and `--radius-lg`. Print flattens the sheet, hides chrome and the TOC, and pins the light palette.
|
||||
- **Print inside the shell:** printing takes the whole document, so the reader ships a second `media="print"` stylesheet that hides the nav rail and the 46px header **and unlocks them**: `.chat-app` / `.chat-panel` are `height: 100%; overflow: hidden`, which would otherwise clip a nine-section report to one page. It lives on the component rather than in `globals.css` so it exists only while the reader is mounted and no other surface has to know about it. The `@page` rule is unchanged.
|
||||
- **Print inside the shell:** printing takes the whole document, so the reader ships a second `media="print"` stylesheet that hides the sidebar and the 46px header **and unlocks them**: `.chat-app` / `.chat-panel` are `height: 100%; overflow: hidden`, which would otherwise clip a nine-section report to one page. It lives on the component rather than in `globals.css` so it exists only while the reader is mounted and no other surface has to know about it. The `@page` rule is unchanged.
|
||||
- **Typography:** Markdown `h2` uses `--type-display-sm` serif at weight 400. `h3` uses `--type-title-md` sans at weight 500. Body is `--type-body-md` at 1.65. Honesty labels (`blocked` / `conflict` / `parameter_sensitive`) stay visible; the reading-nav and quality-matrix sections are not collapsed.
|
||||
- **Charts:** a rehype pass groups each `####` chart heading (optional note paragraph, then the `jyotish-chart` fence) into `.personal-report-chart-card` and consecutive cards into the existing `.personal-report-chart-grid`. A lone card uses `is-single`. The grid is two columns; print and the 760px breakpoint stay one column. Do not float chart headings or figures, and do not use negative margins to fake columns. Occupants are one-character Chinese names plus integer degree (`日 12°`), with `逆` after the name when retrograde (`土逆 3°`). House corners show rasi numbers 1–12. Chart JSON fences are stripped from the `.md` download. Markdown still uses `react-markdown` + `remark-gfm`, `skipHtml`, and no `rehype-raw`. The article tree is memoized; TOC highlight state lives in `ReportToc`, not in `PersonalReportMarkdownView`.
|
||||
- **Accent:** headings stay ink. Dark ink is never a page-scale rule or card edge.
|
||||
@@ -704,7 +725,7 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
|
||||
|
||||
### 结构
|
||||
|
||||
页名「星盘」与出生资料一行在 `SecondaryShell` 的 46px 顶栏里(§ Secondary page shell)。顶栏之下第一件东西是 Tab 行,眉标「主盘直接算 · 分盘按需 · 不消耗点数」跟在 Tab 行下方作为一行静默说明。等待外壳和失败页只有原因/等待句,没有眉标。不得写「打开即有」或任何速度承诺。
|
||||
页名「星盘」与出生资料一行在 `SecondaryHeader` 的 46px 顶栏里(§ Secondary page shell)。顶栏之下第一件东西是 Tab 行,眉标「主盘直接算 · 分盘按需 · 不消耗点数」跟在 Tab 行下方作为一行静默说明。等待外壳和失败页只有原因/等待句,没有眉标。不得写「打开即有」或任何速度承诺。
|
||||
|
||||
五个 Tab 按体系分:**星盘 / 基础信息 / 大运 / 西洋盘 / 七政四余**,画成**一条下划线式 Tab 行**:无边框、无卡面,当前项 2px 下划线 + 主文字色,整行一条 1px 底线。行内 `flex-wrap: nowrap` 且自身横向滚动,所以 375px 下它仍是一行,不折行、不换成第二排——底线画在滚动容器自己的 border 上,不会跟着内容滚走。五个等重的药丸按钮会读成五个可按的东西,Tab 行读成「一个控件 + 一个当前位置」。
|
||||
|
||||
@@ -750,7 +771,7 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
|
||||
|
||||
## 16. 星历页
|
||||
|
||||
独立 route `/ephemeris`。不扣点、不调模型。页面挂在 `SecondaryShell` 上(§ Secondary page shell):页名「星历」与「带这天去提问」在 46px 顶栏里,`.ephemeris-body` 是唯一滚动的东西。
|
||||
独立 route `/ephemeris`。不扣点、不调模型。页面挂在 `app/(secondary)` 共享外壳上(§ Secondary page shell):页名「星历」与「带这天去提问」在 46px 顶栏里,`.ephemeris-body` 是唯一滚动的东西。
|
||||
|
||||
- **结构:** 日期条;今日五要素;当日行运;未来九十天;收尾一句。顺序固定。
|
||||
- **日期条:** 居中的 `‹ 日期 ›`——日期是主语,两枚 44px 箭头按钮是它的把手。箭头字形 `aria-hidden`,可访问名仍是「前一天」「后一天」,读屏不丢方向。**「今天」只在非今日时出现**,作为日期下方一枚文字按钮;在今日时它整个不渲染,而不是渲染成一枚禁用按钮——灰掉的按钮仍要被读、仍要被 Tab 跳过。原本三个并排等宽按钮把「前一天 / 今天 / 后一天」读成三个平级动作,日期本身反而被挤出了这一组。
|
||||
|
||||
Reference in New Issue
Block a user