Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ce0aa8c
test(Resizable): createResizable keeps a resizable region without Rea…
vjeux Nov 2, 2026
f10d880
refactor(Resizable): createResizable keeps the region without React, …
vjeux Nov 2, 2026
edac1bb
docs(Resizable): the reveal comment says the factory resumes
vjeux Nov 3, 2026
5162c7c
test(Resizable): a configuration useResizable was created with reache…
vjeux Nov 3, 2026
4408fef
fix(Resizable): useResizable compares a configuration with the one th…
vjeux Nov 3, 2026
f3e8799
chore(Resizable): drop a createResizable divider that restates the co…
vjeux Nov 3, 2026
dd345f4
docs(Resizable): the options Effect names the factory it passes new o…
vjeux Nov 3, 2026
d288321
test(Resizable): createResizable drives a plain handle element, with …
vjeux Nov 3, 2026
6f3d459
feat(Resizable): createResizable drives a plain handle element, and R…
vjeux Nov 3, 2026
8cc6e84
docs(i18n): architecture:internationalization's INV3a evidence gap na…
vjeux Nov 3, 2026
fe7d6a1
test(Resizable): a disabled handle keeps its tab stop, on ResizeHandl…
vjeux Nov 3, 2026
8a20dec
fix(Resizable): a disabled handle keeps its tab stop, as component:Re…
vjeux Nov 3, 2026
3509bd0
docs(Resizable): the records drop the closed tab-stop gap, and INV3a'…
vjeux Nov 3, 2026
c4b3ee6
docs(Resizable): the tab-stop changeset names the old behavior, the n…
vjeux Nov 3, 2026
ab9c9b0
docs(Resizable): FR3 drops the disabled tab-stop gap, and the disable…
vjeux Nov 3, 2026
0a48784
chore(Resizable): drop a createResizable divider that restates the co…
vjeux Nov 3, 2026
c12c3cb
chore(Resizable): drop a createResizable comment that restates the na…
vjeux Nov 3, 2026
8652e74
docs(Resizable): isReversed says the handle sits before a panel at th…
vjeux Nov 3, 2026
128a6aa
test(Resizable): useResizable(handleRef, options), a ResizeHandle giv…
vjeux Nov 3, 2026
c47b578
feat(Resizable): useResizable(handleRef, options) is createResizable'…
vjeux Nov 3, 2026
6b4febf
docs(Resizable): useResizable's ref-taking form, its API report rows,…
vjeux Nov 3, 2026
2cdb7d0
docs(Resizable): useResizable's doc lists collapsedLabel among the re…
vjeux Nov 3, 2026
1c34892
chore(Resizable): drop a createResizable comment that restates driven…
vjeux Nov 3, 2026
84b3241
refactor(Resizable): a bound factory's gesture ends as the claim ref …
vjeux Nov 3, 2026
16dd829
test(Resizable): a drag goes on through an inline ref callback, which…
vjeux Nov 3, 2026
0352ee0
fix(Resizable): a bound factory's gesture ends only when its element …
vjeux Nov 3, 2026
98f0301
test(Resizable): a server-rendered handle bound by its ref draws its …
vjeux Nov 3, 2026
daa21a7
fix(Resizable): a ref-bound handle's own isReversed combines with its…
vjeux Nov 3, 2026
c5dea36
docs(Resizable): the record names the hook's ref form as builder impa…
vjeux Nov 3, 2026
8e74d0b
feat(Resizable): createResizable is public, with its doc page, story,…
vjeux Nov 1, 2026
eb329f5
docs(Resizable): FR3 names createResizable as the owner ResizeHandle …
vjeux Nov 1, 2026
5db24c9
docs(Resizable): createResizable's page and changeset name collapsedL…
vjeux Nov 2, 2026
ae0931e
docs(Resizable): createResizable's public comments in short sentences…
vjeux Nov 2, 2026
f70a7d6
docs(Resizable): component:Resizable's ownership line says the Grab z…
vjeux Nov 3, 2026
25bc0e2
chore(Resizable): the public createResizable changeset covers the two…
vjeux Nov 3, 2026
57a0d3f
docs(Resizable): the public createResizable changeset says its fixes …
vjeux Nov 3, 2026
f2b0a93
docs(Resizable): createResizable's page says the handle sits before a…
vjeux Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/create-resizable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@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. The handle is named and announced with the message catalog's English messages: `label` names it, and `collapsedLabel` replaces the "Collapsed" it announces while collapsed. `useResizable(handleRef, options)` is its React form. The configuration-object `useResizable` and ResizeHandle build on it, with no behavior change beyond the fixes listed separately.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/resizable-percentage-default-after-hydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A server-rendered `useResizable` region whose `defaultSize` is a percentage and that has no `containerRef` hydrates with the server's markup, then resolves the percentage against the window, instead of keeping the size the server resolved.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/resizable-saved-size-after-hydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A server-rendered `useResizable` region with an `autoSaveId` hydrates with the server's markup and takes its saved size in the next commit, so hydration no longer reports a mismatch and the handle announces the saved size.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/resize-handle-disabled-tab-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A disabled `ResizeHandle` keeps its tab stop. Before, it left the tab order, so keyboard users could not reach it to hear that it is disabled. Now it stays in the tab order, as a disabled control keeps its tab stop, and it still ignores pointer and keyboard input and says so with `aria-disabled`.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/use-resizable-handle-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] `useResizable(handleRef, options)` makes the element behind the ref a window splitter, beside the configuration-object form: give the same ref to `ResizeHandle` (or to a plain element) and render the panel at `size`. It returns the region's size, collapse state and controls, in the commit that `onCollapseChange`'s own state lands in. A `ResizeHandle` given the ref renders the region and drives it, and a plain element is named and announced through the message catalog. Its options type, `CreateResizableOptions`, is exported, and its `collapsedLabel` replaces the "Collapsed" text that a collapsed handle announces.

