From b1b53672db2e75c5fc91523c7d4fd0061e0eae0b Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Mon, 19 Oct 2026 23:00:32 +0800 Subject: [PATCH] fix(ui-primitives): separate the detail document with horizontal rules and keep its preview format checked --- .../adding-a-plugin-preview.i18n.yaml | 4 +- docs/cookbook/adding-a-plugin-preview.md | 2 +- docs/cookbook/adding-a-plugin-preview.zh.md | 2 +- .../src/DetailDocument.module.css | 6 +- scripts/plugin-preview-assets.spec.ts | 67 +++++++++++++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 scripts/plugin-preview-assets.spec.ts diff --git a/docs/cookbook/adding-a-plugin-preview.i18n.yaml b/docs/cookbook/adding-a-plugin-preview.i18n.yaml index 1d6e8a003b..0111677ab5 100644 --- a/docs/cookbook/adding-a-plugin-preview.i18n.yaml +++ b/docs/cookbook/adding-a-plugin-preview.i18n.yaml @@ -18,5 +18,5 @@ en: 20ce358cb110104c zh: abb8d72fed81f3c8 /cookbook-add-a-plugin-feature-preview/5-validate-and-review: - en: 72d9467e354303cd - zh: 1ef571605ba223f1 + en: 2888c02bac267662 + zh: 5b0e95611c8d5d94 diff --git a/docs/cookbook/adding-a-plugin-preview.md b/docs/cookbook/adding-a-plugin-preview.md index 122318d8f3..1683aab10a 100644 --- a/docs/cookbook/adding-a-plugin-preview.md +++ b/docs/cookbook/adding-a-plugin-preview.md @@ -42,4 +42,4 @@ The Chinese document references `preview.zh.svg` with Chinese alternative text. ## 5. Validate and review -Open the real plugin details in English and Chinese before activation. Confirm the image loads, its placement, light/dark readability, and no horizontal overflow at 390 px. No check enforces the SVG rules above; review checks the format and equivalent meaning in both languages. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately. +`scripts/plugin-preview-assets.spec.ts` checks every preview pair in a bundle's `docs` directory: the fixed canvas, absent scripts, stylesheets, text elements, and external references, the 1 MiB pair limit, and English and Chinese files that differ. Then open the real plugin details in English and Chinese before activation and confirm the image loads, its placement, light/dark readability, and no horizontal overflow at 390 px; review owns visual quality and equivalent meaning in both languages. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately. diff --git a/docs/cookbook/adding-a-plugin-preview.zh.md b/docs/cookbook/adding-a-plugin-preview.zh.md index fd8e9eecf5..3d3caeb1bf 100644 --- a/docs/cookbook/adding-a-plugin-preview.zh.md +++ b/docs/cookbook/adding-a-plugin-preview.zh.md @@ -42,4 +42,4 @@ SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器 ## 5. 校验与验收 -在真实插件详情页分别使用英文和中文,启用前确认图片能加载,并检查插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出。没有检查强制执行上述 SVG 规则;审阅需检查格式及双语含义是否一致。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。 +`scripts/plugin-preview-assets.spec.ts` 检查组合包 `docs` 目录中的每一对配图:固定画布;不使用脚本、样式表、`text` 元素或外部引用;每对文件合计不超过 1 MiB;英文和中文文件不相同。然后在真实插件详情页分别使用英文和中文,启用前确认图片能加载,并检查插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出;审阅负责检查视觉质量及双语含义是否一致。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。 diff --git a/packages/client/ui-primitives/src/DetailDocument.module.css b/packages/client/ui-primitives/src/DetailDocument.module.css index f7361a0fa6..3b4fdf0653 100644 --- a/packages/client/ui-primitives/src/DetailDocument.module.css +++ b/packages/client/ui-primitives/src/DetailDocument.module.css @@ -10,9 +10,11 @@ /* A long README scrolls inside the section instead of pushing the page's later sections out of reach. */ max-height: min(560px, 60vh); overflow-y: auto; - /* Without a frame, keep the scrollbar off the text and the text width independent of overflow. */ + /* Horizontal rules separate the document from its neighbors; a full frame would clash with the images' own corners. */ + border-block: 0.5px solid var(--dsw-alias-border-l1); + /* Keep the scrollbar off the text and the text width independent of overflow. */ scrollbar-gutter: stable; - padding: 4px 0; + padding: 12px 0; overflow-wrap: anywhere; } diff --git a/scripts/plugin-preview-assets.spec.ts b/scripts/plugin-preview-assets.spec.ts new file mode 100644 index 0000000000..61b3959af9 --- /dev/null +++ b/scripts/plugin-preview-assets.spec.ts @@ -0,0 +1,67 @@ +/** Plugin feature previews are one fixed-canvas SVG per required UI language, beside their detail document. */ +import { globSync, readFileSync } from 'node:fs' +import { basename, dirname, extname, join } from 'node:path' +import { JSDOM } from 'jsdom' +import { expect, it, onTestFinished } from 'vitest' + +const root = `${import.meta.dirname}/..` +const svgPaths = globSync('packages/*/*/docs/*.svg', { cwd: root }).sort() +/** Combined size of one language pair, as the standalone bundle and Host routes both serve the files individually. */ +const MAX_PAIR_BYTES = 1024 * 1024 + +it('keeps every preview a self-contained 640×300 SVG with English and Chinese variants', () => { + // Guard against a narrowed corpus: each preview moved into a bundle's docs directory stays checked. + expect(svgPaths.length).toBeGreaterThan(0) + const browser = new JSDOM('') + onTestFinished(() => { browser.window.close() }) + const parser = new browser.window.DOMParser() + const pairs = new Map() + for (const path of svgPaths) { + const name = basename(path, '.svg') + const chinese = name.endsWith('.zh') + const stem = join(dirname(path), chinese ? name.slice(0, -3) : name) + const pair = pairs.get(stem) ?? {} + if (chinese) pair.zh = path + else pair.en = path + pairs.set(stem, pair) + } + for (const [stem, pair] of pairs) { + expect(pair, stem).toEqual({ en: `${stem}.svg`, zh: `${stem}.zh.svg` }) + } + for (const [, pair] of pairs) { + const sources: string[] = [] + let bytes = 0 + for (const path of [pair.en, pair.zh]) { + if (path === undefined) throw new Error('Preview pair is incomplete') + const svg = readFileSync(join(root, path), 'utf8') + sources.push(svg) + bytes += Buffer.byteLength(svg) + const document = parser.parseFromString(svg, 'image/svg+xml') + expect(document.querySelector('parsererror'), path).toBeNull() + expect(document.documentElement.namespaceURI, path).toBe('http://www.w3.org/2000/svg') + expect(document.documentElement.localName, path).toBe('svg') + expect(document.documentElement.getAttribute('width'), path).toBe('640') + expect(document.documentElement.getAttribute('height'), path).toBe('300') + expect(document.documentElement.getAttribute('viewBox'), path).toBe('0 0 640 300') + expect(document.querySelector('path, rect, circle, ellipse, line, polyline, polygon'), path).not.toBeNull() + expect(document.querySelector('script, style, foreignObject, text, animate, animateTransform, animateMotion, set'), path).toBeNull() + const ids = new Set([...document.querySelectorAll('[id]')].map(element => element.id)) + for (const element of [document.documentElement, ...document.querySelectorAll('*')]) { + for (const attribute of element.attributes) { + expect(attribute.name, path).not.toMatch(/^on/iu) + const references = [...attribute.value.matchAll(/url\(([^)]+)\)/gu)].map(match => (match[1] ?? '').replace(/['"]/gu, '').trim()) + if (attribute.localName === 'href') references.push(attribute.value) + for (const reference of references) { + if (element.localName === 'image' && attribute.localName === 'href' + && /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z\d+/]+=*$/u.test(reference)) continue + expect(reference.startsWith('#') && ids.has(reference.slice(1)), `${path}: ${reference}`).toBe(true) + } + } + } + } + expect(bytes, pair.en).toBeLessThanOrEqual(MAX_PAIR_BYTES) + // The Chinese variant carries translated content; identical files would mean a forgotten translation. + expect(sources[0], pair.en).not.toBe(sources[1]) + expect(extname(pair.en ?? ''), String(pair.en)).toBe('.svg') + } +})