Skip to content

feat(plugin-detail,app-shell): the default record page is one page at two widths — resident columns on a wide page, the tabs in a drawer - #26758

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-26678-record-page-two-widths
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-26678-record-page-two-widths

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #26678

Clause-②: no
Metadata-protocol: none

Part of the record-page-v2 epic #26676 (child 2 of 4). The drawer top bar and in-place navigation (#26679) and the open-mode metadata (#26677) are not addressed here.

What this does

The maintainer's ruling 1 on the epic, verbatim: 「抽屉和整页是同一张页在两个宽度。」 The synthesized default record page (no authored page, or a slotted page that keeps the default tab strip) is now laid out by the width of the view it occupies, never the viewport:

Container width Layout
1280 px and up Three resident columns, no tab strip: details · timeline (discussion composer on top, then Activity, History) · related (each related list as a card, then the opt-in reference rail and rightRail slot, Approvals, Attachments, the created / updated footer)
1024 – 1279 px Details · timeline; the related column folds into a rail opened from a Related (N) toggle above the columns (closed, its lists read nothing)
below 1024 px, and always when embedded (drawer / split pane) The tab strip, exactly as before

One header (title, actions, highlights, stage path) serves every tier. ?tab= keeps working in the tabbed tier and is ignored by the columns. An authored record page (kind: 'full') and an authored slots.tabs strip are drawn as authored at every width.

Design: the columns are derived, never authored

No metadata key, page template or node type is added, and packages/spec is untouched. The page description stays the tabbed one buildDefaultPageSchema writes, which is also what Studio's page-create seeds and what a slotted page fills. A columns node would have been a new type inside the spec's closed record: / page: namespaces: refused at Studio's save, and a type an author or an AI could write that the spec could not judge. So the column layout is read off the synthesized page by each tab's stable value (details → details; activity, history → timeline after the discussion footer; every related tab, approvals, attachments → related; the aside region folds into the related column, one rail, not two). A strip the partition cannot read (an unknown value, a visibleWhen item) answers null and keeps its tabs. A test enumerates every value buildDefaultTabs emits under all three relatedLayouts, so a new synthesized tab without a column goes red there.

showReferenceRail / hideReferenceRail are BuildPageOptions (TypeScript options of the synthesizer), not spec keys, and their meaning is unchanged: in the tabbed tier the rail is today's hidden xl:flex aside; in the column tiers the same aside content sits in the related column.

Files

  • ui/packages/plugin-detail/src/synth/recordPageColumns.ts (new): recordPageTierFor, recordPageColumnOfTab, buildDefaultPageColumns, the 1280 / 1024 floors.
  • ui/packages/plugin-detail/src/useRecordPageTier.ts (new): measures the container (layout effect plus ResizeObserver), re-renders only on a tier flip, answers null until the first reading so the host never mounts the tab tree only to replace it.
  • ui/packages/plugin-detail/src/RecordPageColumns.tsx (new): the column tiers; frame mirrors the tabbed page frame so the header does not move between tiers; the two-column rail toggle (aria-expanded, aria-controls, Escape closes back to the toggle); folded empty related lists collapse out of the column's gap.
  • ui/packages/plugin-detail/src/relatedListCompact.ts (new) + RelatedList.tsx: inside the related column a list keeps its identity column only and lets its header wrap. Without it the 20rem column cut each table off at its edge and truncated titles to one letter. Anywhere else a related list is unchanged.
  • ui/packages/app-shell/src/views/RecordDetailView.tsx: measures its own root, renders RecordPageColumns in the column tiers (tabbed path untouched), moves the provenance footer into the columns there, and treats History and the feed as on screen at open in a column tier (the audit read no longer waits for a tab click that cannot happen).
  • ui/packages/plugin-detail/src/index.tsx, useDetailTranslation.ts (detail.related default, byte-identical to the en pack).
  • Docs: ui/packages/plugin-detail/README.md (new section, reference-rail condition 3 note), ui/content/docs/guide/slotted-pages.md.
  • ui/e2e/live/detail-related-list.spec.ts, ui/e2e/live/record-history-display.spec.ts: both click a tab on a full record page under Desktop Chrome's 1280 px viewport, which leaves the record view at the two-column floor once the console sidebar takes its share; each now pins a 1200 px viewport to stay in the tabbed tier they test.
  • .changeset/26678-record-page-two-widths.md (patch, Metadata-protocol: none).

Screenshots

HotCRM (1f3f8880, platform 18.2.0 backend) with this branch's console on a Vite dev server. Premise correction: a HotCRM lead renders its authored lead_detail page (kind: 'full', isDefault: true), which the ruling leaves untouched, so it cannot show the tiers. The shots use a contact and a task, objects with no authored record page. Container width = the record view's measured width; the console sidebar takes 256 px of the viewport.

