Skip to content

fix(app-shell): a marketplace README fits its detail page; badges render as images, tables as tables - #30426

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-79xm7b
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-79xm7b

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread
domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

Fixes #30330

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:商店详情页的 README 不再被截掉:长行自动换行,只有代码块和表格在自己框内横向滚动。徽章显示成图片,表格显示成表格,--- 显示成分隔线,不再露出 Markdown 原文。
  • 为什么改:HotCRM 详情页(883 px 窗口)里一条 259 字符的代码行把整个 README 撑到 1926 px,右边被裁掉、无法滚动;徽章行显示成 ![License](LICENSE) … 字面。README 是应用的销售页,半截句子和原始标记让成品像半成品。
  • 风险与回滚:只动 app-shell 商店目录两处(README 渲染器 MarkdownText、详情页两列加 min-w-0)及测试与 changeset,不动 spec、服务端与共享内联渲染器。图片只认绝对 https: 地址并以 no-referrer 加载,其余来源整段略去;控制台 CSP 本就允许 img-src https:。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/console/marketplace/MarkdownText.tsx(renderInline 的图片切分与 absoluteUrl、表格块规则)→ MarketplacePackagePage.tsx(两个网格单元的 min-w-0)→ 测试 MarkdownText.wideReadme-30330.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/console/marketplace/__tests__/MarkdownText.wideReadme-30330.test.tsx packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.readmeWidth-30330.test.tsx,预期全部通过;在 main 的代码上同一命令 26 条红、5 条对照绿。
  • 需要你做的:无,席位落地。

What changes

Which element forced the width (the card left it open). Below the lg breakpoint the detail page's grid gap-4 lg:grid-cols-3 has one implicit auto column, and a grid item's automatic minimum width is its content's min-content width. HotCRM's README has a fenced curl … -d '{"jsonrpc":…}' line of 259 characters; a pre does not wrap, so its min-content width is that line, and it sized the column, the About card and the README at about 1926 px. The pre already had overflow-x-auto, but it never became narrower than its line, so it never scrolled. (At lg and up the template is repeat(3, minmax(0, 1fr)), which has no automatic minimum, so the defect showed only on narrower windows.)

  • MarketplacePackagePage.tsx. Both grid cells (README and Versions) carry min-w-0, so the column keeps the page's width and the fence scrolls inside itself.
  • MarkdownText root. min-w-0 break-words, so a long unbroken word (a URL, a long code span) wraps instead of overflowing; a list item's text span gets min-w-0 beside its bullet for the same reason. A pre keeps white-space: pre and its own scroller; a table scrolls in its own overflow-x-auto box.

