@structuralists/scaffolding 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +66 -0
- package/.storybook/main.ts +1 -1
- package/.storybook/preview.tsx +5 -1
- package/CLAUDE.md +25 -0
- package/README.md +79 -0
- package/bun.lock +211 -202
- package/eslint.config.mjs +85 -84
- package/package.json +21 -20
- package/roadmap.md +27 -0
- package/src/components/Chat/ChatComposer/ChatComposer.stories.tsx +1 -1
- package/src/components/Chat/ChatMessage/ChatMessage.stories.tsx +1 -1
- package/src/components/Chat/ChatRecipientsHeader/ChatRecipientsHeader.stories.tsx +1 -1
- package/src/components/Chat/ChatShell/ChatShell.stories.tsx +1 -1
- package/src/components/Chat/PillCombobox/PillCombobox.stories.tsx +1 -1
- package/src/components/Content/Badge/Badge.stories.tsx +1 -1
- package/src/components/Content/Card/Card.stories.tsx +1 -1
- package/src/components/Content/EditableMarkdown/EditableMarkdown.stories.tsx +1 -1
- package/src/components/Content/Heading/Heading.stories.tsx +1 -1
- package/src/components/Content/Link/Link.stories.tsx +1 -1
- package/src/components/Content/List/List.stories.tsx +1 -1
- package/src/components/Content/LoadingContainer/LoadingContainer.stories.tsx +1 -1
- package/src/components/Content/Markdown/Markdown.stories.tsx +1 -1
- package/src/components/Content/Menu/Menu.stories.tsx +1 -1
- package/src/components/Content/Text/Text.stories.tsx +1 -1
- package/src/components/Forms/Button/Button.stories.tsx +1 -1
- package/src/components/Forms/Field/Field.stories.tsx +1 -1
- package/src/components/Forms/IconButton/IconButton.stories.tsx +1 -1
- package/src/components/Forms/Input/Input.stories.tsx +1 -1
- package/src/components/Forms/Select/MultiSelect/MultiSelect.stories.tsx +1 -1
- package/src/components/Forms/Select/SingleSelect/SingleSelect.stories.tsx +1 -1
- package/src/components/Forms/Textarea/Textarea.stories.tsx +1 -1
- package/src/components/Json/Json/Json.stories.tsx +1 -1
- package/src/components/Json/JsonTable/JsonTable.stories.tsx +1 -1
- package/src/components/Layout/Bar/Bar.stories.tsx +1 -1
- package/src/components/Layout/Debug/Debug.stories.tsx +1 -1
- package/src/components/Layout/Divider/Divider.stories.tsx +1 -1
- package/src/components/Layout/Grid/Grid.stories.tsx +1 -1
- package/src/components/Layout/Panels/Panels.stories.tsx +47 -1
- package/src/components/Layout/Panels/index.tsx +17 -1
- package/src/components/Layout/Panels/types.ts +5 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +1 -1
- package/src/components/Modals/ConfirmModal/ConfirmModal.stories.tsx +1 -1
- package/src/components/Modals/LargeModal/LargeModal.stories.tsx +1 -1
- package/src/components/Modals/MediumModal/MediumModal.stories.tsx +1 -1
- package/src/components/Modals/MediumModal/MediumModal.test.tsx +11 -11
- package/src/components/Navigation/TabBar/TabBar.stories.tsx +1 -1
- package/src/components/Navigation/VerticalNav/VerticalNav.stories.tsx +1 -1
- package/src/components/Overlays/Popover/Popover.stories.tsx +1 -1
- package/src/components/Overlays/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/components/Primitives/EmptyValue/EmptyValue.stories.tsx +1 -1
- package/src/components/Primitives/LinedStack/LinedStack.stories.tsx +1 -1
- package/src/components/Primitives/LongText/LongText.stories.tsx +1 -1
- package/src/components/Primitives/Num/Num.stories.tsx +1 -1
- package/src/components/Primitives/Percent/Percent.stories.tsx +1 -1
- package/src/components/Primitives/RelativeTime/RelativeTime.stories.tsx +1 -1
- package/src/components/Tables/BigTable/BigTable.stories.tsx +1 -1
- package/src/components/Tables/QuickTable/QuickTable.stories.tsx +1 -1
- package/src/forms/CLAUDE.md +144 -0
- package/src/forms/path/path.ts +50 -0
- package/src/forms/path/types.test-d.ts +175 -0
- package/src/forms/path/types.ts +35 -0
- package/src/forms/useFormState/types.ts +13 -0
- package/src/forms/useFormState/useFormState.ts +14 -0
- package/src/forms/validations/types.test-d.ts +26 -0
- package/src/forms/validations/types.ts +15 -0
- package/src/hooks/useClickOutside/index.ts +57 -0
- package/src/hooks/useStableCallback/index.ts +36 -0
- package/src/index.ts +2 -0
- package/src/storybook/Composition.stories.tsx +1 -1
- package/src/storybook/_StoryUtils.stories.tsx +1 -1
package/eslint.config.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
|
2
|
+
import storybook from "eslint-plugin-storybook";
|
|
3
|
+
|
|
1
4
|
import boundaries from 'eslint-plugin-boundaries';
|
|
2
5
|
import tsParser from '@typescript-eslint/parser';
|
|
3
6
|
|
|
@@ -17,90 +20,88 @@ import tsParser from '@typescript-eslint/parser';
|
|
|
17
20
|
// `!(Modals|Chat)` is an extglob negation that excludes umbrella sections from
|
|
18
21
|
// the depth-3 patterns, so files inside Modals/Chat fall through to the
|
|
19
22
|
// depth-2 patterns and resolve to the umbrella as one primitive.
|
|
20
|
-
export default [
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
export default [{
|
|
24
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
25
|
+
plugins: { boundaries },
|
|
26
|
+
languageOptions: {
|
|
27
|
+
parser: tsParser,
|
|
28
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
|
|
29
|
+
},
|
|
30
|
+
settings: {
|
|
31
|
+
'import/resolver': {
|
|
32
|
+
node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
|
|
27
33
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
'boundaries/include': ['src/**/*'],
|
|
35
|
+
'boundaries/elements': [
|
|
36
|
+
// Plain-section component primitives (depth-3 under src/components)
|
|
37
|
+
{
|
|
38
|
+
type: 'primitive-entry',
|
|
39
|
+
pattern: 'src/components/!(Modals|Chat)/*/index.{ts,tsx}',
|
|
40
|
+
mode: 'full',
|
|
41
|
+
capture: ['_section', 'name'],
|
|
31
42
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
from: [{ type: 'primitive', captured: { name: '{{ to.captured.name }}' } }],
|
|
97
|
-
disallow: [{ to: { type: 'primitive-entry' } }],
|
|
98
|
-
message:
|
|
99
|
-
"Files inside a primitive folder must not import their own index barrel; import the neighbor directly.",
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
},
|
|
43
|
+
{
|
|
44
|
+
type: 'primitive-dogfood',
|
|
45
|
+
pattern: 'src/components/!(Modals|Chat)/*/**/*.{stories,test}.{ts,tsx}',
|
|
46
|
+
mode: 'full',
|
|
47
|
+
capture: ['_section', 'name'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'primitive',
|
|
51
|
+
pattern: 'src/components/!(Modals|Chat)/*',
|
|
52
|
+
mode: 'folder',
|
|
53
|
+
capture: ['_section', 'name'],
|
|
54
|
+
},
|
|
55
|
+
// Umbrella-section primitives (depth-2): Modals, Chat
|
|
56
|
+
{
|
|
57
|
+
type: 'primitive-entry',
|
|
58
|
+
pattern: 'src/components/*/index.{ts,tsx}',
|
|
59
|
+
mode: 'full',
|
|
60
|
+
capture: ['name'],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'primitive-dogfood',
|
|
64
|
+
pattern: 'src/components/*/**/*.{stories,test}.{ts,tsx}',
|
|
65
|
+
mode: 'full',
|
|
66
|
+
capture: ['name'],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'primitive',
|
|
70
|
+
pattern: 'src/components/*',
|
|
71
|
+
mode: 'folder',
|
|
72
|
+
capture: ['name'],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
rules: {
|
|
77
|
+
'boundaries/no-unknown': 'off',
|
|
78
|
+
'boundaries/no-unknown-files': 'off',
|
|
79
|
+
'boundaries/dependencies': [
|
|
80
|
+
'error',
|
|
81
|
+
{
|
|
82
|
+
default: 'allow',
|
|
83
|
+
rules: [
|
|
84
|
+
// External imports into a primitive must go through its barrel.
|
|
85
|
+
// Importers that aren't in the same primitive can't reach internals.
|
|
86
|
+
{
|
|
87
|
+
from: [
|
|
88
|
+
{ type: 'primitive', captured: { name: '!{{ to.captured.name }}' } },
|
|
89
|
+
{ type: 'primitive-entry', captured: { name: '!{{ to.captured.name }}' } },
|
|
90
|
+
{ type: 'primitive-dogfood', captured: { name: '!{{ to.captured.name }}' } },
|
|
91
|
+
],
|
|
92
|
+
disallow: [{ to: { type: 'primitive' } }],
|
|
93
|
+
message: "External imports must go through the folder's index barrel.",
|
|
94
|
+
},
|
|
95
|
+
// Inside the same primitive, files must not import their own barrel
|
|
96
|
+
// (cycle risk). Reach the neighbor directly.
|
|
97
|
+
{
|
|
98
|
+
from: [{ type: 'primitive', captured: { name: '{{ to.captured.name }}' } }],
|
|
99
|
+
disallow: [{ to: { type: 'primitive-entry' } }],
|
|
100
|
+
message:
|
|
101
|
+
"Files inside a primitive folder must not import their own index barrel; import the neighbor directly.",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
105
106
|
},
|
|
106
|
-
];
|
|
107
|
+
}, ...storybook.configs["flat/recommended"]];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@structuralists/scaffolding",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"main": "./index.ts",
|
|
5
5
|
"types": "./index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -19,32 +19,33 @@
|
|
|
19
19
|
"lint": "eslint src"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@uiw/react-json-view": "^2.0.0-alpha.
|
|
22
|
+
"@uiw/react-json-view": "^2.0.0-alpha.42",
|
|
23
23
|
"react-markdown": "^10.1.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"react": "^19.
|
|
27
|
-
"react-dom": "^19.
|
|
26
|
+
"react": "^19.2.5",
|
|
27
|
+
"react-dom": "^19.2.5",
|
|
28
28
|
"react-router": "^7.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@happy-dom/global-registrator": "^
|
|
32
|
-
"@storybook/addon-
|
|
33
|
-
"@storybook/
|
|
34
|
-
"@storybook/react": "^
|
|
35
|
-
"@
|
|
36
|
-
"@testing-library/
|
|
37
|
-
"@testing-library/react": "^16.0.0",
|
|
31
|
+
"@happy-dom/global-registrator": "^20.9.0",
|
|
32
|
+
"@storybook/addon-docs": "^10.3.6",
|
|
33
|
+
"@storybook/react": "^10.3.6",
|
|
34
|
+
"@storybook/react-vite": "^10.3.6",
|
|
35
|
+
"@testing-library/dom": "^10.4.1",
|
|
36
|
+
"@testing-library/react": "^16.3.2",
|
|
38
37
|
"@types/bun": "latest",
|
|
39
|
-
"@types/react": "^19.
|
|
40
|
-
"@types/react-dom": "^19.
|
|
41
|
-
"@typescript-eslint/parser": "^8.
|
|
42
|
-
"@vitejs/plugin-react": "^
|
|
43
|
-
"eslint": "^10.
|
|
38
|
+
"@types/react": "^19.2.14",
|
|
39
|
+
"@types/react-dom": "^19.2.3",
|
|
40
|
+
"@typescript-eslint/parser": "^8.59.1",
|
|
41
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
42
|
+
"eslint": "^10.3.0",
|
|
44
43
|
"eslint-plugin-boundaries": "^6.0.2",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
44
|
+
"eslint-plugin-storybook": "10.3.6",
|
|
45
|
+
"react-router": "^7.14.2",
|
|
46
|
+
"storybook": "^10.3.6",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vite": "^8.0.10",
|
|
49
|
+
"vitest": "^4.1.5"
|
|
49
50
|
}
|
|
50
51
|
}
|
package/roadmap.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## Soon
|
|
4
|
+
|
|
5
|
+
### Investigate `@storybook/addon-vitest`
|
|
6
|
+
|
|
7
|
+
Storybook 10's flagship test workflow: stories become tests automatically. Each `*.stories.tsx` is run as a Vitest test in a real browser via Playwright — every story is at minimum a smoke test, and any story with a `play` function becomes a full interaction test. Optional `addon-a11y` integration can fail builds on accessibility violations.
|
|
8
|
+
|
|
9
|
+
**Why it's appealing:**
|
|
10
|
+
|
|
11
|
+
- ~50+ component smoke tests for free, in real browser environments — no test code to write.
|
|
12
|
+
- Real layout, focus, and dialog semantics (catches things happy-dom misses).
|
|
13
|
+
- A11y gating in CI without standing up a separate framework.
|
|
14
|
+
|
|
15
|
+
**What to weigh:**
|
|
16
|
+
|
|
17
|
+
- Adds Vitest + Playwright to the dep tree (~200MB of browser binaries on install). CI install time goes up.
|
|
18
|
+
- Two test runners coexisting: keep `bun:test` for unit tests (utils, modal) and add Vitest for stories. The `test` script would invoke both.
|
|
19
|
+
- The current `MediumModal.test.tsx` and similar are arguably better expressed as story interaction tests once this lands.
|
|
20
|
+
|
|
21
|
+
**Rough plan if we go:**
|
|
22
|
+
|
|
23
|
+
1. `bun add -d @storybook/addon-vitest vitest @vitest/browser playwright @storybook/test`
|
|
24
|
+
2. Run `bunx playwright install chromium` (and add it to the CI workflow).
|
|
25
|
+
3. Add a `vitest.config.ts` that pulls in `@storybook/addon-vitest/vitest-plugin`.
|
|
26
|
+
4. Register the addon in `.storybook/main.ts`.
|
|
27
|
+
5. Add `test:stories` script; decide whether to fold into `test` or run as a separate gate in the workflow.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, type ComponentType } from 'react';
|
|
2
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
import { ChatRecipientsHeader } from './index';
|
|
4
4
|
import type { PillComboboxOption } from '../PillCombobox/types';
|
|
5
5
|
import { Stack } from '../../Layout/Stack';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
import { Panels } from './index';
|
|
4
4
|
import { Bar } from '../Bar';
|
|
5
5
|
import { Button } from '../../Forms/Button';
|
|
@@ -155,6 +155,52 @@ export const RightOverlay: Story = {
|
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
// Same shape as RightOverlay, but passes `onRightOverlayDismiss` so a
|
|
159
|
+
// pointerdown anywhere outside the overlay clears the selection. The
|
|
160
|
+
// parent still owns the open state — the callback just signals intent.
|
|
161
|
+
export const RightOverlayDismissOnOutsideClick: Story = {
|
|
162
|
+
render: () => {
|
|
163
|
+
const Demo = () => {
|
|
164
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Panels
|
|
168
|
+
header={<Bar title="Workbench" />}
|
|
169
|
+
footer={<Bar title="Click outside the panel to dismiss." />}
|
|
170
|
+
leftSidebar={slot(<LeftSidebarContent />, 200)}
|
|
171
|
+
onRightOverlayDismiss={() => setSelected(null)}
|
|
172
|
+
rightOverlay={
|
|
173
|
+
selected
|
|
174
|
+
? slot(
|
|
175
|
+
<Stack gap={3}>
|
|
176
|
+
<Heading level={3}>{selected}</Heading>
|
|
177
|
+
<Text isMuted size="small">
|
|
178
|
+
Click anywhere outside this panel to dismiss it.
|
|
179
|
+
</Text>
|
|
180
|
+
</Stack>,
|
|
181
|
+
)
|
|
182
|
+
: null
|
|
183
|
+
}
|
|
184
|
+
>
|
|
185
|
+
{slot(
|
|
186
|
+
<Stack gap={3}>
|
|
187
|
+
<Heading level={2}>Items</Heading>
|
|
188
|
+
{['Alpha', 'Beta', 'Gamma'].map((name) => (
|
|
189
|
+
<Button key={name} onClick={() => setSelected(name)}>
|
|
190
|
+
Open {name}
|
|
191
|
+
</Button>
|
|
192
|
+
))}
|
|
193
|
+
<Lorem paragraphs={20} as={Text} />
|
|
194
|
+
</Stack>,
|
|
195
|
+
)}
|
|
196
|
+
</Panels>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return <Demo />;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
158
204
|
// The overlay's content is itself a Panels — header/footer stay pinned
|
|
159
205
|
// while the body scrolls. The header carries the title and an X close
|
|
160
206
|
// button; the outer layout is intentionally minimal (no sidebars) to
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
type CSSProperties,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from 'react';
|
|
2
7
|
import { backgroundStyle } from '../../../tokens';
|
|
3
8
|
import { cx } from '../../../utils';
|
|
9
|
+
import { useClickOutside } from '../../../hooks/useClickOutside';
|
|
4
10
|
import type { PanelsProps } from './types';
|
|
5
11
|
import styles from './styles.module.css';
|
|
6
12
|
|
|
@@ -16,6 +22,7 @@ export const Panels = (props: PanelsProps) => {
|
|
|
16
22
|
leftSidebar,
|
|
17
23
|
rightSidebar,
|
|
18
24
|
rightOverlay,
|
|
25
|
+
onRightOverlayDismiss,
|
|
19
26
|
leftSidebarWidth,
|
|
20
27
|
rightSidebarWidth,
|
|
21
28
|
rightOverlayWidth,
|
|
@@ -46,6 +53,14 @@ export const Panels = (props: PanelsProps) => {
|
|
|
46
53
|
const [renderedOverlay, setRenderedOverlay] = useState<ReactNode>(rightOverlay ?? null);
|
|
47
54
|
const [isOverlayClosing, setIsOverlayClosing] = useState(false);
|
|
48
55
|
|
|
56
|
+
// Outside-click dismissal. Gated to the open state so the close
|
|
57
|
+
// animation window — where `rightOverlay` is null but `renderedOverlay`
|
|
58
|
+
// is still mounted — doesn't fire a second dismissal.
|
|
59
|
+
const { ref: overlayRef } = useClickOutside<HTMLDivElement>({
|
|
60
|
+
onOutside: onRightOverlayDismiss,
|
|
61
|
+
enabled: rightOverlay != null,
|
|
62
|
+
});
|
|
63
|
+
|
|
49
64
|
useEffect(() => {
|
|
50
65
|
if (rightOverlay != null) {
|
|
51
66
|
setRenderedOverlay(rightOverlay);
|
|
@@ -97,6 +112,7 @@ export const Panels = (props: PanelsProps) => {
|
|
|
97
112
|
)}
|
|
98
113
|
{renderedOverlay != null && (
|
|
99
114
|
<div
|
|
115
|
+
ref={overlayRef}
|
|
100
116
|
className={cx(styles.rightOverlay, isOverlayClosing && styles.closing)}
|
|
101
117
|
style={rightOverlayStyle}
|
|
102
118
|
>
|
|
@@ -18,6 +18,11 @@ export type PanelsProps = {
|
|
|
18
18
|
* Parent owns the open state and renders its own close affordance —
|
|
19
19
|
* there is no backdrop scrim or built-in dismissal. */
|
|
20
20
|
rightOverlay?: ReactNode;
|
|
21
|
+
/** Optional outside-click dismissal for `rightOverlay`. When provided,
|
|
22
|
+
* a pointerdown anywhere outside the overlay element invokes this
|
|
23
|
+
* callback; the parent is responsible for clearing `rightOverlay` in
|
|
24
|
+
* response. Omit to disable outside-click dismissal entirely. */
|
|
25
|
+
onRightOverlayDismiss?: () => void;
|
|
21
26
|
/** Fixed width for the left sidebar. Numbers → px, strings pass through
|
|
22
27
|
* (`"22rem"`, `"25%"`, etc). Defaults to content-sized. */
|
|
23
28
|
leftSidebarWidth?: number | string;
|