Skip to content

fix(app-shell): a narrow-window toast beside the record drawer no longer covers the record's Edit and more buttons - #26426

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #26418

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:窄窗口(右侧抽屉旁放不下提示)时,提示条不再只躲开抽屉自己的标题栏,而是再往下躲开抽屉里第一排按钮(记录头的「编辑」「⋯」)。
  • 为什么改:1024 宽窗口里新建记录后,「创建成功」提示正好盖在「编辑」上 4 秒,用户下一步要点的按钮点不到。
  • 风险与回滚:只动 ConsoleToaster.tsx 的窄窗口分支;宽窗口(提示放在抽屉左侧)不变。回滚即还原这一个文件。
  • 先看:ConsoleToaster.tsx 的 firstControlRowBottom / clearanceFor;ConsoleToaster.drawerActionsRow-26418.test.tsx。
  • 两分钟验证:把浏览器窗口拉到约 1024 宽,打开任一对象,新建 → 创建;预期提示出现在「编辑」按钮下方,不挡「编辑」和「⋯」。
  • 需要你做的:无,席位落地。

What changes

ui/packages/app-shell/src/chrome/ConsoleToaster.tsx, the objectui#11685 fallback (no room for a toast beside an open right-edge drawer): the toaster used to drop below the drawer's own header only, so on a 1024px window with the 720px record drawer the create-success toast sat at y 69–123 on top of the record header's Edit (93–129) and ⋯ menu. It now drops below the drawer's first row of controls too.

  • The row is measured, not assumed: the topmost visible control in the drawer outside its header (button, a[href], form fields, and the button / link / menuitem / tab roles), widened by every control that overlaps it vertically (the title's star button beside a taller Edit). The toast's top is max(header bottom, row bottom) + 24px on desktop and + 16px on phones, where sonner spans the toaster across the window and the same row was covered.
  • Positions are layout offsets summed up the offsetParent chain, like the existing header measurement: the slide-in transform does not skew them, and offsetTop ignores the body's scroll position, so a toast raised mid-scroll still clears the row once the user scrolls back (measured in Chromium, below).
  • A control hidden with display: none (no offsetParent) does not count. A first control in the drawer's lower half is body or footer content, not a header row, so it does not count either: dropping below it would push the toast off the window.
  • The record renders after the drawer opens, so the hook now also observes each open drawer's subtree (childList) and re-measures when the row arrives, coalesced to one measurement per animation frame as before.
  • Wide windows are unchanged: the toast stays in the strip left of the drawer (objectui#11685).

Why measuring, not the drawer's bottom-right corner. The card offered both. The bottom-right corner of the record drawer is where InlineEditSaveBar sticks (sticky bottom-0, Save / Cancel right-aligned), and a failed inline save raises its error toast exactly while that bar is up, so the corner option would move the same defect onto Save. It would also flip the toaster to a bottom anchor, the corner objectui#7482 keeps toasts out of. Measuring the first control row carries no page-specific pixel and follows whatever the drawer holds.

Tests

New ConsoleToaster.drawerActionsRow-26418.test.tsx (happy-dom; sizes assigned from the card's Chromium reading, as ConsoleToaster.drawerClearance-11685 does):

  • 1024px window, 720px drawer: the toast's rect does not intersect the Edit / ⋯ row; desktop and phone offsets sit below the row.
  • Control: 1440px window, 864px drawer keeps the toast beside the drawer (--offset-right: 888px, --offset-top: 24px).
  • A record header appended after the drawer opened still moves the toast.
  • A hidden control and a control in the drawer's lower half do not move the toast; the same drawer with a real header row then does (non-vacuity).

No existing test was edited.

Ablation 1, the fix reverted (the whole source file restored from the merge base 436d2840, blob 252cf760, then restored to HEAD blob 24ccab6c, git diff HEAD empty): the new file goes 3 failed / 1 passed. The three cases above the control go red; the wide-window control stays green, as a control should. drawerClearance-11685 stays 6/6 green both ways.

Ablation 2, the drawer-subtree observer removed (scripts/ablation-replace.mjs, anchor 1 → 0, restored blob == HEAD): the late-render case and the non-vacuity leg go red (2 failed / 2 passed), so the observer is load-bearing.

Acceptance notes

  • Real-layout reading, Chromium 1024×768 and 1440×768, the actual ConsoleToaster (bundled from source) over a hand-built drawer shaped like NavigationOverlay (45px header, scrolling body, record header with Edit and ⋯ arriving after the drawer opened). It is a one-off probe and nothing re-runs it.
    • Merge base, 1024 / 720: toast 644–1000 × 69–123, which is the card's reading exactly. It covers Edit, and elementFromPoint at Edit's centre is the toast.
    • This branch, 1024 / 720: toast 644–1000 × 129–183, clear of Edit (69–105 in the probe) and ⋯. elementFromPoint at Edit's centre is Edit. The same reading holds with the body scrolled 60px when the toast is raised.
    • Both, 1440 / 864: toast 195–551 × 24–78, beside the drawer, unchanged.
  • In a drawer whose first control is a form field (a form sheet), the toast now sits below that first field's row instead of on it. Neither placement is a header row, and this card does not change that case.
  • The probe did not run the full Console with a backend. The two-minute check in the summary above is that reading.

Verification

Local, at head e8590370:

  • pnpm exec vitest run packages/app-shell/src/chrome/ConsoleToaster packages/app-shell/src/chrome/consoleToasterAnchor (from ui/, under the verify lock): 5 files / 21 tests passed.
  • pnpm --filter @object-ui/app-shell type-check (both tsc passes, after building the ^... closure): exit 0. tsconfig.test.json includes src/**/*.test.tsx, so the new test is compiled.
  • eslint over the two touched files: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 derived families, each run with its exit captured before any pipe: 34 exit 0. --ran reconciliation: "34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero, every line carries its exit code).

The full app-shell suite and every repo-wide family are left to CI (Test UI, Type Check UI, Lint UI, Lint & Repo Gates).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint passes on the touched files.
  • 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: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer summary is in Chinese by AGENTS.md's communication rule).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmQQBJvECrfY7vVyetV


Generated by Claude Code

…record's actions row

The fallback for a window too narrow for a toast beside a right-edge
drawer cleared only the drawer's own header, so on a 1024px window the
create-success toast covered the record header's Edit and more buttons.
It now also drops below the drawer's first row of controls, measured from
layout offsets.

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

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-27T03:51Z

Head e8590370 (read from the PR object).

  • Shape. Draft on main, Fixes #26418. 3 files, +279/-7: ConsoleToaster.tsx, the new ConsoleToaster.drawerActionsRow-26418.test.tsx, and a root changeset (app-shell patch).
    • On windows too narrow for the toast to sit beside the drawer, the fallback now places it below the drawer's first row of controls. That row is the topmost visible control outside the drawer header, widened by vertical overlap. It is measured with offsetTop sums, so it ignores transforms and scroll position.
    • A drawer-subtree MutationObserver re-measures when the record header renders late. The observer goes through the existing schedule() and is disconnected on both teardown paths, beside the size and portal observers.
    • The wide-window placement (objectui#11685) is unchanged.
    • Clause-②: no, Metadata-protocol: none.
  • Choice. The dev measured rather than moving the toast to the drawer's bottom-right, because InlineEditSaveBar's Save / Cancel sticks there. The seat agrees. The open question (a half-height cut-off for a first control that isn't in the header) keeps the dev's recommendation.
  • Checks pinned to the head. All check runs completed: 0 failing, 0 pending. The seat's landing script checked this before posting.
  • Governed surface. check-governed-merges.mjs --pr 26426: not governed.
  • Evidence (dev).
    • 5 toaster files / 21 tests. app-shell type-check, and eslint on the touched files, exit 0. 34/34 gates.
    • Ablation: reverting the source turns 3 of the 4 new cases red, and the wide-window control stays green. Removing the observer turns the late-render case red.
    • Chromium probe: at base the toast sits at y 69–123 and covers Edit, as the card reads; at head it sits at 129–183 and clears it, also when scrolled.
  • Serial constraints. No other open PR touches ConsoleToaster.tsx. app-shell has no sdui record. The ⛔ paths are untouched.

Landing: ready + auto-merge through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 03:52
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 03:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 26e7c75 Oct 27, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-z125nk branch October 27, 2026 04:01
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40446655069 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (7/16) — 失败步骤: Run the ui tests (shard 7/16)

    × pins every workflow-anchored claim to the version .github/workflows declares 11ms
    × reads every declaration the tree contains, so the anchor cannot shrink unnoticed 23ms
    FAIL   unit  scripts/__tests__/doc-version-claims.test.ts > doc version claims - the workflow-version assertion > pins every workflow-anchored claim to the version .github/workflows declares
      ↳ 失败原因: AssertionError: implausibly few node-version declarations were read from ../.github/workflows - the anchor is thinner than the claims it backs, or the parser stopped reading them: expected 1 to be gre
    FAIL   unit  scripts/__tests__/doc-version-claims.test.ts > doc version claims - the workflow-version assertion > reads every declaration the tree contains, so the anchor cannot shrink unnoticed
      ↳ 失败原因: AssertionError: control for the agreement above: no node-version line was found in .github/workflows at all, so two empty lists just agreed with each other: expected 1 to be greater than or equal to 1
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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/m tooling

Projects

None yet

2 participants