Contact, container 1440 px: three columns three columns
Contact, container 1100 px: two columns two columns
Contact, container 1100 px: related rail open rail open
Contact, container 844 px: tabs tabs
Task in the drawer (Task Board, embedded): tabbed tier drawer
The same task as a full page, container 1440 px: same header, same sections task page

Evidence branch claude/issue-26678-evidence-20261027 at 5c2f4343db70bd213abef0895804cb66f02df5d5 (never merged).

Reading worth a look: with the console sidebar open, a 1440 px screen gives the record view 1184 px, so it lands in the two-column tier. Three columns need about 1536 px of screen with the sidebar open, or a collapsed sidebar. The floors are the card's (1280 / 1024 of container).

Tests

Readings at 177cf60f6f (branch head after merging origin/main 4373103165, an unrelated auth change); the README snippet fix ecfa2c5574 re-ran pnpm --dir ui run check:doc-snippets (803 of 803 blocks judged, 0 failed), the derived 33 families (all exit 0) and check:new-line-citations (0 new) at ecfa2c5574 — this line edited by the seat on the dev's report, the body is otherwise the dev's:

  • pnpm --dir ui exec vitest run packages/plugin-detail/src/synth/__tests__/recordPageColumns.test.ts packages/plugin-detail/src/__tests__/RecordPageColumns.test.tsx packages/plugin-detail/src/__tests__/RelatedList.compactColumn-26678.test.tsx packages/app-shell/src/views/RecordDetailView.recordPageTiers-26678.test.tsx: 4 files, 25 tests passed.
  • RecordDetailView.recordPageTiers-26678: 1440 px is three columns, no tablist, related lists in the related column, the sys_audit_log read at open; 1100 px is two columns, the toggle, lists mounted only on open; 900 px and embedded at 1440 px are the tab strip, with no audit read at 900; ?tab=related selects the tab at 900 and is ignored at 1440.
  • Full packages/plugin-detail/: 316 files passed, 1 skipped; 2917 tests passed, 18 skipped.
  • packages/app-shell/src/views/RecordDetailView.* (72 files, 489 tests) plus the 121 other app-shell test files that import the record view, plugin-detail or DETAIL_DEFAULT_TRANSLATIONS (1312 tests): all passed. Declared narrowing: the remaining app-shell files are CI's Test UI.
  • pnpm --filter @object-ui/plugin-detail run type-check, pnpm --filter @object-ui/app-shell run type-check: exit 0 (on the pre-merge head; the merge touched only ui/packages/auth).
  • Existing tab tests (?tab=, related-tab order, hideRelatedTab, referenceRailRoute-9816) pass unchanged: jsdom measures 0 px, which is the tabbed tier.

