feat(ui): 标题不再落宋体,强调色分阶换 coral,删输入框常驻底栏(BUG-737/738)
BUG-737:--font-display 以两个从未加载的 Anthropic 授权字体开头 (Tiempos Headline / StyreneB:无 @font-face、public/ 无字体、layout.tsx 只 vendor 了 Inter),于是每个中文标题都掉到 Songti SC / SimSun——20 条 规则宽,含助手回答正文的 h2/h3。display 与 body 合并为同一条无衬线栈, 层级改由字重承担(33 条 display 规则 400→500)。 拉丁衬线方案实测否决:Newsreader 拉丁子集 132 KB(Inter 的 2.7 倍), 只为给一个品牌字上衬线,且会把「D10 事业盘怎么读」劈成两种字形。 BUG-738:亮色 #85432f 与暗色 #d78064 不同源;--color-ring 在 @theme inline 里硬编码不跟随 :root。产品拍板换 Claude coral #cc785c, 但实测它作文字色只有 3.14:1,而 62 个调用点里 53 个是 color:。 按角色拆两阶:--color-action #a9583e(文字 4.85:1)、 --color-action-strong #cc785c(填充/导轨/焦点环,3:1 非文字阈值)。 --report-accent 刻意保持 #85432f(报告是纸面,不跟应用强调色)。 T1.3/T1.4:模型选择器进 ChatComposer 的 toolbar 插槽,删掉常驻 44px 的 .composer-footer;顶栏 68→46px,「分析对象:」改为标题旁静默 chip。 防复发:新增 font-stack-loadable-contract,断言字体栈里每个 family 要么 vendored 要么是系统字体,且 --font-display 不得触达任何 CJK 衬线。 测试 3346→3350,fail 仍 31 且清单与基线逐条一致;/ 仍 Static; CSS gzip 41,095→41,096(+0.002%);快速门 pytest 段 798 passed / 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Opus 5
parent
3fd74daf34
commit
66d9f59d84
+48
-18
@@ -23,9 +23,10 @@ Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet
|
||||
| Body | `--color-ink-strong` | `#32322f` | Strong body copy |
|
||||
| Secondary text | `--color-ink-secondary` | `#5f5f59` | Supporting copy and labels, section headings |
|
||||
| Tertiary text | `--color-ink-tertiary` | `#6a6963` | Hints, metadata, empty-state copy |
|
||||
| Primary action | `--color-action` | `#85432f` | High-signal links, rings, and compact actions |
|
||||
| Primary soft | `--color-action-soft` | `#f4e8e2` | Editorial emphasis without a dark block |
|
||||
| Primary active | `--color-action-hover` | `#6f3627` | Hover and pressed action |
|
||||
| Primary action (text rank) | `--color-action` | `#a9583e` | Links, tinted labels, icons — anything the user reads |
|
||||
| Primary action (fill rank) | `--color-action-strong` | `#cc785c` | Button and avatar fills, the active session rail, rings, carets |
|
||||
| Primary soft | `--color-action-soft` | `#f7ece5` | Editorial emphasis without a dark block |
|
||||
| Primary active | `--color-action-hover` | `#8f4630` | Hover and pressed action |
|
||||
| Dark punctuation | `--color-surface-dark` | `#1d1d1f` | Compact primary buttons and user-authored emphasis only |
|
||||
| On dark | `--color-on-dark` | `#fbfaf7` | Text on compact dark controls |
|
||||
| Hairline | `--color-border` | `#d8d6cf` | Default separators and controls |
|
||||
@@ -33,7 +34,7 @@ Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet
|
||||
| Success | `--color-success` | `#28633e` | Available and completed states |
|
||||
| Warning | `--color-warning` | `#b07b22` | Caution states |
|
||||
| Error | `--color-danger` | `#9a2f2f` | Errors and destructive actions |
|
||||
| Accessible focus | `--color-focus` | `#85432f` | Keyboard focus and input focus |
|
||||
| Accessible focus | `--color-focus` | `#cc785c` | Keyboard focus and input focus |
|
||||
|
||||
The personal report reuses the product palette. `--report-*` tokens exist so
|
||||
print can pin a light sheet independently of the screen theme; they are the
|
||||
@@ -44,24 +45,39 @@ outside `.personal-report-*` and `.report-center-*` may use them.
|
||||
|---|---|---:|---|
|
||||
| Report paper | `--report-paper` | `#fbfaf7` | Same as `--color-canvas`; the reading sheet |
|
||||
| Report rule | `--report-rule` | `#d8d6cf` | Same as `--color-border`; rules inside the sheet |
|
||||
| Report accent | `--report-accent` | `#85432f` | Same as `--color-action`; scarce evidence links |
|
||||
| Report accent | `--report-accent` | `#85432f` | **Deliberately not the app accent.** Kept at the old deep brown; see the note below. |
|
||||
|
||||
Rules: neutrals stay warm; the action color is scarce; roughly ninety percent of the interface remains light. Dark ink is punctuation, never a page-scale surface. No raw color may appear in UI styles outside these tokens and their documented alpha mixes.
|
||||
|
||||
### Where the action color appears
|
||||
|
||||
Scarce is a budget, not zero. The sidebar spends it in exactly three places, and
|
||||
a fourth needs a reason:
|
||||
The accent has **two ranks**, because one hex cannot do both jobs. Claude coral
|
||||
`#cc785c` measures **3.14:1** against the `#fbfaf7` canvas — fine for a fill or a
|
||||
border, not for text. And 53 of the 62 accent call sites are `color:`, including
|
||||
the markdown links inside assistant answers.
|
||||
|
||||
- `--color-action` (`#a9583e`) is the **text and icon rank**: 4.85:1 on canvas.
|
||||
It is the same coral darkened, not the old brown returning.
|
||||
- `--color-action-strong` (`#cc785c`) is the **fill rank**, where the 3:1
|
||||
non-text threshold applies: button and avatar fills, the send/stop control, the
|
||||
2px active-session rail, the focus ring, the typing caret, checked radios.
|
||||
|
||||
One known gap, accepted by the product owner: white on `#cc785c` is **3.28:1**,
|
||||
below AA for normal text. It sits on `.button-primary` and the two avatar
|
||||
circles. claude.ai ships the same value, and "looks like Claude" was the stated
|
||||
goal. Body text is unaffected — that is what `--color-action` protects.
|
||||
|
||||
Scarce is still a budget. The sidebar spends it in exactly three places, and a
|
||||
fourth needs a reason:
|
||||
|
||||
1. `新建对话` — the one high-signal action in the nav, as tinted text and icon on
|
||||
a transparent row (`--color-action`, deepening to `--color-action-hover`).
|
||||
Never a filled block: a full-width terracotta surface would break the
|
||||
ninety-percent-light rule.
|
||||
2. The 2px bar on the active session (`--sidebar-ring`). This is the sidebar only —
|
||||
the settings dialog's pane menu carries no accent bar and separates hover from
|
||||
current with surface and ink rank instead.
|
||||
3. The profile initial's circle (`--color-action`), which only renders when the
|
||||
account has no uploaded avatar.
|
||||
2. The 2px bar on the active session (`--sidebar-ring`, which follows
|
||||
`--color-focus`).
|
||||
3. The profile initial's circle (`--color-action-strong`), which only renders
|
||||
when the account has no uploaded avatar.
|
||||
|
||||
2 and 3 both disappear on a new account with an avatar — no sessions, no
|
||||
initial — which is how the sidebar ended up with no accent pixel at all. Item 1
|
||||
@@ -69,6 +85,11 @@ is what guarantees the nav still has a visual anchor in that state. `我的报
|
||||
and every section label stay neutral on purpose; giving them color too would
|
||||
flatten the hierarchy the accent exists to create.
|
||||
|
||||
**The report is not on this system.** `--report-accent` stays `#85432f` while the
|
||||
app moved to coral. The report is a printed-paper surface, not app chrome; coral
|
||||
reads washed out on the cream sheet. The two palettes are now independent on
|
||||
purpose — do not "fix" the mismatch.
|
||||
|
||||
The glass surface assumes a light backdrop. The mobile drawer floats above the
|
||||
scrim instead, so the translucency darkened it to `#E3E1DC` and flattened the
|
||||
warmth; the drawer takes `--color-sidebar-solid` and drops `backdrop-filter`.
|
||||
@@ -97,8 +118,8 @@ identical.
|
||||
| Page floor | `#f3f2ee` | `#1a1a19` | On light, raised surfaces step *down* in lightness; on dark they step *up*. The floor is the darkest surface in dark mode, the second-lightest in light mode. |
|
||||
| Reading surface | `#fbfaf7` | `#262624` | Warm, never neutral black. A cool grey would change the product's identity rather than its brightness. |
|
||||
| Warm card | `#ebe9e3` | `#30302d` | One step above the reading surface in both themes, by opposite directions. |
|
||||
| Primary action | `#85432f` | `#d78064` | The clay hue is kept and lifted. `#85432f` is 2.1:1 on `#262624` — unreadable. |
|
||||
| Action hover | `#6f3627` | `#e59273` | Hover darkens on light and lightens on dark. |
|
||||
| Primary action | `#a9583e` | `#d78064` | Two ranks on light (text `#a9583e`, fill `#cc785c`) collapse to one on dark: `#d78064` already reads on `#262624`, where the light values do not. |
|
||||
| Action hover | `#8f4630` | `#e59273` | Hover darkens on light and lightens on dark. |
|
||||
| Dark punctuation | `#1d1d1f` | `#f2f0ea` | A dark block on a light page becomes a light block on a dark page; the token name describes the light-theme value, the role is "highest-contrast block". |
|
||||
| Semantic hues | `#9a2f2f` / `#28633e` / `#b07b22` | `#e08573` / `#74b189` / `#d8a758` | Lifted for contrast; their `-muted` pairs stop being pale washes and become dark tints of the same hue. |
|
||||
| Scrim | `rgba(29,29,31,.34)` | `rgba(0,0,0,.58)` | A scrim has to darken a surface that is already dark. |
|
||||
@@ -137,8 +158,10 @@ is read through an external store so a change in one tab reaches the others.
|
||||
|
||||
### Font stacks
|
||||
|
||||
- Display: `"Tiempos Headline", "Songti SC", "STSong", "Noto Serif CJK SC", Georgia, serif`. The licensed Copernicus/Tiempos files are unavailable; the Chinese Song serif stack is the declared production substitute.
|
||||
- Body/UI: `StyreneB, var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`. Inter is loaded with `next/font/local` from `src/app/fonts/InterVariable-latin.woff2` (`display: "swap"`, CSS variable `--font-inter`) so Windows/Linux no longer silently fall back past a never-requested Inter, and image builds do not call fonts.googleapis.com. Tiempos Headline remains unloaded; the Songti stack is still the declared production substitute.
|
||||
- Display: `var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif` — the same stack as body. Display rank comes from **weight and tracking, not family**: display and title rules run at weight 500 against body's 400.
|
||||
The old value led with `"Tiempos Headline", "Songti SC", STSong, …, serif`. Tiempos Headline is an Anthropic licensed face this app has never loaded (no `@font-face`, nothing in `public/`, `layout.tsx` vendors only Inter), so in production every CJK heading fell through to **Songti SC on Apple and SimSun on Windows** — 20 rules wide, including the `h2`/`h3` inside assistant answers. See BUG-737.
|
||||
A Latin-serif-first stack (`"Newsreader", "PingFang SC", …`) was measured and rejected: the Newsreader latin variable subset is 132 KB — 2.7× the entire Inter file — to serif one wordmark, and it splits a mixed heading such as “D10 事业盘怎么读” into two scripts, which reads as a font-loading failure. **CJK never takes a serif here.**
|
||||
- Body/UI: `var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`. Inter is loaded with `next/font/local` from `src/app/fonts/InterVariable-latin.woff2` (`display: "swap"`, CSS variable `--font-inter`) so Windows/Linux no longer silently fall back past a never-requested Inter, and image builds do not call fonts.googleapis.com. `StyreneB` was removed from the head of this stack for the same reason as Tiempos: it never loaded, so it was dead configuration that made the stack look intentional.
|
||||
- Code/data: `"JetBrains Mono", "SFMono-Regular", Consolas, monospace`, exposed as `--font-mono`.
|
||||
- Root boundary pages (`error.tsx`, `not-found.tsx`, `forbidden.tsx`, `global-error.tsx`) sit in the shared root layout segment and must not import `globals.css` — importing it would drag the chat stylesheet onto every admin route. They therefore cannot read `--font-mono` or any token, and inline their own values: a system stack for body text and `ui-monospace, SFMono-Regular, Menlo, monospace` for code. Keep those literals in step with the stacks above by hand.
|
||||
|
||||
@@ -193,11 +216,12 @@ Radii have two visual steps. Controls use 8px (`--radius-md`; `--radius-xs` and
|
||||
|
||||
### Input and composer
|
||||
|
||||
- **Structure:** warm canvas field, hairline, typed value, optional icon action.
|
||||
- **Structure:** a column, not a row — warm canvas field on top, the composer's own bottom row beneath it. The row carries the model picker on the left and the send/stop control on the right, with the character count inline beside it.
|
||||
- **No strip below the composer.** `.composer-footer` used to sit under the field at a permanent `min-height: 44px`, holding the model picker and a count that only appears near the cap — so most of the time it was 44px of empty chrome on every screen. The row is inside the box now and only renders what it has.
|
||||
- **States:** default, hover, focus with deep-brown ring, disabled only for structural reasons (readonly, session loading, onboarding, the other surface open), invalid. Generating does not disable the textarea; the send control becomes stop.
|
||||
- **Queue:** one pending card (`.composer-queue`) sits above the field while an answer is in flight. Copy: “已排队,回答结束后发出”, plus the text and a 44px “撤回”. A second Enter appends to the same card with a newline. Successful settlement sends it; stop, failure, or recovery puts the text back in the field. The send button is disabled while a card is waiting. No spinner.
|
||||
- **Accessibility:** persistent label where practical; composer has an explicit accessible label.
|
||||
- **One composer:** the rectification surface renders the same `ChatComposer` as the main chat; there is no second composer. A surface that owns its own draft passes it as `value`; the main chat reads the draft store. Both count down from the same 500-character ceiling through `CharacterRemaining`.
|
||||
- **One composer:** the rectification surface renders the same `ChatComposer` as the main chat; there is no second composer. A surface that owns its own draft passes it as `value`; the main chat reads the draft store. Both count down from the same 500-character ceiling through `CharacterRemaining`, which the composer renders inline and only once `characterRemainingVisible()` is true — it no longer has a permanent container. Controls that belong to the composer go through the `toolbar` slot; nothing is stacked below it.
|
||||
|
||||
### Jump to latest
|
||||
|
||||
@@ -291,6 +315,12 @@ The birth-time rectification session is the consultation transcript plus a house
|
||||
- **Accessibility:** the trigger and every option meet the 44px touch target; options are a native radio group, with a small roving-focus fallback so Tab, arrow keys, Space, and screen readers consistently expose the selected model inside the popover.
|
||||
- **Motion:** the popup enters over 120ms with opacity and a 4px vertical translation; reduced-motion removes the translation.
|
||||
|
||||
### Chat header
|
||||
|
||||
- One line, 46px. Sidebar trigger, session title, and the credit pill. It was 68px and carried a second line reading `分析对象:{盘名}`.
|
||||
- The chart a session analyses is now a quiet `--color-ink-tertiary` chip beside the title (`.chat-header-chart`), and it drops out below 768px rather than wrapping the header to two rows. "分析对象" was internal vocabulary; the chart name alone carries the same fact.
|
||||
- With the composer strip gone as well, the two pieces of permanent chrome went from 68 + 148 to 46 + 124 px.
|
||||
|
||||
### Navigation item
|
||||
|
||||
- **Structure:** title, optional subtitle for another person's chart, current-state marker.
|
||||
|
||||
Reference in New Issue
Block a user