Repository navigation
refactor(Resizable): one internal createResizable factory, which useResizable's configuration-object form and ResizeHandle's resizable path go through - #9297
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 needs a spec decision before it can be approved. The rewritten component:Resizable FR3 owner sentence is a new normative statement, so an engineering owner (the author qualifies) needs to approve it by commenting /approve-spec <full head SHA>, or say what to change. Separately, these need changes:
- Split the 5,756 changed lines: land #9197 first, then the factory, the
useResizable(handleRef, options)overload, and the move of the configuration-object form and ResizeHandle onto the factory. - A
ResizeHandleor plain element that a child mounts from its own state is never bound to the region (no value, and keys and drags do nothing), becauseuseResizablere-reads the ref only when its own component commits. - A configuration object kept stable across renders no longer follows a container replaced behind its
containerRef, as it does on the base, so the changeset's "behavior unchanged" does not hold yet.
[Automated review]
| // attributes, and a factory with no handle keeps the region answering | ||
| // collapse, expand and resize; a new element gets a new factory. Each new | ||
| // factory starts where the old one stood. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This effect re-reads ref.current only when this hook's component commits. A ResizeHandle or plain element that a child mounts from its own state is therefore never bound: the separator has no aria-valuenow, and keys and drags do nothing. An element a child removes keeps its factory until the owner re-renders. ResizeHandle already receives this ref object as ref, so registering the slot against the ref object and letting ResizeHandle's claimRef ask it to re-check on attach and detach would cover the component case.
There was a problem hiding this comment.
Fixed in #9370: ResizeHandle's callback ref tells the hook watching its ref as the element attaches and lets go, so a handle a child mounts, or hides and shows in its own <Activity>, is bound at once (tests there).
| }, | ||
| [slot], | ||
| ); | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
update runs only when options changes identity, and the factory re-reads containerRef.current only inside commit(). With a config kept stable (a module constant or useMemo) and its container replaced behind the same ref, the factory keeps observing the old element: maxSize: '50%' stays at 199px, where the base moves to 399px. Re-syncing the container on every commit would keep the released behavior.
There was a problem hiding this comment.
Fixed here: the factory re-reads the container at each commit of the hook's component, so a configuration object kept across renders follows a swapped container (test added).
| name: 'options.isDisabled', | ||
| type: 'boolean', | ||
| description: | ||
| 'Ignores the drag and the keys and sets `aria-disabled`. The handle keeps its tab stop, so it stays discoverable. On a ResizeHandle, its own `isDisabled` prop does the same: either one disables the handle.', |
There was a problem hiding this comment.
On a ResizeHandle this option gives tabindex="-1" (ResizeHandle.tsx:599, the FR3 gap), so "keeps its tab stop" holds only for a plain element. Likewise, a ResizeHandle ignores collapsedLabel.
There was a problem hiding this comment.
The doc page, now in #9372, says a plain element keeps its tab stop and a ResizeHandle takes its own label; collapsedLabel is gone.
eedebe1 to
73ca4d9
Compare
There was a problem hiding this comment.
This needs a spec decision before it can be approved. The rewritten component:Resizable FR3 owner sentence and the factory's new handle options (isReversed, isDisabled, label, collapsedLabel) have no current spec authority, so an engineering owner (the author qualifies) needs to approve them by commenting /approve-spec <full head SHA>, or say what to change. Separately, these need changes:
- A configuration-object region returns to its default size when an
<Activity>hides and shows it, because the hook's unmount cleanup discards the factory's state. - A ref-form
ResizeHandlethat a child mounts, or hides and shows inside its own<Activity>, is never bound, so it has no value and ignores keys and drags. - A configuration object kept stable across renders no longer follows a container swapped behind
containerRef, so the changeset's "behavior unchanged" does not hold yet. - The new warn-once Effect in
ResizeHandleshould useuseDevWarning, and the 5,045 changed lines should be split into the factory, the ref form, and the migration onto it.
[Automated review]
| useIsomorphicLayoutEffect( | ||
| () => () => { | ||
| slot.get()?.resizable.destroy(); | ||
| slot.set(null); | ||
| }, | ||
| [slot], | ||
| ); |
There was a problem hiding this comment.
This cleanup also runs when an <Activity> hides the subtree, and the reveal then creates a fresh factory from optionsRef.current, so the user's size is lost: a sidebar dragged to 300px comes back at its 250px default, where the base keeps 300px. Keeping readResizableSelection(...) of the destroyed factory in the slot and seeding the next factory from it, as the swap path above does, would keep it.
There was a problem hiding this comment.
Fixed here: the hook keeps where the region stood while an <Activity> hides it and seeds the next factory from it (test added).
| export function releaseResizeHandle( | ||
| element: Element, | ||
| claim: ResizeHandleClaim, | ||
| ): void { | ||
| if (claims.get(element) === claim) { | ||
| claims.delete(element); | ||
| claim.set(null); | ||
| } | ||
| } |
There was a problem hiding this comment.
Releasing clears the claim, but the factory binds only once, at creation (createResizable.ts:981), and the hook's effect returns early for the same element. After a child's <Activity> hides and shows the handle it stays unbound, with no aria-valuenow and no key or drag input, even after the owner re-renders. Keeping the binding per element and restoring it in claimResizeHandle would cover this.
There was a problem hiding this comment.
Fixed in #9370: a factory stays bound to its element while the factory lives, so a handle that lets go and claims the element again renders it again.
| useEffect(() => { | ||
| if (resizable != null && binding != null && !didWarnBothRef.current) { | ||
| didWarnBothRef.current = true; | ||
| devWarn( | ||
| 'ResizeHandle', | ||
| 'has a `resizable` prop and a useResizable(handleRef) region at ' + | ||
| 'once. The `resizable` prop wins; pass one of them.', | ||
| ); | ||
| } | ||
| toggleCollapse(resizable); | ||
| }, [isDisabled, resizable]); | ||
| }, [resizable, binding]); |
There was a problem hiding this comment.
useDevWarning('ResizeHandle', message, resizable != null && binding != null) (hooks/useDevWarning.ts) already owns warn-once-per-mount; this Effect and didWarnBothRef copy it, and the axis warning above can move to it too.
There was a problem hiding this comment.
Fixed in #9370: the warn-once uses useDevWarning.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] `createResizable(handle, options)` makes a plain element a window splitter that resizes a region, without React: a pointer drag that holds its pointer across embedded frames, the arrow keys, Home, End, Enter and a double-click, with bounds, snap points, collapse and saved sizes. It reports the size through `subscribe` and `onSizeChange`, and you size the panel; `destroy` removes every listener and attribute it added. `useResizable(handleRef, options)` is its React form, beside the configuration-object form: give the same ref to `ResizeHandle` (or to a plain element) and render the panel at `size`; a plain element is named and announced through the message catalog. `ResizeHandle` and the configuration-object form go through the same implementation, with their behavior unchanged. |
There was a problem hiding this comment.
"with their behavior unchanged" does not hold yet for the configuration-object form: its size is lost across an <Activity> hide and reveal, and a memoized config no longer follows a container swapped behind containerRef.
There was a problem hiding this comment.
This pull request's [chore] changeset says no behavior changes, and both cases named here are fixed and tested.
73ca4d9 to
a8a8a2f
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision: no current spec decides createResizable's isReversed, isDisabled and label options, the controller's update and subscribe, or a null handle, so an owner needs to name the record that governs them, or have them added to component:Resizable's public contract and record the approval by commenting /approve-spec <full head SHA> (the author may do so as an engineering owner). Independently of that decision, these need changes:
resizeGesture.tsandcreateResizable.tsre-implement ResizeHandle's input anduseResizable's region state; route ResizeHandle throughcreateResizeGestureand share the region logic so each concern has one implementation.update()replaces every option, so a partial call reopens a collapsed region and drops its bounds, label and saving; merge the options or document that the full set is required.- The change is 2,325 authored lines, over the 2,000-line limit; recheck the size once the points above are settled.
[Automated review]
| * region holding an embedded frame otherwise loses the drag the moment the | ||
| * pointer crosses the frame. | ||
| */ | ||
| export function createResizeGesture( |
There was a problem hiding this comment.
This re-implements ResizeHandle's own handlers (ResizeHandle.tsx:518-688, with adjacentSnapPoint, toggleCollapse and the body cursor at :107-115 and :373-419), and ResizeHandle does not call it. Routing ResizeHandle through createResizeGesture here (its event types already accept React events) would leave one implementation of the splitter's input.
There was a problem hiding this comment.
Done here: ResizeHandle's resizable path now goes through createResizeGesture, so the splitter's input has one implementation.
| } | ||
|
|
||
| /** The region's state from its options, its basis and its selection. */ | ||
| function compute(): void { |
There was a problem hiding this comment.
compute(), commit(), persist() and the model below repeat useResizable's single-region state (useResizable.ts:278-291, :482-570, :628-653, :668-759, :792-805), so a clamping or saving fix in one misses the other. Sharing these decisions through resizableModel.ts, or having the hook delegate to this factory, keeps one path.
There was a problem hiding this comment.
Done here: useResizable(config) builds on this factory, so the region's state has one copy.
| if (isDestroyed) { | ||
| return; | ||
| } | ||
| opts = next; |
There was a problem hiding this comment.
update replaces the whole options object. On a collapsed, saved region with bounds 200–400, update({isDisabled: true}) reopens it at 300px, resets the bounds to 50 and unbounded, renames the handle "Resize handle", stops saving and drops onSizeChange, while the JSDoc and doc page say state is kept. Either merge next over the current options or document that the full set is required.
There was a problem hiding this comment.
Documented: update() takes the full set of options, as a new render does; an option left out returns to its default, and the region keeps its size and collapse state. A test pins it.
| }, | ||
| sidebar: { | ||
| flexShrink: 0, | ||
| padding: spacingVars['--spacing-3'], |
There was a problem hiding this comment.
With content-box sizing this padding stays when width is 0px, so the collapsed sidebar keeps a 24px strip with a clipped "S" and "0". Padding an inner element avoids it.
There was a problem hiding this comment.
Fixed in #9372: the sidebar pads an inner element, so collapsed to 0 it leaves no strip.
a8a8a2f to
86812b8
Compare
There was a problem hiding this comment.
Changes requested: the internal factory looks correct and its tests pass, but resizeGesture.ts and createResizable.ts copy ResizeHandle's input and useResizable's single-region state instead of extracting them, so after this merges every splitter fix has to land twice, and the copies already differ (a disabled handle's tab stop, and which document gets the drag cursor). Please extract rather than copy, as #9197 did for the model: route ResizeHandle's handlers through createResizeGesture, and share the region decisions through resizableModel.ts (or land the factory together with the stacked move of useResizable onto it). No spec decision is needed for this cut, since nothing exports the factory, and the earlier notes about update() and the story no longer apply.
[Automated review]
…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.
f76c7f8 to
f8deefe
Compare
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved, and three changes either way.
Because this branch is stacked on #9529, it amends component:Resizable FR1 with text an engineering owner approved on #9529 but not on this pull request, and #9529 has since added a 150 ms delay before a hover-revealed grip hides, which FR1 does not state. An engineering owner records the decision by commenting /approve-spec <full head SHA> here and saying whether it covers that delay (the author may, as an engineering owner), or this waits until #9529 lands and FR1 is current on main.
Required either way:
useResizableskipsupdate()when it is given the same configuration object it was created with, so switching between two stable objects (WIDE→NARROW→WIDE) keepsNARROW's bounds (useResizable.ts:551); please compare against the options last passed to the factory and add a test for that switch.- In right-to-left, SideNav's hover-revealed grip sits 4px outside the Grab zone and hides once the pointer has been off the zone for 150 ms, so a slower approach can no longer drag from it, while left-to-right and main have no time limit; please keep the grip reachable from the zone with no time limit, and add a browser case that waits in the gap.
- At 3,653 changed lines, 841 of them from #9529, please land #9529 first (with the fix above), or take it out from under this branch, so this pull request ships only the factory and the hook moved onto it.
The factory, the hook migration and the two hydration [fix] entries look right, and both hydration tests fail on main.
[Automated review]
| ); | ||
| useIsomorphicLayoutEffect(() => { | ||
| const entry = slot.get(); | ||
| if (entry != null && options !== entry.createdWith) { |
There was a problem hiding this comment.
createdWith is the object the factory was created with and is never refreshed, so a caller that switches WIDE → NARROW → WIDE (two stable objects) skips the last update() and keeps NARROW's bounds; the base follows WIDE again. Comparing against the options last passed to the factory would fix it, with a test for that switch.
| onPointerLeave: () => { | ||
| if (!isDragging && hoverExitRef.current == null) { | ||
| hoverExitRef.current = setTimeout(() => { | ||
| hoverExitRef.current = null; | ||
| setIsHovered(false); | ||
| }, PILL_HOVER_GRACE_MS); |
There was a problem hiding this comment.
In right-to-left SideNav the end pill sits 4px outside the zone, so this 150 ms delay is the only thing keeping it reachable: a pointer that rests in the gap for 400 ms finds the pill hidden and a drag from it does nothing, while left-to-right (pill inside the zone) and main have no time limit. Keeping the pill reachable from the zone with no time limit, plus a browser case that waits in the gap, would close it.
…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.
f8deefe to
555b70d
Compare
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved, and two changes either way.
Because this branch is stacked on #9529, it amends component:Resizable FR1 (the Grab zone centred on the Handle for every pillPlacement, the painted grip as its own grab target, the zone shrinking on a short pane) with text an engineering owner approved on #9529 but not on this pull request, and #9529 has since added an extension of a hover-revealed grip's hit area that FR1 does not state. An engineering owner records the decision by commenting /approve-spec <full head SHA> here and saying whether it covers that extension (the author may do so as an engineering owner), or this waits until #9529 lands and FR1 is current on main.
Blocking either way:
- A hover-revealed grip's extension (
ResizeHandle.tsx:225-244, applied at:658-662) covers the whole upper half of a pane under 24px across the grip's width, so a 12px item dragged from near its top resizes instead of moving, against the changeset's "a short item … keeps its own drag"; limit the extension to the gap it bridges (or clamp it on a short pane, as the zone is) and add a short-items browser case withisAlwaysVisible={false}. - The diff is 3,679 authored lines, 855 of them #9529's separately reviewable Grab zone change; land #9529 first or take it out from under this branch, leaving this pull request's own 2,824 lines.
The configuration-object regression and the right-to-left SideNav grip from the last round are fixed, and the factory, the hook migration and both hydration fixes match the current records.
[Automated review]
| !isAlwaysVisible && | ||
| effectiveSide !== 'center' && | ||
| (isHorizontal | ||
| ? styles.pillBridgeHorizontal | ||
| : styles.pillBridgeVertical), |
There was a problem hiding this comment.
This extension is added on both sides of a hover-revealed grip, also where the grip already touches the zone. On a pane under 24px the far side covers the pane's upper half: on a 12px item with isAlwaysVisible={false}, after the pointer touches the zone, every point of the item's upper half at the grip's column hits the grip, and a drag there resizes it from 12 to 6px instead of reaching the item's own drag. Extending only toward the Handle, and only where the grip sits outside the zone (or clamping it on a short pane as the zone is), would keep the short-item drag the changeset describes.
…side The hover-pill browser case now checks the point 3 px past the pill's side away from the zone: it must find the pane, not the pill. So the pill's hit area cannot cover more of a short item than the pill does. It fails here: the pill's bridge reaches out on both sides, so the point past the far side finds the pill.
…e Handle The pill's invisible extension reached out on both sides along the resize axis. On a short item, its far side covered the item's own drag at the pill's column, past what FR1 lets the pill take. It now spans only the gap toward the Handle, so it reaches no further than the pill. The pill's offset is a physical translate, so the bridge picks its physical side, with logical insets that swap under RTL.
…oss the gap to the Grab zone An owner ruling adds the sentence verbatim: a pill shown only on hover also takes the pointer across the gap between it and the Grab zone, never past the pill's far side.
…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.
555b70d to
1bc43bc
Compare
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved, and one change either way.
Because this branch is stacked on #9529, it amends component:Resizable FR1 (the Grab zone centred on the Handle for every pillPlacement, the painted grip as its own grab target, the zone shrinking on a short pane, and a hover-revealed grip taking the pointer across the gap to the zone) with text an engineering owner approved on #9529 but not on this pull request. An engineering owner records the decision by commenting /approve-spec <full head SHA> here (the author may do so as an engineering owner), or this waits until #9529 lands and FR1 is current on main.
Blocking either way:
- The diff is 3,735 authored lines, 909 of them #9529's separately reviewable Grab zone change; land #9529 first or take it out from under this branch, leaving this pull request's own 2,826 lines.
The hover-revealed grip no longer takes a short pane's upper half, and the factory, the hook migration and both hydration fixes match the current records.
[Automated review]
| // ============================================================================= | ||
| // The resize factory in React | ||
| // ============================================================================= |
There was a problem hiding this comment.
Non-blocking: this banner restates the code under it. Dropping it, as the branch already did for createResizable.ts, leaves the code to explain itself.
Stacked on #9529; until it merges, this branch shows its commits. Its own commits: vjeux/astryx@6693aa4...1bc43bc.
One internal resize region, with the configuration-object
useResizablemoved onto it. Next come the factory's element path (#9718),useResizable(handleRef, options)(#9370), the public export (#9372), the LayoutPanel slide (#9300) and the deprecation (#9202).Maintainer impact
architecture:public-component-apiINV22 has every public interaction hook delegate to acreate…(element, options)factory (spec:AST-107FR6). The region's state lived in useResizable. A factory beside it would leave two copies of the size model, so each sizing fix would land twice. This pull request adds the region and moves the configuration-object form onto it at once.Intended invariant
No public API or record changes. The configuration-object form keeps its signature, props, warnings, one-render-pass mounts and same-commit reports (
component:ResizableFR8).Two behaviors change. Each is a
[fix]that restores whatcomponent:ResizableFR3 promises for persistence and size (spec:AST-017FR5):autoSaveIdhydrates with the server's markup, then takes its saved size.defaultSizeand nocontainerRefhydrates with the server's markup, then resolves against the window.Change
createResizable(options)keeps a resizable region with no React and no element. It resolves, clamps, snaps, collapses and saves the size throughresizableModel.ts, follows a percentage's basis, and reports the size. ResizeHandle'sresizablepath drives it through the gesture model. feat(Resizable): createResizable drives a plain handle element, with one separator derivation #9718 adds the element path.<Activity>hides it, the factory keeps the region, socollapse(),expand()andresize()always apply.useResizable.tssites. One[chore]and two[fix]changesets.Evidence
onCollapseChange's commit, the size across an<Activity>, early and hidden calls, the warning words and a swapped container. The ResizeHandle, Layout, SideNav and AppShell suites pass unchanged.Scope
[fix]entries above.Testing
Local checks at 1bc43bc: core typecheck, the Resizable, Layout, SideNav and AppShell units (775 passed), strict eslint and prettier on the changed files, and the comment-length check. The test commits fail before their changes: the region's module is missing, the stand-in tests fail, both hydration cases fail, and a WIDE, NARROW, WIDE switch keeps NARROW's bounds. The full gates run at this head once CodeBunny approves.