外壳换成组件库:侧栏可拖宽,会话可排序(#105)。
Independent Staging Quality Gate / validate (push) Failing after 9m12s
Independent Staging Quality Gate / publish (push) Skipped

桌面侧栏可在 200 到 360 像素之间拖宽,拖到最左收起,宽度记在本机。手机从左边滑出。会话在同一组里拖动排序,顺序写入可空的 sidebar_order,收藏仍置顶。账户菜单、设置、反馈和退出确认改用组件库。顶栏点数只显示图标和数字。启程清单还在。菜单、拖宽、拖动排序和翻页在首屏之后再载入。

自测:tsc 退出码 0;npm run lint 0 error、121 warning;改到的前端测试 205 项全过;npm run build 后首页仍是 static;首屏 gzip 630350,相对 8f7c0f7d 的 621226 为 +1.47%。test:db 92 项里 79 过、13 败,失败名与 Windows 已知清单一致,排序列在通过的 ok 37。没有跑全量 npm test,没有跑 Python 快速门。没有登录态,没有在浏览器里拖侧栏。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-25 01:41:19 +08:00
co-authored by Grok (agent loop)
parent 8f7c0f7db4
commit 352d45b2ef
59 changed files with 1721 additions and 573 deletions
+11 -7
View File
@@ -1,5 +1,9 @@
# Jyotisha Web Design System
## 外壳(2026-10-24)
桌面侧栏在可拖分栏里,宽 200–360px,默认 288。拖到最左收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。翻页、高亮滑动、拖宽和拖动排序用的库在页面出来后再载入,不进首屏。
## 启程清单与一次性提示(2026-10-24)
首页顶栏右侧、点数按钮左边,是一枚胶囊「启程 n/N」(`.onboarding-checklist-trigger`)。它沿用顶栏按钮的 32px 高度,圆角 999px,字和图标间距 6px。左边是 18×18 的进度环,描边按已完成的项画满。视口还没分出手机或桌面时,这枚胶囊不可打开。手机(侧栏视口小于 768)从底部抽屉拉出,带现有的下拉把手。平板和桌面用现有弹出层,靠右对齐,宽 `min(22rem, calc(100vw - 2rem))`,行是 `1.25rem minmax(0, 1fr) auto`,句子 `overflow-wrap: anywhere`。已完成的行用对勾,没有「去做」。底部是「先不看了」。
@@ -499,7 +503,7 @@ The birth-time rectification session is the consultation transcript plus a house
- Rectification and daily horoscope do not get this control. Their quiet note, when present, remains the static `.chat-header-chart` span and still drops out below 768px. The person button stays on narrow screens and ellipsizes so the header remains one line.
- The chart page birth line is a button (`.chart-birth-line`) after the person: `日期 时间 · 地点` and a pen. The name is not repeated. The whole row opens an editor on this page. Self can change every field. Someone else's already-saved birth time stays locked. The sentence `填报的出生时间保存后不能再改` appears once, beside that field, and is not repeated in the form description. Changing the date, time, or place shows `改了出生时间、日期或地点,之前生时校正采用的时间会作废,星盘改按你新填的时间排。` Saving uses the shared other-person write (self still goes through the account). A click on the dialog's padding, or releasing a text selection there, does not close the dialog or drop unsaved edits. Escape still closes it, and so does a click on the backdrop outside the dialog box. Below 768px that row wraps onto a second line and the chart header grows to fit it.
- With the composer strip gone as well, the two pieces of permanent chrome went from 68 + 148 to 46 + 124 px.
- On viewports ≤767px the header row is 52px plus the safe-area inset. The credit pill and the rectification 「当前盘面」 chip share one size: 40px visual height, 14px tabular type, the same radius and padding, no 64px min-width. The 44px touch target is the transparent `::before` ring.
- On viewports ≤767px the header row is 52px plus the safe-area inset. The credit control (`.credit-balance`) is a normal outline button, 44px, with a coins icon and the number; 点 stays in the accessible name. The rectification 「当前盘面」 chip stays 40px. The shared `::before` hit area stays inset -2px. The onboarding checklist trigger stays 32px.
- Window-level scroll is reset by `ViewportScrollLock` after the on-screen keyboard closes, so the header cannot be left above the screen.
### Navigation item
@@ -508,16 +512,16 @@ The birth-time rectification session is the consultation transcript plus a house
- **States:** default, hover, current, focus, disabled.
- **Hierarchy:** section labels stay tertiary; session titles and primary actions use ink so history rows do not collapse into the same gray as “收藏对话 / 历史对话”. History groups use the overline token for “今天 / 昨天 / 最近 7 天 / 最近 30 天 / 更早”. When the chart is not the account holder, a secondary line shows the chart name under the title. A session title is named when it is created; opening a stored session must not rename it. `updatedAt` advances only on conversation activity, including rectification turns, choice, adopt, and stop. Opening, refresh, and metadata PATCH do not bump it. A `?c=` that is not on the loaded page is looked up with `GET /api/sessions/{id}` before anyone may say it was deleted. Locked by `frontend/tests/session-open-preserves-identity.test.ts` and `frontend/tests/session-lookup-unlisted.test.ts`.
- **Request behavior:** existing sessions remain selectable for reading while a request is active; creating or sending another request stays locked until the active request settles.
- **Surface:** translucent warm-gray sidebar. Current and hover fill the whole `.session-row` — title and the ⋯ share one chip. The menu trigger has no canvas of its own.
- **Surface:** translucent warm-gray sidebar. Current and hover fill the whole `.sidebar-session` — title and the ⋯ share one chip. The menu trigger has no canvas of its own. A drag handle sits on the row only when that list can be reordered.
### Sidebar shell
- **Composition:** provider, fixed header, one scroll-owning content region, fixed footer, trigger, rail, and flexible chat inset. `/`、`/chart`、`/ephemeris`、`/reports` share one `(app)` layout: one `SessionListProvider` and one `SidebarProvider`. The session list is fetched once per tab; walking between those four pages does not remount the sidebar or refetch `/api/sessions`.
- **Home registers shell controls** (new chat, select session, rename, pin, share, delete, account menu). The other three pages leave `controls` unset, so rows are links to `/?c=<id>`.
- **Trigger placement:** the single visible collapse/expand trigger sits beside the active session title in the chat header. The sidebar brand row has no duplicate trigger.
- **Desktop:** 288px expanded by default at 1024px and above; 64px collapsed icon rail.
- **Tablet:** 64px collapsed by default from 768px through 1023px; 240px when expanded.
- **Mobile:** no icon rail; an off-canvas drawer uses `min(86vw, 320px)` and closes through its scrim or Escape.
- **Desktop:** a resizable panel, default 288px at 1024px and above, drag range 200–360px. Dragging to the left edge collapses to the 64px rail. The open width is remembered per browser under `sidebar_width`. The 64px rail is not stored as that width.
- **Tablet:** 64px collapsed by default from 768px through 1023px. Expanding uses the same panel; with nothing stored yet the width starts at 240px, then follows `sidebar_width`.
- **Mobile:** no icon rail. A left sheet uses `min(86vw, 320px)` and closes through the sheet, a navigation choice, or Escape.
- **Collapsed content:** logo, new-chat action, the page actions, one history expansion action, and account avatar. Individual sessions do not become indistinguishable repeated icons.
- **Expanded content:** the header actions (new chat, 星盘, 星历, 我的报告), then **one flat list** under a single 「最近」 heading: pinned sessions as the first labelled group, then history grouped by recency.
It used to be three stacked sections — a 星盘列表 chip grid and two `<details>` wrappers for 收藏对话 / 历史对话 — so the nav carried two collapse affordances before the first session row, and neither had anything worth collapsing. The chip grid went entirely: every chip, and the add button beside it, opened the same chart-library dialog that the account menu already offers, so a list of names provided no navigation. Chart data is managed from one place now (account menu → 星盘资料).
@@ -816,7 +820,7 @@ page. Three parts now, in reading order:
### Account popover
- **Structure:** identity header, then 个人资料 / 通用设置 / 账户与点数 (with the current balance), theme preference, divider, and logout. 星盘档案不在这个菜单里,它是侧栏导航页 `/people`。
- **Structure:** the trigger is the avatar and the name only. The menu is 个人资料 / 通用设置 / 账户与点数, then theme as a radio group, then 反馈与投诉, then logout. The balance is not repeated on the trigger. 星盘档案不在这个菜单里,它是侧栏导航页 `/people`。
- **Surface:** 280px elevated canvas popover anchored above the sidebar account trigger; warm hairline, existing elevated shadow, no nested cards.
- **States:** closed, open, hover, focus-visible, and logout confirmation.
- **Accessibility:** `aria-expanded`, `aria-controls`, menu semantics, 44px rows, outside-click and Escape dismissal, and focus return.
@@ -844,7 +848,7 @@ The 👍 / 👎 under an answer (回复评价) is saved per conversation and mes
- **Placement:** the overlay and the onboarding paywall portal to `document.body`. `SidebarInset` stays `inert` while a dialog is open (the BUG-744~746 focus contract); the dialog itself must not sit inside that subtree (BUG-968). Closing still uses the existing overlay click, the header button, and the window-level Escape listener.
- **Redeem paywall timing (2026-09-28, TASK-birth-sky-polish-20260928 P4):** the 「解锁完整咨询」 paywall no longer opens when the starter home appears. The first send on the page refused for lack of credits (the pre-flight balance check or a 402) opens it, and the typed question stays in the composer; every later refusal opens the billing dialog as before. Rectification's own 402 path is unchanged.
- **Structure:** one fixed chrome for three panes — 个人资料, 账户与点数, 通用设置. The chart archive is no longer a pane; `?settings=chart` leaves for `/people`. Left nav is 200px, has a hairline boundary, and does not scroll; the title bar stays put; only the right-hand content pane scrolls. The pane menu uses icon + label rows without a trailing chevron because these controls switch content inside the same dialog. Logout stays a separate 400px confirmation.
- **Structure:** one fixed chrome for three panes — 个人资料, 账户与点数, 通用设置. The names are unchanged. The pane switch is Tabs. The chart archive is no longer a pane; `?settings=chart` leaves for `/people`. Left nav is 200px, has a hairline boundary, and does not scroll; the title bar stays put; only the right-hand content pane scrolls. The pane menu uses icon + label rows without a trailing chevron because these controls switch content inside the same dialog. Theme on the general pane is the same segmented control. Logout is a separate AlertDialog, still capped at 400px. Feedback is a Dialog.
- **Width / height:** desktop `width: min(100vw - 32px, 880px)`, with `height: min(84vh, 640px)` as the base and `min(84dvh, 640px)` applied inside `@supports (height: 1dvh)`. The right content pane uses 32px horizontal and bottom inset on desktop. All four panes share one class (`.settings-modal`), so switching panes cannot change the frame. At ≤767px the dialog is full-screen with four equal tabs along the top and 16px content inset.
- **Pane menu states:** default is transparent with secondary ink; hover is a 55% wash of `--color-canvas-muted` keeping secondary ink; current is the solid muted surface with primary ink. No accent bar, no trailing navigation chevron, and no weight change — hierarchy here comes from ink rank and surface, matching “Hierarchy inside the nav comes from ink rank, not hue”. The sidebar's 2px `--sidebar-ring` on the active session is deliberately **not** changed to match; the two surfaces read differently on purpose until that is revisited.
- **Content width:** the 880px frame now gives the content pane a deliberate inset instead of leaving controls against the divider. Form panes (个人资料, 通用设置) cap their children at 560px and stay left aligned (`.settings-dialog-content--form > *`); list panes (星盘资料, 账户与点数) stay full-bleed within the inset so tables and card grids keep their columns. The cap sits on the children of the scroll container only — never on `.settings-modal` or `.account-settings-shell` — so it cannot make the frame resize between panes (BUG-554 / BUG-698).