Skip to content

feat(ui): record pane v4 mechanics — split beside the list, non-modal drawer over a list, maximize in place, narrowed list faces - #27953

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-bsp3dh
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-bsp3dh

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread

Refs #27545

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:Record drawer v4: a bounded peek over the list — md cap instead of the field-count bucket, no dim overlay, the list stays readable and clickable, ⤢ maximizes in place at the same URL; the full page is reached only from the row ↗ and the header ⋯ #27545 拆成顺序两段,本 PR 是 A(机制层)。NavigationOverlay 新增列表旁的分栏面(非模态、列表不重挂)、盖在列表上的非模态抽屉、⤢ 原地最大化(宽度 = 视口减实时侧栏宽)、拖拽 360px 到 60vw;列表的两种窄面(≥440px 保留前三列,更窄改卡片行)和当前行订阅;ObjectGrid 画这两种窄面;i18n 加两个键;Console 记录抽屉默认宽度统一 720px。
  • 为什么改:维护者 10-29 裁决的 Target 1–3 需要这些机制。整卡测得 3421 行,超过 3000 行人工合并阈值,座位 2 批准按 Target 拆成顺序两段:A 合并后,B(Console 接线:默认分栏、行点击换根、当前行高亮、⋯「在整页打开」、显示菜单第三项「整页」、步进不空白)从新 main 开,写 Fixes #27545。
  • 风险与回滚:A 单独合入后读者能看到三处变化,正文「What a console reader sees from A alone」逐条列出:抽屉的整页按钮图标 ⤢ 换成 ↗;抽屉宽固定 720px(原来按字段数 480–1200px,且被抬到至少 min(60vw, 880px));已选「分栏」偏好的读者看到新的侧栏面。不动 spec、不动服务端、不加元数据键;回滚即 revert 本 PR。
  • 先看:ui/packages/components/src/custom/navigation-overlay.tsx → record-list-face.tsx → ui/packages/plugin-grid/src/ObjectGrid.tsx 的窄面 → 新测试 navigation-overlay-split-27545.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/components/src/__tests__/navigation-overlay-split-27545.test.tsx packages/components/src/__tests__/record-list-face-27545.test.tsx packages/plugin-grid/src/__tests__/ObjectGrid.recordPaneFace-27545.test.tsx,预期 3 个文件 25 条全过。
  • 需要你做的:无。Clause-②: yes (widening) 由座位做声明审查;epic 座位在预览里验收后座位才 ACCEPT,本 PR 保持 draft。

What changes

Part A of #27545, the mechanics. The console wires them in part B; until then it passes none of the new props, and its only change of its own is the pane's default width.

