Skip to content

Commit 3d2da4f

Browse files
feat(plugin-detail)!: one record page laid out by its own width; 动态 is the right column's first panel (#28116)
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgQSrPxYRF6Q9RRD48Snj7Ks)_ Part of #27524 Clause-②: yes (narrowing) Metadata-protocol: none Skills-docs: content — `content/docs/ui/pages.mdx` described the synthesized record page's tab strip, its column tiers, the related rail and the timeline column; it now describes the details and the right column, 动态 first, laid out by one rule. No file under `skills/**` teaches the record page's layout (a `git grep` for the tiers, the switch and the stream names nothing there). ## 维护者速读 - 改了什么:合成记录页只按容器宽度一条规则排版。≥ 760 px 详情 | 右栏 320 px;≥ 1120 px 右栏 380 px、详情字段 3 列、动态 5 条;< 760 px(分屏 720、手机)单列:详情,再按顺序接右栏各面板。三列档、页签档、「相关 N」导轨、常驻时间线列和它的 ⊞ 开关全部退役。动态(原时间线)成为右栏第一个面板:一行输入框、最新 3 条(宽档 5 条)、「查看全部 N 条」打开动态页签(`?related=timeline`,完整流:筛选、回复、加载更多)。右栏开关 ⊟(「右栏」,`⌘⇧,`)只剩一个图标。 - 为什么改:#27524 的 Target 4、5、6(画布 60 定稿),四个串行 PR 中的第二个;⊟ 移到顶栏在 PR ④,页签行在 PR ③。 - 风险与回滚:只动 ui 包(plugin-detail、components、app-shell、十个语言包)、文档和两个夜间 live 测试 spec。合成页的动态在每个宽度都开页即读(原先窄档要等页签)。`detail.timeline` 的值改成 动态 / Activity,所以手写页面的 `Timeline` 页签不再映射到它,仍读「时间线」。回滚即 revert 本 PR。 - 先看:`ui/packages/plugin-detail/src/RecordPageColumns.tsx`(档位与右栏);`RecordTimelinePanel.tsx` + `RecordActivityTimeline.tsx` 的 peek 面;`synth/recordPageColumns.ts`(宽度规则);app-shell `RecordDetailView.tsx`(⊟ 与开页读)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-detail/src/__tests__/RecordTimelinePanel-27524.test.tsx packages/app-shell/src/views/RecordDetailView.recordPageTiers-26678.test.tsx packages/app-shell/src/views/RecordDetailView.panelToggles-27052.test.tsx`,预期 3 个文件、23 个测试全绿。 - 需要你做的:本 PR 单独改动 3571 行(+2052 / −1519,71 个文件),超过 3000 行人工合并线(AGENTS.md §7(c)),按 Tier H 落地:需要 os-zhuang 或 hotlong 的 APPROVED 审查,或你亲自合并。三成多是退役代码和按新结构改写的测试(验收第 4 条),第 5、6 条共用时间线列的退役,拆开反而要造一个只为删除而存在的过渡布局。 - 审查:契约审查 6523984004 在 `a38376b7be` 上判 FAIL。④-1:`⌘⇧,` 会串到抽屉下面的页面。④-2:在抽屉里按 Esc 会关掉整个抽屉,而不是先退出放大面。两个修复提交之后,复审 6525247890 判 PASS。预览审查两次 PASS(6524207275、6525118794)。遗留 #28156:抽屉最大化或在下层时,字段内的 Esc 会被抽屉接走;那段代码属 #27545。 ## What changes The synthesized record page is laid out by one rule, the width of the container it occupies, and 动态 becomes the right column's first panel (record page v4, canvas 60). This is PR ② of four serial PRs for #27524, approved by the seat as a split. It restarts from `main` after PR ① merged and leaves `main` coherent on its own. | Target item | Where | |---|---| | 1, 2, 3, 10, 11, 12 | PR ① (merged) | | 4 ⊟ the right-column switch | **this PR**, at the end of the record header's action row; PR ④ moves it to the top bar | | 5 动态: a panel and a tab, never a column | **this PR** (the tab is the interim `?related=timeline` face; PR ③ makes it a tab of the tab row) | | 6 tiers by the container's own width | **this PR** | | 8, 9 the tab row, enlarged = active tab | PR ③ | | 7 the top bar | PR ④, the one that closes the card | **6. One rule** (`synth/recordPageColumns.ts`, `useRecordPageTier.ts`, `RecordPageColumns.tsx`). `recordPageTierFor(width)` answers `wide` from 1120 px (`RECORD_PAGE_WIDE_MIN_WIDTH`), `two-column` from 760 px, `stack` below it and for an unknown width. The embedded record (split pane, overlay drawer, maximized) and the phone take the same answer: `useRecordPageTier` lost `embedded`. Both column tiers draw details | the right column, 320 px (`grid-cols-[minmax(0,1fr)_20rem]`) or 380 px when wide (`_23.75rem`). The stack is one column: the details, then the right column's panels in their order. A details section takes up to three field columns when wide and two beside a 320 px column (`DetailsColumnCapContext`, read by `framelessSectionColumns`). The three-column tier, the tabbed tier, the 「相关 N」 rail (`relatedRail`, `record-related-rail-toggle`), the resident timeline column, its ⊞ promotion and `recordTimelineDefaultReason` / `recordPageColumnDefaults` / `recordPageArrangementFor` retire. The right column is an `<aside aria-label="右栏">` in every tier. **5. 动态 is a panel and a tab** (`RecordTimelinePanel.tsx`, `renderers/record-activity.tsx`, `RecordActivityTimeline.tsx`). The panel leads the right column and has a related panel's header: 「动态」, the stream's count (`data-record-timeline-count`, absent while the feed loads), and the header row as one stretched button that opens the 动态 tab (the census rule from PR ①). Its body is the page's own `record:activity` block in a peek face, chosen by `RecordTimelinePanelContext`: - a one-line composer (「写一条备注… @ 提到同事」, `detail.timelineComposer`). Pressed, it becomes the full composer, focused; a sent note folds it back; - the newest 3 roots (5 when wide), one line each: icon, text, time. A reply is not a line of its own; - 「查看全部 N 条 →」 when the stream holds more than it draws. The 动态 tab is `?related=timeline`, the existing enlarged face: the whole stream with the 全部 · 活动 · 讨论 · 变更 chips, threaded replies and "Load more" (`data-stream-load-more`, one page per press; it replaces the stream's own 「查看全部」). The synthesized tab strip, where a page keeps one, orders details / 动态 / related for every object; the worked-object promotion is gone. **4. ⊟** (app-shell `RecordDetailView.tsx`, components `panel-toggles.tsx`). One toggle, 「右栏」, drawn at the end of the record header's action row only where the tier has a right column and the page has a panel for it. `⌘⇧,` flips it; `⌘⇧.` (the timeline column's) is gone. Off, the details take the row. The stored preference keeps its name, `record.columns.related`, so a reader's earlier related-column choice holds; `record.columns.timeline` is no longer read. `PanelToggles` draws icon-only toggles named by their label (no border group, `h-8 w-8`). **Naming.** `detail.timeline` is 「动态」 / "Activity" in all ten packs (the values `detail.timelineFolded` had). `detail.relatedColumn` is 「右栏」 / "Right column"; `console.shortcuts.toggleRelated` follows. `detail.timelineFolded` and `console.shortcuts.toggleTimeline` leave every pack; `detail.timelineComposer` arrives in all ten. **Tab labels** (components `containers.tsx`). With `detail.timeline` now naming the stream, the strip's well-known-token map could no longer send the bare word `Timeline` to it: the served approval page's history tab is labelled `Timeline`, and in en it read "Activity" (`approvalRequestsDataSource.servedPage` went red on exactly that). The synthesizer writes its own token, `Activity stream`, for its stream tab (read as `detail.timeline`), and an authored `Timeline` reads `console.objectView.viewTypeTimeline`, the pack's word for a timeline (zh-TW keeps its dict form). Pinned in `page-tabs-builtin-label-i18n-4645`. **Open waves.** The 动态 panel is on screen at open in every tier, so a synthesized page reads its feed, the mention list and the audit log at open at every width (before, the narrow tier waited for the Timeline tab). Opening the 动态 tab reads none of them again. An authored page with tabs still waits for the tab that holds them (`recordOpenWaves-13010` keeps those cases unchanged). **Review fixes.** The contract review 6523984004 on `a38376b7be` was FAIL, with must-fix ④-1 and should-fix ④-2. The delta review 6525247890 on `90828f16cb` is PASS. - ④-1 (`d98543cb28`). A record page keeps its tree mounted under the drawer it opens. A drawer wide enough for its own right column draws its own ⊟, so one `⌘⇧,` ran both views' listeners. The page's listener ran first and flipped the hidden page's stored switch. Now only the topmost record root (the last `[data-record-page-root]` in the document) answers the chord. - ④-2 (`d98543cb28`, `90828f16cb`). Inside the record pane, `Esc` on an open face (the 动态 tab, or a related list's whole face) closed or stepped the pane, because the face's guard found the pane's own dialog. The face now listens on the window in the capture phase and ignores the dialog that holds it. It takes the key ahead of the drawer's dialog layer and the split pane's listener. - An `Esc` pressed in a text field inside the face still reaches the field, which closes its mention list or cancels an inline edit. The face marks it handled, so the drawer does not dismiss on it. The preview re-check on `d98543cb28` found this case, and `90828f16cb` fixes it. - Not fixed here, filed as #28156. When the pane is maximized or below the stack's root, its `onEscapeKeyDown` acts without reading `defaultPrevented`. So `Esc` in a field inside a face restores or steps the pane there. That handler is in `navigation-overlay.tsx`, #27545's surface, and the behaviour is on `main` today at level 2 and deeper. **Declared.** `Clause-②: yes (narrowing)`. Two changesets: - `27524-record-page-tiers-activity-panel.md` (plugin-detail, components, app-shell): eight exports and three members leave the plugin-detail entry, under `not-required (runtime-export-retired …)` with one FROM → TO line each. The gate verifies all eleven. `RecordPageTier` becomes `'stack' | 'two-column' | 'wide'`, and `recordPageTierFor` takes one argument. Both are said in prose. - `27524-record-page-activity-i18n.md` (i18n): `TranslationKeys` loses the two keys, under `not-required (runtime-interface-only …#TranslationKeys)`, as PR ① did. **Size: over the human-merge threshold.** This PR changes 3,571 lines (+2,052 / −1,519 over 71 files, `additions + deletions`), over the 3,000 of AGENTS.md §7(c), so it lands the way a Tier H surface does: an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat, or the maintainer's merge. It is not split further. More than a third of it is retired code and pins rewritten to the new anatomy (acceptance item 4), and Target 5 and 6 share one retirement, the timeline column: a split would have to build an interim layout that exists only to be deleted by the next PR. **Source re-measured.** The card names `record.timeline.*` keys. No such namespace exists: the record page's labels are `detail.timeline` (value changed), `detail.timelineFolded` and `console.shortcuts.toggleTimeline` (both retired). The 1200 px board draws the details grid at three columns; this follows the card's rule, two columns beside the right column below 1120 px. **File surface.** Outside the card's list: `components/src/custom/panel-toggles.tsx` (⊟ is icon-only), `components/src/renderers/layout/containers.tsx` (the `Timeline` token above), `app-shell/src/utils/workspacePreference.ts` (the timeline switch's preference retires), `RichTextCommentInput.tsx` (`autoFocus` for the expanded composer), `DetailSection.tsx` / `defaultRecordPage.ts` (the details column cap), the plugin-detail README, `content/docs/ui/pages.mdx`, three items of `docs/qa/platform-checklist/areas/records-forms.json` that located the stream by the retired column, and `ui/e2e/live/record-history-display.spec.ts` (opens `?related=timeline`; on `main` #28009 had re-pointed it at the folded Activity card that this PR retires). Seat-approved addition (the dispatch keeps `examples/**` off limits): `examples/app-showcase/e2e/detail-shapes.spec.ts`, the Contoso case only. #28009 pinned it to the tabbed tier at a 780 px window, and Target 6 retires that tier, so the nightly showcase smoke would go red on this PR's merge. The case now reads the one-column stack tier instead (seat 2 widened the surface to this one file; the rest of `examples/**` is untouched). ## Tests New: - `plugin-detail/src/__tests__/RecordTimelinePanel-27524.test.tsx` (7): the real `record:activity` block in the panel. Header 「Activity」 and the whole stream's count, the header opens the tab; the newest three roots (a reply is not a line), no chips, no load-more, 「查看全部 8」 opens the tab; five when wide; no 「查看全部」 when the stream holds no more; no count while the feed loads; the one-line composer expands focused and folds after a sent note; CONTROL: outside the panel the same block is the full stream. - `plugin-detail/src/__tests__/RecordPageColumns.enlargedEscapeInPane-27524.test.tsx` (6). It runs through the real `NavigationOverlay` (its Radix sheet and its split listener), with the page's blocks stubbed: - in the drawer, `Esc` on the 动态 tab returns to the record's page, and the next `Esc` dismisses the drawer; - below the drawer's root, the face closes and the level stays; - in the split pane beside a list, the face closes and the pane stays; - an open menu owns `Esc`; - a text field in the face gets its `Esc` while the face and the pane stay, in the drawer and in the split pane. Rewritten to the new anatomy, not deleted (acceptance item 4): - `RecordDetailView.recordPageTiers-26678` (6): 1440 and 1200 wide (380 px, details grid at three), 1100 two-column (320 px, two), 720 and 390 one column with 动态 after the details, embedded at 1440 and 720 by the same rule, `?tab=` addresses no strip and `?related=timeline` is the full stream. - `RecordDetailView.panelToggles-27052` (10): one icon 「Right column」 at the end of the action row; off and on; survives a remount, per object; non-worked and container objects open with the right column; `⌘⇧,` by key and by character, inert in a text field; `⌘⇧.` is not the page's; not drawn in the one column; drawn for the embedded record at 1440; with a 1000 px drawer over a 1440 px page, `⌘⇧,` flips the drawer's switch alone, and the page's once the drawer is gone. - `recordPageV3Face-27055` (the details column cap: two beside a 320 px right column), `RecordPageColumns.test`, `RecordPageColumns.switches-27052`, `synth/recordPageColumns.test`, `synth/recordPageColumns.switches-27052`, `RelatedCardStack-27055`, `recordTimelineStream-27055`, `buildDefaultPageSchema.*` (order details / 动态 / related for every object), `panel-toggles.test`, `workspacePreference.*`. - `examples/app-showcase/e2e/detail-shapes.spec.ts`, the Contoso case (nightly showcase smoke): at 780 px (732 px of view) the page is `stack`, `data-record-columns-shown="details"`, no Details / Related tab; Invoices and Projects are body tables in the details with their rows read at open; the right column's first panel is 动态, Invoices and Projects are not panels; the Contacts panel reads 「Carl Fox」 once scrolled into view. The flat-details, money-strip and footer assertions are unchanged. - The app-shell pins that opened the tabbed tier read the laid-out page: approvals (`approvalLock-12220`, `approvalLock-25934`, `approvalRecallGate`: the first of the page's probes), the feed and audit pins (`?related=timeline`), the related-list wire pins (`relatedBadgeArity-8882` and `relatedListFilter-4664` read the panel's count and rows; `relatedListInheritedSort-5795` and `childWriteRefreshesParent-12244` open the list's whole face), `relatedCountsFirst-27056`, `relatedListsOrder-27241`, `bodyTablePredicate-27478` (the right column), `accessibleNames-12625` (the right column and its panels are named), `recordOpenWaves-13010` (the synthesized cases read at open), and console `approvalRequestsDataSource.decisionPage` (the slotted request page is laid out; its actions list opens by `?related=`). <details><summary>Runs</summary> Head `90828f16cb` (the two review-fix commits on `a38376b7be`), under the verify lock: - `pnpm --dir ui exec vitest run` over 105 test files (by `git ls-files`: `RecordDetailView.*`, the drawer tests, `navigation-overlay*` and console `approvalRequestsDataSource.*`) plus `packages/plugin-detail` → `Test Files 452 passed | 1 skipped (453)`, `Tests 3820 passed | 18 skipped (3838)`, VERDICT command-exit 0. - `type-check` of `@object-ui/plugin-detail` and `@object-ui/app-shell`: each exit 0. eslint on the four changed files: 0 errors. - The new `Esc` pin was red on `d98543cb28`: the drawer's `setIsOpen(false)` was called for an `Esc` in a field inside the face. It is green on `90828f16cb`. - CI on `90828f16cb`: all 61 check-runs concluded success, or skipped by path filter or opt-in. `Test UI` (16/16), `Type Check UI`, `Lint UI` and `Live E2E UI` are among them. Head `a38376b7be`, on `main` `f0857d29c1` (PR ① merged as `ad030d805b`; #27953 is in), under the verify lock unless said: - `pnpm build` → `Tasks: 115 successful, 115 total`, VERDICT command-exit 0. - `pnpm --dir ui exec vitest run packages/plugin-detail packages/i18n` plus 322 test files across `ui/`: the 311 that name a module or id this PR changes (`RecordDetailView`, `RecordPageColumns`, `useRecordPageTier`, `record:activity`, `page:tabs`, `PanelToggles`, `workspacePreference`, `detail.timeline` and the rest; derived by `git grep` on this head) and the 11 test files #27953 added or changed for the pane → `Test Files 760 passed | 1 skipped (761)`, `Tests 9018 passed | 31 skipped (9049)`, VERDICT command-exit 0. The whole `ui/` suite is left to CI Test UI: a full local run outlives this container's background ceiling. - `type-check` (`tsc --noEmit` + `tsc -p tsconfig.test.json`) of `@object-ui/i18n`, `@object-ui/components`, `@object-ui/plugin-detail`, `@object-ui/app-shell`, and `@objectstack/example-showcase typecheck` (its `tsconfig` includes `e2e/`): each exit 0. eslint on the 64 touched `ts`/`tsx` files under `ui/`: 0 errors (unlocked). - The nightly harness itself, after `pnpm console:build`: `cd examples/app-showcase && OS_TEST_TIERS=nightly pnpm exec vitest run test/showcase-smoke.live.test.ts test/console-live-specs.live.test.ts` → `Test Files 2 passed (2)`. The showcase smoke's Playwright run: `53 passed`, the three detail-shapes cases among them (Contoso on the stack tier). The console live specs: `19 passed`, `record-history-display` among them (the Changes chip on the 动态 tab). Before the restart, on `c888a82d32` (PR ①'s `8b20d10d4c`): the same derivation gave 742 files green. That run found one type-check error, in plugin-detail's test project: `recordPageV3Face-27055` passed `framelessSectionColumns` straight to `Array.map`, which now hands it the index as the new cap. `c888a82d32` (now `81683b80b7`) calls it with the count and pins the cap. Three console approval page pins (`decisionPage`, `servedPage`, `servedPageMemberPermissions-13324`) were red on the first run, before the Tab labels fix above and the `decisionPage` rewrite. </details> ## Acceptance notes - Screenshots: on `examples/app-crm`, not HotCRM (deviation, as the order says); eleven shots `pr2-*.png` in `/mnt/project-files/ui-seat2/card-27524/shots/` (project files, not a repository branch), zh, through the console's dev server on `c888a82d32` (before the restart onto `main`; the record page code is unchanged by it). Measured in the page (container width → tier, right column width, 动态 lines): - `pr2-01-account-1440` (container 1184 → `wide`, right column 380, 动态 shows 5 of 6 and 「查看全部 6 条 →」), `pr2-02-account-1200` (944 → `two-column`, 320, 3 lines), `pr2-03-account-1000` (952 → `two-column`, 320). - `pr2-04-account-720` and `pr2-09-account-390` (phone, full page): one column, the details, then 动态, then the related panels; no ⊟. - `pr2-05-activity-panel`, `pr2-06-composer-open` (the one-line composer pressed: the full composer, focused), `pr2-07-activity-tab` (「查看全部」 → `?related=timeline`: chips 全部 · 活动 · 讨论 · 变更, threaded entries), `pr2-08-right-column-off` (⊟ off at 1440: the details take the row). - `pr2-10-lead-1440` (details grid at three columns beside 380) and `pr2-11-lead-1000` (two beside 320). app-crm's account shows no details body at all: every field it holds a value for is a highlight, the same on `main`. - The tier follows the container, not the window: with the sidebar open, a 1200 px window gives a 944 px page, which is `two-column`. - Preview review (proxy for epic:#26676#1, per ruling B 6520722066): - PASS on `a38376b7be` (6524207275). It covered Target 4, 5 and 6, plus a regression pass on ①, at 1440, 1200, 1000, 720 and 390 and in the pane opened from a list. - PASS on `90828f16cb` (6525118794). This was a delta re-check of the two pane keys, `Esc` and `⌘⇧,`, at 1440 and 720. ## Verification <details><summary>Gates and the dogfood run</summary> Head `90828f16cb`, change set vs the merge base `f0857d29c1`: 71 paths, 3,571 changed lines (OVER the 3,000 line). - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `90828f16cb` → 84 commands, the same list as on `a38376b7be`. Each was run and its exit code captured before any pipe. All 84 are `:: exit 0`, `node scripts/check-sdui-manifest.mjs` among them. One more was run beyond the derivation, `pnpm --dir ui run check:i18n-drift :: exit 0`. - `pnpm check:type-check-debt` first answered exit 3, NOT MEASURED: its closure build queued behind the dogfood run for the verify lock. Rerun under the lock, it exits 0 (`0 raw tsc error(s) total, none above its recorded number`). That rerun is the code recorded below. - `--ran` reconcile: `84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN`. - Dogfood, once on `90828f16cb` (the title has `!`), under the verify lock: `pnpm --filter @objectstack/dogfood test` → VERDICT command-exit 1, `Test Files 1 failed | 359 passed | 1 skipped (361)`, `Tests 1 failed | 2891 passed | 9 skipped (2901)`. This is the same single red as on `a38376b7be`: `api-action-door-forward.dogfood.test.ts`, "the switch holds: ban_user OFF answers 409 ACTION_DISABLED…". - It expects `sys_metadata_activation` in the disabled-action sentence that `3e0fd7a8f7` (#27964) rewrote. That red is `main`'s own, already carded as #28082. This PR touches neither the action door nor that sentence. - CI's `Dogfood Regression Gate` is success on this head. - NOT MEASURED here, CI's own: the six path-scheduled CI jobs and the five type-check lanes the derivation names. The per-package `type-check` runs above stand in for the lanes of the four packages this PR changes. - `main` has moved 33 commits past `f0857d29c1` since the restart, and `git merge-tree` with it is clean. The shared paths are the ten locale packs, `RecordDetailView.tsx` and `containers.tsx`, in different hunks. CI's merge ref covers it, and no merge was pushed (seat, 13:12Z). ``` 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-adr-links.mjs :: exit 0 node scripts/check-adr-links.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-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-docs-section-name.mjs :: exit 0 node scripts/check-docs-section-name.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: 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/check-section-landing-index.mjs :: exit 0 node scripts/check-section-landing-index.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.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/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 pnpm --filter @objectstack/spec run check:docs :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm --filter @objectstack/spec run check:empty-state :: exit 0 pnpm --filter @objectstack/spec run check:liveness :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-anchors :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:doc-frontmatter :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:docs-redirects :: exit 0 pnpm check:docs-single-h1 :: exit 0 pnpm check:docs-spec-enumerations :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:examples-live-imports :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-checklist :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:published-readme-links :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:react-page-adapter-contract :: 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:role-word :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: 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:vendor-version-stamps :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm --dir ui run check:i18n-drift :: exit 0 ``` </details> <sub>Body refreshed 2026-10-30T14:49Z for head `90828f16cb`: the two review fixes, their runs, the preview re-check and the gates and dogfood run on this head.</sub> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CaA9ZdTcj97mzGtHYt)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6212fdc commit 3d2da4f

