diff --git a/docs/ui-review/collections-ux.md b/docs/ui-review/collections-ux.md index da9ae13..a8fba83 100644 --- a/docs/ui-review/collections-ux.md +++ b/docs/ui-review/collections-ux.md @@ -14,6 +14,12 @@ 浏览器脚本使用可控 API fixture;没有上传实际文件,也未进行 Android/iOS 真机测试。 +电脑端适配(2026-09-21):从 1024px 起,书架导航与搜索并排,书架、书库和文件管理共用最大 80rem 内容宽度与两侧留白。文件操作收至标题右侧,列表采用文件名、元数据、菜单分列布局。书架设置改为右侧 26rem 面板,支持 Esc 关闭并恢复触发按钮焦点;缩窄窗口时恢复底部 sheet。书源及通用插件页面使用紧凑左对齐 tab,标题与正文保持统一宽度。 + +桌面验证:生产 Web 在 Chromium 中覆盖 1024×768、1366×768、1920×1080;检查书架、书库、文件列表、设置侧栏、键盘焦点、暗色主题和打开面板时跨断点缩放;同时保留 320/390px 手机回归及短视口吸顶断言。34 项书架/书库组件测试通过;真实 HTTP 服务、SQLite、Node 插件的订阅、隔离、筛选搜索、入架、阅读和换源端到端通过。桌面截图为 `collections-*-desktop-*.png`,插件页面截图为 `plugin-config-desktop-*.png`。尚未验证 Firefox、Safari。 + +阅读页桌面适配:底部工具栏改为最大 68rem 的居中横排,目录使用左侧面板,界面、朗读和设置使用右侧面板。面板支持 Esc 关闭及焦点恢复,聚焦不触发页面滚动。`desktop-reader.mjs` 验证三种桌面尺寸下四个面板、工具栏显隐、正文尺寸/页码/滚动位置不变,以及 1024×420 短窗口的滚轮操作。6 项阅读工具栏组件测试及移动端 `reader-regression.mjs` 全部通过,后者覆盖正文翻页、阅读信息、主题、插图、漫画、PDF 和 320px 面板。截图为 `desktop-reader-*.png`。 + 书架设置吸顶修复:移除滚动区域顶部内边距,并取消首个分组标题的负外边距,避免标题吸顶时露出下方控件。Web 构建及浏览器检查通过;在 320/390/1280px 宽、568px 高的视口实际滚动面板,验证吸顶标题与面板头部无间隙,顶部命中区域始终由标题覆盖。截图为 `collections-shelf-settings-sticky-*.png`。 ```powershell diff --git a/web/src/styles/reader.css b/web/src/styles/reader.css index b4cb7b3..fdc5191 100644 --- a/web/src/styles/reader.css +++ b/web/src/styles/reader.css @@ -3898,8 +3898,51 @@ section[aria-label="切换书源"] button { margin:.35rem .4rem .35rem 0; paddin .shelf-settings .panel-header > .icon-button { flex: none; margin-inline-start: auto; width: 44px; height: 44px; } .shelf-settings .panel-body { padding-top: 0; } .shelf-settings .section-title:first-child { margin-top: 0; } -@media (min-width: 72rem) { - .collection-header, .library-browse-body { width: 100%; max-width: 68rem; margin-inline: auto; } +.shelf-discovery { display: contents; } +/* Desktop keeps navigation compact and settings beside the collection. */ +@media (min-width: 64rem) { + .shelf { --shelf-measure: 80rem; } + .shelf-head, .shelf-discovery, .shelf-section { width: calc(100% - 3rem); } + .shelf-head { padding-top: 2rem; padding-bottom: 1.25rem; } + .shelf-discovery { display: flex; align-items: center; gap: 1.5rem; max-width: var(--shelf-measure); margin: 0 auto 1rem; padding-inline: .9rem; } + .shelf-discovery .shelf-links { flex: none; margin: 0; padding: 0; } + .shelf-discovery .shelf-search { flex: 1; max-width: 36rem; margin: 0 0 0 auto; } + .collection-link.button { flex: none; min-width: 9rem; min-height: 44px; } + .collection-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .5rem 2rem; padding-top: 2rem; } + .collection-header .collection-links { margin-top: 0; } + .collection-header .manager-crumbs { grid-column: 1 / -1; } + .collection-header, .library-browse-body, .library-files-screen .manager-body { width: calc(100% - 3rem); max-width: 80rem; margin-inline: auto; } + .library-files-screen .manager-row { grid-template-columns: 24px minmax(0, 1fr) minmax(15rem, 22rem) 44px; min-height: 64px; padding-block: .5rem; } + .library-files-screen .manager-check { grid-row: 1; } + .library-files-screen .manager-meta { grid-column: 3; grid-row: 1; justify-content: flex-end; padding-left: 0; } + .library-files-screen .manager-more { grid-column: 4; grid-row: 1; } + .shelf-settings.panel { inset: 1rem 1rem auto auto; width: 26rem; height: min(42rem, calc(100dvh - 2rem)); max-height: calc(100dvh - 2rem); border: 1px solid var(--reader-line); border-radius: 1rem; box-shadow: 0 12px 48px #0003; overflow: hidden; animation-name: shelf-settings-in; } + .shelf-settings .switch-field { grid-template-columns: minmax(0, 1fr) auto; } + .sources-header { padding-inline: max(1rem, calc((100% - 64rem) / 2)); padding-block: 1.5rem; } + .sources-hub .sources-tabs [role=tab], .extension-tabs button { flex: 0 1 auto; min-width: 9rem; padding-inline: 1.5rem; } + .sources-hub .sources-body { width: 100%; max-width: 66rem; margin-inline: auto; } + .reader-screen { --reader-footer-height: 5.5rem; } + .reader-screen .topbar { padding-inline: max(2rem, calc((100% - 68rem) / 2)); } + .reader-screen .footer { display: flex; align-items: center; gap: 2rem; inset-inline: 2rem; bottom: 1rem; width: calc(100% - 4rem); max-width: 68rem; height: 4.5rem; margin-inline: auto; padding: .5rem 1rem; border: 1px solid var(--reader-line); border-radius: 1rem; } + .reader-screen .progress-row { flex: 1; min-width: 0; } + .reader-screen .reader-actions { display: flex; gap: .25rem; } + .reader-screen .reader-actions .top-button { flex-direction: row; min-width: 4.75rem; min-height: 44px; padding: .5rem; } + .reader-screen .reader-actions .icon { font-size: 1.1rem; } + .reader-screen .reader-rail { right: max(2rem, calc((100% - 68rem) / 2)); } + .reader-screen .panel { inset: 1rem 1rem 1rem auto; width: 26rem; height: calc(100% - 2rem); max-height: none; border: 1px solid var(--reader-line); border-radius: 1rem; overflow: hidden; animation-name: shelf-settings-in; } + .reader-screen .panel[data-placement='start'] { inset-inline: 1rem auto; width: 23rem; animation-name: reader-contents-in; } + .reader-screen .panel-grip { display: none; } + .reader-screen .panel-header { flex: none; padding: .75rem 1.25rem; } + .reader-screen .panel-body { padding-top: 0; } + .reader-screen .section-title:first-child { margin-top: 0; } +} +@keyframes reader-contents-in { + from { opacity: 0; transform: translateX(-1rem); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes shelf-settings-in { + from { opacity: 0; transform: translateX(1rem); } + to { opacity: 1; transform: translateX(0); } } @media (max-width: 600px) { .source-entry-actions { flex-direction: column; gap: 0; } diff --git a/web/src/ui/reader-chrome.tsx b/web/src/ui/reader-chrome.tsx index e41e835..01bc706 100644 --- a/web/src/ui/reader-chrome.tsx +++ b/web/src/ui/reader-chrome.tsx @@ -20,7 +20,7 @@ import type { AppSettings } from '../store/settings.ts'; import { DEFAULT_APP_SETTINGS, READOUT_FIELDS, READOUT_OPTIONS, type ReadoutMode } from '../store/settings.ts'; import { ReaderIndicators } from './reader-indicators.tsx'; import type { SpeechEngineKind } from '../render/speech.ts'; -import { type ComponentChildren, type JSX } from './vendor/preact.ts'; +import { type ComponentChildren, type JSX, useLayoutEffect, useRef } from './vendor/preact.ts'; import { Button, IconButton, IconTextButton, SectionTitle } from './toolkit.tsx'; import { Icon, type IconName } from './icon.tsx'; @@ -216,7 +216,7 @@ export function ReaderChrome({ state, stage, handlers }: ReaderChromeProps): JSX {state.tocOpen ? ( - + {state.canRefresh ? ( (null); + useLayoutEffect(() => { + const trigger = document.activeElement; + panel.current?.querySelector('.panel-header button')?.focus({ preventScroll: true }); + return () => { if (trigger instanceof HTMLElement && trigger.isConnected) trigger.focus({ preventScroll: true }); }; + }, []); return ( <> {/* `onClick` on the scrim rather than a document listener: the scrim *is* the dismiss control, so it should be a node the reader can hit, with the same processing as any other tap on the page. */}
-
+
{ if (event.key === 'Escape') { event.stopPropagation(); onClose(); } }}>