Skip to content

refactor(Resizable): one internal createResizable factory, which useResizable's configuration-object form and ResizeHandle's resizable path go through - #9297

Draft
vjeux wants to merge 14 commits into
meta:mainfrom
vjeux:create-resizable-factory
Draft

vjeux wants to merge 14 commits into
meta:mainfrom
vjeux:create-resizable-factory

Conversation

@vjeux

@vjeux vjeux commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #9529; until it merges, this branch shows its commits. Its own commits: vjeux/astryx@b79aa8e...f8deefe.

One internal resize region, with the configuration-object useResizable moved 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-api INV22 has every public interaction hook delegate to a create…(element, options) factory (spec:AST-107 FR6). 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:Resizable FR8).

Two behaviors change. Each is a [fix] that restores what component:Resizable FR3 promises for persistence and size (spec:AST-017 FR5):

  • A server-rendered region with an autoSaveId hydrates with the server's markup, then takes its saved size.
  • A server-rendered region with a percentage defaultSize and no containerRef hydrates with the server's markup, then resolves against the window.

Change

  • The region. createResizable(options) keeps a resizable region with no React and no element. It resolves, clamps, snaps, collapses and saves the size through resizableModel.ts, follows a percentage's basis, and reports the size. ResizeHandle's resizable path drives it through the gesture model. feat(Resizable): createResizable drives a plain handle element, with one separator derivation #9718 adds the element path.
  • One state. The configuration-object form is the region in React, with one factory. While its component is unmounted or an <Activity> hides it, the factory keeps the region, so collapse(), expand() and resize() always apply.
  • Hydration. The hook's server snapshot is the state the server renders, so a hydrating render matches the server's markup.
  • Housekeeping. Comments cite no records, and the provenance baseline loses the useResizable.ts sites. One [chore] and two [fix] changesets.

Evidence

  • Failure before the change: the region's tests fail (no module), the configuration-object form's stand-in tests fail, and both hydration cases fail. On main, the saved size logs a hydration mismatch, and the percentage stays at 360, not 240.
  • Success after the change: those tests pass.
  • Product/runtime behavior verified unchanged: guards that pass before and after cover 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

  • No intended public API, visual direction, or policy change. The behavior changes are the two [fix] entries above.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

Local checks at f8deefe: core typecheck, the Resizable, Layout, SideNav and AppShell units (779 passed), strict eslint and prettier on the changed files, the comment-length check, and pnpm review:pre. The test commit fails before the change: the region's module is missing, the stand-in tests fail, and both hydration cases fail. Under load, utils.test.ts's package-root import can time out at an older base; main fixed that in #8471. The full gates run at this head once CodeBunny approves.

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 9:35am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 1, 2026
@astracat-bot

astracat-bot Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🔄 In progress (for maintainers only) Nov 3, 2026, 9:35 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ResizeHandle or plain element that a child mounts from its own state is never bound to the region (no value, and keys and drags do nothing), because useResizable re-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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc page, now in #9372, says a plain element keeps its tab stop and a ResizeHandle takes its own label; collapsedLabel is gone.

@vjeux
vjeux force-pushed the create-resizable-factory branch from eedebe1 to 73ca4d9 Compare November 1, 2026 11:12
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 1, 2026
@cixzhang
cixzhang marked this pull request as ready for review November 1, 2026 11:37

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ResizeHandle that 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 ResizeHandle should use useDevWarning, and the 5,045 changed lines should be split into the factory, the ref form, and the migration onto it.

[Automated review]