72 files changed

Lines changed: 2080 additions & 1521 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
feat(i18n)!: 「动态」 (Activity) names the record page's stream in all ten packs; the timeline column's two keys leave
6+
7+
Clause-②: yes (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys) the key type of the console's own translation catalogue: no Zod schema, no spec declaration and no stored metadata reads it, so the compiler is the channel that reaches every caller -->
11+
12+
The record page's stream is called 「动态」 in zh and "Activity" in en, in the record page's panel, its tab and the right-column switch's labels (record page v4).
13+
14+
- `detail.timeline` changes value in all ten packs: 动态 · Activity · アクティビティ · 활동 · Aktivität · Activité · Actividad · Atividade · Активность · النشاط.
15+
- `detail.relatedColumn` now names the right column (「右栏」, "Right column"), and `console.shortcuts.toggleRelated` shows or hides it.
16+
- `detail.timelineComposer` (the 动态 panel's one-line composer, 「写一条备注… @ 提到同事」) arrives in all ten packs.
17+
18+
**BREAKING** (`@object-ui/i18n`, TypeScript only): the exported `TranslationKeys` (`typeof en`) loses `detail.timelineFolded` and `console.shortcuts.toggleTimeline`, which named the retired timeline column and its shortcut. Both leave every pack. A `t()` call naming one still compiles, since its key is a `string`, and reads the key back. Nothing replaces them: the column is gone.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/components': patch
4+
'@object-ui/app-shell': patch
5+
---
6+
7+
feat(plugin-detail)!: one record page laid out by its own width — details beside a right column from 760 px, one column below; 动态 is the right column's first panel; ⊟ shows or hides the right column
8+
9+
Clause-②: yes (narrowing)
10+
Metadata-protocol: none
11+
12+
<!-- adr-0087: not-required (runtime-export-retired @object-ui/plugin-detail#RECORD_PAGE_THREE_COLUMN_MIN_WIDTH, @object-ui/plugin-detail#recordPageArrangementFor, @object-ui/plugin-detail#recordPageColumnDefaults, @object-ui/plugin-detail#recordTimelineDefaultReason, @object-ui/plugin-detail#RecordPageColumnSwitches, @object-ui/plugin-detail#RecordPageArrangement, @object-ui/plugin-detail#RecordPageObjectLike, @object-ui/plugin-detail#RecordTimelineDefaultReason, @object-ui/plugin-detail#DefaultPageColumns.relatedCount, @object-ui/plugin-detail#UseRecordPageTierOptions.embedded, @object-ui/plugin-detail#RecordPageColumnsProps.switches) the retired operands are the synthesized record page's layout helpers, their option and prop members, and the types they took, in a published ui package's entry that only the console's own record view imported; no packages/spec schema, no registered node type, no ADR-0087 conversion surface and no stored shape names them, so there is nothing for `objectstack migrate meta` to rewrite -->
13+
14+
The synthesized record page (record page v4) is laid out by one rule, the width of the container it occupies. The page, the split pane, the overlay drawer and the maximized pane all follow it.
15+
16+
- **At 760 px and wider**, the details sit beside a right column 320 px wide. **At 1120 px and wider** the right column is 380 px, a details section takes up to three field columns (two otherwise), and the 动态 panel shows five entries (three otherwise).
17+
- **Below 760 px** (the split pane at 720 px, a phone) the page is ONE column: the details, then the right column's panels in their order.
18+
- The three-column tier, the tabbed tier and the 「相关 N」 rail are gone, and so are the resident timeline column, its ⊞ switch and the per-object default that decided it.
19+
- **动态** (formerly the timeline) is the first panel of the right column. It has the same header as a related panel: 「动态」 and the stream's count, and the header row opens the 动态 tab. Its body is a one-line composer (pressed, it opens the full composer, focused), the newest entries one line each, and 「查看全部 N 条」 when the stream holds more. The 动态 tab (`?related=timeline`) is the full stream: the 全部 · 活动 · 讨论 · 变更 chips, threaded replies, and "Load more" for the next page.
20+
- **⊟** (「右栏」, `⌘⇧,`) is one icon at the end of the record header's action row, drawn only where the tier has a right column. Off, the details take the row. It keeps the reader's earlier related-column choice: the stored preference `record.columns.related` is unchanged. The record header's `PanelToggles` (`@object-ui/components`) now draws icon-only toggles named by their label.
21+
- The 动态 panel is on screen at open in every tier, so a synthesized page reads its feed and its audit history at open. An authored page with tabs still waits for the tab that holds them.
22+
- **Tab labels** (`page:tabs`, `@object-ui/components`). A synthesized page that keeps its tab strip (one the layout cannot partition) labels its stream tab `Activity stream`, which the strip reads as 「动态」. An authored `Timeline` tab, such as the served approval page's history, is no longer read as the record stream: it keeps the pack's own word for a timeline (「时间线」, タイムライン, and so on).
23+
24+
**BREAKING** (`@object-ui/plugin-detail`, TypeScript only). The only importer is the console's own record view. `RecordPageTier` is now `'stack' | 'two-column' | 'wide'`: there is no tabbed tier, and the widest tier is called `wide`. `recordPageTierFor(width)` takes no second argument. `useRecordPageTier` answers `null` only while the page is not laid out (disabled, or not yet measured). `RecordPageColumnsProps.tier` takes every `RecordPageTier`. A caller still naming a retired symbol gets a compile error that names it.
25+
26+
What leaves, and what to write instead:
27+
28+
- FROM `RECORD_PAGE_THREE_COLUMN_MIN_WIDTH` TO `RECORD_PAGE_WIDE_MIN_WIDTH`, the same 1120 px floor under its new name.
29+
- FROM `recordPageArrangementFor` TO `recordPageTierHasRightColumn(tier)` and the reader's ⊟: every laid-out page is the details and, where the tier has room, the right column.
30+
- FROM `recordPageColumnDefaults` TO no call: every object's page opens with its right column, and ⊟ defaults to on.
31+
- FROM `recordTimelineDefaultReason` TO no call, for the same reason: no object decides a timeline column any more.
32+
- FROM `RecordPageColumnSwitches` TO a `boolean`, the right column's switch.
33+
- FROM `RecordPageArrangement` TO the `RecordPageTier` itself, read through `recordPageTierHasRightColumn`.
34+
- FROM `RecordPageObjectLike` TO no type: no helper takes an object definition any more.
35+
- FROM `RecordTimelineDefaultReason` TO no type, with `recordTimelineDefaultReason`.
36+
- FROM `DefaultPageColumns.relatedCount` TO the length of `columns.related`; the rail it labelled is gone.
37+
- FROM `UseRecordPageTierOptions.embedded` TO no option: an embedded page follows the same width rule.
38+
- FROM `RecordPageColumnsProps.switches` TO `RecordPageColumnsProps.rightColumn`, one `boolean`.
39+
40+
`RECORD_PAGE_WIDE_MIN_WIDTH`, `recordPageTierHasRightColumn` and `RECORD_PAGE_TIMELINE_PANEL` (the 动态 tab's `?related=` id) are new.

‎content/docs/ui/pages.mdx‎

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -180,33 +180,33 @@ replacement.
180180
### The synthesized default
181181

182182
A record page needs no authoring. With no page assigned, the Console synthesizes one from
183-
the object: a header, the highlights strip and a tab strip of **Details**, **Timeline** and
184-
**Related**, plus **Approvals** when the record has approval requests. A relationship whose
185-
child field declares `relatedList: 'primary'` is part of the record's body: its list is a
186-
full-width table inside **Details**, after the first field section, with no tab of its own.
187-
The other related lists are cards in the shared **Related** tab, which an object with
188-
`enable: { files: true }` closes with its attachments. **Timeline** is one stream: the
189-
discussion composer on top, then the discussion, the activity and, with
190-
`enable: { trackHistory: true }`, the record's field changes, filtered by All / Activity /
191-
Discussion / Changes; there is no separate History tab. Moving a child table into the
192-
record's body is therefore a one-word change on the relationship field, not a
193-
page-authoring task, and `relatedList: false` leaves a child off the page.
194-
195-
The Console lays the page out by the width of the surface it occupies (the drawer, the
196-
split pane or the full page), never by the viewport. From 1280 px the tabs become three
197-
columns: details, the timeline stream and the
198-
related lists as cards with Approvals, Attachments and the created/updated line. From
199-
1024 px the related column folds into a rail behind a **Related (N)** toggle. Below that, and
200-
always in a drawer or split pane, the tab strip is drawn. `?tab=` selects a tab in the strip.
201-
202-
In the column tiers the related column loads in steps, and the details column never waits for
203-
it. The page asks every card's count in one request set, sent beside the record read, and the
204-
cards show placeholders until those counts land. A card counted at 0 is its title row alone
205-
and reads no rows. A card above 0 reads its first three rows once it scrolls into view, asking
206-
only for the record's name field and one status or date field. When the timeline column is
207-
switched off, its **Activity** card in the related column stays closed, and reads nothing, until
208-
it is opened. In a drawer, the record that **↑** / **↓** would show next has its related counts
209-
read ahead.
183+
the object: a header, the highlights strip, the record's **details**, and a right column of
184+
panels: **动态** (Activity) first, then the related lists, **Approvals** when the record has
185+
approval requests, and the attachments of an object with `enable: { files: true }`. A
186+
relationship whose child field declares `relatedList: 'primary'` is part of the record's body:
187+
its list is a full-width table in the details, after the first field section, with no panel of
188+
its own. **动态** is one stream: the discussion, the activity and, with
189+
`enable: { trackHistory: true }`, the record's field changes; there is no separate History tab.
190+
Its panel shows a one-line composer and the newest entries, and its header opens the whole
191+
stream (`?related=timeline`), filtered by All / Activity / Discussion / Changes. A related
192+
panel's header opens that list's whole table the same way (`?related=<object>.<field>`).
193+
Moving a child table into the record's body is therefore a one-word change on the
194+
relationship field, not a page-authoring task, and `relatedList: false` leaves a child off the
195+
page.
196+
197+
The Console lays the page out by the width of the surface it occupies (the full page, the split
198+
pane or the drawer), never by the viewport, and by one rule. From 760 px the details sit beside
199+
the right column, 320 px wide; from 1120 px the right column is 380 px and a details section
200+
takes up to three field columns. Below 760 px (the split pane, a phone) the page is one column:
201+
the details, then the right column's panels in their order. The **⊟** switch at the end of the
202+
record header's actions (`⌘⇧,`) hides the right column for the reader, per object.
203+
204+
The related panels load in steps, and the details never wait for them. The page asks every
205+
panel's count in one request set, sent beside the record read, and the panels show
206+
placeholders until those counts land. A panel counted at 0 reads no rows, and two or more of
207+
them fold into one line. A panel above 0 reads its first three rows once it scrolls into view,
208+
asking only for the record's name field and one status or date field. In a drawer, the record
209+
that **↑** / **↓** would show next has its related counts read ahead.
210210

211211
### Slotted pages
212212

@@ -219,12 +219,12 @@ synthesizer fills in the rest. A slotted page authors `slots`, not `regions`:
219219
| `actions` | The action bar (`record:quick_actions`) |
220220
| `alerts` | The alert banners above the body |
221221
| `highlights` | The highlights strip |
222-
| `details` | The body of the Details tab; the other tabs stay synthesized |
222+
| `details` | The record's details; the rest of the page stays synthesized |
223223
| `discussion` | The discussion panel |
224224

225225
Each slot takes one component or an array of them, and replaces its part whole: there is no
226-
deep merge. There is no `tabs` slot: the tab strip is composed from the object's fields and
227-
relationship flags. To change which related lists appear or their order, set `relatedList` /
226+
deep merge. There is no `tabs` slot: the page's panels are composed from the object's fields
227+
and relationship flags. To change which related lists appear or their order, set `relatedList` /
228228
`relatedList: 'primary'` on the child's reference field; a page that needs its own strip is
229229
`kind: 'full'`, drawn as authored. `slots.tabs`, `slots.related` and `slots.relatedLists` are
230230
refused by name, and `os migrate meta --from 17` drops a stored `slots.tabs`.

0 commit comments

Comments
 (0)