DSH / Atlas
2026-09-11implementedfeaturearchived 2026-09-30

Sidebar document preview polish

侧边栏文档预览打磨

The Sidebar document preview accumulated several experience defects (issue #3974). Images rendered at their intrinsic CSS-pixel size, so a wide image overflowed the pane and forced horizontal scrolling. The viewer dropdown always appended the plain-text fallback, so bitmap and PDF files offered a "Plain text" choice whose result is unreadable bytes, and files with one real renderer still showed a control with nothing

Source compatibility: Chinese title uses a compatible non-canonical header

English

Problem

The Sidebar document preview accumulated several experience defects (issue #3974). Images rendered at their intrinsic CSS-pixel size, so a wide image overflowed the pane and forced horizontal scrolling. The viewer dropdown always appended the plain-text fallback, so bitmap and PDF files offered a "Plain text" choice whose result is unreadable bytes, and files with one real renderer still showed a control with nothing meaningful to switch to. Binary containers with no renderer at all (video, archives, office documents) fell into the plain-text reader and surfaced a read error instead of a designed empty state. Each renderer carried its own loading copy and position, so opening a file flashed through several differently worded, differently placed indicators. PDF pages sat inside a double inset that shrank every page below the pane's width. Switching sidebar tabs remounted the file tree at scroll top, losing the reader's place.

Decision

Image width fit. The image frame follows the scroller's width with a 12px inset; the image itself carries max-width: 100% and an 8px corner radius, so a wider image scales down to the pane's width at its aspect ratio, a smaller image keeps its intrinsic size centred by auto margins, and a taller image scrolls vertically in the shared body. Height fitting was considered and dropped: it needs a fixed-height frame, and mixed portrait cases produced surprising layouts for no user request.

Binary viewer choices. DocumentPreviewDefinition gains optional binaryExtensions, the suffixes among a renderer's extensions whose bytes are not readable text; register rejects an entry absent from extensions. The renderer owns this knowledge: the image body declares png, jpg, jpeg, gif, webp, bmp, ico and leaves svg out because SVG source is readable XML; the PDF body declares pdf. binaryDocumentPath in the registry module answers whether any registered definition declares a filename's suffix binary, and the preview owner skips the plain-text fallback for such files. The header renders the viewer menu only when at least two candidates exist; a single candidate shows no viewer control at all.

Unsupported empty state. The owner-side list in document/unviewable.ts names binary container suffixes (video, audio, archives, office documents, executables, fonts, disk images, design formats) that no renderer claims. The owner consults it only when no implementation matches, so any renderer registration always wins; a matching file shows the path header, the file-type icon, and one unsupportedFile line, and never issues a read. An uncertain suffix stays out of the list and keeps the plain-text fallback; a text-claimed file whose bytes fail the reader's checks reports the same copy through error.notText.

Unified loading. LoadingIndicator renders icon-only, carrying its label as aria-label with no visible text; every renderer's loading copy is the shared "Reading…". Every wait before content exists — the owner's first read, PDF parsing, HTML packaging, image decoding — centres the spinner in the pane, so opening a file shows one spinner in one position until the body appears. An unrendered PDF page holds its place as a static 3:4 placeholder block on the theme's skeleton token --dsw-alias-bg-skeleton with no spinner; the document-open wait stays the owner's centred read spinner rather than the first page's placeholder, because the read spinner precedes the placeholder and a handoff between them visibly jumps positions. Placeholders carry no shimmer animation, whose per-page cost outweighs its value.

PDF full-bleed and copy. The PDF body and page insets are removed so pages fill the pane's width edge to edge; the image renderer keeps its own 12px inset. Chinese error and status lines across the preview dictionaries drop trailing full stops.

Files tree scroll restore. ui-sidebar-files follows the document preview's own pattern: the tree store gains scrollTop with a scrolled action, the body tracks its scroll offset locally and commits it once, on unmount and only while the owner's signal is live, and a remount restores it in a layout effect. Loaded levels already outlive the body in the store, so the remounted tree lays out at full height before the offset re-lands.

Alternatives considered

A boolean binary flag per definition. The image renderer covers both bitmaps and SVG under one registration, so binariness is a property of the suffix, not the renderer.

Filtering in the preview owner by a hardcoded suffix list for registered renderers. The owner would duplicate knowledge each renderer already holds, and external renderers could not extend the set; the owner-side list exists only for suffixes no renderer claims.

A one-item static viewer label. Rendering the single remaining candidate as a static name puts a control that offers no action in the header; it is noise, so the header renders nothing.

Consequences

Images never scroll horizontally; the pane's width is the only layout input, so no zoom control was added. Bitmap and PDF tabs show no viewer control; SVG keeps the menu with the plain-text choice; unclaimed binary containers show the empty state without reading. From open to first content every preview shows one centred icon-only spinner, and PDF pages appear as quiet placeholder blocks. The file tree comes back where the reader left it after any tab switch. Registry unit tests pin binaryDocumentPath matching and register's rejection of a binary suffix outside extensions, toolbar tests pin the fallback/menu/empty-state branches, files-body tests pin scroll capture on unmount and restore across a remount, and the keyless Web document-preview scenario measures the width-fitted SVG against the pane and asserts the viewer control's absence per suffix.

中文

问题

侧边栏文档预览积累了多处体验缺陷(issue #3974)。图片按固有 CSS 像素尺寸渲染,宽图撑出面板、只能横向滚动。查看器下拉总是追加纯文本兜底,位图和 PDF 文件因此提供一个结果是不可读字节的「纯文本」选项,而只有一个真实渲染器的文件仍显示一个没有可切换项的控件。完全没有渲染器的二进制容器(视频、压缩包、office 文档)落入纯文本读取器,呈现的是读取报错而非设计过的空态。各渲染器自带不同的 loading 文案和位置,打开文件时会闪过多个措辞、位置都不同的指示器。PDF 页面被双层内边距包裹,每一页都窄于面板宽度。切换侧栏 tab 会让文件树重挂载回到顶部,丢掉读者原来的位置。

决定

图片宽度适配。 图片外框跟随滚动容器的宽度并带 12px 内边距;图片本身使用 max-width: 100% 和 8px 圆角,宽图按纵横比缩小到面板宽度,小图保持固有尺寸并由 auto margin 居中,超高图在共享正文中纵向滚动。高度适配曾被考虑后放弃:它需要外框定高,且混合纵向场景会在没有用户诉求的情况下产生意外布局。

二进制查看器选项。 DocumentPreviewDefinition 新增可选的 binaryExtensions,即渲染器 extensions 中字节不可按文本阅读的后缀;register 拒绝不在 extensions 内的条目。这份知识由渲染器持有:图片正文声明 png, jpg, jpeg, gif, webp, bmp, ico,不含 svg,因为 SVG 源码是可读的 XML;PDF 正文声明 pdf。注册表模块的 binaryDocumentPath 判断是否有已注册定义把文件名后缀声明为二进制,预览 owner 对这类文件跳过纯文本兜底。头部仅在候选不少于两个时渲染查看器菜单;只剩一个候选时完全不渲染查看器控件。

不支持预览的空态。 owner 侧的 document/unviewable.ts 列出没有渲染器认领的二进制容器后缀(视频、音频、压缩包、office 文档、可执行文件、字体、磁盘镜像、设计格式)。owner 仅在没有实现匹配时查询该列表,因此任何渲染器注册始终优先;匹配的文件显示路径头部、文件类型图标和一行 unsupportedFile 说明,并且不会发起读取。不确定的后缀不进入列表、保留纯文本兜底;被文本认领但字节未通过读取器检查的文件通过 error.notText 报告同一句文案。

统一 loading。 LoadingIndicator 只渲染图标,标签作为 aria-label 携带、没有可见文字;所有渲染器的 loading 文案统一为共享的「正在读取…」。内容出现前的每个等待——owner 首次读取、PDF 解析、HTML 打包、图片解码——都把 spinner 居中在面板中,打开文件到正文出现始终是同一位置的一个 spinner。未渲染的 PDF 页以主题骨架色 --dsw-alias-bg-skeleton 上的静态 3:4 占位块保持位置、不带 spinner;文档打开的等待保持为 owner 的居中读取 spinner,而不是特化进第一页的占位块,因为读取 spinner 在占位块之前出现,两者交接会明显跳位。占位块不带扫光动画,其逐页动画成本高于价值。

PDF 铺满与文案。 移除 PDF 正文和页面的内边距,页面贴边占满面板宽度;图片渲染器保留自己的 12px 内边距。预览各字典的中文报错与状态文案去掉句尾句号。

文件树滚动恢复。 ui-sidebar-files 沿用文档预览自己的模式:树存储新增 scrollTop 与 scrolled action,正文在本地记录自己的滚动偏移、只在卸载时且 owner 的 signal 仍存活的情况下提交一次,重挂载时在 layout effect 中恢复。已加载的层本就在存储中比正文活得久,因此重挂载的树在偏移落回之前已按完整高度布局。

曾考虑的替代方案

按定义级布尔 binary 标志。 图片渲染器在一个注册中同时覆盖位图和 SVG,二进制与否是后缀的属性,不是渲染器的属性。

在预览 owner 中按硬编码后缀列表过滤已注册渲染器。 owner 会重复各渲染器已持有的知识,外部渲染器也无法扩展该集合;owner 侧列表只服务于没有渲染器认领的后缀。

单项静态查看器标签。 把唯一候选渲染为静态名称,会在头部放一个不提供任何操作的控件;它只是噪音,因此头部什么都不渲染。

后果

图片不再产生横向滚动;面板宽度是唯一布局输入,因此未增加缩放控件。位图和 PDF tab 不显示查看器控件;SVG 保留含纯文本选项的菜单;未被认领的二进制容器显示空态且不读取。从打开到首个内容,每种预览都只显示一个居中的纯图标 spinner,PDF 页面以安静的占位块出现。任何 tab 切换之后,文件树都回到读者离开的位置。注册表单元测试固定 binaryDocumentPath 的匹配行为与 register 对 extensions 之外二进制后缀的拒绝,工具栏测试固定兜底/菜单/空态分支,files-body 测试固定卸载时的滚动捕获与跨重挂载的恢复,keyless Web document-preview 场景按面板测量适配宽度后的 SVG 并按后缀断言查看器控件的有无。