@object-ui/components — NavigationOverlay

  • Split face (Target 1). The pane sits beside the list the host hands over (mainContent), at that list's full height, as a complementary landmark, not a dialog. The list is one element in every state (closed, open, another record, closed again), so the overlay never remounts it. Below a 1000 px content area (RECORD_SPLIT_MIN_CONTENT_PX, measured on the host, not the window) the split face falls back to the drawer.
  • Drawer over a list (Target 2). Over a mainContent list the drawer is non-modal and draws no dimming overlay: an outside click leaves it open, and the list behind it stays clickable; ✕ and Esc close it. A drawer with no list (plugin views, the record page's related drawer) is the modal sheet it was.
  • Width. The divider drags from 360 px to 60vw and is remembered per object under the key the host names; a remembered width is drawn inside that range (min(Npx, 60vw), so a remembered 1300 clamps). An authored width is no longer floored at min(60vw, 880px).
  • New maximizable prop (Target 3). It draws ⤢ / ⤡ and maximizes the pane in place: the pane's width becomes the viewport less the live sidebar width, read from the sidebar's own state on every render, never cached (--sidebar-width, or --sidebar-width-icon when collapsed; SIDEBAR_WIDTH_FALLBACK / SIDEBAR_WIDTH_ICON_FALLBACK where the variable does not reach a portalled drawer). The list stays mounted and inert under it. Esc restores and only the next Esc closes; ✕ closes the path; a pane closed while maximized opens at its own width next time.
  • New activeListRecordId prop. A list under the pane reads its narrowed face (useRecordListFace(): columns at 440 px and wider, cards below, full with no pane beside it) and which row is the open record (ActiveListRow, useIsActiveListRecord, useScrollActiveListRecordIntoView, ActiveListRecordProvider, RecordListFaceProvider). The open row is a per-row useSyncExternalStore subscription: a change re-renders the row it leaves and the row it reaches, never the list around them.
  • onExpand is drawn as ↗ (the full page), so ⤢ is free for maximize.
  • DataTable marks the open record's row (data-active-record, aria-current) and scrolls it into view.

@object-ui/plugin-grid — ObjectGrid. Narrowed to columns, it keeps its first three view columns that are not avatars (by the column's type or the object field's), plus its row actions. Narrowed to cards, it draws one card row per record, the name and two chips, that opens the record like a table row. Under no pane nothing changes.

@object-ui/i18n. detail.maximize and detail.restoreSize in all ten packs (en Maximize / Restore size, zh 最大化 / 还原).

@object-ui/app-shell. The console's record pane defaults to md (720 px, at most 92vw) whatever the object's field count: size: 'auto' and no size both read as md; an authored size or width stands. This applies to the object page's drawer and to the record page's related-record drawer. It is in A because removing the 880 floor would otherwise leave a narrow object's drawer at 480 px.

What a console reader sees from A alone

  1. The record drawer's page button is drawn as ↗ instead of ⤢. It still opens the full page, under the same accessible name.
  2. The record drawer is 720 px wide (at most 92vw) whatever the object's field count. Before, it took 480 to 1200 px by field count and was widened to at least min(60vw, 880px): 864 px at a 1440 px viewport for an object of up to eight fields.
  3. A reader who chose Split in the Display menu sees the pane as the new side panel: no title heading, a divider that drags from 360 px to 60vw, the list beside it narrowed to three columns (card rows below 440 px), and below a 1000 px content area a non-modal drawer over the list. The open row is not tinted yet: the console does not pass activeListRecordId until B.

Nothing else a reader sees moves: the default preference, the row click, the ⤢ / full-page route hop and the record header menu are untouched until B.

Split plan (approved by seat 2)

The whole card measured 3421 changed lines, over the 3000-line human-merge threshold. Seat 2 approved a serial split along the Target items: this PR (A, Refs #27545) first; when it merges, the branch is reset to the new main and B opens with Fixes #27545. B is the console wiring: split as the default preference and the Display menu's third option 「整页」, the row click swapping the root, the open row tinted and followed by ↑ / ↓, ⤢ in the record top bar, the full page only from ↗ and the header ⋯ (「在整页打开」, with handleExpandDrawer's route hop retired and 「收为抽屉」 only for ↗ arrivals), stepping that keeps the previous record and prefetches both neighbours, the ?recordId= render-count test, the #26679 / #27031 pin updates, and the docs. B's commits are kept on a local branch and in a patch under the seat's project files. Each PR leaves main coherent.

Tests

New:

  • components/src/__tests__/navigation-overlay-split-27545.test.tsx (16): the pane beside the list with no dialog and the list clickable; one list element from closed to open to another record to closed; the narrowed faces; the fallback below 1000 px and the 1000 px control; the drawer over a list draws no overlay and survives an outside click, with the modal-sheet CONTROL for a drawer with no list; ⤢ / ⤡, the list inert while maximized, Esc restores before it closes, ✕ closes while maximized, maximize not remembered, the sidebar read live expanded and collapsed, the portal fallbacks equal the sidebar provider's widths; the drag range and the remembered-1300 clamp.
  • components/src/__tests__/record-list-face-27545.test.tsx (5): the open row follows the record and re-renders only the two rows that change (render counts, list count unchanged), scrolls into view, no provider means no open row, row ids, the 440 px face threshold; DataTable tints and names the open row and moves with it.
  • plugin-grid/src/__tests__/ObjectGrid.recordPaneFace-27545.test.tsx (4): columns keeps the first three non-avatar columns in view order; full CONTROL draws them all; cards draws name + two chips and opens the record; the open row is marked in both faces.

Updated, not deleted (width now drawn as given, the dragged range min(Npx, 60vw)): navigation-overlay-9299, gridNavigationMembers-8071, the kanban / calendar / gantt navWidthDefault pins, and the kanban / calendar / timeline NavigationMembers pins.

Runs (from ui/, NODE_OPTIONS=--max-old-space-size=4096):

  • The affected set (components overlay / data-table / record-list-face, app-shell ObjectView.record*, recordDrawerStack*, record navigator, interface list hydration, row-click payload, defaults-map mirror, plugin-kanban / gantt / calendar / timeline / tree, plugin-view split popover and record surface, react hooks, i18n): Test Files 504 passed | 2 skipped (506), Tests 4635 passed | 266 skipped (4901).
  • packages/plugin-grid (whole package): Test Files 262 passed (262), Tests 2297 passed (2297) on the head after merging main (with app-shell ObjectView.headerSelectionGate-27831, the one main test that reads the merged ObjectGrid change); before the merge 261 / 2290.
  • The three new files: Test Files 3 passed (3), Tests 25 passed (25).
  • Type-check (pnpm run type-check, both programs): components, plugin-grid, i18n, plugin-kanban, plugin-calendar, plugin-gantt, plugin-timeline, app-shell: exit 0 each.
  • eslint on the 26 changed source and test files: 0 errors.
  • i18n: check-i18n-call-site-keys, check-i18n-en-drift, check-i18n-dead-keys: exit 0.

Gates

Derived on head afeb540a12 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 35 families, 1751 changed lines (+1395 / -356, 29 files) against the 3000 threshold. Each was run and its exit code recorded before any pipe; --ran reconciles 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Every one exited 0: check-adr-0087-registration (base + self-test), check-changeset-no-major, check-closing-keyword-parity (+ self-test), check-comment-mask-corpus, check-empty-changeset (base + self-test), check-sdui-manifest (+ self-test), release-rehearsal-clone --self-test, release-pending-publish --self-test, pnpm --dir ui exec vitest run scripts/, spec check:duration-unit-keys, and check: changeset-gate-self-tests, closed-ceremony-text, cross-package-test-inputs, dispatcher-error-vocabulary, driver-memory-census, engine-double-contract, error-status-conformance, gitlink-declared, nul-bytes, pm-changeset-deadline-census, published-files, query-options-erasure, refd-timer-probe, skill-identifier-liveness, test-source-alias, tier-file-adoption, type-check-coverage, type-check-debt, undeclared-dep-imports, watch-hint-literal, where-matcher. The record is gates-ran-A.txt in the seat's card folder. The pre-commit and pre-push checks (check-regen-pending, check-commit-card-trailers --pre-push) were run by hand: exit 0.

Not run locally: the root check-i18n-bundles / check-i18n-coverage / check-i18n-walk-parity (they read server metadata bundles, not the ui packs; they answer exit 3 without the server closure's dist, and they are not in the derived list). Every family CI runs is cited from the PR's check-runs at ACCEPT, not re-run here.

Acceptance notes

  • Screenshots (Acceptance 2), deviation declared: taken on examples/app-crm, not HotCRM (outside this session's repository scope). Saved under the seat's project files, ui-seat2/card-27545/shots/, at 1440 x 900 and 1000 x 800 on the crm_lead list (five seeded leads, the admin account, sidebar expanded at 1440, auto-collapsed to its icon rail at 1000):
    • What A alone shows (this head): a-01-drawer-default-1440.png (the default drawer, 720 px, ↗ in its bar), a-02-split-preference-1440.png (a reader on Split: the side panel, the list at three columns), a-03-split-preference-fallback-1000.png (the same reader below a 1000 px content area: a drawer with no dimming, the list visible and clickable behind it).
    • The card's Acceptance 2, the end state with B applied (taken from the local full-stack branch, not this head): b-01-split-1440.png (split by default, the open row tinted), b-02-split-another-row-1440.png (another row clicked: the root swapped, the tint followed, the URL's recordId changed), b-03-maximized-1440.png (⤢: the pane 1184 px = 1440 less the 256 px sidebar, the sidebar kept, the same URL, ⤡ in the bar), b-04-overlay-fallback-1000.png (the fallback drawer at 1000, the open row still tinted behind it).
  • Two things the shots show that B fixes, not this PR: the split face prints the record count twice (the console's own record-count-footer under the list and ListView's count bar; this is already so on main's Split preference); and at the default 720 px pane beside an expanded sidebar the narrowed list is 464 px against 487 px of columns, so the sticky Actions column covers the end of the Status chip (a 23 px scroll).
  • Acceptance 3: the epic seat verifies in the console preview before the seat's ACCEPT. This PR stays a draft; nobody is assigned or requested.
  • No metadata key; nothing under packages/spec/**, no server file, nothing under examples/**.

Verification

  1. Open an object list in the console with the Display menu on Split and click a row: the pane opens beside the list, the list narrows to three columns; drag the divider (it stops at 360 px and 60vw).
  2. Narrow the window below a 1000 px content area: the pane becomes a drawer over the list with no dimming; click a list row behind it and the drawer stays open.
  3. On the default (drawer) preference, open a record: the drawer is 720 px and its page button is ↗.

Generated by Claude Code

… drawer over it, maximize in place, narrowed list faces

Part A of objectstack#27545 (Refs, not Fixes): the components, grid and
i18n mechanics the console wires in the following change, plus the
console's 720 px default pane width.

- NavigationOverlay: the split face beside the host's list, the
  non-modal drawer over a list, the 360 px–60vw drag range, the
  `maximizable` ⤢/⤡ in place, `activeListRecordId`, ↗ for `onExpand`.
- record-list-face: the narrowed list faces and the open-row
  subscription; DataTable marks and scrolls the open row.
- ObjectGrid: three columns beside the pane, card rows below 440 px.
- i18n: detail.maximize / detail.restoreSize in all ten packs.
- app-shell: the pane defaults to md (720 px) whatever the field count.

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

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Contract review

Served-tier: TIER_DEFAULT
Head-sha: afeb540a12aed8080a32ffdc0bace4016d4c862f
Local-runs: none
Scope: ui/packages/components/src/custom/index.ts, ui/packages/components/src/custom/navigation-overlay.tsx, ui/packages/components/src/custom/record-list-face.tsx, ui/packages/components/src/renderers/complex/data-table.tsx, ui/packages/plugin-grid/src/ObjectGrid.tsx + ui/packages/components/src/ui/sheet.tsx

Face: the Clause-②: yes (widening) declaration alone (PR body, changeset). None of the 29 files is under packages/spec/** or a governed path (check-governed-merges: NOT governed), and nothing is declared as narrowing, so no CONTRACT_REVIEW_TIER face applies. Read against origin/main after the head's merge of d4064cc159: 29 files, +1395 / −356.

① Derived judgments

  • Widened, correctly declared: @object-ui/components (published; no private).
    • The custom barrel adds the record-list-face set: RecordListFaceProvider, ActiveListRecordProvider, recordListFaceFor, useRecordListFace, useIsActiveListRecord, useScrollActiveListRecordIntoView, ActiveListRow, listRecordIdOf, RECORD_LIST_CARD_FACE_BELOW_PX, RECORD_LIST_NARROW_COLUMN_COUNT, and the type RecordListFace.
    • navigation-overlay (already export *) adds RECORD_SPLIT_MIN_CONTENT_PX, SIDEBAR_WIDTH_FALLBACK, SIDEBAR_WIDTH_ICON_FALLBACK, plus two OPTIONAL props on NavigationOverlay: maximizable? and activeListRecordId?.
    • The seat's grep of the removed lines finds no export, prop or member removed. So yes (widening) is the honest line, and no narrowing hides in it.
  • Not widened: ObjectGrid (@object-ui/plugin-grid) reads the narrowed face from context and adds no prop. DataTable marks the open row (data-active-record, aria-current) from the same context and adds no prop. @object-ui/app-shell is private.
  • Behaviour change for an existing caller, stated, not hidden:
  • Metadata-protocol: none holds: no metadata key is added, removed or read differently.

② Semver level

patch on @object-ui/components, @object-ui/plugin-grid, @object-ui/i18n and @object-ui/app-shell, following the repo's rule that the level follows Metadata-protocol: (none ⇒ patch). The changeset carries Clause-②: yes (widening) and names each new export and prop.

③ Boundary flags

Implemented-by: claude/project-thread-bsp3dh
Reviewed-by: session_01Utp6CY2n1JmZZKrHgtp9jC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Preview review (proxy for epic:#26676#1) · afeb540 · PASS

Acceptance 3 for A, run under the #26676 ruling B (6520722066). A fresh review subagent with no development context had three inputs: #27545's Acceptance, the v4 boards (a7825499aa:docs/design/record-page-v2-2026-10-27/v4/, RecordDrawerV4Peek / Max / Enlarged + notes.md) and this PR's "What changes" and "What a console reader sees from A alone". It walked the live console built from this head on examples/app-crm (crm_lead, five seeded leads, the admin account) at 1440 × 900 with the sidebar expanded, at 1000 × 800, and at the 1255 / 1256 px boundary. It judged A against A's own claims, not against B's end state. The head merges with main cleanly.

Reason: every reader-visible claim A makes is delivered, and the default path is unchanged. The deviations from the boards it found are either declared in this PR and owned by B, or sit in code A does not touch.

Claim Observed Shots
1. The page button is ↗ and still opens the full page The default drawer bar is ↑ ↓ ↗ ✕. ↗ is lucide-arrow-up-right, named "Open as full page"; there is no ⤢ in the dialog. Clicking it routes to /crm_lead/record/<id> (the full page, with 「Collapse to drawer」), and back returns to the list. 01, 03, 04
2. The drawer is 720 px (at most 92vw) whatever the field count Leads (10+ fields) at 1440: the dialog is 720.00 px, --ov-w: min(92vw, 720px). At a 770 px window it is 708 px = 92vw. 01, 18
3. The Split preference shows the new side panel The pane is an <aside aria-label="Lead"> beside the list: host 1184 = list 464 + pane 720. There is no dialog, no dimming and no title heading. The list keeps three view columns plus actions (9 columns closed), and a row click does not remount it. The divider clamps at 60vw (864 px) and 360 px and is remembered per object. Card rows (name + two chips, clickable) replace the table below 440 px. The fallback is decided on the content area, not the window: host 1000 → split, host 999 → a 720 px drawer with no aria-modal and no overlay. The list behind the fallback stays clickable, ✕ and Esc close it, and widening the window brings the split back. 05–17
Nothing else moves No stored value means Drawer, and the row click opens the modal sheet as before. ↗ still does the route hop. The header ⋯ is Add to favorites / Share / Delete, with no new item. 00, 02, 15

Deviations from RecordDrawerV4Peek inside A's scope (minor, both already declared in the PR body as B's):

  • The 464 px narrowed list holds 487 px of columns, so the sticky Actions column covers the end of the Status chip ("Disqualified" is cut in 06).
  • The split face prints "5 records" twice: ListView's bar plus the console's record-count-footer.

B scope, not counted:

Other notes (not A's):

  • No uncaught page errors. One 501 from /api/v1/approvals/records/crm_lead/<id>/lock on each open: app-crm's server has no approvals lock endpoint.
  • Double-clicking the divider resets the pane to 720 for that open, but the remembered width survives to the next open. useDrawerResize is unchanged by A; worth a check on main before carding.
  • After the window shrinks to 1000, the sidebar stays collapsed when it grows back. This is shell behaviour.

Screenshots (project files, ui-seat2/card-27545/preview-A/):

  • preview-A-00-display-menu-original.png
  • preview-A-01-drawer-default-1440.png
  • preview-A-02-drawer-header-menu-1440.png
  • preview-A-03-full-page-after-arrow-1440.png
  • preview-A-04-back-from-full-page-1440.png
  • preview-A-05-display-menu-split-1440.png
  • preview-A-06-split-default-1440.png
  • preview-A-07-split-another-row-1440.png
  • preview-A-08-split-60vw-cards-1440.png
  • preview-A-09-split-card-click-1440.png
  • preview-A-10-split-360-1440.png
  • preview-A-11-split-fallback-1000.png
  • preview-A-12-fallback-row-click-behind-1000.png
  • preview-A-13-fallback-closed-by-x-1000.png
  • preview-A-14-split-after-resize-back-1440.png
  • preview-A-15-display-menu-restored-1440.png
  • preview-A-16-boundary-host-1000-split-1256.png
  • preview-A-17-boundary-host-999-drawer-1255.png
  • preview-A-18-drawer-default-92vw-770.png
  • The boards rendered at 1440 for comparison: boards/RecordDrawerV4Peek.png, boards/RecordDrawerV4Max.png, boards/RecordDrawerV4Enlarged.png

Per the ruling, B gets one full walk against the boards before its ACCEPT.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head afeb540a12aed8080a32ffdc0bace4016d4c862f · 2026-10-30T11:07Z

  1. Shape: draft → main, Refs #27545 (part A of the serial A/B split; B says Fixes #27545), attribution block on top, root changeset (@object-ui/components, plugin-grid, i18n, app-shell patch). 29 files, +1395 / −356. No assignee, no reviewer.
  2. Card rule met for A (seat read the diff and the shots): the split face beside a never-remounted list with the 1000 px fallback (Target 1), the non-modal undimmed drawer over a list (Target 2), maximizable with the live sidebar width, Esc restores first (Target 3), the 360 px – 60vw drag with the remembered-width clamp, the narrowed list faces and the open-row subscription, and the pane's 720 px default. Shots a-01–a-03 match; the reader-visible changes of A alone are listed in the body. The double record count and the 23 px Actions overlap at 464 px are B's, as the body says.
  3. Gates (check-runs on this head): 44 success, 11 skipped, 0 red. Named runs: Lint & Repo Gates 123902709882, TypeScript Type Check 123902186484, Type Check UI 123901088874, Test UI 123905130910. Dev: 35/35 derived gates exit 0 (incl. check-sdui-manifest).
  4. Contract review: Clause-②: yes (widening) → TIER_DEFAULT declaration review, PASS 6515412359 on this head (check-owed-reviews: DONE).
  5. Acceptance 3 (preview verification): ruled B (6520722066) on the maintainer's 「你可以自主验收合并,有问题会开新卡」 (2026-10-30T10:39Z). The card thread's context-free proxy preview review on this head: PASS (6521163553). The seat also read the dev's console-preview shots on examples/app-crm, which is the accepted surface; HotCRM is not required. The epic seat re-checks the result on main and files cards for any deviation.
  6. Landing: not governed (check-governed-merges --pr 27953: 0 of 29 paths), 1751 changed lines ≤ 3000. The seat lands it: ready + auto-merge now. B opens from the new main after it merges.

Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

1 participant