@vjeux
124 changes: 124 additions & 0 deletions apps/storybook/stories/createResizable.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

import {useEffect, useRef} from 'react';
import type {Meta, StoryObj} from '@storybook/react';
import * as stylex from '@stylexjs/stylex';
import {
colorVars,
radiusVars,
spacingVars,
} from '@astryxdesign/core/theme/tokens.stylex';
import {createResizable} from '@astryxdesign/core/Resizable';

const s = stylex.create({
shell: {
display: 'flex',
height: 300,
width: '100%',
borderWidth: 1,
borderStyle: 'solid',
borderColor: colorVars['--color-border'],
borderRadius: radiusVars['--radius-container'],
overflow: 'hidden',
},
// The width the region reports is the sidebar's whole width, so the
// padding sits on an inner element: a sidebar collapsed to 0 leaves no
// strip.
sidebar: {
flexShrink: 0,
backgroundColor: colorVars['--color-background-muted'],
overflow: 'hidden',
},
sidebarContent: {
padding: spacingVars['--spacing-3'],
whiteSpace: 'nowrap',
},
divider: {
flexShrink: 0,
width: 8,
cursor: 'col-resize',
backgroundColor: colorVars['--color-border'],
outline: {
default: 'none',
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
touchAction: 'none',
},
preview: {
flexGrow: 1,
minWidth: 0,
borderWidth: 0,
},
});

/**
* A split view whose divider is a plain element: createResizable makes it a
* window splitter, and the sidebar's width follows `size` through
* `subscribe`. React renders the elements once and holds no resize state.
* The preview is an embedded frame: a drag across it keeps going, because the
* divider holds its pointer for the whole drag.
*/
function PlainSplit() {
const sidebarRef = useRef<HTMLDivElement>(null);
const sizeRef = useRef<HTMLSpanElement>(null);
const dividerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const sidebar = sidebarRef.current;
const size = sizeRef.current;
const divider = dividerRef.current;
if (sidebar == null || size == null || divider == null) {
return;
}
const resizable = createResizable(divider, {
defaultSize: 160,
minSize: 120,
maxSize: 320,
collapsible: true,
label: 'Resize sidebar',
});
const paint = () => {
sidebar.style.width = `${resizable.size}px`;
size.textContent = `${Math.round(resizable.size)}px`;
};
paint();
const stop = resizable.subscribe(paint);
return () => {
stop();
resizable.destroy();
};
}, []);
return (
<div {...stylex.props(s.shell)}>
<div ref={sidebarRef} data-testid="sidebar" {...stylex.props(s.sidebar)}>
<div {...stylex.props(s.sidebarContent)}>
Sidebar <span ref={sizeRef} data-testid="sidebar-size" />
</div>
</div>
<div ref={dividerRef} {...stylex.props(s.divider)} />
<iframe
title="Preview"
srcDoc="<!doctype html><body style='margin:0;font:14px system-ui;display:grid;place-items:center;height:100vh'>Embedded preview</body>"
{...stylex.props(s.preview)}
/>
</div>
);
}

const meta: Meta<typeof PlainSplit> = {
title: 'Core/Hooks/createResizable',
component: PlainSplit,
parameters: {
docs: {
description: {
component:
'createResizable(handle, options) makes a plain element a window splitter, without React. It reports the size; the caller sizes the panel. In React, use useResizable(handleRef, options) with ResizeHandle.',
},
},
},
};

export default meta;
type Story = StoryObj<typeof PlainSplit>;

/** Drag the divider right, across the preview, and back; or focus it and use the arrow keys, Home, End and Enter. */
export const WithEmbeddedFrame: Story = {};
2 changes: 1 addition & 1 deletion docs/architecture/internationalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ callers remain conformance work and do not relax INV1a, INV8, INV9, or INV10.
| INV1, INV1a, INV7 | hardcoded-string lint, catalog tests, component translation tests with a non-English provider | Astryx-owned text bypasses the catalog, an announcement or count is assembled from fragments or a hand-rolled plural, or non-string output reaches visible/AT attributes |
| 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, or a flat option under a provider, 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 |
| INV6 | `check:i18n-catalog`, key-format lint, and focused mutation tests | stale/malformed keys, malformed ICU, or source/translation runtime-contract drift passes CI |
| INV15 | generator tests, `check:i18n-runtime`, package-export drift check, provider/resolver runtime-catalog tests; per-module English resolution-parity test (every key each Core module names, in every shipped locale’s fallback path, its own map versus the whole catalog); import-cost check (component bundles carry only the English they name; public paths keep the whole catalog) | a generated module carries descriptions or is committed, a malformed entry projects instead of failing, a rich export path changes, or a `RuntimeCatalog` input is rejected or leaks a non-rich context shape, a key a module names resolves differently from the whole catalog, a component bundle carries the whole catalog, or a public path stops resolving a shipped key |
| INV16 | build-plugin unit and real-build tests | a rich or generated locale import fails to resolve through the standard plugin |
Expand Down
Loading