From 47f6eb479196bbea910d0d2d640d266cd3a6ffa1 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 27 Oct 2026 17:02:54 +0800 Subject: [PATCH] Composer no longer greys out when empty; tidy sidebar session rows (BUG-1466) InputGroup's has-disabled: classes matched the disabled send button and greyed the whole empty composer; ChatComposer cancels both. Sortable session rows drop the always-on grip column: mouse drags the row (6px), touch long-presses (250ms), a drag never opens the row, and the grip stays only for keyboard sorting, visible on focus. The row menu shows on hover, focus or the current row on hover-capable devices. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v --- CHANGELOG.md | 6 ++ docs/BUG_HISTORY.md | 16 ++++++ docs/testing/chart-board-114-20261027.md | 6 ++ frontend/DESIGN.md | 2 +- frontend/src/app/globals.css | 12 +++- frontend/src/components/chat-composer.tsx | 4 +- .../src/components/sidebar-session-row.tsx | 8 ++- .../components/sidebar-session-sortable.tsx | 32 +++++++++-- .../composer-sidebar-tidy-20261027.test.tsx | 57 +++++++++++++++++++ frontend/tests/sidebar-contract.test.ts | 11 ++-- 10 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 frontend/tests/composer-sidebar-tidy-20261027.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 99e62266..81c12b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-10-27 — 输入框不再发灰,会话列表清爽一些 + +- 首页和对话页的输入框:没打字时不再整块变灰、文字发淡。 +- 首页侧栏的会话:去掉每行左边的六点把手,「…」只在鼠标移上去、当前那条或键盘选中时出现(手机上照旧显示)。排序改成按住整行拖动,手机上长按再拖。 +- Skill 版本不变(BUG-1466)。 + ## 2026-10-27 — 星盘页重新排整齐(issue #114 第三轮) - 北印度盘:每一宫的星座名移到这一宫最宽的外侧,不再在盘中心和交叉点挤成一团;右边的星不再伸出盘外。手机上整张盘完整显示在卡片里。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 91b21988..7ba0873d 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -19979,3 +19979,19 @@ - 相关记录:issue #114;BUG-1083(手机星盘撑宽,同类:网格/弹性子项最小内容宽)。 - 复发自:无 - 修复版本:分支 `codex/chart-ui-114-20261027` 第三轮。 + +## BUG-1466 | 首页输入框整块发灰;首页会话列表每行把手加「…」很乱 + +- 状态:resolved(输入框、列表样子);其他页面会话行没有菜单另见下文,未改 +- 首次发现:2026-10-27(issue #114 产品评论,附三张截图) +- 最近更新:2026-10-27 +- 影响面:首页和对话页的输入框;首页侧栏会话列表。 +- 用户现象:一、输入框外框里面又套了一块灰底圆角块,占位字和模型名都发淡。二、首页侧栏每条会话左边一个六点把手、右边一个「…」,常驻显示,显得乱;进到星盘等其他页面,同一列表只有标题和日期。 +- 触发条件:一、输入框为空时(发送按钮是禁用的)。二、首页登录后、会话排序零件载入后。 +- 根因:一、组件库 `InputGroup` 带 `has-disabled:bg-input/50 has-disabled:opacity-50`,本意是整个输入框被禁用时变灰;但它匹配的是「里面有任何禁用元素」,空输入框时发送按钮禁用,整块就变灰、半透明。`ChatComposer` 只覆盖了 `bg-transparent`,没覆盖 `has-disabled:` 两项。二、K1(BUG-1433)给可排序的行加了一列常驻 44px 把手,「…」又是 .64 常驻。其他页面没有把手和菜单,是因为会话管理(重命名、删除确认、收藏、转发、排序和它们的乐观更新与回滚)都在首页组件里,只有首页挂着时才登记给外壳(`use-home-shell-registration.ts`);别的页面外壳只拿到一份只读列表,行是链接(TASK-sidebar-unify-20260916 D1 的设计)。 +- 修复:一、`ChatComposer` 的 `InputGroup` 加 `has-disabled:bg-transparent has-disabled:opacity-100`。二、可排序的行与其他行同为「标题 + 44px 菜单」两列;鼠标按住整行拖(移动 6px 才算拖),触摸长按 250ms 拖,拖完松手不会误开那条对话;把手改成只在键盘聚焦时出现(键盘排序保留);能悬停的设备上「…」只在悬停、聚焦、当前行和菜单打开时出现,触摸设备仍常驻。其他页面没有菜单这一点本轮未改,要改需把会话管理从首页搬进外壳,待产品决定。 +- 验证:`frontend/tests/composer-sidebar-tidy-20261027.test.tsx`(空输入框的 `InputGroup` 带两项覆盖,换回旧 `chat-composer.tsx` 失败;可排序行没有三列标记、把手仍在);`sidebar-contract.test.ts` 改一条断言(三栏说明在测试里)。 +- 防复发:上面两条测试。 +- 相关记录:BUG-1433(K1 外壳换组件库)、BUG-1453;TASK-sidebar-unify-20260916。 +- 复发自:无 +- 修复版本:分支 `codex/chart-ui-114-20261027` 第四轮。 diff --git a/docs/testing/chart-board-114-20261027.md b/docs/testing/chart-board-114-20261027.md index 1ee46f75..91b5bb86 100644 --- a/docs/testing/chart-board-114-20261027.md +++ b/docs/testing/chart-board-114-20261027.md @@ -46,3 +46,9 @@ 23. 盘卡只有两排控件(盘型、图层);北印度 / 南印度在卡片标题右边。开「虚星」再点「八宫点」,虚星胶囊消失、八宫点数字出现(一次一个)。 24. 南印度盘开「映射」:同一格里的 AL、A11 两枚胶囊不重叠。 25. 宫位主题只有有星的宫;时运流年的日期框不再占满整行。 + +## 第四轮:输入框与会话列表(BUG-1466) + +26. 首页输入框没打字时:外框里没有灰底块,占位字和模型名颜色正常。 +27. 电脑首页侧栏:会话行没有左边的六点把手;「…」只在鼠标移上去或当前那条出现。按住一条往上拖到别处松手,顺序变了、没有打开那条对话;只点一下则正常打开。刷新后顺序还在。 +28. 手机侧栏:轻扫能正常滚动列表;长按一条再拖可以排序。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 27452667..a368c541 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -1291,7 +1291,7 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting` ## 外壳(2026-10-24) -桌面侧栏宽 200–360px,默认 288。右侧把手是组件库按钮,仍是 6px 透明细条;拖它调整宽度。把手能用 Tab 选中,左右方向键每次 16px,Home / End 到最窄 / 最宽,键盘不会把侧栏收起;读屏读作竖向分隔条,带当前宽度(BUG-1457)。拖到比 200 更窄就收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。侧栏和主区始终在各自的容器里:视口就绪、跨过手机断点,都不会把整页卸掉再挂上。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。翻页、高亮滑动和拖动排序用的库在页面出来后再载入,不进首屏。拖宽不再另载一套分栏库。 +桌面侧栏宽 200–360px,默认 288。右侧把手是组件库按钮,仍是 6px 透明细条;拖它调整宽度。把手能用 Tab 选中,左右方向键每次 16px,Home / End 到最窄 / 最宽,键盘不会把侧栏收起;读屏读作竖向分隔条,带当前宽度(BUG-1457)。拖到比 200 更窄就收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。侧栏和主区始终在各自的容器里:视口就绪、跨过手机断点,都不会把整页卸掉再挂上。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。可排序的行和其他行长得一样(2026-10-27,BUG-1466):没有常驻把手,鼠标按住整行移动 6px 开始拖,触摸长按 250ms;把手只在键盘聚焦时出现,供键盘排序。能悬停的设备上「…」只在悬停、聚焦、当前行、菜单打开时出现,触摸设备常驻。输入框为空时发送按钮禁用,但输入框本身不变灰(覆盖 `InputGroup` 的 `has-disabled:` 两项)。翻页、高亮滑动和拖动排序用的库在页面出来后再载入,不进首屏。拖宽不再另载一套分栏库。 ## 对话页(2026-10-25) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index d039a8e8..7d873b14 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -946,8 +946,11 @@ button.shell-resize-handle:active { position: absolute; top: 0; right: -3px; z-i /* Visual chip stays 32px; wrap gap is 8px, so a 44px overlay would overlap. */ .session-list { min-height: 0; display: flex; flex-direction: column; gap: var(--space-1); } .sidebar-session { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: stretch; overflow: hidden; border-radius: var(--radius-lg); color: var(--sidebar-foreground); transition: background-color 120ms ease-out, box-shadow 120ms ease-out, color 120ms ease-out; } -.sidebar-session[data-sortable="true"] { grid-template-columns: 44px minmax(0, 1fr) 44px; } -.session-drag-handle { width: 44px; min-height: 44px; padding: 0; color: inherit; } +/* Sortable rows look like every other row (BUG-1466): mouse and touch drag the row itself; the grip is + only for keyboard sorting and shows only while it has keyboard focus. */ +.session-drag-handle { position: absolute; inset: 0 auto 0 0; z-index: 1; width: 16px; min-height: 0; height: auto; padding: 0; color: inherit; opacity: 0; } +.session-drag-handle:focus-visible { opacity: 1; background: var(--sidebar-accent); } +.session-drag-handle > svg { width: 14px; height: 14px; } .sidebar-session:hover, .sidebar-session:focus-within { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); } .sidebar-session:has(.session-main[data-active="true"]) { background: var(--sidebar-accent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sidebar-ring) 22%, transparent); color: var(--sidebar-accent-foreground); } .session-main { position: relative; width: 100%; min-height: 44px; display: grid; gap: 2px; padding: var(--space-2); border: 0; border-radius: 0; background: transparent; color: inherit; cursor: pointer; text-align: left; text-decoration: none; transition: color 120ms ease-out, transform 120ms ease-out; } @@ -973,6 +976,11 @@ button.shell-resize-handle:active { position: absolute; top: 0; right: -3px; z-i .session-list-sentinel { height: 1px; } .session-menu-trigger { width: 44px; min-height: 44px; align-self: stretch; display: grid; place-items: center; justify-self: stretch; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; cursor: pointer; opacity: .64; transition: color 120ms ease-out, opacity 120ms ease-out; } .session-menu-trigger > svg { width: 18px; height: 18px; } +/* With a pointer that hovers, the 「…」 shows on the hovered, focused or current row only; touch keeps it visible. */ +@media (hover: hover) { + .session-menu-trigger { opacity: 0; } + .sidebar-session:has(.session-main[data-active="true"]) .session-menu-trigger { opacity: .64; } +} .sidebar-session:hover .session-menu-trigger, .sidebar-session:focus-within .session-menu-trigger, .session-menu-trigger[aria-expanded="true"] { opacity: 1; } .session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: transparent; color: inherit; } .session-actions-positioner { z-index: 30; max-width: calc(100vw - var(--space-6)); max-height: calc(100dvh - var(--space-6)); outline: 0; } diff --git a/frontend/src/components/chat-composer.tsx b/frontend/src/components/chat-composer.tsx index 58f64006..62e1c40b 100644 --- a/frontend/src/components/chat-composer.tsx +++ b/frontend/src/components/chat-composer.tsx @@ -100,7 +100,9 @@ export function ChatComposer({ ) : null}
- + {/* `has-disabled:` in InputGroup greys and fades the whole group when any child is disabled, and the + send button is disabled whenever the box is empty (BUG-1466). Only the frame's own disabled look is wanted here. */} + void; readonly onShare: () => void; readonly onDelete: () => void; + /** Keyboard sorting: a grip that shows only on keyboard focus. */ readonly dragHandle?: ReactNode; + /** Mouse and touch sorting start on the row itself, so the row needs no visible grip. */ + readonly dragListeners?: Pick, "onMouseDown" | "onTouchStart">; }; /** @@ -117,6 +120,7 @@ export const SidebarSessionRow = forwardRef
{ event.preventDefault(); if (!disabled) onMenuOpenChange(true); @@ -143,6 +146,7 @@ export const SidebarSessionRow = forwardRef diff --git a/frontend/src/components/sidebar-session-sortable.tsx b/frontend/src/components/sidebar-session-sortable.tsx index 0ccc7152..5f4ab032 100644 --- a/frontend/src/components/sidebar-session-sortable.tsx +++ b/frontend/src/components/sidebar-session-sortable.tsx @@ -4,11 +4,13 @@ import { closestCenter, DndContext, KeyboardSensor, - PointerSensor, + MouseSensor, + TouchSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; +import { useRef, type ButtonHTMLAttributes, type KeyboardEventHandler } from "react"; import { SortableContext, sortableKeyboardCoordinates, @@ -58,6 +60,9 @@ function SortableSessionRow({ const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition } = useSortable({ id: session.id, }); + // Mouse and touch start on the row; the keyboard one stays on the focus-only grip, + // so Enter on the row still opens the conversation. + const { onKeyDown, ...pointerListeners } = listeners ?? {}; return ( , "onMouseDown" | "onTouchStart">} dragHandle={( @@ -106,12 +112,21 @@ export function SortableSessionGroup({ readonly renderRow: (session: SidebarSession) => RowProps; }) { const ids = sessions.map((session) => session.id); + // A drag ends with the pointer released over a row; that click must not open the row. + const dragging = useRef(false); const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), + useSensor(MouseSensor, { activationConstraint: { distance: 6 } }), + // Long-press on touch, so a swipe still scrolls the list. + useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); + function settle() { + window.setTimeout(() => { dragging.current = false; }, 0); + } + function onDragEnd(event: DragEndEvent) { + settle(); const activeId = String(event.active.id); const overId = event.over ? String(event.over.id) : ""; if (!overId || activeId === overId) return; @@ -119,11 +134,18 @@ export function SortableSessionGroup({ } return ( - + { dragging.current = true; }} + onDragCancel={settle} + onDragEnd={onDragEnd} + > {sessions.map((session) => { const row = renderRow(session); - return ; + const select = row.onSelect; + return { if (!dragging.current) select(); }} />; })} diff --git a/frontend/tests/composer-sidebar-tidy-20261027.test.tsx b/frontend/tests/composer-sidebar-tidy-20261027.test.tsx new file mode 100644 index 00000000..6719fafe --- /dev/null +++ b/frontend/tests/composer-sidebar-tidy-20261027.test.tsx @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import test from "node:test"; + +import { ChatComposer } from "../src/components/chat-composer.tsx"; +import { SidebarSessionRow } from "../src/components/sidebar-session-row.tsx"; +import { SidebarProvider } from "../src/components/ui/sidebar.tsx"; + +Object.assign(globalThis, { React }); + +// BUG-1466: the send button is disabled whenever the box is empty, and InputGroup's `has-disabled:` +// classes then greyed and half-faded the whole input. The composer cancels both. +test("an empty composer is not drawn as a disabled field", () => { + const markup = renderToStaticMarkup(React.createElement(ChatComposer, { + inputRef: { current: null }, + value: "", + inputLabel: "输入", + placeholder: "想聊什么都可以", + maxLength: 4000, + inputDisabled: false, + submitLabel: "发送", + submitBlocked: true, + stopVisible: false, + stopLabel: "停止", + stopTitle: "停止", + onSubmit: () => {}, + onChange: () => {}, + onKeyDown: () => {}, + onStop: () => {}, + })); + assert.match(markup, /]*disabled=""/, "the empty box keeps its send button disabled"); + const group = markup.match(/]*data-slot="input-group"[^>]*>/)?.[0] ?? ""; + assert.match(group, /has-disabled:bg-transparent/); + assert.match(group, /has-disabled:opacity-100/); +}); + +// BUG-1466: a sortable row is the same two columns as any other row; its grip only serves the keyboard. +test("a sortable session row keeps the plain row shape and forwards drag listeners to the row", () => { + const markup = renderToStaticMarkup(React.createElement(SidebarProvider, null, React.createElement(SidebarSessionRow, { + session: { id: "s1", title: "示例对话", pinned: false, updatedAt: 0 }, + active: false, + disabled: false, + menuOpen: false, + onMenuOpenChange: () => {}, + onSelect: () => {}, + onTogglePinned: () => {}, + onRename: () => {}, + onShare: () => {}, + onDelete: () => {}, + dragHandle: React.createElement("button", { type: "button", className: "session-drag-handle" }), + dragListeners: { onMouseDown: () => {} }, + }))); + assert.match(markup, /class="sidebar-session"/); + assert.doesNotMatch(markup, /data-sortable/); + assert.match(markup, /session-drag-handle/); +}); diff --git a/frontend/tests/sidebar-contract.test.ts b/frontend/tests/sidebar-contract.test.ts index 63568640..89b5a5be 100644 --- a/frontend/tests/sidebar-contract.test.ts +++ b/frontend/tests/sidebar-contract.test.ts @@ -572,12 +572,15 @@ test("nests sidebar lists under one heading scale without an archive toggle", () }); test("renders each session title and menu as one unified row surface", () => { - // 原值:`.session-row` 两列。 - // 新值:`.sidebar-session` 仍是标题加 44px 菜单;可排序时前面再加 44px 手柄。 - // 原因:K1 会话行改名,并给拖动排序留出手柄,菜单列还在。 + // 原值:可排序时 `.sidebar-session[data-sortable="true"]` 前面加 44px 手柄列(44px 1fr 44px)。 + // 新值:可排序的行与其他行同为「标题 + 44px 菜单」两列;手柄绝对定位、只在键盘聚焦时出现,鼠标和触摸拖整行。 + // 原因:BUG-1466,产品 10-27 说首页会话列表每行一个把手加「…」很乱,且与其他页面的同一列表长得不一样。 assert.match(cssBlock(".sidebar-session"), /grid-template-columns:\s*minmax\(0,\s*1fr\)\s+44px/); assert.match(cssBlock(".sidebar-session"), /overflow:\s*hidden/); - assert.match(cssBlock('.sidebar-session[data-sortable="true"]'), /grid-template-columns:\s*44px\s+minmax\(0,\s*1fr\)\s+44px/); + assert.doesNotMatch(globalStyles, /\.sidebar-session\[data-sortable="true"\]\s*\{[^}]*grid-template-columns/); + assert.match(cssBlock(".session-drag-handle"), /position:\s*absolute/); + assert.match(cssBlock(".session-drag-handle"), /opacity:\s*0/); + assert.match(globalStyles, /\.session-drag-handle:focus-visible\s*\{[^}]*opacity:\s*1/); assert.match(globalStyles, /\.sidebar-session:has\(\.session-main\[data-active="true"\]\)[^{]*\{[^}]*background:\s*var\(--sidebar-accent\)/); assert.match(cssBlock(".session-main"), /border-radius:\s*0/); assert.match(cssBlock('.session-main[data-active="true"]'), /background:\s*transparent/);