Comment on lines +569 to +575
useIsomorphicLayoutEffect(
() => () => {
slot.get()?.resizable.destroy();
slot.set(null);
},
[slot],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here: the hook keeps where the region stood while an <Activity> hides it and seeds the next factory from it (test added).

Comment on lines +468 to +476
export function releaseResizeHandle(
element: Element,
claim: ResizeHandleClaim,
): void {
if (claims.get(element) === claim) {
claims.delete(element);
claim.set(null);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +532 to +541
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #9370: the warn-once uses useDevWarning.

Comment thread .changeset/create-resizable.md Outdated
'@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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request's [chore] changeset says no behavior changes, and both cases named here are fixed and tested.

@vjeux vjeux changed the title feat(Resizable): createResizable, with useResizable(handleRef, options) as its React wrapper feat(Resizable): createResizable makes a plain element a window splitter that resizes a region, without React Nov 1, 2026
@vjeux
vjeux force-pushed the create-resizable-factory branch from 73ca4d9 to a8a8a2f Compare November 1, 2026 15:58
@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 1, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts and createResizable.ts re-implement ResizeHandle's input and useResizable's region state; route ResizeHandle through createResizeGesture and 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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: useResizable(config) builds on this factory, so the region's state has one copy.

if (isDestroyed) {
return;
}
opts = next;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #9372: the sidebar pads an inner element, so collapsed to 0 it leaves no strip.

@vjeux
vjeux force-pushed the create-resizable-factory branch from a8a8a2f to 86812b8 Compare November 1, 2026 17:31
@vjeux vjeux changed the title feat(Resizable): createResizable makes a plain element a window splitter that resizes a region, without React feat(Resizable): createResizable, internal to the package, makes a plain element a window splitter that resizes a region, without React Nov 1, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

@vjeux vjeux changed the title feat(Resizable): createResizable, internal to the package, makes a plain element a window splitter that resizes a region, without React refactor(Resizable): one internal createResizable factory, which useResizable's configuration-object form and ResizeHandle's resizable path go through Nov 1, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs an engineering owner's decision before it can be approved, and one change either way.

Because this branch is stacked on #9529, it also ships the Grab zone change, which no current spec settles: component:Resizable FR1 describes the Grab zone as a fixed --spacing-4 strip and says nothing about keeping it centred for every pillPlacement, narrowing it beside a short pane, or letting the painted pill start a drag. An engineering owner records that decision by amending FR1 (here or in #9529) and commenting /approve-spec <full head SHA> on the pull request; the author may do so as an engineering owner.

Required either way:

  • At 4,347 changed lines, 1,571 of them from #9680 and #9529, please land those first (or take #9529 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 three [fix] entries look right: the Resizable, Layout, SideNav and AppShell suites pass, each new fix test fails on main, and two non-blocking notes are inline.

[Automated review]

Comment on lines +569 to +571
// When an `<Activity>` shows this component, React subscribes to the slot
// again but does not read it. The factory made for the reveal reports in
// that same commit, so render again with what it reports.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: on a reveal, follow() resumes the same factory (:516-525), so no factory is made for the reveal. Suggested: "When an <Activity> shows this component, React subscribes to the slot again but does not read it. The factory resumes and reports in that same commit. Render again with what it reports."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in f76c7f8: "The factory resumes and reports in that same commit. Render again with what it reports."

Comment on lines +460 to +467
// A stand-in controller without the internal channel reports only its
// size and collapse state.
const next = readResizableInternals(resizable)?.snapshot() ?? {
...initial,
size: resizable.size,
isCollapsed: resizable.isCollapsed,
expandedSize: resizable.size,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: createResizableAs always registers the internal channel (createResizable.ts:795), so only the vi.mock('./createResizable') stand-in in useResizableConfig.test.tsx reaches this branch (and the ones at :429-432 and :539-545). Registering the stand-in through the channel, or testing against the real factory, would let the production code drop it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that only the stand-in reaches these branches. I'm keeping them for now, because dropping them means the config test's stand-in has to register the internal channel. That reshapes the hook code the next two pull requests in this stack build on. I'll take it as a follow-up once the stack lands.

@vjeux
vjeux force-pushed the create-resizable-factory branch from ee538dd to f76c7f8 Compare November 3, 2026 06:29
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stays centred on the Handle, the painted grip is its own grab target, and the zone shrinks on a short pane). An engineering owner approved that same text on #9529, but this pull request's head has no approval, so an engineering owner records the decision by commenting /approve-spec <full head SHA> here (the author may, as an engineering owner), or this waits until #9529 lands and FR1 is current on main.

Required either way:

  • A grip shown only on hover (isAlwaysVisible={false}) and drawn outside the Grab zone hides as the pointer moves onto it, so the resizable SideNav can no longer be dragged from its grip in right-to-left (it still can in left-to-right); please keep that grip reachable from the zone and add browser cases for an overlay Handle with a hover-revealed grip in left-to-right start and right-to-left end.
  • At 4,367 changed lines, 1,579 of them from #9680 and #9529, please land those first (with the fix above in #9529), or take #9529 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 three [fix] entries look right: the Resizable, Layout, SideNav and AppShell suites pass, and each new fix test fails on main.

[Automated review]

Comment on lines +548 to +552
onPointerLeave: () => {
if (!isDragging) {
setIsHovered(false);
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In right-to-left, SideNav's overlay Handle draws its end grip 4px outside this zone. Leaving the zone to reach it clears the hover, so pillHidden (:231) hides the grip and drops its pointer events before the pointer arrives; a drag from the grip then does nothing (260px on this head, 300px on main). Keeping a hover-revealed grip reachable from the zone, and a browser case for isAlwaysVisible={false} in LTR start and RTL end, would close it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #9529 (b79aa8e), which this carries: a hover-revealed pill stays reachable from the zone, with a browser case in left-to-right start and right-to-left end.

vjeux added 14 commits November 2, 2026 23:15
…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.
…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.

This branch was successfully deployed

1 active deployment
Preview — f8deefea Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants