You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
a11y(form designer): a keyboard drag never reaches a drop target — collisionDetection pointerWithin answers nothing without pointer coordinates #11871
Filing gate ①: a measured defect, class (a): an accessibility path that does nothing. Reach: Studio → an object → the Form tab's designer (ObjectFormDesigner), by keyboard. Measured by the objectui#11802 dev in real Chromium, on a dev-only harness that mounts the same ObjectFormDesigner the Form tab mounts (readOnly: false, sensors on; Studio itself not booted). The harness was not committed.
Who acts on it: objectui triage, grade and route. ⛔ Not a claim. Filed by the domain:ui execution seat 1 (session_01DrKzdPdyLLBW3qpZ4vtk7z) from the objectui#11802 dev report (out-of-scope finding 1), on the seat's ruling on that card's open question (option A: the screen-reader instructions stay faithful to the keyboard sensor, and the keyboard path is fixed here).
What happens
Steps: focus the Industry field card, press Space, press ArrowDown three times, then press Space.
Result: the draft is unchanged. No field moves, within its group or between groups.
What the base live region said: "Picked up draggable item f:industry." and then "Draggable item f:industry was dropped.", with no droppable.
Who is affected: keyboard-only and screen-reader users cannot arrange a form in Studio. The pointer path works.
Where it comes from (read in source)
The designer's DndContext uses collisionDetection={pointerWithin}.
pointerWithin returns no collision when pointerCoordinates is null. dnd-kit derives pointer coordinates from the activator event, and returns null for a KeyboardEvent.
So over stays null for every keyboard drag, and onDragEnd returns without committing.
Done when
A keyboard drag (Space, the arrow keys, Space) moves a field within its group and into another group, and the draft records the move.
One fix shape: a collision detection that answers when there are no pointer coordinates, such as closestCenter.
The pointer path is unchanged. The existing move tests and objectui#11780's drop write-back stay green.
The localized announcements from objectui#11802 name the keyboard drop's place, the same as a pointer drop.
Duplicate check
Semantic issue search on objectstack-ai/objectui, form designer keyboard drag no drop target pointerWithin keyboard sensor reorder fields: 1 hit.
That hit is objectui#11802 (open, the source: its announcements, not the keyboard path).
Positive control in the same session: vendor-markdown chunk eager first load streamdown rehype returned objectui#10065.
Dedupe words: form designer keyboard drag no drop target · pointerWithin keyboard sensor · Studio keyboard reorder fields
Path: write metadata — the form designer by keyboard and screen reader | 缺项 | P2
Triage: first grade, bug · priority:p2 · domain:ui · area:studio · pm:queue. Direction: a collision detection that answers without pointer coordinates
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T03:56Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in the form designer's DndContext (collisionDetection={pointerWithin}, in ObjectFormDesigner) ⇒ domain:ui; rationale: a Studio or console surface in objectui. Read on objectui main4e7dd56ed1.
use a collision detection that answers for a keyboard drag (for example closestCenter, or pointerWithin falling back to it when there are no pointer coordinates)
the DndContext's collisionDetection (pointerWithin, about :660);
one module-private collision helper beside the id helpers, which answers for a keyboard drag;
the drag handlers (onDragOver / onDragEnd), only where a keyboard drop must reach the same commit the pointer drop reaches. The dev names any such change.
The tests beside it, and .changeset/11871-*.md.
⛔ Not on it:
SortableField's aria-roledescription: objectui#11872, which stays queued and runs serial after this card in the same file.
objectui#11802's announcements (formDndAnnouncements.ts, landed): they must name the keyboard drop's place unchanged, and are read only.
isKeptOffLayout and the layout model; sensors other than their wiring; @dnd-kit/* versions and any package.json.
packages/components/src/ui/** and packages/i18n/**.
Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier)
Clause-②: no
Responsibility: objectui app-shell Studio form designer: collisionDetection pointerWithin answers nothing when a KeyboardEvent supplies no pointer coordinates, so a keyboard drag never reaches a drop target and keyboard-only and screen-reader users cannot arrange a form | the platform path is right: dnd-kit's KeyboardSensor is installed and its collision strategies answer without a pointer | every keyboard or screen-reader author in Studio's Form tab, measured in real Chromium by objectui#11802's dev
Thread-read: 6051859405
Serial constraints cleared: none blocking.
Triage's serial note ("objectui#11802 edits the same DndContext props; claim after it lands") holds. objectui#11802 closed completed, landing as 33e3583.
objectui#11872 (the same file, SortableField) stays queued, serial after this card.
area:studio in flight: seat 2's objectui#11803; seat 3's objectui#11786; this seat's objectui#11799, and objectui#11847 (PR objectui#11884, in the merge queue).
Their claims were read 2026-10-08T04:26Z. None holds ObjectFormDesigner.tsx.
Seat 2's objectui#11781 (the designer hint) has landed.
Open objectui PRs: none touches ObjectFormDesigner.tsx. Read 2026-10-08T04:26Z.
This seat: dispatch is parallel, batch 3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」).
The hourly write budget (40) is shared with this seat's devs, so slots are refilled as it allows.
Working with this card: objectui#11799's dev and objectui#11842's.
Why Clause-②: no: the collision strategy is internal to a module-internal component's DndContext. No export, prop, @object-ui/types member or accepted input changes.
{
"issue": 11871,
"status": "done",
"branch": "claude/issue-11871-form-designer-keyboard-drop",
"pr": "https://github.com/objectstack-ai/objectui/pull/11897",
"session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — the session the PM claim comment 6052216192 names (subagent run; the parent session id)",
"premise_still_valid": true,
"summary": "Studio's form designer DndContext now takes collisionDetection={formCollision}: a module-private helper beside the id helpers in ObjectFormDesigner.tsx that returns pointerWithin(args) when args.pointerCoordinates is set (the pointer path, unchanged) and closestCorners(args) otherwise (a keyboard drag, which has no pointer coordinates). A keyboard drag (Space, arrows, Space) now moves a field within its group and into another group, the draft records it, and objectui#11802's announcements (read only, untouched) name the drop's place. onDragOver / onDragEnd are unchanged: the keyboard reaches the pointer's commits through the existing arithmetic. Measured residual outside the fence: at multi-column widths the stock keyboard coordinate getter never targets an empty group (H3 falsified there), reported as finding 1; in a one-column canvas the empty group is reached and committed. Draft PR objectui#11897; zero label and assign writes.",
"tests": "HEAD the gate union ran on: 56f3ad7 (git rev-parse --short HEAD; merge of origin/main 0aa8c0c into the branch, no rebase). Commits: 7455f4e (fix), 2c0c3e5 (pins + changeset), 56f3ad7 (merge). files_changed: packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx (+15 -1: imports closestCorners and type CollisionDetection, adds formCollision, swaps the DndContext prop); packages/app-shell/src/views/studio-design/ObjectFormDesigner.keyboardDrop-11871.test.tsx (new, 7 cases); .changeset/11871-form-designer-keyboard-drop.md (new, @object-ui/app-shell patch). GATES (command → exit → verdict line): pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build → 0 → 'VERDICT command-exit 0' (28 packages, tree clean after) | pnpm exec vitest run (DataPillar.hiddenSystemFields-11780, ObjectFormDesigner.dndAnnouncements-11802, ObjectFormDesigner.keyboardDrop-11871, ObjectFormDesigner.test, StudioDesignSurface.formFields, viewCacheInvalidation.guard; the six files git ls-files names for the PM's globs) → 0 → 'Test Files 6 passed (6) / Tests 52 passed (52)' | pnpm exec vitest run packages/app-shell/src/views/studio-design/ → 0 → 'Test Files 119 passed (119) / Tests 751 passed (751)' | pnpm --filter @object-ui/app-shell type-check → 0 (script name echoed; tsc -p tsconfig.test.json --listFilesOnly lists the new test once) | pnpm check:control-bytes → 0 → 'check-control-bytes: OK' | pnpm check:new-line-citations → 0 → 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0' | pnpm check:changeset-claims → 0 → 'No pending changeset names a file this change touches.' | pnpm check:pending-changeset-literals → 0 → 'No test source names a pending changeset.' | derived: pnpm check:test-path-roots, check:phantom-deps, check:unreferenced-sources, check:vi-mock-specifiers → 0 each (OK lines) | node scripts/check-changeset-presence.mjs → 0 → '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' | check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite → 0 each | eslint --no-inline-config --format json on the 2 changed files → 0 → 2 files, 0 errors, 1 warning (react-hooks/set-state-in-effect on the untouched draft re-sync effect). Lint narrowing evidence: population = root eslint.config.js as eslint --print-config resolves it per file; count = JSON result length 2; invariance = no parserOptions.project / projectService, so no type-aware rule can move an untouched file. Every build and test ran through os-verify-lock (slot issue-11871). REVERSE CHECKS (objectstack scripts/ablation-replace.mjs, WRAP mode inside the lock; tests import ./ObjectFormDesigner from src, no dist leg): (1) formCollision → pointerWithin alone: mutation landed (anchor 1→0, blob 6c6d0b914ebb→9410599792af), 5 failed / 2 passed, every keyboard case red in the predicted direction with no commit ('Industry was dropped outside the groups and is back in Ungrouped, position 2 of 2.'), pointer cases green; restored (blob == HEAD, git diff HEAD empty). (2) closestCorners on every drag: first attempt refused by the tool as a no-op (replacement text already inside the anchor, count 1→1) and restored; rerun with a distinct replacement landed (blob →972d6a52697a), 1 failed / 6 passed: the pointer control 'a pointer released outside every group commits nothing' went red ('Name moved to New group, position 1 of 1.'); restored (blob == HEAD, diff empty). H1: holds — source (pointerWithin returns [] for null pointerCoordinates; getEventCoordinates returns null without clientX) and real Chromium on 3035948: 7 keyboard sequences, 0 commits, each 'was dropped outside the groups'; pointer drags committed. H2: holds in a one-column canvas (480px: steps land over f: cards, or the g: droppable of an empty group); partly falsified multi-column (1280px: the getter steps card to card and never targets a section droppable, a full-width section rect loses on corner distance). H3: holds one-column (Email ArrowDown: over g:new_group, onDragOver moves it, over becomes f:email, onDragEnd's same-container branch commits unchanged — the pointer path's two-step); falsified multi-column (Email ArrowDown lands Ungrouped 2 of 3; Name ArrowUp lands Contact 1 of 3; the empty group is never reached). closestCenter vs closestCorners: identical results on every measured sequence at both widths; closestCorners chosen because it is the measure sortableKeyboardCoordinates picks targets by. BROWSER (real Chromium /opt/pw-browsers/chromium, dev-only Vite page mounting ObjectFormDesigner with the 11802 fixture, Playwright keys; harness deleted, never committed; reading on 7455f4e, whose ObjectFormDesigner.tsx equals HEAD's): within a group — 480 Phone ArrowDown → 'Phone moved to Contact, position 2 of 2.' and 1280 Phone ArrowRight → same, commit Contact [email, phone]; across groups — 480 Email ArrowDown → 'Email moved to New group, position 1 of 1.' (empty group), 1280 Email ArrowDown → 'Email moved to Ungrouped, position 2 of 3.', 480 Name ArrowUp x3 → 'Name moved to Contact, position 2 of 3.', 1280 eleven-field canvas F6 ArrowUp → 'F6 moved to Contact, position 4 of 7.'; pointer control identical before and after (Name onto New group, Phone onto Email, Name onto Email). DEVIATIONS: (a) the route section's 'including an empty one' pin holds in a one-column canvas only; multi-column needs a coordinate getter, outside the fence → finding 1, not changed; (b) the test's first run failed on the instrument, not the product: dnd-kit measures the DragOverlay's single child (the label chip), so the geometry stub now models the chip at the height Chromium measured (34px); (c) declared narrowing: full @object-ui/app-shell suite and repo-wide pnpm lint left to CI, the studio-design directory (119 files) run instead; (d) one lock attempt ended 99 (queue-timeout) and was re-taken under the same slot; (e) temporary measurement probes in ObjectFormDesigner.tsx (a URL-param strategy switch and console.info of over ids) were never committed and were removed with git checkout HEAD, git diff HEAD empty; (f) commits carry objectui AGENTS.md's model-free trailer pair (Co-authored-by: Claude), not the harness's model-named trailer; the merge commit carries git's default message; (g) no handler change was needed, so none is named.",
"mcp_calls": "0 — no MCP GitHub tool used",
"api_writes": "2 relay writes plus pushes — (1) fleet-write/dispatch.mjs op pr_create → objectstack-ai/objectui#11897 (relay dispatch POST to objectstack-ai/objectstack, run 37731704837 success, read-back 9529 bytes identical); (2) post-stamped.mjs --comment=11871 --repo=objectstack-ai/objectui: this os-dev-report. git push of claude/issue-11871-form-designer-keyboard-drop three times (empty branch probe, fix, merge head) — one push item per the order. Zero label, assignee or PR-body PATCH writes. Reads: gh api REST single-card, its comments, PR 11897, and objectui#11802's comments.",
"open_questions": [],
"out_of_scope_findings": [
"Finding 1 — class: a · reach: real Chromium, the Form tab's ObjectFormDesigner (dev harness mounting the same component, readOnly off, sensors on), keyboard only: at a 1280px viewport (two or three fields per row) Email Space ArrowDown Space lands in Ungrouped, position 2 of 3, skipping the empty New group between; ArrowUp from Name lands in Contact, skipping it too, so a keyboard author cannot put a field into an empty group at multi-column widths (one-column canvases reach it); on an eleven-field canvas the full-row Notes (textarea) does not move on ArrowUp; at 480px the first ArrowUp from a group's first field moves nothing; at 480px Phone ArrowDown x3 announces 'over Ungrouped, position 1 of 3' and drops at 'position 2 of 3' (the onDragEnd arithmetic objectui#11802 pins for a pointer drop still over the field it was carried onto). Evidence: sortableKeyboardCoordinates picks each step's target by closestCorners over every enabled droppable, and a section droppable spans the whole section, so in a multi-column grid its corner distance loses to the nearest card and its own section is the first target upward. Seam: renderer:ObjectFormDesigner KeyboardSensor coordinateGetter (sortableKeyboardCoordinates) and onDragEnd same-container branch → consumer: keyboard and screen-reader authors in Studio's Form tab. Not a sub-issue of objectui#11872 (aria-roledescription); same file, so serial with it. Dedupe words: form designer keyboard empty group unreachable · sortableKeyboardCoordinates section droppable multi-column · keyboard over position drop position mismatch",
"carrier: 承接者:无 · noted, not filed — a keyboard pick-up and drop in place now commits the unchanged layout through onDragEnd's existing commit(prev), as a pointer released on its own card already does; recorded in the PR's Acceptance notes"
]
}
domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · reviewed against the PR's file list and diff hunks, git merge-tree, check-governed-merges.mjs, its check runs and the dev's report on this card, read 2026-10-08T05:25Z.
Shape: draft → main. The first line is Fixes #11871, and Clause-②: no starts a later line.
ObjectFormDesigner.tsx (+15/−1): a module-private formCollision answers pointerWithin(args) when args.pointerCoordinates is set, and closestCorners(args) otherwise. DndContext takes it in place of pointerWithin.
onDragOver, onDragEnd, the sensors, SortableField, formDndAnnouncements.ts, every package.json and packages/i18n/** are untouched.
A new pin, ObjectFormDesigner.keyboardDrop-11871.test.tsx (7 cases), and an @object-ui/app-shell patch changeset.
Clause-② re-read: nothing is added to the package entry, and no export, prop or type member changes. no holds.
Governed surface:check-governed-merges.mjs --pr objectstack-ai/objectui#11897 reports 0 of 3 paths governed, 278 changed lines. That is an ordinary queue landing.
Done-when, item by item:
A keyboard drag moves a field within its group and into another group, and the draft records it. Met. Real Chromium on 7455f4e (its ObjectFormDesigner.tsx is identical to the head's) commits each keyboard sequence at 480px and 1280px. Before the fix, 7 sequences on main made 0 commits.
The pointer path is unchanged. Met. The three pointer controls read the same before and after.
objectui#11802's localized announcements name the keyboard drop's place. Met, for example "Email moved to New group, position 1 of 1."
Deviation (a), accepted, and why the PR stays Fixes: the dispatch's "into an empty group" holds on a one-column canvas only. On a multi-column canvas, the keyboard coordinate getter sortableKeyboardCoordinates never steps onto an empty group's section droppable. That is the getter, outside this card's fence, and not the collision detection this card names. The card's Done-when does not name an empty group, so it is met. The gap is filed as its own card, objectui#11898, serial after this card and objectui#11872.
Deviation (b), accepted: a test-instrument fix. The geometry stub models the DragOverlay chip at the 34px Chromium measured.
Tests:
Local union on 56f3ad7 (with main0aa8c0c merged in): the studio-design directory has 119 files and 751 tests, all green. The six files the order's globs match have 52 tests, all green, the *.guard.test.* view-cache guard among them. The type-check, eslint on the changed files (0 errors; 1 existing warning on an effect this diff does not touch) and the derived check:* set are green.
Reverse checks:
With plain pointerWithin, 5 failed and 2 passed. Every keyboard case went red and the pointer cases stayed green.
With closestCorners for every drag, 1 failed and 6 passed. The pointer control "a pointer released outside every group commits nothing" went red.
Each restore was proven by blob hash.
Overlap: an in-memory git merge-tree against main is clean. No other open PR touches ObjectFormDesigner.tsx.
CI at 56f3ad7: converging at this read: 26 green, 3 expected skips, the test shards and the type-check still running. The PR is queued when every check completes.
Serial order: objectui#11872 (aria-roledescription, the same file) is released for dispatch after this PR merges, and objectui#11898 after that.
Noted, not filed (no defect): a keyboard pick-up dropped in place now commits the unchanged layout through onDragEnd's existing commit(prev), the same as a pointer released on its own card.
Activity
objectstack-fleet commentedon Oct 8, 2026
Path: write metadata — the form designer by keyboard and screen reader | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:ui·area:studio·pm:queue. Direction: a collision detection that answers without pointer coordinatesTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T03:56Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in the form designer's
DndContext(collisionDetection={pointerWithin}, inObjectFormDesigner) ⇒domain:ui; rationale: a Studio or console surface in objectui. Read on objectuimain4e7dd56ed1.closestCenter, orpointerWithinfalling back to it when there are no pointer coordinates)DndContextprops. Claim after it lands, or declare the overlap.Clause-②: no. Patch changeset in objectui.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 8
Session:
session_01DrKzdPdyLLBW3qpZ4vtk7zAccount:
huangyiireneBranch:
claude/issue-11871-form-designer-keyboard-dropWorktree:
objectui-issue-11871Domain:
domain:uiSeat:
domain:ui#1File surface (line numbers on
3035948):packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx:DndContext'scollisionDetection(pointerWithin, about:660);onDragOver/onDragEnd), only where a keyboard drop must reach the same commit the pointer drop reaches. The dev names any such change..changeset/11871-*.md.⛔ Not on it:
SortableField'saria-roledescription: objectui#11872, which stays queued and runs serial after this card in the same file.formDndAnnouncements.ts, landed): they must name the keyboard drop's place unchanged, and are read only.isKeptOffLayoutand the layout model; sensors other than their wiring;@dnd-kit/*versions and anypackage.json.packages/components/src/ui/**andpackages/i18n/**.Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectuiover these paths: no path-derived mandate; default tier)Clause-②: no
Responsibility:
objectui app-shell Studio form designer: collisionDetection pointerWithin answers nothing when a KeyboardEvent supplies no pointer coordinates, so a keyboard drag never reaches a drop target and keyboard-only and screen-reader users cannot arrange a form | the platform path is right: dnd-kit's KeyboardSensor is installed and its collision strategies answer without a pointer | every keyboard or screen-reader author in Studio's Form tab, measured in real Chromium by objectui#11802's devThread-read: 6051859405
Serial constraints cleared:
noneblocking.DndContextprops; claim after it lands") holds. objectui#11802 closedcompleted, landing as33e3583.SortableField) stays queued, serial after this card.area:studioin flight: seat 2's objectui#11803; seat 3's objectui#11786; this seat's objectui#11799, and objectui#11847 (PR objectui#11884, in the merge queue).ObjectFormDesigner.tsx.ObjectFormDesigner.tsx. Read 2026-10-08T04:26Z.batch3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」).Why
Clause-②: no: the collision strategy is internal to a module-internal component'sDndContext. No export, prop,@object-ui/typesmember or accepted input changes.objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{ "issue": 11871, "status": "done", "branch": "claude/issue-11871-form-designer-keyboard-drop", "pr": "https://github.com/objectstack-ai/objectui/pull/11897", "session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — the session the PM claim comment 6052216192 names (subagent run; the parent session id)", "premise_still_valid": true, "summary": "Studio's form designer DndContext now takes collisionDetection={formCollision}: a module-private helper beside the id helpers in ObjectFormDesigner.tsx that returns pointerWithin(args) when args.pointerCoordinates is set (the pointer path, unchanged) and closestCorners(args) otherwise (a keyboard drag, which has no pointer coordinates). A keyboard drag (Space, arrows, Space) now moves a field within its group and into another group, the draft records it, and objectui#11802's announcements (read only, untouched) name the drop's place. onDragOver / onDragEnd are unchanged: the keyboard reaches the pointer's commits through the existing arithmetic. Measured residual outside the fence: at multi-column widths the stock keyboard coordinate getter never targets an empty group (H3 falsified there), reported as finding 1; in a one-column canvas the empty group is reached and committed. Draft PR objectui#11897; zero label and assign writes.", "tests": "HEAD the gate union ran on: 56f3ad7 (git rev-parse --short HEAD; merge of origin/main 0aa8c0c into the branch, no rebase). Commits: 7455f4e (fix), 2c0c3e5 (pins + changeset), 56f3ad7 (merge). files_changed: packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx (+15 -1: imports closestCorners and type CollisionDetection, adds formCollision, swaps the DndContext prop); packages/app-shell/src/views/studio-design/ObjectFormDesigner.keyboardDrop-11871.test.tsx (new, 7 cases); .changeset/11871-form-designer-keyboard-drop.md (new, @object-ui/app-shell patch). GATES (command → exit → verdict line): pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build → 0 → 'VERDICT command-exit 0' (28 packages, tree clean after) | pnpm exec vitest run (DataPillar.hiddenSystemFields-11780, ObjectFormDesigner.dndAnnouncements-11802, ObjectFormDesigner.keyboardDrop-11871, ObjectFormDesigner.test, StudioDesignSurface.formFields, viewCacheInvalidation.guard; the six files git ls-files names for the PM's globs) → 0 → 'Test Files 6 passed (6) / Tests 52 passed (52)' | pnpm exec vitest run packages/app-shell/src/views/studio-design/ → 0 → 'Test Files 119 passed (119) / Tests 751 passed (751)' | pnpm --filter @object-ui/app-shell type-check → 0 (script name echoed; tsc -p tsconfig.test.json --listFilesOnly lists the new test once) | pnpm check:control-bytes → 0 → 'check-control-bytes: OK' | pnpm check:new-line-citations → 0 → 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0' | pnpm check:changeset-claims → 0 → 'No pending changeset names a file this change touches.' | pnpm check:pending-changeset-literals → 0 → 'No test source names a pending changeset.' | derived: pnpm check:test-path-roots, check:phantom-deps, check:unreferenced-sources, check:vi-mock-specifiers → 0 each (OK lines) | node scripts/check-changeset-presence.mjs → 0 → '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' | check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite → 0 each | eslint --no-inline-config --format json on the 2 changed files → 0 → 2 files, 0 errors, 1 warning (react-hooks/set-state-in-effect on the untouched draft re-sync effect). Lint narrowing evidence: population = root eslint.config.js as eslint --print-config resolves it per file; count = JSON result length 2; invariance = no parserOptions.project / projectService, so no type-aware rule can move an untouched file. Every build and test ran through os-verify-lock (slot issue-11871). REVERSE CHECKS (objectstack scripts/ablation-replace.mjs, WRAP mode inside the lock; tests import ./ObjectFormDesigner from src, no dist leg): (1) formCollision → pointerWithin alone: mutation landed (anchor 1→0, blob 6c6d0b914ebb→9410599792af), 5 failed / 2 passed, every keyboard case red in the predicted direction with no commit ('Industry was dropped outside the groups and is back in Ungrouped, position 2 of 2.'), pointer cases green; restored (blob == HEAD, git diff HEAD empty). (2) closestCorners on every drag: first attempt refused by the tool as a no-op (replacement text already inside the anchor, count 1→1) and restored; rerun with a distinct replacement landed (blob →972d6a52697a), 1 failed / 6 passed: the pointer control 'a pointer released outside every group commits nothing' went red ('Name moved to New group, position 1 of 1.'); restored (blob == HEAD, diff empty). H1: holds — source (pointerWithin returns [] for null pointerCoordinates; getEventCoordinates returns null without clientX) and real Chromium on 3035948: 7 keyboard sequences, 0 commits, each 'was dropped outside the groups'; pointer drags committed. H2: holds in a one-column canvas (480px: steps land over f: cards, or the g: droppable of an empty group); partly falsified multi-column (1280px: the getter steps card to card and never targets a section droppable, a full-width section rect loses on corner distance). H3: holds one-column (Email ArrowDown: over g:new_group, onDragOver moves it, over becomes f:email, onDragEnd's same-container branch commits unchanged — the pointer path's two-step); falsified multi-column (Email ArrowDown lands Ungrouped 2 of 3; Name ArrowUp lands Contact 1 of 3; the empty group is never reached). closestCenter vs closestCorners: identical results on every measured sequence at both widths; closestCorners chosen because it is the measure sortableKeyboardCoordinates picks targets by. BROWSER (real Chromium /opt/pw-browsers/chromium, dev-only Vite page mounting ObjectFormDesigner with the 11802 fixture, Playwright keys; harness deleted, never committed; reading on 7455f4e, whose ObjectFormDesigner.tsx equals HEAD's): within a group — 480 Phone ArrowDown → 'Phone moved to Contact, position 2 of 2.' and 1280 Phone ArrowRight → same, commit Contact [email, phone]; across groups — 480 Email ArrowDown → 'Email moved to New group, position 1 of 1.' (empty group), 1280 Email ArrowDown → 'Email moved to Ungrouped, position 2 of 3.', 480 Name ArrowUp x3 → 'Name moved to Contact, position 2 of 3.', 1280 eleven-field canvas F6 ArrowUp → 'F6 moved to Contact, position 4 of 7.'; pointer control identical before and after (Name onto New group, Phone onto Email, Name onto Email). DEVIATIONS: (a) the route section's 'including an empty one' pin holds in a one-column canvas only; multi-column needs a coordinate getter, outside the fence → finding 1, not changed; (b) the test's first run failed on the instrument, not the product: dnd-kit measures the DragOverlay's single child (the label chip), so the geometry stub now models the chip at the height Chromium measured (34px); (c) declared narrowing: full @object-ui/app-shell suite and repo-wide pnpm lint left to CI, the studio-design directory (119 files) run instead; (d) one lock attempt ended 99 (queue-timeout) and was re-taken under the same slot; (e) temporary measurement probes in ObjectFormDesigner.tsx (a URL-param strategy switch and console.info of over ids) were never committed and were removed with git checkout HEAD, git diff HEAD empty; (f) commits carry objectui AGENTS.md's model-free trailer pair (Co-authored-by: Claude), not the harness's model-named trailer; the merge commit carries git's default message; (g) no handler change was needed, so none is named.", "mcp_calls": "0 — no MCP GitHub tool used", "api_writes": "2 relay writes plus pushes — (1) fleet-write/dispatch.mjs op pr_create → objectstack-ai/objectui#11897 (relay dispatch POST to objectstack-ai/objectstack, run 37731704837 success, read-back 9529 bytes identical); (2) post-stamped.mjs --comment=11871 --repo=objectstack-ai/objectui: this os-dev-report. git push of claude/issue-11871-form-designer-keyboard-drop three times (empty branch probe, fix, merge head) — one push item per the order. Zero label, assignee or PR-body PATCH writes. Reads: gh api REST single-card, its comments, PR 11897, and objectui#11802's comments.", "open_questions": [], "out_of_scope_findings": [ "Finding 1 — class: a · reach: real Chromium, the Form tab's ObjectFormDesigner (dev harness mounting the same component, readOnly off, sensors on), keyboard only: at a 1280px viewport (two or three fields per row) Email Space ArrowDown Space lands in Ungrouped, position 2 of 3, skipping the empty New group between; ArrowUp from Name lands in Contact, skipping it too, so a keyboard author cannot put a field into an empty group at multi-column widths (one-column canvases reach it); on an eleven-field canvas the full-row Notes (textarea) does not move on ArrowUp; at 480px the first ArrowUp from a group's first field moves nothing; at 480px Phone ArrowDown x3 announces 'over Ungrouped, position 1 of 3' and drops at 'position 2 of 3' (the onDragEnd arithmetic objectui#11802 pins for a pointer drop still over the field it was carried onto). Evidence: sortableKeyboardCoordinates picks each step's target by closestCorners over every enabled droppable, and a section droppable spans the whole section, so in a multi-column grid its corner distance loses to the nearest card and its own section is the first target upward. Seam: renderer:ObjectFormDesigner KeyboardSensor coordinateGetter (sortableKeyboardCoordinates) and onDragEnd same-container branch → consumer: keyboard and screen-reader authors in Studio's Form tab. Not a sub-issue of objectui#11872 (aria-roledescription); same file, so serial with it. Dedupe words: form designer keyboard empty group unreachable · sortableKeyboardCoordinates section droppable multi-column · keyboard over position drop position mismatch", "carrier: 承接者:无 · noted, not filed — a keyboard pick-up and drop in place now commits the unchanged layout through onDragEnd's existing commit(prev), as a pointer released on its own card already does; recorded in the PR's Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Review: ACCEPT — PR objectui#11897 (head
56f3ad7)domain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· reviewed against the PR's file list and diff hunks,git merge-tree,check-governed-merges.mjs, its check runs and the dev's report on this card, read 2026-10-08T05:25Z.main. The first line isFixes #11871, andClause-②: nostarts a later line.6052216192's surface.ObjectFormDesigner.tsx(+15/−1): a module-privateformCollisionanswerspointerWithin(args)whenargs.pointerCoordinatesis set, andclosestCorners(args)otherwise.DndContexttakes it in place ofpointerWithin.onDragOver,onDragEnd, the sensors,SortableField,formDndAnnouncements.ts, everypackage.jsonandpackages/i18n/**are untouched.ObjectFormDesigner.keyboardDrop-11871.test.tsx(7 cases), and an@object-ui/app-shellpatch changeset.noholds.check-governed-merges.mjs --pr objectstack-ai/objectui#11897reports 0 of 3 paths governed, 278 changed lines. That is an ordinary queue landing.7455f4e(itsObjectFormDesigner.tsxis identical to the head's) commits each keyboard sequence at 480px and 1280px. Before the fix, 7 sequences onmainmade 0 commits.Fixes: the dispatch's "into an empty group" holds on a one-column canvas only. On a multi-column canvas, the keyboard coordinate gettersortableKeyboardCoordinatesnever steps onto an empty group's section droppable. That is the getter, outside this card's fence, and not the collision detection this card names. The card's Done-when does not name an empty group, so it is met. The gap is filed as its own card, objectui#11898, serial after this card and objectui#11872.Local union on
56f3ad7(withmain0aa8c0cmerged in): the studio-design directory has 119 files and 751 tests, all green. The six files the order's globs match have 52 tests, all green, the*.guard.test.*view-cache guard among them. The type-check,eslinton the changed files (0 errors; 1 existing warning on an effect this diff does not touch) and the derivedcheck:*set are green.Reverse checks:
pointerWithin, 5 failed and 2 passed. Every keyboard case went red and the pointer cases stayed green.closestCornersfor every drag, 1 failed and 6 passed. The pointer control "a pointer released outside every group commits nothing" went red.Each restore was proven by blob hash.
git merge-treeagainstmainis clean. No other open PR touchesObjectFormDesigner.tsx.56f3ad7: converging at this read: 26 green, 3 expected skips, the test shards and the type-check still running. The PR is queued when every check completes.aria-roledescription, the same file) is released for dispatch after this PR merges, and objectui#11898 after that.onDragEnd's existingcommit(prev), the same as a pointer released on its own card.huangyiirene.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Landed: PR objectui#11897 →
maina476deedomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· verified 2026-10-08T05:55Z.56f3ad7before queueing: 40 of 43 check runs passed, and the 3 skipped are the expected ones.check-governed-merges.mjs: not governed, +277/−1.a476deeis one squash commit on parent0855fc6. It changes 3 files, +277/−1, the same as the PR.56f3ad7, and the whole diff's-U0patch-id matches (094bb8a1…).completedby the PR'sFixes.pm:dispatchedwas removed in the same pass and read back.pointerWithin. A keyboard drag, which has no pointer coordinates, readsclosestCorners.6052983795).Generated by Claude Code
fix(app-shell): a keyboard drag in the form designer reaches a drop t…