From 45ec46176dc61ed496b4ab19ae63f05f5abe6d80 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Tue, 22 Sep 2026 13:05:07 +0800 Subject: [PATCH] fix(web): soften the home entry pill borders --- CHANGELOG.md | 4 ++ .../PROGRESS-starter-entry-soften-20260922.md | 8 ++++ docs/tasks/README.md | 2 +- docs/testing/starter-entry-soften-20260922.md | 10 +++++ frontend/DESIGN.md | 33 ++++++++++++---- frontend/src/app/globals.css | 27 +++++++++---- frontend/tests/starter-entry-soften.test.ts | 39 +++++++++++++++++++ 7 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 docs/tasks/PROGRESS-starter-entry-soften-20260922.md create mode 100644 docs/testing/starter-entry-soften-20260922.md create mode 100644 frontend/tests/starter-entry-soften.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef449fb..0f00dc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-09-22 — 今日星语和生时校正入口边框变轻 + +首页两枚入口仍是 36px 的按钮,点按区域保持 44px。默认边框改成更淡的 1px,悬停不再换成强调色,键盘焦点仍是更清楚的 3px 描边。Skill 版本不变。 + ## 2026-09-22 — 开口前可以选定聊天的人物 普通聊天顶栏的人名可以点开。还没发消息时能换成另一位人物;发过之后要换人就新建一段对话。本人是默认,而且看得见。对方资料删了,旧对话还能看,不能再发。Skill 版本不变。 diff --git a/docs/tasks/PROGRESS-starter-entry-soften-20260922.md b/docs/tasks/PROGRESS-starter-entry-soften-20260922.md new file mode 100644 index 00000000..0df557fa --- /dev/null +++ b/docs/tasks/PROGRESS-starter-entry-soften-20260922.md @@ -0,0 +1,8 @@ +# 进度 · 首页入口弱边框(2026-09-22) + +- 基线:`92e79642`(`origin/staging`,人物选择器已合入)。 +- 只改了 `.starter-entry` 的默认边框、悬停、按下、焦点和 375px 宽度。组件仍是两枚 native button,动作没变。 +- 视觉合同写在 `frontend/DESIGN.md`:36px 视觉高度,`::after { inset: -4px }` 保持 44px 命中,默认边框 `--starter-entry-border` = `color-mix(in srgb, var(--color-border) 62%, transparent)`,悬停不再使用 `--color-action`,焦点是 3px / 80% focus。 +- 没有恢复旧卡片 class,没有加 spinner,没有改人物选择器。 +- 断言变化:无既有断言改值。新增 `frontend/tests/starter-entry-soften.test.ts`。 +- 浏览器截图与 375px 真机未跑,见 `docs/testing/starter-entry-soften-20260922.md`。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 45ebc23f..a5ae842c 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -154,7 +154,7 @@ | `TASK-chat-subject-picker-20260922.md` | `PROGRESS-chat-subject-picker-20260922.md` | 开始聊天选择人物;顶部“当前星盘”可访问入口;第一条消息后锁定,换人新建会话;依赖 subject binding 合入 staging | 待验收 | `2a3013f2` | | `TASK-report-public-content-20260922.md` | `PROGRESS-report-public-content-20260922.md` | 普通报告 canonical allowlist/projection:页面、下载、API 统一只展示用户可读正文、结论和必要限制说明;隐藏 workflow/技法/评分/raw tool/任务元数据 | 待验收 | `dd4141a0` | | `TASK-home-bootstrap-reliability-20260922.md` | `PROGRESS-home-bootstrap-reliability-20260922.md` | BUG-936 首屏 bundle 无关 fallback、可读失败状态和用户触发重试;复用既有 first-paint 任务,不重复添加 loading,不提前标记 resolved | 待验收 | `337d8200` | -| `TASK-starter-entry-soften-20260922.md` | `PROGRESS-starter-entry-soften-20260922.md` | 弱化“今日星语 / 生时校正”入口默认边框与交互状态;保留 native button、44px 命中区、既有动作和 reduced-motion,不恢复旧卡片 | 待领取 | — | +| `TASK-starter-entry-soften-20260922.md` | `PROGRESS-starter-entry-soften-20260922.md` | 弱化“今日星语 / 生时校正”入口默认边框与交互状态;保留 native button、44px 命中区、既有动作和 reduced-motion,不恢复旧卡片 | 待验收 | 本分支 | | `TASK-icon-motion-audit-20260922.md` | `PROGRESS-icon-motion-audit-20260922.md` | P2 图标/轻量动效库存与合同审计;优先 Lucide/既有 glyph,不强行换图标、不新增 loading 或循环动画 | 待领取 | — | | `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` | | `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 | diff --git a/docs/testing/starter-entry-soften-20260922.md b/docs/testing/starter-entry-soften-20260922.md new file mode 100644 index 00000000..9c1abcbd --- /dev/null +++ b/docs/testing/starter-entry-soften-20260922.md @@ -0,0 +1,10 @@ +# 首页入口弱边框 · 浏览器清单(2026-09-22) + +本轮没有登录态,也没有在浏览器里量过像素。下面留给真人: + +1. 桌面空首页:今日星语、生时校正两枚按钮边框比输入框更淡,悬停不变成强调色。 +2. 键盘 Tab 到这两枚按钮:出现 3px 焦点环,比默认边框更清楚。 +3. 按下时只有浅底,没有转圈,也没有「正在加载」。 +4. 375px 宽:两枚按钮的文字和图标不重叠、不裁切,点按区域仍盖过 44px。 +5. 系统「减少动态效果」打开时,边框颜色可以变,但不要看到过渡动画。 +6. 点今日星语仍直接开始今日会话;点生时校正仍走原来的校正入口。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index ed130309..e60f2650 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -521,15 +521,32 @@ page. Three parts now, in reading order: 1040px against the session's 760px, so the input visibly narrowed the moment a conversation started. - **Entry pills** (`.starter-entry`): the two product entrances, rendered inside - `.composer-wrap` *below* the composer. 36px pills, hairline, accent icon, one - word each. Below them one line merges the daily trend with the rectification - hint, and an accuracy-boundary line appears when no natal minute is available. + `.composer-wrap` *below* the composer. The visual pill is 36px tall + (`min-height: 36px`). The transparent hit area is the `::after` box with + `inset: -4px`, so the touch target is 44px. One word each, accent icon 15px. + Below them one line merges the daily trend with the rectification hint, and + an accuracy-boundary line appears when no natal minute is available. -- **States:** default, hover, pressed, focus-visible, disabled, and `data-opening` - while a Case is being opened. Hover only moves the hairline; the surface tint - belongs to the pressed state, because a hover that shades the surface reads as - "selected" and sticks on touch. Opening a Case changes the cursor to `progress` - and nothing else — no spinner (unified-loading ruling). +- **States, measured:** + - Default border is `1px solid var(--starter-entry-border)`, and that token is + `color-mix(in srgb, var(--color-border) 62%, transparent)`. It is not + `--color-border-strong` and not `--color-action`. A `1px` shadow uses + `color-mix(in srgb, var(--color-ink) 4%, transparent)`. + - Hover (`@media (hover: hover)` only) darkens the border to + `color-mix(in srgb, var(--color-border-strong) 45%, var(--color-border))`. + It does not switch the border to `--color-action` and does not tint the fill. + - Pressed (`:active`) keeps the soft border, drops the shadow, and fills with + `color-mix(in srgb, var(--color-canvas-soft) 80%, var(--color-canvas))`. + - Focus-visible is a `3px` outline at + `color-mix(in srgb, var(--color-focus) 80%, transparent)` with + `outline-offset: 2px`. That ring is wider and more opaque than the 1px + default border. + - Disabled uses the shared `button:disabled` opacity. `data-opening="true"` + sets `cursor: progress` and adds no spinner and no “正在加载” copy. + - `prefers-reduced-motion: reduce` already collapses transition duration to + `.01ms` on `*`. Do not add a loop animation here. + - At `max-width: 375px` the row and each pill are `max-width: 100%` so the + label and 15px icon stay inside the row. The 44px hit area does not shrink. - **Copy:** chat history titles daily readings as “8月22日 · 今日节奏”, typed natal questions as “事业 · …”, and rectification as “8月21日 · 生时校正”. Clicking the daily pill starts a consultation session immediately; it does not place the diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 9bf2978f..04f79a2f 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -2238,19 +2238,21 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class } .starter-entry { + --starter-entry-border: color-mix(in srgb, var(--color-border) 62%, transparent); position: relative; min-height: 36px; display: inline-flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-4); - border: 1px solid var(--color-border-strong); + border: 1px solid var(--starter-entry-border); border-radius: 999px; background: var(--color-canvas); + box-shadow: 0 1px 1px color-mix(in srgb, var(--color-ink) 4%, transparent); color: var(--color-ink); cursor: pointer; font-size: var(--type-body-sm); - transition: border-color 140ms ease-out; + transition: border-color 120ms ease-out, background-color 120ms ease-out, box-shadow 120ms ease-out; } .starter-entry > svg { @@ -2272,22 +2274,31 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class content: ""; } -/* Pressed feedback is transient; hover only moves the hairline. A hover that - shades the surface reads as "selected" and sticks on touch devices. */ +/* Pressed feedback is a quiet surface tint. Hover only darkens the same soft + border; it does not switch to the action color, which read as a selected + control and stuck on touch. */ .starter-entry:not(:disabled):active { - border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border-strong)); - background: var(--color-action-soft); + border-color: var(--starter-entry-border); + background: color-mix(in srgb, var(--color-canvas-soft) 80%, var(--color-canvas)); + box-shadow: none; } @media (hover: hover) { - .starter-entry:not(:disabled):hover { border-color: var(--color-action); } + .starter-entry:not(:disabled):hover { + border-color: color-mix(in srgb, var(--color-border-strong) 45%, var(--color-border)); + } } .starter-entry:focus-visible { - outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); + outline: 3px solid color-mix(in srgb, var(--color-focus) 80%, transparent); outline-offset: 2px; } +@media (max-width: 375px) { + .starter-entry-row { max-width: 100%; } + .starter-entry { max-width: 100%; } +} + .starter-entry-hint, .starter-entry-boundary { max-width: 560px; diff --git a/frontend/tests/starter-entry-soften.test.ts b/frontend/tests/starter-entry-soften.test.ts new file mode 100644 index 00000000..07dadeb2 --- /dev/null +++ b/frontend/tests/starter-entry-soften.test.ts @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +const starterHome = readFileSync(new URL("../src/components/starter-home.tsx", import.meta.url), "utf8"); +const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); +const design = readFileSync(new URL("../DESIGN.md", import.meta.url), "utf8"); + +function starterEntryBlock(): string { + const start = css.indexOf(".starter-entry {"); + const end = css.indexOf(".starter-entry > svg"); + assert.ok(start >= 0 && end > start); + return css.slice(start, end); +} + +test("starter entries stay native buttons with a 36px pill and a 44px hit area", () => { + assert.match(starterHome, / { + const hover = css.slice(css.indexOf(".starter-entry:not(:disabled):hover"), css.indexOf(".starter-entry:focus-visible")); + assert.match(hover, /color-mix\(in srgb, var\(--color-border-strong\) 45%, var\(--color-border\)\)/); + assert.doesNotMatch(hover, /border-color:\s*var\(--color-action\)/); + assert.match(css, /\.starter-entry:focus-visible\s*\{[^}]*outline:\s*3px solid color-mix\(in srgb, var\(--color-focus\) 80%, transparent\)/); + assert.match(css, /\.starter-entry\[data-opening="true"\]\s*\{\s*cursor:\s*progress/); + assert.match(css, /@media \(max-width:\s*375px\)\s*\{[^}]*\.starter-entry-row\s*\{\s*max-width:\s*100%/); + assert.match(css, /@media \(prefers-reduced-motion:\s*reduce\)[\s\S]*transition-duration:\s*\.01ms !important/); + assert.match(design, /--starter-entry-border/); + assert.match(design, /inset: -4px/); +});