Skip to content

feat(app-shell,components): on a phone the record is always a page, with a compact header, a bottom action bar and related records pushed as pages - #28477

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-pxe7fj
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-pxe7fj

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#5 · os-sales · session_01L6bhgT93xhnhe2yKWX7YPc

Fixes #27058

Clause-②: yes (widening) — @object-ui/components gains PageHeaderPhoneContext (+ type PageHeaderPhoneFace) and an optional PageHeaderActionsProps.face; MobileViewSwitcherContext is not on the @object-ui/app-shell entry, so its retirement narrows nothing.
Metadata-protocol: none

维护者速读

  • 改了什么:手机上(窗口宽度小于 768)记录永远是整页:列表点一行直接进记录页,不再弹全屏抽屉或分屏;列表的视图切换器「全部线索 ▾ · 5」挪到列表工具栏首位,顶部应用栏不再画第二个视图下拉;记录页顶栏只剩一个返回和计数 ↓ ↑,顶层页写「‹ 对象列表」,推入的下一层写父记录名(「‹ Acme Corp」);头部用对象声明的前两个文本高亮做副标题,后面最多 4 个高亮;业务操作放进页面底部的操作栏(主操作、一个次操作、⋯);关联行和查找值整页推入,顶栏下有路径行「线索 Marie Curie › 客户」,‹ 返回上一层并回到原来的滚动位置。
  • 为什么改:console mobile (≤ 640 px): list as card rows with view chips and a FAB, the record page with a compact header, sticky tabs and a bottom action bar, related records pushed as full pages with a path row and back — the record is always a page on a phone #27058(画布 60 的 RecordMobile 板):手机是 v4 记录页压到 390 px,再加三件只有手机才有的事:记录永远是整页、业务操作在底栏、关联记录整页推入带路径行。
  • 风险与回滚:只动 ui 包(app-shell、components),不碰 spec、不碰服务端;768 px 及以上一切照旧(每个新测试都带桌面对照)。components 新增一个宿主插槽和一个可选 prop,是 widening;退役的 MobileViewSwitcherContext 从未在包入口导出。回滚即 revert。
  • 先看:ui/packages/components/src/renderers/layout/containers.tsx 的 barQueue(底栏按钮怎么排、编辑何时进 ⋯);ui/packages/app-shell/src/views/RecordDetailView.tsx 的 phoneBack / phonePathRow 和 data-record-action-bar 容器;ui/packages/app-shell/src/views/objectViewRecordNavigation.ts 的 phone 分支(?recordId= 换成整页)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/components/src/__tests__/page-header-phone-bar-27058.test.tsx packages/app-shell/src/views/ObjectView.phoneRecordPage-27058.test.tsx packages/app-shell/src/views/RecordDetailView.phoneFace-27058.test.tsx packages/app-shell/src/views/__tests__/recordPagePush-27058.test.tsx packages/app-shell/src/layout/__tests__/AppHeader.phoneNoViewDropdown-27058.test.tsx,预期 5 个文件 31 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

Below 768 px (the useIsMobile breakpoint, read synchronously by the new hooks/usePhoneViewport.ts, because what it decides is mounted, not styled) the console is on a phone. What follows is the v4 record page (#27524) in a container under 760 px plus the three things only a phone adds. No metadata key, no reader preference, no packages/spec file.

Target 1: the record is always a page

Target 2: one view switcher

Target 3: the top bar

RecordDetailView's page bar on a phone is RecordTopBar with one way back on the left and the counter + ↓ ↑ on the right when the page knows its rows. No ⊟ (the stack has no right column) and no ⤡ (no drawer to collapse to). On a top-level page the way back reads 「‹ OBJECT_PLURAL」 and returns to the list the page came from, else the object's list. On a page one level down it is named by where it returns (Target 8, see Acceptance notes): the parent record's title from the ?from= trail (「‹ Acme Corp」), or its short id when the trail carries none; it links to the trail's parent, or goes back in the history on a page pushed in this session.

Target 4: the header

For a default (synthesized) page, views/recordPhoneHeader.ts takes the subtitle from the first two declared highlightFields whose type is text, joined by 「 · 」 from the values the reader can see, and keeps at most four highlights after them (PHONE_HIGHLIGHTS_MAX). The subtitle reaches page:header through the host slot below and is drawn only when the header declares no subtitle of its own. An object that declares no highlights gets the derived strip and no subtitle; an authored page draws its own header.

Target 7: the bottom action bar

  • @object-ui/components gains PageHeaderPhoneContext (renderers/layout/page-header-phone-context.ts, beside PageHeaderTrailingContext): a host slot, never metadata. The record page provides it on a phone with its bar element and the subtitle.
  • Under it PageHeaderRenderer draws no action row: the same PageHeaderActions resolver renders in its new face: 'bar' and is portaled (createPortal) into the host's bar, so the actions keep the record's contexts. Nothing is drawn until the bar element exists.
  • The bar face holds the primary action, one more and the ⋯ (PHONE_ACTION_BAR_BUTTONS = 2; a page's declared mobileMaxVisible still wins). Each button shares the bar's width; the host's Delete stays in the ⋯'s destructive band.
  • The bar is the page column's last flex row (border-t, pb-[max(env(safe-area-inset-bottom),0.75rem)]), under the scroller, so the body ends where the bar starts. There is no FAB on a record page (the FAB is the list's).

Target 8: one level down

  • RelatedRecordActionsBridge.openRecord on a phone navigates to the record route with the ?from= trail extended (as before) and the history state recordPagePushState builds (views/recordPagePush.ts): the parent page as from, the related list's rows as from.siblings, and a recordPagePushed marker. No drawer stack is mounted on a phone (RecordDetailView returns its page tree without RecordPageDrawerHost).
  • A path row under the top bar draws the trail (「线索 Marie Curie › 客户 Acme Corp › 商机」), each ancestor a link; on a pushed page the parent entry and 「‹ Acme Corp」 go back in the history (navigate(-1)).
  • useRecordPageScrollMemory remembers the page scroller's position per history entry and restores it when the reader comes back by Back / Forward, retrying while the panels load, for up to 3 s, until the reader touches the scroller.

Unchanged

768 px and up; the pane's split / overlay / maximize mechanics (#27545); navigation-overlay.tsx and recordDrawerStack.tsx (not mounted on a phone, not edited); the record page's layout rule (the stack below 760 px, the tab row's own width count, the enlarged tab's card rows: all already read the page's own width and needed no change); plugin-list (its toolbarLeading and recordOpen props already existed).

File surface beyond the claim amendment 6548379901

  • The phone context lives at ui/packages/components/src/renderers/layout/page-header-phone-context.ts, beside page-header-trailing-context.ts, not under custom/ as the amendment spelled it.
  • ui/packages/app-shell/src/views/viewTabLabel.ts and one comment in ObjectView.tsx: comment-only lines that named the retired mobile switcher.
  • Tests in ui/packages/plugin-detail and ui/packages/plugin-grid (pins only, see Tests).
  • Not needed and not touched: objectViewRecordLinks.ts, objectViewPageHeader.tsx, RecordPageColumns.tsx, synth/recordPageColumns.ts, RecordTabRow.tsx, RelatedList.tsx, record-top-bar.tsx, the i18n packs (no key added; the bar reuses detail.goBack, common.breadcrumb and the header's own keys).

Tests

New (31):

  • components/src/__tests__/page-header-phone-bar-27058.test.tsx (6): one declared action → [Convert Lead][Edit][⋯] in the host's bar and no row in the header; a declared second action takes slot 2 and Edit folds into the ⋯ after the page's folded action, before Delete; a declared mobileMaxVisible: 1 sets the budget; nothing is drawn before the bar element exists; the host subtitle stands in for an undeclared one and an authored one wins; control: without the context the row face, Edit inline.
  • app-shell/src/views/ObjectView.phoneRecordPage-27058.test.tsx (5): at 390 a row tap lands on the record route with from.pathname the list and from.siblings its rows; a ?recordId= URL is REPLACED by the record route; a stored split / drawer preference draws nothing and the Display menu has no Open records; control at 1440: the same ?recordId= opens the split pane.
  • app-shell/src/views/RecordDetailView.phoneFace-27058.test.tsx (10), a real RecordDetailView at 390: 「‹ Leads」 to the list, 1 / 3, no Panels group, no ⤡, no crumb; opened directly: 「‹ Leads」 and no counter; subtitle 「Atlas Construction · Director of Sales」 and four highlights; the bottom bar [Convert Lead][Assign][⋯] with Edit in the ⋯, below the body, the header with no toolbar; one declared action → [Convert Lead][Edit]…; a pushed page: path row 「Lead Mira CostaTask」, 「‹ Mira Costa」 a button, 1 / 3, back to the parent, whose own way back reads 「‹ Leads」 again (both texts pinned in one case); the path row's parent goes back too; a ?from= page from a link: 「‹ Mira Costa」 and the path row both link to the parent; a trail entry with no title: 「‹ #L-1」; control at 1440: ⊟, the header's own row, no bar.
  • app-shell/src/views/__tests__/recordPagePush-27058.test.tsx (9): the bridge at 390 pushes a related row with the extended trail and { from: { pathname, label, siblings }, recordPagePushed: true }, a lookup without siblings; control at 1440: no state; the state helpers; Back restores 286 and a fresh visit starts at 0; phoneRecordHeader's rule (text only, two at most, trimmed, masked values skipped, array-form fields, no declared highlights → no subtitle).
  • app-shell/src/layout/__tests__/AppHeader.phoneNoViewDropdown-27058.test.tsx (1): at 390 on a two-view list, no 「Switch view」 trigger, the page's name only.

Pins updated to the phone face, not deleted:

Ablations (each through scripts/ablation-replace.mjs, restore proven: blob == HEAD and git diff HEAD empty)

At head 1511473e38:

  • PHONE_BELOW_WIDTH = 768 → 0 (the phone signal off): 15 failed | 22 passed over the four phone files + the tiers pin + the 27594 pin. Every phone case red; every control and every unit case green. The 27594 pin stays green there by design: the toolbar title follows the header breakpoint, ablated next.
  • Four held together, one run: the bar queue with the host's buttons before the page's own (containers.tsx), the subtitle reading no field (recordPhoneHeader.ts), phoneTitle never set (ObjectView.tsx), the scroll restore never armed (recordPagePush.ts): 10 failed | 35 passed. Red: the Edit-folds case (components) and the bottom-bar case (record page); the three subtitle unit cases and the header case; the three phone switcher pins (27594, 27950, 13114); the Back-restores case.

At head f33dd4f88c:

  • The way back one level down named by the object plural again (RecordDetailView.tsx phoneBack: ? parent.t || shortRecordId(parent.i) → ? plural): 3 failed | 7 passed in RecordDetailView.phoneFace-27058. Red: the pushed page (「‹ Mira Costa」), the ?from= link page, the untitled trail entry (「‹ #L-1」); the two top-level 「‹ Leads」 cases stay green.

Acceptance notes

Screenshots at 390 × 844 (Chromium mobile emulation, zh-CN) on examples/app-crm, run from this branch's own source and server. They are committed in 08421f1c82 and removed in the next commit, so they never reach main (an earlier set, 74dd2d29a6, predates the way-back label change and is superseded). The folder's README says how they were made; run.log records each shot's route, the bar's box, the top bar's way back as drawn and the scroll readings; evidence.cjs is the script.

Shot What it shows
01-lead-list The lead list: 「全部线索 ▾ · 5」 leads the toolbar row with the filter and display icons; card rows; the app bar draws no view dropdown.
02-lead-record A row tapped, a top-level page: 「‹ 线索列表」, 5 / 5, ↓ ↑; name and object chip; four highlights in two columns; the stage path; the tab row; the details; the bar [Convert Lead][编辑][⋯].
03-lead-record-mid-scroll Scrolled to the middle of the body (scrollTop 106 of 212): the bar stays at the bottom edge (y 779 + h 65 = 844).
04-lead-record-end The end of the stack: the details, then the 动态 panel; the bar under them.
05-lead-to-account-pushed The lead's Account lookup tapped: Acme Corp pushed as a page, one level down: 「‹ Marie Curie」 and the path row 「线索 Marie Curie › 客户」.
06-account-related-panels That page scrolled to its related panels, in their order, with the bar under them.
07-account-to-opportunity-pushed A related opportunity row tapped: one more level, 「‹ Acme Corp」, 「线索 Marie Curie › 客户 Acme Corp › 商机」, 1 / 3 over the account's shown opportunity rows; the tab row 「概览 · 动态 0 · 活动列表 0 · 线索列表 0 · +1 ▾」.
08-back-to-account 「‹ Acme Corp」: the account where it was left, scrollTop 315 → 315, its own 「‹ Marie Curie」.
09-back-to-lead 「‹ Marie Curie」: the lead where it was left, 212 → 212, the top-level 「‹ 线索列表」 again.
10-account-record An account from its list: 「‹ 客户列表」 2 / 3; the tab row 「概览 · 动态 0 · 活动列表 2 · 联系人列表 1 · +2 ▾」 at 390 px; the bar [编辑][⋯] (the account declares no action).
11-account-related-tab-active 活动列表 active (?related=crm_activity.account): the child object's card rows fill the body under the enlarged toolbar.
12-lead-bar-more The bar's ⋯: 刷新, 添加到收藏, 分享, then 删除 in its own band.
13-lead-header-declared-highlights Rig only. app-crm declares no highlightFields, so this shot patches the served lead metadata in the browser (page.route; nothing in the diff) with company, title, email, phone, source, lead_score: the subtitle 「Radium Labs · Director of Research」, then four highlights.

The two points the seat asked to see stated:

  • The app bar (AppHeader) is kept on the phone record page. The board's frame starts at the record's top bar, but on a phone AppHeader is the only door to the app menu (the sidebar trigger), search, notifications and the account menu, on every route, the record page included. The card names no other place for them, so retiring the bar on record routes would leave a reader on a record page with no way to another app or to search. What this PR removes from the bar is only the second view dropdown (Target 2). If the preview gate wants the record page without it, that is a separate decision about where those four doors go on a phone.
  • Edit and the bar's second slot. In the bar face the host's Edit is one of the record's actions: it queues after the page's own actions (barQueue: the primary, then the authored actions in their order, then the host's non-destructive buttons). So a page that declares ONE action shows [that action][编辑][⋯] (shot 02, Convert Lead); a page that declares a second action shows [primary][second][⋯] and 编辑 is the first host entry in the ⋯, after the page's own folded actions (pinned in page-header-phone-bar-27058 and RecordDetailView.phoneFace-27058); a page that declares none shows [编辑][⋯] (shot 10). On the desktop row nothing changes: the host's buttons stay inline beside the authored ones.

Readings and deviations (the first three agreed with the seat before the build):

  • No avatar initials in the phone header (hotcrm#1199, ruling A). An object with a declared picture field still shows its picture through the header's existing chip.
  • The subtitle comes only from declared text highlightFields on a default page; no declared list means no subtitle (app-crm declares none: shots 02 and 13).
  • The bar is a flex row at the foot of the page column, not position: fixed with a scroll-pb. It stays visible while the body scrolls (shot 03, bar box in run.log for every record shot), and the body ends where the bar starts, which is what the scroll-pb was for.
  • The ‹ label: Target 3 governs the top level, Target 8 names the next level (the seat's reading). Target 3's 「‹ OBJECT_PLURAL」 is the way back of a top-level page, which returns to the object's list. Target 8 says a page one level down returns to its parent record, so its 「‹」 is named by that return target: the parent's title from the ?from= trail (「‹ Acme Corp」, as the board's pushed frame reads), else its short id. A pushed page and a ?from= page opened from a link read the same; back on the parent, its own 「‹」 reads the object plural again. Both texts are pinned in one case of RecordDetailView.phoneFace-27058.
  • The card's lead → task shots. app-crm has no task object, so the push is shown as lead → account (a lookup) → opportunity (a related row), and the 任务 tab as the account's 活动列表 tab.
  • 640–767 px (a large phone in landscape, a small tablet): the record is a page (below 768), and the page header with its title switcher is drawn (from sm, 640), as at that width before.
  • Not built here: the counter's group scope, as in Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 ④ (ListView reports rows only for the ungrouped grid).

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec is not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint on the changed files: 0 errors.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the project's Chinese summary).
  • The Before Submitting list in CONTRIBUTING.md is done.

Verification

All local readings are at head 15455616ba unless named. Its tree is identical to f33dd4f88c (git diff f33dd4f88c 15455616ba is empty: the two commits after it add and remove only evidence/), so a reading taken at f33dd4f88c is a reading of this tree. Against the merge base 8e2d56dcce the change is 31 paths, +2,042 / −348; the evidence folders are added and removed inside the range, so they are not among them. git merge-tree against origin/main (11 commits ahead of the base) is clean.

Tests: 643 files, 4624 tests green over every touched surface; tsc 0; eslint 0 errors
  • The touched surfaces, one run under the verify lock: pnpm --dir ui exec vitest run --maxWorkers=3 packages/components/src/__tests__/page-header packages/components/src/__tests__/snapshot-critical.test.tsx packages/app-shell/src/views/ObjectView. packages/app-shell/src/views/RecordDetailView. packages/app-shell/src/views/__tests__ packages/app-shell/src/layout packages/app-shell/src/console packages/plugin-detail/src/__tests__/RecordTabRow packages/plugin-grid/src/__tests__/groupedMobileCards
    • Test Files 643 passed (643) · Tests 4624 passed (4624) · VERDICT command-exit 0 (at 1511473e38 the same set read 643 / 4623; the +1 is the untitled-trail case).
  • The five phone files plus the tiers pin, at f33dd4f88c: Test Files 5 passed (5) · Tests 36 passed (36).
  • npx tsc --noEmit -p tsconfig.json in ui/packages/app-shell at f33dd4f88c: exit 0 (ui/packages/components, unchanged since 1511473e38: exit 0 there).
  • eslint on every changed source and test file: 0 errors (warnings are the files' existing no-explicit-any).
  • The 34 derived gates include pnpm --dir ui exec vitest run scripts/: Test Files 169 passed | 2 skipped (171) · Tests 4901 passed | 2 skipped (4903).
Derived gate families: 34 derived, 34 run, every exit 0, 0 NOT-MEASURED (at f33dd4f, this tree)
  • 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:pm-changeset-deadline-census: exit 0
  • pnpm check:published-files: exit 0
  • pnpm check:query-options-erasure: exit 0
  • pnpm check:refd-timer-probe: 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

node scripts/pm/dispatch-gates.mjs --ran over that record: 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED, 0 UNRUN.

CI check-runs: not started at the time of writing.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgT93xhnhe2yKWX7YPc

…h a bottom action bar and pushed related pages

Below 768 px the object list opens a record as a page (deriveRecordSurface
with viewport 'mobile'): no split pane or drawer is mounted, a ?recordId=
link is replaced by the record page, and the Display menu drops Open records.
The list's title switcher leads its toolbar row below sm, and the app bar's
second view dropdown (MobileViewSwitcherContext) retires.

The record page on a phone: a top bar with one way back and the counter,
a header whose subtitle is the first two declared text highlights with at
most four highlights after them, and the header's actions drawn into a
bottom bar (PageHeaderPhoneContext, PageHeaderActions face 'bar': primary,
one more, the overflow menu). A related row or a lookup pushes its record as
a page with a path row; going back restores the parent's scroll position.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgT93xhnhe2yKWX7YPc
… parent record

On a phone the top bar's way back of a top-level record page still reads the
object plural; a page under a `?from=` trail (pushed from a related row or a
lookup, or opened from a link) is named by where it returns, the parent
record's title, or its short id when the trail carries none.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgT93xhnhe2yKWX7YPc
… way-back label change (removed in the next commit)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgT93xhnhe2yKWX7YPc
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling labels Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 15455616ba699b833015fa16917ecf0a21a27045
Local-runs: none
Scope: ui/packages/components/src/renderers/layout/page-header-phone-context.ts, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/components/src/index.ts + ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/app-shell/src/layout/MobileViewSwitcherContext.tsx

① Derived judgments

The face is the Clause-②: yes (widening) declaration alone: no packages/spec/src/** file and no governed path in the diff. What widens: @object-ui/components exports PageHeaderPhoneContext and the type PageHeaderPhoneFace (a host slot, never metadata: no page:header key reads it), and PageHeaderActionsProps gains an optional face: 'row' | 'bar' whose default row is the old behaviour. What retires is MobileViewSwitcherContext, an app-shell file that was never on the @object-ui/app-shell entry, so no public name narrows. No authored metadata key, stored shape, i18n key or server path moves; the phone reads the spec's own deriveRecordSurface(def, { viewport: 'mobile' }).

② Semver level

patch for both packages, as the changeset declares: one new export pair and one optional prop; a host that provides no phone context draws the header's action row exactly as before (pinned by the control case in page-header-phone-bar-27058), and every behaviour change is gated below 768 px.

③ Boundary flags

On a phone the record page mounts no RecordPageDrawerHost and the list mounts no ObjectViewRecordOverlay; a ?recordId= link is replaced (not pushed) by the record route, with ?recordId= stripped from the page's origin. The bar face queues the host's Edit after the page's own actions, so it folds into ⋯ when two authored actions fill the bar; the host's Delete stays in the destructive band. Scroll memory is an in-module map keyed by history entry, bounded at 50, and stops restoring at the reader's first touch or wheel or after 3 s. navigation-overlay.tsx and recordDrawerStack.tsx are not edited.

Implemented-by: session_01L6bhgT93xhnhe2yKWX7YPc
Reviewed-by: session_01L6bhgUhKx1XHzkoikGjRB4

VERDICT: PASS


Generated by Claude Code

…ss literal; pass phone to the list-rows test

The record-header width e2e reads the page header's action tail as the last
class literal before the toolbar role, so the row face and the phone bar face
each draw their own toolbar element, the row's first, with the row's literal
as on main. The list-rows navigation test passes the hook's new `phone` flag.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgT93xhnhe2yKWX7YPc
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

Contract review

Served-tier: TIER_DEFAULT
Head-sha: af3acee1f73a4f132ec80ff4e520f93a60756b42
Local-runs: none
Scope: ui/packages/components/src/renderers/layout/page-header-phone-context.ts, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/components/src/index.ts + ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/app-shell/src/layout/MobileViewSwitcherContext.tsx

① Derived judgments

The face is the Clause-②: yes (widening) declaration alone: no packages/spec/src/** file and no governed path in the diff. What widens: @object-ui/components exports PageHeaderPhoneContext and the type PageHeaderPhoneFace (a host slot, never metadata: no page:header key reads it), and PageHeaderActionsProps gains an optional face: 'row' | 'bar' whose default row is the old behaviour. What retires is MobileViewSwitcherContext, an app-shell file that was never on the @object-ui/app-shell entry, so no public name narrows. No authored metadata key, stored shape, i18n key or server path moves; the phone reads the spec's own deriveRecordSurface(def, { viewport: 'mobile' }).

② Semver level

patch for both packages, as the changeset declares: one new export pair and one optional prop; a host that provides no phone context draws the header's action row exactly as before (pinned by the control case in page-header-phone-bar-27058), and every behaviour change is gated below 768 px.

③ Boundary flags

On a phone the record page mounts no RecordPageDrawerHost and the list mounts no ObjectViewRecordOverlay; a ?recordId= link is replaced (not pushed) by the record route, with ?recordId= stripped from the page's origin. The bar face queues the host's Edit after the page's own actions, so it folds into ⋯ when two authored actions fill the bar; the host's Delete stays in the destructive band. Scroll memory is an in-module map keyed by history entry, bounded at 50, and stops restoring at the reader's first touch or wheel or after 3 s. navigation-overlay.tsx and recordDrawerStack.tsx are not edited. Re-recorded on this head: af3acee1f7 over 15455616ba only splits the header toolbar into one div per face (the row face keeps main's class literal, which ui/e2e/record-header-title-width.spec.ts reads) and passes phone: false in one existing test; no contract change.

Implemented-by: session_01L6bhgT93xhnhe2yKWX7YPc
Reviewed-by: session_01L6bhgUhKx1XHzkoikGjRB4

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

ACCEPT · 2026-10-31T22:42Z
Head-sha: af3acee

席位核对

  1. Shape: 32 files (+2069 / −353), Fixes #27058: below 768 px the record is always a page (deriveRecordSurface(def, { viewport: 'mobile' }); no pane, no drawer stack, ?recordId= replaced by the record route); one view switcher (the Console lists: the list-view switcher becomes a dropdown on the list title (search, pinned default, recent), the flat view-tab row retires, and a starred view appears in the sidebar's 收藏 group #27594 title leads the list toolbar row; AppHeader's dropdown and MobileViewSwitcherContext retire); the phone top bar 「‹ 对象列表」 / 「‹ 父记录名」 with n / N ↓ ↑; the header subtitle from the first two declared text highlights plus at most four highlights; the bottom action bar (PageHeaderPhoneContext, the header's PageHeaderActions in its bar face, portaled); related rows and lookups pushed as pages with the path row and scroll restore. Inside claim 6547925555 as amended by 6548379901; the file surface beyond it is named in the PR body (the context file beside page-header-trailing-context.ts, comment-only lines, pins in plugin-detail / plugin-grid). packages/spec untouched. Not governed, 2422 lines ≤ 3000 (check-governed-merges --pr 28477).
  2. Contract: Clause-②: yes (widening) (PageHeaderPhoneContext + PageHeaderPhoneFace exported, optional PageHeaderActionsProps.face; the retired context was never on a package entry); TIER_DEFAULT review record by this seat, PASS on this head (6549952200; the first record 6549766372 was on 15455616ba, before the two CI fixes).
  3. Preview gate (ruling 6520722066): the seat read the 13 app-crm shots at 390 × 844 in 08421f1c82 against RecordMobile.dc.html at a7825499aa (three frames: list · record page · one level down), through the card's Targets. Matches: the list's title switcher 「全部线索 ▾ · 5」 in the toolbar row and no second dropdown (01); the record page's one way back with 5 / 5 ↓ ↑, name + object chip, highlights in two columns, the stage bar, the tab row, the < 760 stack, then 动态 (02, 04); the bottom bar [Convert Lead][编辑][⋯] that stays at the bottom edge while the body scrolls (03, y 779 + h 65 = 844); one level down with the path row 「线索 Marie Curie › 客户 Acme Corp › 商机」 and 「‹ Acme Corp」, 1 / 3 over the parent's related rows (07), and ‹ returning to the scroll position it left (08 315 → 315, 09 212 → 212); the enlarged related tab with the child's card rows (11); the bar's ⋯ with 删除 in its own band (12); the subtitle from declared text highlights (13, rig-patched metadata). Differences, accepted below or owed as follow-ups (item 6).
  4. Gates (check-runs on this head): 55 runs, 44 success, 11 skipped by path, 0 other; Test UI 16/16, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Check Changeset success. The first head 15455616ba was red on Build & E2E UI (the record-header width source scan lost the toolbar's row class literal) and Type Check UI (an existing test lacked the new phone argument); af3acee1f7 fixes both, and the dev reproduced each red on the old file and proved the new pin with an ablation. The dev's battery: 31 new tests plus moved pins (Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524, Console lists: the list-view switcher becomes a dropdown on the list title (search, pinned default, recent), the flat view-tab row retires, and a starred view appears in the sidebar's 收藏 group #27594, On a phone, a grouped list (grouping) keeps its desktop grid — titles cut mid-word, other columns behind the pinned Actions column — while the same console turns a plain list into cards #26896, Console object list: with the view title as the switcher, nothing on screen names the object once the sidebar is collapsed or the list is opened from a link — restore the object crumb (#27594 premise) #27950, fix(metadata-core,service-cluster): stop emitting and publishing the CJS half of ./testing #13114) updated not deleted; ablations (the phone signal off: every phone case red, every control green; four held together: 10 red; the ‹ label: 3 red), each restored; dispatch-gates --commands 34/34, 0 NOT MEASURED.
  5. Deviations accepted: no avatar initials (hotcrm#1199 ruling A); the bar is the page column's last flex row rather than position: fixed + scroll-pb (same reading: visible while scrolling, body ends at the bar); the ‹ of a page one level down is named by its parent record (seat reading of Target 3 vs 8); the push shown as lead → account → opportunity (app-crm has no task object); the host's Edit queues after the page's authored actions in the bar; the counter has no group scope (as Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 ④, Record pane: on a grouped list or a kanban board the counter and ↓ ↑ are missing — step within the row's group or lane and name it in the tooltip #28461).
  6. Follow-ups owed by this seat (filed after this lands): (a) the phone record page keeps AppHeader above the record's top bar, so the page has two bars where the board has one, and the app bar's mobile label repeats the record name the header draws directly below (Target 3: 「the record name … is not repeated」; the label is main's, not this PR's); where the app menu, search, notifications and account go on a phone record page is a product question, put to the maintainer on that card. (b) The phone list's card rows (01) draw every shown column as a label / value line, where Target 2 and the board's list frame have the name, one subtitle line from the first two shown columns and one status chip (plugin-grid, outside this card's surface); the enlarged related tab's cards (11) sit inset from the body width.
  7. Changeset, read sentence by sentence: patch for @object-ui/app-shell and @object-ui/components; each paragraph is true against the diff.
  8. Landing: pr_ready + automerge_enable through the relay (Sales: seats land what they ACCEPT under pm-dispatch), MERGED confirmed; console mobile (≤ 640 px): list as card rows with view chips and a FAB, the record page with a compact header, sticky tabs and a bottom action bar, related records pushed as full pages with a path row and back — the record is always a page on a phone #27058 then closes by Fixes.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 22:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 22:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit ee29ac3 Oct 31, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-pxe7fj branch October 31, 2026 22:59
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 tooling

Projects

None yet

1 participant