Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
This review needs an engineering owner's decision, and it also asks for changes that stand whatever that decision is.
Owner decision needed. This branch carries the Grab zone changes from #9529. The zone stays centred on the divider whatever pillPlacement says, it narrows to half of a short pane, and the Grip pill now starts a drag. The Resizable component record states only a --spacing-4 Grab zone strip. An engineering owner (the author qualifies) needs to name the current clause that governs these behaviors, or approve them as an amendment to that record.
Required changes:
- Land #9680, #9529 and #9297 first and rebase: today the diff carries 5,269 authored lines from four pull requests, and only 1,018 of them are this pull request's own commits.
- Keep a disabled factory handle's tab stop:
separatorAttributesreturnstabIndex: -1while disabled, but the Resizable record (FR3) and the factory's ownisDisableddoc say a disabled handle keeps its tab stop. - Keep the provider case in INV3a's evidence gap: the new factory tests cover a flat option only outside a provider, so the gap should read "no factory test covers a
labelskey, or a flat option under a provider, yet".
The factory path, the shared separator derivation and the carried hydration and mid-drag fixes look right. The Resizable, Layout, SideNav and AppShell suites and the core typecheck pass at this head.
[Automated review]
| : undefined, | ||
| 'aria-valuetext': value?.isCollapsed ? value.collapsedText : undefined, | ||
| 'aria-disabled': state.isDisabled || undefined, | ||
| tabIndex: state.isDisabled ? -1 : 0, |
There was a problem hiding this comment.
A disabled separator leaves the tab order here, and the factory writes this on a plain element. The Resizable component record's FR3 says a disabled Handle keeps its tab stop, and so does the factory's own isDisabled doc. Return 0 here (and update FR3's ResizeHandle gap note and the parity tests), or keep the tab stop in the factory.
There was a problem hiding this comment.
Fixed in 5f4a410: separatorAttributes keeps tabIndex: 0 while disabled, so a disabled ResizeHandle and a factory's plain element both keep their tab stop, as FR3 says. The record drops its gap lines, and a [fix] changeset records it. The tests fail before the fix.
| | INV2, INV13, INV14 | export drift checks, public i18n tests, compatibility review | A parallel/replacement runtime lands without migration, or client hooks claim server behavior | | ||
| | INV3–INV5 | resolver and provider rerender tests | no-provider output depends on the host, locale swaps stay stale, fallback order changes, or malformed-locale behavior is described as graceful | | ||
| | INV3a | factory unit tests: a flat option or `labels` key replaces its message, including under a provider, and outside a provider its absence announces or shows shipped English; wrapper tests: under a non-English provider its absence gives the translation. Evidence gap: no factory test covers a flat option or `labels` key yet | a factory announces or shows Astryx text that no option or `labels` key can replace, groups more than one string outside one `labels` object, takes a translator, ignores a given string, or gives something other than the catalog message when it is absent | | ||
| | INV3a | factory unit tests: a flat option or `labels` key replaces its message, including under a provider, and outside a provider its absence announces or shows shipped English; wrapper tests: under a non-English provider its absence gives the translation. Evidence gap: no factory test covers a `labels` key yet | a factory announces or shows Astryx text that no option or `labels` key can replace, groups more than one string outside one `labels` object, takes a translator, ignores a given string, or gives something other than the catalog message when it is absent | |
There was a problem hiding this comment.
The new factory tests cover a flat option only outside a provider, and this row asks for the case under a provider too. Suggest: "Evidence gap: no factory test covers a labels key, or a flat option under a provider, yet."
There was a problem hiding this comment.
Taken in 5f4a410: "no factory test covers a labels key, or a flat option under a provider, yet".
d76b9f9 to
5f4a410
Compare
There was a problem hiding this comment.
This pull request needs an engineering owner's spec decision, and it also needs changes that stand whatever that decision is.
Owner decision needed. The branch carries the Grab zone changes from #9529 and writes them into the Resizable record's FR1: the zone stays centred on the divider whatever pillPlacement says, shrinks so a short pane keeps half of itself, and the Grip pill takes the pointer. The current FR1 states only a --spacing-4 strip. The same FR1 text was approved on #9529's head, but that approval does not carry over to this head. An engineering owner records the decision by commenting /approve-spec with this pull request's full head SHA (the author may, as an engineering owner), or #9529 lands first. The decision should also square the shrunk zone and an offset pill with the bidirectionality record's DR8, which keeps a pointer target centred on the visible control at its required size.
Required changes:
- Land #9680, #9529 and #9297 first and rebase: the diff carries 5,290 authored lines from four pull requests, and only 1,037 of them are this pull request's own commits.
- Delete FR3's "Implementation gap: a disabled Handle sets
tabIndex={-1}" sentence and the matching verification-row gap in the Resizable record, which this pull request's own fix makes false, and reword the disabled state's summary inResizeHandle.a11y.states.ts.
The factory now keeps a disabled handle's tab stop and INV3a's evidence gap names the provider case, as asked last time. The Resizable, Layout and SideNav suites and the core typecheck pass at this head.
[Automated review]
| | FR1 | ResizeHandle renders one focusable Handle and one enlarged Grab zone (a `--spacing-4` strip on the resize axis); the default render also contains a Grip pill, and caller `children` replace the pill. | Current source, docs, and tests | | ||
| | FR1 | ResizeHandle renders one focusable Handle and one enlarged Grab zone (a `--spacing-4` strip on the resize axis); the default render also contains a Grip pill, and caller `children` replace the pill. The grab zone is centred on the Handle for every `pillPlacement`, and the painted pill is its own grab target. On a pane too short for the full strip, the zone shrinks so the pane keeps at least half of itself. | Current source, docs, tests, and an owner ruling | | ||
| | FR2 | The Handle carries `resize-handle`, the Grip pill carries `resize-handle-pill`, and the Grab zone carries no public target. | Current source and docs | | ||
| | FR3 | Pointer drag, keyboard, double-click collapse, snapping, persistence, reversal, and size behavior are owned by ResizeHandle and useResizable; dragging under right-to-left inverts the pointer delta so the divider follows the pointer. A disabled Handle ignores pointer and keyboard input and exposes `aria-disabled`, and keeps its tab stop (`spec:AST-085` FR1). With `autoSaveId`, storage holds the last settled size and receives the final size once when a drag ends, cancels, or unmounts. Implementation gap: a disabled Handle sets `tabIndex={-1}` (Current-state impact). | Current source, docs, and tests | |
There was a problem hiding this comment.
This FR3 sentence still says a disabled Handle sets tabIndex={-1} and points at a Current-state entry this pull request deletes, while the code now keeps tabIndex 0. Drop "Implementation gap: a disabled Handle sets tabIndex={-1} (Current-state impact)."
| | FR1 | `ResizeHandle.test.tsx`; stable visual baseline `core-resizable--horizontal-split` | Default, collapsed, both axes | Handle and Grab zone regressions fail focused tests; a changed default handle comes back changed for review. Gap: no test renders `children` to prove they replace the Grip pill. | `audit:Resizable/anatomy` | | ||
| | FR1 | `ResizeHandle.test.tsx`; `grabZone.interaction.browser.spec.ts` for the zone's place and its short-pane limit; stable visual baseline `core-resizable--horizontal-split` | Default, collapsed, both axes | Handle and Grab zone regressions fail focused tests; a changed default handle comes back changed for review. Gap: no test renders `children` to prove they replace the Grip pill. | `audit:Resizable/anatomy` | | ||
| | FR2 | `themingTargets.test.ts`; source review | Handle, Grip pill, Grab zone | A removed target fails the inventory. `themingTargets.test.ts` catches only a renamed or undocumented target; no check fails if the Handle or Grip pill stops carrying its target. | `audit:Resizable/theming` | | ||
| | FR3 | `ResizeHandle.test.tsx` and `useResizable.test.ts`; storage timing: `ResizeHandle.test.tsx` ("saves the size once, when a drag ends on …", "saves the dragged size once when the split unmounts mid-drag", "still saves each keyboard step at once") and `useResizable.test.ts` ("writes the size a drag reached once, after … releases it", "writes the final size once when the release comes before the last move renders", "keeps the size a region was dragged to when it unmounts mid-drag", "writes only the re-clamped size when the container shrank mid-drag") | Pointer, keyboard, snap, collapse, persistence; RTL drag; disabled keyboard | A changed transformation, an RTL drag that moves against the pointer, keyboard input that resizes a disabled Handle, or a per-move, stale or missing final storage write fails. Gap: no test covers disabled pointer input or the disabled tab stop. | `audit:Resizable/behavior` | |
There was a problem hiding this comment.
ResizeHandle.test.tsx ("ignores keyboard input when disabled, and keeps its tab stop") now covers the disabled tab stop. End this gap at "no test covers disabled pointer input".
There was a problem hiding this comment.
Narrowed in 1aca7db: the gap now ends at disabled pointer input.
| return direction === 'vertical' ? window.innerHeight : window.innerWidth; | ||
| } | ||
|
|
||
| /** A warning already given, so a resolution repeated silently stays quiet. */ |
There was a problem hiding this comment.
Non-blocking (INV29): this comment is vague. Suggest: "Starts as warned. The factory warns; this first-render read does not."
There was a problem hiding this comment.
Taken in 1aca7db, slightly reworded: "Starts as warned: the factory warns, and this first-render read does not."
…ment says, and a short item keeps half of itself for its own drag (component:Resizable FR1) A browser fixture story draws a calendar column at 48px an hour with a 30-minute and a 15-minute event, each moved by a drag on its body and resized by the ResizeHandle on its bottom edge. Red before the fix: - with pillPlacement auto, start or end, the Grab zone's centre sits 5.5 to 8.5px off the Handle's, because it follows the pill; - the zone covers 13.5px of the 24px event with the pill on the event's side, and 8px of the 15-minute event's 12px even when centred, so the events' own drag misses points of their upper half; - the same holds for position="overlay". A drag on the 30-minute event's body moves it, and a drag on its Handle's line resizes it.
…ent says, and takes at most half of a short pane (component:Resizable FR1) FR1 puts the Grab zone, a --spacing-4 strip on the resize axis, on the Handle; pillPlacement only places the pill. The zone followed the pill instead, shifting up to a strip's width toward it, so on a short item (a 30-minute calendar event resized from its edge) it took most of the item and stole its own drag. The zone now sits centred on the Handle for every pillPlacement. On a pane shorter than the zone, it takes at most half the pane, as spec:AST-094 FR42's grown targets stop at half the gap: an inline Handle's zone is at most the pane's length, so half of it reaches into the pane, and an overlay Handle takes at most half of the pane it sits in, with the zone kept inside it. A collapsed pane keeps the whole zone, which reopens it. The unit tests that pinned the zone to the pill now pin it to the Handle, with one for the short-pane limit and the collapsed pane; the docs say pillPlacement moves only the pill, and a [fix] changeset records it.
…Grab zone A region at 0px that is not collapsed gets a zero-length inline zone (min(var(--spacing-4), 0px)) and an overlay Handle clamped to half of nothing, so no drag can reopen it. Both cases fail here.
…rab zone A pane with no length has nothing to protect, so it is treated like a collapsed one: the inline zone and the overlay Handle keep their full length, and a drag can reopen the pane. The duplicate comment at the zone's call site goes.
With the Grab zone centred on the Handle, a pill that pillPlacement draws outside the zone takes no pointer: on an overlay Handle the start pill sits before the Handle's box, so a drag on it moves a 30-minute event instead of resizing it, and an inline end pill on a short pane hangs past the zone's end. The browser spec drags the pill's centre for every pillPlacement and both positions, and the unit test starts a drag from the pill. The upper-half check now skips the pill's own box.
The pill is the Grip, so it stays grabbable wherever pillPlacement paints it: while it is painted, it takes the same pointer handlers as the Grab zone, with the resize cursor, and a hidden or disabled pill takes none. The Grab zone keeps its short-pane limit, and pillPlacement still moves only the pill. The docs and the [fix] changeset say so, and the changeset now says that an overlay Handle takes at most half of the pane it sits in.
Each comment sentence the Grab zone change adds has at most 20 words, and the test comment cites no record. ResizeHandle's header drops the paragraph on the pill's dynamic offset, which now sits on that style. No code change.
…s and how it shrinks on a short pane FR1 named the Grab zone only as a --spacing-4 strip. It now adds the owner's ruling, word for word: the grab zone is centred on the Handle for every pillPlacement, and the painted pill is its own grab target; on a pane too short for the full strip, the zone shrinks so the pane keeps at least half of itself. FR1's basis names the owner ruling, and its verification row names the browser spec that checks the zone's place and its short-pane limit.
…e it starts, and a restating comment goes A drag that starts on the Grip pill takes capture on the pill, not on the grab zone, so the header names the element the drag starts on. The comment over the overlay zone's width restated its min(…, 100%).
… reachable from the Grab zone An overlay Handle can draw its pill outside the Grab zone: at start in left-to-right, and at end in right-to-left. A pill that shows only on hover then hides as the pointer leaves the zone to reach it, and a drag from where it showed does nothing. A story puts a hover-revealed pill beside an overlay Handle, and the browser spec crosses from the zone to the pill and drags it, in both directions. They fail here: the size stays 250 after the drag.
An overlay Handle can draw its pill outside the Grab zone, so the pointer leaves the zone to reach it. The hover now stays for 150 ms after the pointer leaves the zone or the pill, and entering either one keeps it. So a hover-revealed pill stays shown and takes the pointer while the pointer crosses to it. The pill's offset functions take -1 | 1.
5f4a410 to
1aca7db
Compare
|
/approve-spec 1aca7db |
There was a problem hiding this comment.
Changes requested: two changes stand, and the spec question is settled.
- Land #9529 and #9297 first and rebase: the diff carries 4,602 authored lines, and only 1,045 of them are this pull request's own commits.
- Add a one-line note to the new hover-grace Effect in
ResizeHandle.tsxnaming what it synchronizes (the pill's hover timer, cleared on unmount), as the React component runtime record's INV3 requires of every new Effect.
The Resizable record's FR1 Grab zone amendment, with the 150 ms pill hover grace, is approved at this head, and FR3's stale tab-stop gap is fixed. The Resizable, Layout, SideNav and AppShell suites and the core typecheck pass at this head.
[Automated review]
| const handleRef = useRef<HTMLDivElement>(null); | ||
| const [isHovered, setIsHovered] = useState(false); | ||
| const hoverExitRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
| useEffect( |
There was a problem hiding this comment.
This new Effect names no external system, where architecture:react-component-runtime INV3 asks every new Effect to. It is needed (nothing else runs at unmount to clear the 150 ms timer onPointerLeave starts), so one line above it is enough, for example:
// The pill's hover grace timer: cleared on unmount, so it sets no state after.| } | ||
| } | ||
|
|
||
| // --- The handle's attributes --- |
There was a problem hiding this comment.
Non-blocking (INV29): this divider, and the ones at lines 766 and 910, restate the code under them. Drop them.
…rests between the Grab zone and the pill The browser case now rests 300 ms in the gap between the zone and the pill before it reaches the pill and drags. A pointer that pauses on the way must not lose the pill, so the pill has to stay reachable by its geometry, not by a grace time. It fails here: the 150 ms hover grace runs out during the rest, and the pill hides (opacity 0).
…rab zone, with no hover timer The 150 ms hover grace was a second hover-timing implementation, which architecture:interaction-modality INV11 keeps in the shared hover recognizer. A pointer that rested in the gap also lost the pill. The pill shown on hover now carries an invisible extension along the resize axis that spans the --spacing-1 gap to the zone. It takes the pointer only while the pill does. The timer and its unmount Effect go. The header keeps its tags and the capture sentence, and the short-pane rule is stated once, where the pane length is read.
…ct, the configuration-object useResizable builds on it, and a server-rendered region hydrates with the server markup - createResizable: collapse, expand and resize on request, a gesture through the region's model as a ResizeHandle drives it, bounds and snap points, a percentage against its container, saving once per gesture, update() with the full set of options, subscribe() and destroy, and an import that does no DOM work. They fail here: the module does not exist. - With a stand-in factory, useResizable(config) creates one, from the configuration, passes later options through update(), destroys it on unmount and returns what it reports. They fail here. - A region with an autoSaveId whose size is saved hydrates over server markup with the default, and a '30%' default with no container hydrates over markup the server resolved at its 1200px basis. Both should hydrate cleanly, then take the client's size. Both fail here: the first logs a hydration mismatch and keeps 200, the second keeps 360. - Guards that pass here and after: the configuration-object form reports in the commit of onCollapseChange, controlled or not; it keeps its size and collapse state across an <Activity>; collapse(), expand() and resize() apply before its first layout effect and while hidden; its warnings start useResizable: in the words they always used; it follows a container swapped behind its ref; and a '30%' default resolves against its container once measured, after an <Activity> hid and showed it.
…and the configuration-object useResizable builds on it createResizable, internal to the package, keeps a resizable region with no React and no element: it resolves, clamps, snaps, collapses and saves the size through the model in resizableModel.ts, follows a percentage's basis, and reports through subscribe and onSizeChange. ResizeHandle's resizable path drives it through resizeGesture.ts's model. The configuration-object useResizable is that region in React, with one factory, so the region's state has one copy. Its props carry what the factory reports, and its signature, props, warnings and one-render-pass mounts are unchanged. While its component is unmounted or an <Activity> hides it, the factory lets go of the page and keeps the region, so collapse(), expand() and resize() apply and report whenever they are called. Its server snapshot is the state the server renders, so a hydrating render matches the server's markup, and a saved size or a percentage default resolved against the window applies in the next commit. The comment-provenance baseline drops the useResizable.ts sites this removes. A [chore] changeset records the move, and two [fix] changesets record the hydration behaviors.
On an <Activity> reveal, follow() resumes the same factory; none is made for the reveal. The comment now says so.
…s the factory again after another one A caller that switches between two stable configuration objects, WIDE, then NARROW, then WIDE, must get WIDE's bounds back. It fails here: the hook compares each configuration with the one it created the factory with, so the last switch never reaches update().
…e factory has now The hook kept the options it created the factory with and compared every later configuration against them. So WIDE, then NARROW, then WIDE skipped the last update() and kept NARROW's bounds. The slot entry now holds the options last passed to the factory, at creation or at an update.
…one separator derivation - createResizable on a plain element: the separator it makes, keeping a role, name and tab stop the element has, label and collapsedLabel, a pointer drag that holds its pointer (an iframe's element included), bounds and snap points, right-to-left and a reversed handle, the keys, Enter and a double-click, disabled input that leaves the tab order, saving through a drag, and a destroy that leaves nothing behind. - separatorAttributes: an open, collapsed, disabled and unbounded region, and no region. - ResizeHandle and a factory driving a plain element announce the same separator attributes, enabled, disabled and collapsed. - The region's tests and the configuration-object form's stand-in take the factory's handle argument, null for a region with no handle. They fail here: createResizable takes no handle, and separatorAttributes does not exist.
…esizeHandle and the factory derive the separator's attributes once createResizable(handle, options) takes a handle element, or null for a region with no handle yet. On an element it gives role="separator", a tab stop and a name when the element has none, its orientation and value, and the window splitter's input from resizeGesture.ts: a pointer drag that holds its pointer, the keys, Enter and a double-click. Its options add label, collapsedLabel, isReversed and isDisabled, and destroy takes all of it away. Its name and collapsed text are the shipped English messages, and collapsedLabel replaces the collapsed one. separatorAttributes derives the separator's ARIA and tab stop once. ResizeHandle renders it and the factory writes it on a plain element, so a disabled handle leaves the tab order in both. The configuration-object useResizable creates its factory with no handle. The factory stays internal, with a [chore] changeset.
…rrows to the labels key A factory test now covers a flat option: the factory announces collapsedLabel in place of the catalog's "Collapsed" on a plain handle. The gap that remains is a labels key.
…e and on a factory's plain element component:Resizable FR3 keeps a disabled Handle's tab stop, as spec:AST-085 FR1 does for every disabled control, so keyboard users can reach it and hear that it is disabled. ResizeHandle, the factory on a plain element and separatorAttributes each keep tabindex 0 while disabled. They fail here: a disabled separator leaves the tab order.
…sizable FR3 says separatorAttributes gave a disabled separator tabIndex -1, so a disabled ResizeHandle left the tab order and the factory wrote the same gap onto a plain element. FR3, after spec:AST-085 FR1, keeps a disabled Handle's tab stop so keyboard users can reach it and hear that it is disabled. The derivation now keeps tabIndex 0, and both still ignore input and say aria-disabled. A [fix] changeset records it.
…s evidence gap names the provider case component:Resizable no longer lists a disabled Handle leaving the tab order: its Current-state line goes, the disabled state row no longer calls the kept tab stop a gap, and the AST-085 relation no longer says FR3 records one. architecture:internationalization's INV3a evidence gap now reads: no factory test covers a labels key, or a flat option under a provider, yet.
…ew one and its target An accessibility [fix] names what changed from what, and toward what: before, a disabled ResizeHandle left the tab order; now it keeps its tab stop, as a disabled control does.
…d state's summary keeps it in the Tab order The fix below made FR3's implementation gap false: a disabled Handle keeps tabindex 0. FR3 drops that sentence, and its verification row no longer lists the disabled tab stop as untested, since ResizeHandle.test.tsx covers it. The disabled binding state's summary says the handle stays in the Tab order. A comment on the first-render read says why it starts as warned.
1aca7db to
6a1ed94
Compare
There was a problem hiding this comment.
This pull request needs an engineering owner's decision, and changes that stand either way.
Owner decision needed. A hover-revealed Grip pill now gets an invisible 13px extension in ResizeHandle.tsx that keeps the hover and starts a drag, in place of the approved 150 ms hover grace, while the Resizable record's FR1 names only the Grab zone and the painted pill as grab targets. An engineering owner either names the current clause that covers it, or approves it as part of FR1 by commenting /approve-spec <full head SHA> once FR1 states it (the author may do so as an engineering owner).
Required changes:
- Land #9529 and #9297 first and rebase: the diff carries 4,626 authored lines, and only 1,043 of them are this pull request's own commits.
- Add a one-line note to the options Effect in
useResizable.tsnaming what it synchronizes (the factory it passes new options to), as the React component runtime record's INV3 requires of every changed Effect.
The plain-handle path, the shared separator derivation and the disabled tab-stop fix look right, and the Resizable, Layout, SideNav and AppShell suites and the core typecheck pass at this head.
[Automated review]
| }, | ||
| [slot], | ||
| ); | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This changed Effect names no external system, where architecture:react-component-runtime INV3 asks every new or changed Effect to. It is needed (update reaches the factory, a store outside React, and cannot run during render), so one line above it is enough, for example:
// The factory: each new options object reaches it through update(), as a new render's would.| pointerEvents: 'auto', | ||
| backgroundColor: colorVars['--color-border-emphasized'], | ||
| }, | ||
| // A pill shown on hover can sit outside the Grab zone. This invisible |
There was a problem hiding this comment.
Owner decision needed: this invisible extension keeps a hover-revealed pill shown and starts a drag in the gap beside the Grab zone, while the Resizable record's FR1 names only the zone and the painted pill as grab targets. An engineering owner either names the clause that covers it or approves it as part of FR1.
Stacked on #9297 and #9529, whose commits this branch shows until they merge. Its own commits: vjeux/astryx@555b70d...6a1ed94. #9370's ref form calls this path.
Maintainer impact
spec:AST-107FR6 has each interaction hook be its factory's thin React wrapper, so the factory must drive a handle element. This gives the internal factory that path and one separator derivation shared with ResizeHandle, split from #9370.Intended invariant
No public API or visual change; the factory stays internal until #9372 exports it. One behavior changes, a
[fix]that restorescomponent:ResizableFR3 (spec:AST-085FR1): a disabled ResizeHandle keeps its tab stop, so keyboard users can reach it and hear that it is disabled.Change
createResizable(handle, options)takes an element, or null for a region with no handle. On an element it adds the separator's role, tab stop, name, orientation and value, and the input ofresizeGesture.ts;destroyremoves them. The options addlabel,collapsedLabel,isReversedandisDisabled, with shipped English as the default text.separatorAttributes.tsderives the separator's ARIA and tab stop. ResizeHandle renders it and the factory writes it, so a disabled handle keeps its tab stop in both.[chore]and[fix]changesets. INV3a's evidence gap narrows to alabelskey or a flat option under a provider, and component:Resizable drops the closed tab-stop gap.Evidence
separatorAttributes; 3 failures: a disabled handle left the tab order).Scope
[fix]above.Writing rubric results
Over
architecture:internationalization(INV3a's evidence gap) andcomponent:Resizable(the closed tab-stop gap, in FR3, its verification row, the Current-state line, the state row and the relation). The records are owner-gated, so they wait for a spec owner's/approve-spec.Testing
Local checks at 6a1ed94: core typecheck, the Resizable, Layout, SideNav and AppShell units (801 passed), strict eslint and prettier on the changed files, and the comment-length check. The test commits fail before their changes (36 failures, then 3). The full gates run at this head once CodeBunny approves.