Why the badges printed as markup. HotCRM writes [![License](https://img.shields.io/…svg)](LICENSE). The renderer had no image rule, and the shared inline link rule read [![License](https://…svg) as a link whose text is ![License, leaving ](LICENSE) as text, which reads exactly as the card measured.

  • Images. MarkdownText now matches ![alt](src "title") and the linked form [![alt](src)](href) before handing the rest of the text to @object-ui/components' renderInlineMarkdown (unchanged; it is shared with element:text and outside this card's surface). An image renders only from an absolute https: URL, with referrerPolicy="no-referrer", loading="lazy" and max-w-full; any other source (a repository-relative path, /…, //…, http:, data:, javascript:, any whitespace or control character) is left out, alt text included. A linked image keeps its anchor only for an absolute http(s): destination (new tab, noopener noreferrer, the README link look): a relative destination such as LICENSE would resolve against the console, not the repository. A code span is matched first, so `![x](y)` stays code. No fetch path is invented for relative sources.
  • Tables. The card's Expected names wide tables, and HotCRM's README has five pipe tables that printed as one long line of pipes. A GFM pipe table (a line with a pipe above a delimiter row with the same cell count, then rows up to a blank line or another block) renders as a table with the delimiter row's alignment, cells drawn by the same inline pass, short rows padded and extra cells dropped, \| read as a literal pipe. A pipe in prose with no delimiter row stays text.
  • Thematic breaks (bounded in-place fix, same class: Markdown printed as literal text). ---, ***, ___ (spaces allowed) between blocks render as hr; * * * is a rule, not a list item. A --- line directly under a paragraph line stays that paragraph's text, as before (CommonMark reads it as a setext underline, which this renderer does not draw). HotCRM's README has four such rules that printed as ---.

The file header's "If a README really needs tables or images, the publisher can host their own docs" is replaced by the rules above. No raw HTML is rendered and no dependency is added.

Tests

  • MarkdownText.wideReadme-30330.test.tsx (new): HotCRM's badge row renders four img and no ![/](; the referrer, lazy-load and max-w-full attributes; only the absolute-https: badge link keeps its anchor; eight refused sources leave the image out (alt included) in a paragraph and in a list item; a refused link scheme keeps the image without the anchor; an image title, and image syntax inside a code span staying code; tables (cells, inline marks, escaped pipe, alignment, own scroller, padding/truncation, HotCRM's screenshot table); five thematic-break spellings; the compact size; the width classes. CONTROL: headings, links, lists, quotes and fences render as before, a pipe in prose stays text, a --- under a paragraph line stays text, a relative text link keeps its href.
  • MarketplacePackagePage.readmeWidth-30330.test.tsx (new, the releaseNotes-28891 harness): both grid cells carry min-w-0; CONTROL: the README fence renders in the About cell with its own scroller, and the Versions notes render.

Runs (all under scripts/pm/os-verify-lock.sh, pnpm --dir ui exec vitest run …):

  • Red: the two new files against origin/main's MarkdownText.tsx and MarketplacePackagePage.tsx (checked out over the fix, then restored with git checkout HEAD --, git diff HEAD empty): Tests 26 failed | 5 passed (31), the 5 being the CONTROL rows.
  • Green, after the fix: the two new files plus every MarkdownText.* and MarketplacePackagePage* test in the directory (21 files): Test Files 21 passed (21), Tests 159 passed (159). On the final head 6a86665c: the 7 files that render MarkdownText plus scripts/__tests__/markdown-test-inputs.test.ts: Tests 80 passed (80).
  • Type-check: pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2: Tasks: 68 successful, 68 total; on the final head, pnpm --filter @object-ui/app-shell type-check: VERDICT command-exit 0. Lint: eslint on the touched files: 0 errors (the warnings are pre-existing no-explicit-any in MarketplacePackagePage.tsx).

The whole change is the ablation: the red run above is the new pins against the old code, each flipped by the code it names.

Real-browser width measurement (Chromium, 883 px viewport)

jsdom does no layout, so the tests pin the classes. The mechanism was measured in Chromium with a static page whose CSS mirrors the Tailwind classes (an 835 px overflow: hidden parent as the card measured, a one-column grid, the card's 24 px padding, the README's flex column, HotCRM's 259-character fence line at 12 px monospace, and a 200-character unbroken URL in a paragraph):

  • Before (no min-width: 0, no overflow-wrap): parent scrollWidth 1947 in clientWidth 835; card 1945 wide; pre 1895 wide with nothing to scroll; the paragraph's right edge at 1922 px.
  • After (min-width: 0 on the grid cell and the README root, overflow-wrap: break-word): parent 835/835; card 833; pre clientWidth 783, scrollWidth 1895 (scrolls inside itself); the paragraph's right edge at 810 px.

The measurement page is a reconstruction, not the Console build; the Console's own render on a live stack was not exercised.

Acceptance notes

  • ui/AGENTS.md ✨ Set up Copilot instructions #2 (docs-driven): no page under content/docs/ describes which Markdown a marketplace README may use (deployment/marketplace-handbook.mdx names the readmePath and readme fields only), so no doc changes; that tree is also outside this card's surface.
  • A text link to a relative path ([the guide](docs/guide)) still resolves against the console, as before; the shared inline pass owns that rule and element:text reads it too, so it is untouched here. Observation, not filed.
  • An image nested inside a text link's label ([see ![x](y)](z)) is not a shape this renderer reads; the image renders or drops and the link's brackets print. Not seen in any measured README.

Verification

Derived gates (dispatch-gates --commands on merge head d13ed5b; re-runs on final head 6a86665): 35 run, all exit 0
  • node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
  • node scripts/check-empty-changeset.mjs --self-test :: exit 0
  • node scripts/check-sdui-manifest.mjs :: exit 0
  • node scripts/check-sdui-manifest.mjs --self-test :: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
  • node scripts/release-pending-publish.mjs --self-test :: exit 0
  • pnpm --dir ui exec vitest run scripts/ :: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
  • pnpm check:changeset-gate-self-tests :: exit 0
  • pnpm check:closed-ceremony-text :: exit 0
  • pnpm check:cross-package-test-inputs :: exit 0
  • pnpm check:dispatcher-error-vocabulary :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:engine-double-contract :: exit 0
  • pnpm check:gitlink-declared :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:published-files :: exit 0
  • pnpm check:query-options-erasure :: exit 0
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:registry-log-declared :: exit 0
  • pnpm check:rest-log-spy-declared :: exit 0
  • pnpm check:skill-identifier-liveness :: exit 0
  • pnpm check:test-source-alias :: exit 0
  • pnpm check:tier-file-adoption :: exit 0
  • pnpm check:type-check-coverage :: exit 0
  • pnpm check:type-check-debt :: exit 0
  • pnpm check:undeclared-dep-imports :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • pnpm check:where-matcher :: exit 0

pnpm --dir ui exec vitest run scripts/ first exited 1: markdown-test-inputs.test.ts read the new test's CHANGELOG.md badge fixture as a markdown input the ledger does not describe. The fixture now spells the path CHANGELOG; re-run on the final head: Test Files 170 passed | 2 skipped (172), exit 0. Because that commit landed while the loop ran, the gates that read test files were re-run on the final head, all exit 0: check:test-source-alias, check:cross-package-test-inputs, check:nul-bytes, check:undeclared-dep-imports, check-comment-mask-corpus, check-sdui-manifest. node scripts/pm/dispatch-gates.mjs --ran: 35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED.

Standing ui gates
  • node scripts/check-sdui-manifest.mjs (and --self-test): in the derived list above, exit 0.
  • pnpm --dir ui check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0.

Dogfood battery: not owed (no ! in the title, no (narrowing) arm).


Generated by Claude Code

… as images, tables as tables

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZbFePHuuidesS6BLC
…ithout a control-character regex

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZbFePHuuidesS6BLC
…arkdown extension

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZbFePHuuidesS6BLC
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

维护者速读

  • 改了什么: 商店详情页的 README 不再被截掉。长行会自动换行,只有代码块和表格在各自的框里横向滚动。徽章显示成图片,表格显示成表格,--- 显示成分隔线,不再露出 Markdown 原文。
  • 为什么改: HotCRM 详情页里有一条 259 字符的代码行,把整个 README 撑到 1926 px 宽,右边被裁掉,也无法滚动。徽章行显示成 ![License](LICENSE) 这样的原文(Marketplace detail page cuts off a wide README with no scrolling, and shows its badges as raw text #30330)。
  • 风险与回滚: 只改了商店目录下的 MarkdownText 和详情页两个网格单元的 min-w-0,没有动共享内联渲染器、spec 和服务端。图片只接受绝对 https: 地址,并且以 no-referrer 方式加载;其他来源整段略去。回滚就是撤销本 PR。
  • 先看: MarkdownText.tsx 里的 renderInline / absoluteUrl → 表格块规则。
  • 两分钟验证: pnpm --dir ui exec vitest run packages/app-shell/src/console/marketplace/__tests__/MarkdownText.wideReadme-30330.test.tsx,预期全部通过。
  • 需要您做的: 无。本 PR 不在受管面,共 610 行,由席位经合并队列落地。
  • 席位意见: ACCEPT。

席位核对 — ACCEPT · domain:ui#2 · head 6a86665ce22b63bc0daf4364a817493b19facb56

  • 形态:
    • draft,作者 objectstack-fleet[bot](中继 pr_create),无 assignee,无 review request。
    • 5 个文件,+599 −11。
    • check-governed-merges --pr 30426 结果为 NOT governed,610 行,未超 3000。
  • 前提实测:
    • 撑宽的根因:lg 以下网格是单列 auto,网格项的最小宽度等于内容宽度。修复是给两个单元加 min-w-0,渲染根加 min-w-0 break-words,代码块和表格各自 overflow-x-auto。
    • 新增的渲染路径没有 dangerouslySetInnerHTML,React 会转义文本。
    • absoluteUrl 先拒绝含空白或控制字符的地址,再用 new URL 判定协议:图片只认 https:,图片外层的链接认 https:/http:,所以 javascript:、data:、相对路径都会落空。链接带 rel="noopener noreferrer"。
    • 代码段里的 ![..](..) 不会被切成图片。
    • 新测试在 main 上 26 条红、5 条对照绿;修后 21 个文件 159 条全绿。
  • 门禁 check-runs: 6a86665c 上 55 个 run:44 success,11 skipped,0 失败。
  • 申报:
    • Clause-②: no,Metadata-protocol: none。
    • changeset 是 @object-ui/app-shell patch。
    • 没有新增 i18n 键,也没有命中任何契约面。
  • 接受的偏差: 分隔线(thematic break)不在卡片原文里,属于同一渲染器里同一类问题,已在 PR 正文点名,就地修复。
  • 范围外去向: 无。
  • 落地条款: Fixes Marketplace detail page cuts off a wide README with no scrolling, and shows its badges as raw text #30330。由本席位按普通队列在这个 head 上执行 pr_ready 加 automerge_enable,但要先等维护者在席位线程里同意。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marketplace detail page cuts off a wide README with no scrolling, and shows its badges as raw text

1 participant