Ablations (one-shot, through scripts/ablation-replace.mjs, restore proven by blob equal to HEAD and empty git diff HEAD; the tests resolve @object-ui/* to source through ui/vitest.config.mts, so no dist was involved):

  1. recordPageTierFor answering tabs at 1280 px and up: 3 tests red (the tier boundary, the 1440 px mount, the ?tab= case). Restored.
  2. The column-tier clause of the History read gate removed: the 1440 px "History is read at open" test red. Restored.

Local gate subset (derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack over this branch's 17 changed paths): all 33 node / pnpm check:* families exit 0. pnpm check:new-line-citations (ui): 0 new citations. ESLint on every changed ui file: 0 errors. The 34th family, pnpm --dir ui exec vitest run scripts/, exits 1 here for two host reasons, neither a reading of this change, so it is NOT MEASURED locally and CI's run is the reading: select-affected-tests-wiring (2 tests) runs a bash script using mapfile, which macOS's stock bash 3.2 lacks (exit 127); check-i18n-dead-keys (2 tests: "POSITIVE: every property-access-read key names LoadingScreen.tsx as a hit", "keeps every key it makes reachable OUT of both tiers") timed out at the 15 s budget scanning the real tree on this machine (16 s and 656 s). The other 176 files and 5062 tests pass. detail.related, the one key this change reads anew, was already read by the tab strip's label map.

Verification ran in declared unlocked mode: os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable flock on this host, so the shared verify lock was NEVER taken and NOTHING was serialized.

Acceptance notes

  • The two-column toggle sits on its own row, right-aligned directly under the record header, not inside the page:header action row: putting it there would need a host slot in @object-ui/components' header.
  • Whether the rail is open is ephemeral view state (closed on every open), not a stored preference.
  • The related card's footer link reads "View All" (existing detail.viewAll text), not 「查看全部 N 条 →」; the count is in the card header.
  • At 20rem the compact card's row-actions column header is clipped at the card edge (the data-table's own minimum widths). Cosmetic, left as is.
  • In the three-column tier every related list is mounted at open, so each issues its read at open (the tab strip deferred them to the tab click). The two-column rail keeps them deferred until opened.
  • The two live specs above were not run (no showcase backend in this container). NOT MEASURED: live e2e, reason: no running showcase backend.
  • DetailTabs.tsx (named in the card) draws the legacy DetailView strip; the synthesized page's strip is page:tabs (PageTabsRenderer in @object-ui/components), so it is not part of this change.

Generated by Claude Code

hotlong and others added 5 commits October 27, 2026 21:12
…n tiers by container width

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…partition and the related rail

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…ds and collapses folded lists

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…d-pages guide; changeset; live specs pin the tabbed tier

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…les as a complete host component

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · PR #26758 ← #26678 · epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff · 2026-10-27T15:37Z

Verdict: ACCEPT — ordinary queue landing (governed register: 0 of 17 paths; +1302 / −16, under the 3000-line human-merge threshold).

Checklist, judged on GitHub and on the branch, not on the report:

  • Shape: draft → main; first line Fixes #26678; one closing-keyword hit in the whole body, none beside epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676 / spec(+ui): navigation.mode keeps page | drawer | new_window | none — modal, split and popover are retired; the default becomes drawer in the schema and in the console (no field-count guess); list-page recordAction drops modal; the console's split pane becomes a per-user preference read #26677 / console record drawer v2: the bar is ↑ ↓ · ⤢ · ✕ (no object name, no counter); a related row or a lookup opens its record in place with a trail, ← / Esc go back one level; ⤢ carries the trail into the page; on the record page a related row opens a drawer over the page #26679; Clause-②: no and Metadata-protocol: none line-initial in the body and in .changeset/26678-record-page-two-widths.md (patch for @object-ui/plugin-detail and @object-ui/app-shell — AGENTS.md step 3 makes none a patch; the card's "minor if an export is added" yields to that).
  • Scope: 17 files; the surface grew past the claim (RelatedList.tsx, relatedListCompact.ts, two ui/e2e/live specs, README, slotted-pages guide) and the report says why for each — the 20 rem related column breaks the related tables without the compact context, the e2e specs pin a viewport that now lands on the two-column floor; accepted as the same change, not an adjacent fix. No content/docs/releases/, no packages/spec touched (the ⛔ in the claim held).
  • Tests: 25 new tests in 4 files; plugin-detail 2917 passed; the 72 app-shell record-view files 489 passed; type-check exit 0; two ablations through ablation-replace.mjs with the restore proven by blob hash equal to HEAD (3 red / 1 red, then green). Positive control present.
  • Gates: 34 derived, 34 run, 0 unrun at the patch head; pnpm --dir ui exec vitest run scripts/ NOT MEASURED locally (host bash 3.2, scan timeouts) and owned by CI. Pinned to head ecfa2c5574: Lint UI, Type Check UI, every Lint · / Type Check · lane, Test Core, Test UI 16/16, Build & E2E UI, Check Changeset — all success at the time of this comment. The first head's Lint UI red (README block at README.md:832 not compiling under check:doc-snippets) was one patch round; the fix is a complete host component (read the diff), 803 of 803 blocks judged.
  • Spot checks (seat readings): RecordDetailView.tsx measures its own root (data-record-page-root), excludes embedded, never measures an authored full page or an authored slots.tabs, opens the feed / history read gates in the column tiers, and moves the meta footer into the columns; recordPageColumns.ts partitions by each tab's stable value (details / activity+history → timeline / related:*+approvals+attachments → related, the opt-in reference rail folded in); useRecordPageTier.ts reads getBoundingClientRect().width under a ResizeObserver with the 1280 / 1024 floors as exported constants; RecordPageColumns.tsx grid minmax(0,1fr) minmax(0,1.1fr) 20rem, the two-column rail behind a toggle.
  • Browser: this seat ran the PR head's console (Vite 5180 → HotCRM 1f3f8880 on 4001): a contact record at a 1361 px view draws three resident columns; the dev's screenshots at 1696 / 1356 (+rail open) / 1100 / drawer are on the never-merged branch claude/issue-26678-evidence-20261027 @ 5c2f4343.
  • Open question (tier floors measured on the record view, so a 1440 px screen with the sidebar open lands on two columns): PM discretion A — keep 1280 / 1024; at 1184 px three columns are cramped and the two-column tier already pairs details with the timeline. The maintainer has the preview and can override; B is a one-constant follow-up card if asked.
  • Acceptance notes (three, no carrier): agreed not filed — View All is the existing detail.viewAll key; the clipped row-actions header at 20 rem is cosmetic; the toggle row placement needs a header slot that does not exist.
  • Deviations recorded: the PR was opened through with-fleet.sh -- gh api because the relay refuses this desktop session id shape; verify lock in declared-unlocked mode (disclosed in the body); the body's Tests line was moved to the patch head by the seat, as os-dev.md reserves body edits to the seat.

Landing: ready + auto-merge follow as the seat's next stroke; the queue decides the rest.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 22:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 22:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 0d07de2 Oct 27, 2026
60 of 61 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-26678-record-page-two-widths branch October 27, 2026 23:08
This was referenced Oct 27, 2026
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/xl tests tooling

Projects

None yet

2 participants