Repository navigation
Commit c1b52de
feat(ui): record pane v4 mechanics — split beside the list, non-modal drawer over a list, maximize in place, narrowed list faces (#27953)
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_016iMXWSZDzqWRWvWzdNo8wgP6XkfpjCfyzzG2KwQPUZfy)_
Refs #27545
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:#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](https://claude.ai/code/session_01Utp6CY5hd7qEmdXd6Wg8Ap)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 847e424 commit c1b52de
29 files changed
Lines changed: 1395 additions & 356 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views
- components/src
- __tests__
- custom
- renderers/complex
- i18n/src/locales
- plugin-calendar/src
- __tests__
- plugin-gantt/src
- plugin-grid/src
- __tests__
- plugin-kanban/src
- __tests__
- plugin-timeline/src/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
34 | 45 | | |
35 | 46 | | |
36 | 47 | | |
| |||
78 | 89 | | |
79 | 90 | | |
80 | 91 | | |
81 | | - | |
82 | | - | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
83 | 95 | | |
84 | 96 | | |
85 | 97 | | |
86 | 98 | | |
87 | | - | |
| 99 | + | |
88 | 100 | | |
89 | 101 | | |
90 | 102 | | |
91 | 103 | | |
92 | 104 | | |
93 | 105 | | |
94 | 106 | | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
99 | 112 | | |
100 | 113 | | |
101 | 114 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
55 | 56 | | |
56 | 57 | | |
57 | 58 | | |
| |||
625 | 626 | | |
626 | 627 | | |
627 | 628 | | |
628 | | - | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
629 | 632 | | |
630 | 633 | | |
631 | 634 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
29 | 32 | | |
30 | 33 | | |
31 | 34 | | |
| |||
121 | 124 | | |
122 | 125 | | |
123 | 126 | | |
124 | | - | |
125 | | - | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
126 | 130 | | |
127 | 131 | | |
128 | 132 | | |
| |||
138 | 142 | | |
139 | 143 | | |
140 | 144 | | |
141 | | - | |
| 145 | + | |
142 | 146 | | |
143 | 147 | | |
144 | 148 | | |
| |||
151 | 155 | | |
152 | 156 | | |
153 | 157 | | |
154 | | - | |
| 158 | + | |
155 | 159 | | |
156 | 160 | | |
157 | 161 | | |
| |||
167 | 171 | | |
168 | 172 | | |
169 | 173 | | |
170 | | - | |
| 174 | + | |
171 | 175 | | |
172 | 176 | | |
173 | 177 | | |
| |||
0 commit comments