@zyncat/ui 0.9.0 → 0.10.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/README.md +2 -2
- package/dist/alert.js +1 -1
- package/dist/button.css +10 -2
- package/dist/button.d.ts +2 -6
- package/dist/button.js +1 -1
- package/dist/{chunk-U5QQZC3U.js → chunk-2F5PITAO.js} +2 -6
- package/dist/chunk-2F5PITAO.js.map +1 -0
- package/dist/{chunk-EQ32F2QJ.js → chunk-62P3KD57.js} +5 -5
- package/dist/chunk-62P3KD57.js.map +1 -0
- package/dist/{chunk-W5YM2FV4.js → chunk-FTOAUJL5.js} +9 -6
- package/dist/chunk-FTOAUJL5.js.map +1 -0
- package/dist/{chunk-FLO4A6BI.js → chunk-K5X2GCXU.js} +54 -52
- package/dist/chunk-K5X2GCXU.js.map +1 -0
- package/dist/{chunk-Q5B7JTSR.js → chunk-OMHQ4U4J.js} +8 -8
- package/dist/chunk-OMHQ4U4J.js.map +1 -0
- package/dist/{chunk-GY7JOPUQ.js → chunk-RCONNA7I.js} +5 -5
- package/dist/chunk-RCONNA7I.js.map +1 -0
- package/dist/{chunk-GLT5IKQY.js → chunk-UXONUUPI.js} +9 -5
- package/dist/chunk-UXONUUPI.js.map +1 -0
- package/dist/{chunk-7RNRLIKT.js → chunk-Y7AJQVWJ.js} +3 -3
- package/dist/{chunk-7RNRLIKT.js.map → chunk-Y7AJQVWJ.js.map} +1 -1
- package/dist/{chunk-LDC3RIYA.js → chunk-ZCOUPPTI.js} +3 -3
- package/dist/{chunk-LDC3RIYA.js.map → chunk-ZCOUPPTI.js.map} +1 -1
- package/dist/date-field.js +6 -6
- package/dist/date-range-field.js +7 -7
- package/dist/datetime-field.js +6 -6
- package/dist/dialog.css +3 -0
- package/dist/dialog.d.ts +4 -1
- package/dist/dialog.js +7 -5
- package/dist/dialog.js.map +1 -1
- package/dist/modal.css +9 -0
- package/dist/modal.d.ts +35 -0
- package/dist/modal.js +63 -0
- package/dist/modal.js.map +1 -0
- package/dist/motion-devtools.js +1 -1
- package/dist/multi-select.js +4 -4
- package/dist/overlay.css +3 -0
- package/dist/pagination.js +1 -1
- package/dist/popover.d.ts +10 -3
- package/dist/popover.js +4 -4
- package/dist/select.js +4 -4
- package/dist/sheet.d.ts +4 -1
- package/dist/sheet.js +4 -4
- package/dist/table.js +1 -1
- package/dist/toast.js +1 -1
- package/llms.txt +31 -7
- package/package.json +16 -11
- package/src/components/composites/dialog/Dialog.tsx +30 -23
- package/src/components/composites/dialog/dialog.css +3 -0
- package/src/components/composites/modal/Modal.tsx +95 -0
- package/src/components/composites/modal/modal.css +9 -0
- package/src/components/composites/popover/Popover.tsx +12 -3
- package/src/components/composites/select/core/trigger.tsx +8 -12
- package/src/components/composites/sheet/Sheet.tsx +10 -1
- package/src/components/internal/overlay/layer.tsx +3 -3
- package/src/components/internal/overlay/modal.tsx +45 -36
- package/src/components/internal/overlay/overlay.css +3 -0
- package/src/components/internal/overlay/position.ts +8 -3
- package/src/components/primitives/button/Button.tsx +0 -16
- package/src/components/primitives/button/button.css +10 -2
- package/dist/chunk-EQ32F2QJ.js.map +0 -1
- package/dist/chunk-FLO4A6BI.js.map +0 -1
- package/dist/chunk-GLT5IKQY.js.map +0 -1
- package/dist/chunk-GY7JOPUQ.js.map +0 -1
- package/dist/chunk-Q5B7JTSR.js.map +0 -1
- package/dist/chunk-U5QQZC3U.js.map +0 -1
- package/dist/chunk-W5YM2FV4.js.map +0 -1
package/dist/pagination.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { animate } from './chunk-YGCVWGUG.js';
|
|
3
3
|
import { Icon } from './chunk-3SBCBYB7.js';
|
|
4
4
|
import { UIMotion } from './chunk-S5ZTZANS.js';
|
|
5
|
-
import { Button } from './chunk-
|
|
5
|
+
import { Button } from './chunk-2F5PITAO.js';
|
|
6
6
|
import { cx } from './chunk-QGNC7LH5.js';
|
|
7
7
|
import './pagination.css';
|
|
8
8
|
import { useRef, useEffect } from 'react';
|
package/dist/popover.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { D as DisableableAnimation } from './timing-7JA_zQHP.js';
|
|
|
4
4
|
import { D as DataAttributes } from './dom-props-C0jrllBw.js';
|
|
5
5
|
import './motion-scale-Canq4stZ.js';
|
|
6
6
|
|
|
7
|
+
interface VirtualAnchor {
|
|
8
|
+
getBoundingClientRect(): DOMRect;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
interface PopoverProps {
|
|
8
12
|
/** Controlled open state. Omit to stay uncontrolled. */
|
|
9
13
|
open?: boolean;
|
|
@@ -11,8 +15,11 @@ interface PopoverProps {
|
|
|
11
15
|
defaultOpen?: boolean;
|
|
12
16
|
/** Fires whenever the open state changes. Pair with `open` for controlled use. */
|
|
13
17
|
onOpenChange?: (open: boolean) => void;
|
|
14
|
-
/** Cloned to toggle the panel
|
|
18
|
+
/** Cloned to toggle the panel, and used as the anchor unless `anchor` is set. */
|
|
15
19
|
trigger?: ReactElement | null;
|
|
20
|
+
/** Anchor to an arbitrary rect instead of the trigger - any `{ getBoundingClientRect() }`, which an element also satisfies.
|
|
21
|
+
* Drive `open` yourself; pass a new object to re-place a moving anchor. */
|
|
22
|
+
anchor?: VirtualAnchor | null;
|
|
16
23
|
/** Preferred side; flips to the opposite side when cramped. Default 'bottom'. */
|
|
17
24
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
18
25
|
/** Cross-axis alignment against the trigger. Default 'start'. */
|
|
@@ -30,6 +37,6 @@ interface PopoverProps {
|
|
|
30
37
|
/** The ENTIRE surface - paint AND semantics. Drive dismissal with `open`/`onOpenChange`. */
|
|
31
38
|
children: ReactNode;
|
|
32
39
|
}
|
|
33
|
-
declare function Popover({ open: controlledOpen, defaultOpen, onOpenChange, trigger, side, align, arrow, dismissible, id, htmlProps, animation, children, }: PopoverProps): react.JSX.Element;
|
|
40
|
+
declare function Popover({ open: controlledOpen, defaultOpen, onOpenChange, trigger, anchor, side, align, arrow, dismissible, id, htmlProps, animation, children, }: PopoverProps): react.JSX.Element;
|
|
34
41
|
|
|
35
|
-
export { Popover, type PopoverProps };
|
|
42
|
+
export { Popover, type PopoverProps, type VirtualAnchor };
|
package/dist/popover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
'use client';export { Popover } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
1
|
+
'use client';export { Popover } from './chunk-FTOAUJL5.js';
|
|
2
|
+
import './chunk-62P3KD57.js';
|
|
3
|
+
import './chunk-ZCOUPPTI.js';
|
|
4
|
+
import './chunk-RCONNA7I.js';
|
|
5
5
|
import './chunk-UE63OOVG.js';
|
|
6
6
|
import './chunk-NEF5XAHG.js';
|
|
7
7
|
import './chunk-356HZMT4.js';
|
package/dist/select.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
'use client';import { useListbox, SelectTrigger, ListboxPanel } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
1
|
+
'use client';import { useListbox, SelectTrigger, ListboxPanel } from './chunk-OMHQ4U4J.js';
|
|
2
|
+
import './chunk-62P3KD57.js';
|
|
3
|
+
import './chunk-RCONNA7I.js';
|
|
4
4
|
import './chunk-UE63OOVG.js';
|
|
5
5
|
import './chunk-WM5J5TDI.js';
|
|
6
6
|
import './chunk-NEF5XAHG.js';
|
|
@@ -11,7 +11,7 @@ import './chunk-2ERBBFZS.js';
|
|
|
11
11
|
import './chunk-3SBCBYB7.js';
|
|
12
12
|
import { useControllable } from './chunk-MMRIQNEN.js';
|
|
13
13
|
import './chunk-S5ZTZANS.js';
|
|
14
|
-
import './chunk-
|
|
14
|
+
import './chunk-2F5PITAO.js';
|
|
15
15
|
import './chunk-7SZCJ7WI.js';
|
|
16
16
|
import './chunk-VTJTGVZZ.js';
|
|
17
17
|
import { cx } from './chunk-QGNC7LH5.js';
|
package/dist/sheet.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ interface SheetProps {
|
|
|
17
17
|
side?: 'right' | 'bottom';
|
|
18
18
|
/** Scrim/Esc dismissal + drag-to-edge. Default true. */
|
|
19
19
|
dismissible?: boolean;
|
|
20
|
+
/** Dock inside this element instead of the viewport - it must be positioned (`position: relative` or similar).
|
|
21
|
+
* Scrim, scroll lock and inert scope to it; the rest of the page stays interactive. */
|
|
22
|
+
container?: HTMLElement | null;
|
|
20
23
|
/** Base id for the panel; drives the trigger's `aria-controls`. Auto-generated when omitted. */
|
|
21
24
|
id?: string;
|
|
22
25
|
/** Standard attributes (className, style, data-*, ...) forwarded to the sheet panel. */
|
|
@@ -26,6 +29,6 @@ interface SheetProps {
|
|
|
26
29
|
/** The ENTIRE surface - paint AND semantics. Drive dismissal with `open`/`onOpenChange`. */
|
|
27
30
|
children: ReactNode;
|
|
28
31
|
}
|
|
29
|
-
declare function Sheet({ open: controlledOpen, defaultOpen, onOpenChange, trigger, side, dismissible, id, htmlProps, animation, children, }: SheetProps): react.JSX.Element;
|
|
32
|
+
declare function Sheet({ open: controlledOpen, defaultOpen, onOpenChange, trigger, side, dismissible, container, id, htmlProps, animation, children, }: SheetProps): react.JSX.Element;
|
|
30
33
|
|
|
31
34
|
export { Sheet, type SheetProps };
|
package/dist/sheet.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
'use client';export { Sheet } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
1
|
+
'use client';export { Sheet } from './chunk-UXONUUPI.js';
|
|
2
|
+
import './chunk-K5X2GCXU.js';
|
|
3
|
+
import './chunk-ZCOUPPTI.js';
|
|
4
|
+
import './chunk-RCONNA7I.js';
|
|
5
5
|
import './chunk-NEF5XAHG.js';
|
|
6
6
|
import './chunk-356HZMT4.js';
|
|
7
7
|
import './chunk-XWUWYSCT.js';
|
package/dist/table.js
CHANGED
|
@@ -5,7 +5,7 @@ import './chunk-YGCVWGUG.js';
|
|
|
5
5
|
import { Icon } from './chunk-3SBCBYB7.js';
|
|
6
6
|
import { Odometer } from './chunk-CONSWNIV.js';
|
|
7
7
|
import { UIMotion } from './chunk-S5ZTZANS.js';
|
|
8
|
-
import { Button } from './chunk-
|
|
8
|
+
import { Button } from './chunk-2F5PITAO.js';
|
|
9
9
|
import { Checkbox } from './chunk-QDK66S4G.js';
|
|
10
10
|
import './chunk-NNXJCLTV.js';
|
|
11
11
|
import { cx } from './chunk-QGNC7LH5.js';
|
package/dist/toast.js
CHANGED
|
@@ -8,7 +8,7 @@ import { animate, startDrag } from './chunk-YGCVWGUG.js';
|
|
|
8
8
|
import { Icon } from './chunk-3SBCBYB7.js';
|
|
9
9
|
import { fireGlint } from './chunk-S7B34VGN.js';
|
|
10
10
|
import { UIMotion } from './chunk-S5ZTZANS.js';
|
|
11
|
-
import { Button } from './chunk-
|
|
11
|
+
import { Button } from './chunk-2F5PITAO.js';
|
|
12
12
|
import { cx } from './chunk-QGNC7LH5.js';
|
|
13
13
|
import './toast.css';
|
|
14
14
|
import { useEffect, useSyncExternalStore, useState, useRef, useLayoutEffect } from 'react';
|
package/llms.txt
CHANGED
|
@@ -27,8 +27,9 @@ Agents: prefer the bundled MCP server over scanning this file or node_modules -
|
|
|
27
27
|
============================================================ PRIMITIVES
|
|
28
28
|
|
|
29
29
|
Button - @zyncat/ui/button
|
|
30
|
-
One control for every click; exactly one primary per view. variant primary|secondary|ghost|danger|link, size sm|md|lg,
|
|
31
|
-
<Button variant="primary"
|
|
30
|
+
One control for every click; exactly one primary per view. variant primary|secondary|ghost|danger|link, size sm|md|lg|icon, loading, disabled. Icons are plain children - Button lays them out in a centered row with gap, no iconLeft/iconRight slots.
|
|
31
|
+
<Button variant="primary"><Icon name="plus" size="sm" />Schedule post</Button>
|
|
32
|
+
<Button size="icon" aria-label="Close"><Icon name="x" size="sm" /></Button>
|
|
32
33
|
|
|
33
34
|
Collapse - @zyncat/ui/collapse
|
|
34
35
|
Layout-transition primitive; eases height open/closed, never teleports. Closed content also
|
|
@@ -168,16 +169,39 @@ Tooltip - @zyncat/ui/tooltip
|
|
|
168
169
|
<Tooltip content="Save changes" shortcut="⌘S"><Button>Save</Button></Tooltip>
|
|
169
170
|
|
|
170
171
|
Dialog - @zyncat/ui/dialog
|
|
171
|
-
|
|
172
|
+
DEFAULT CHOICE for modals - the styled surface: header (icon/title/description/close), scrolling body,
|
|
173
|
+
footer actions, on top of scrim + focus trap + scroll lock. Reach for Modal only if this chrome is wrong.
|
|
174
|
+
open/onOpenChange, tone, icon, title, description, footer: (close) => node. Header is omitted entirely
|
|
175
|
+
when there is no icon/title/description and dismissible={false}.
|
|
176
|
+
container mounts it inside a positioned element instead of the viewport; scrim, scroll lock and
|
|
177
|
+
inert then scope to that box and the rest of the page stays live.
|
|
172
178
|
<Dialog open={o} onOpenChange={setO} tone="danger" title="Delete this project?" footer={(close) => <>...</>}>...</Dialog>
|
|
173
179
|
|
|
180
|
+
Modal - @zyncat/ui/modal
|
|
181
|
+
The SAME modality as Dialog (scrim, focus trap, scroll lock, inert, Esc, motion) with ZERO paint - no
|
|
182
|
+
header, no body padding, no width cap, no border/radius/shadow. You supply the whole surface AND its
|
|
183
|
+
semantics (role="dialog" + a label).
|
|
184
|
+
Use it ONLY when Dialog's chrome is the problem: lightbox / image viewer, full-bleed media, video,
|
|
185
|
+
a custom layout that owns its own edges. If the content is a title + text + buttons, use Dialog instead.
|
|
186
|
+
htmlProps paints the panel (centered by default; width+height 100% = full-bleed), layerProps reaches the
|
|
187
|
+
box behind it (set --bg-overlay to retint the scrim). Also takes trigger, dismissible, container, animation.
|
|
188
|
+
<Modal open={!!src} onOpenChange={() => setSrc(null)} htmlProps={{ className: 'lightbox' }} layerProps={{ style: { '--bg-overlay': 'rgb(0 0 0 / 0.92)' } }}>
|
|
189
|
+
<div role="dialog" aria-label="Image viewer"><img src={src} alt="" /></div>
|
|
190
|
+
</Modal>
|
|
191
|
+
|
|
174
192
|
Popover - @zyncat/ui/popover
|
|
175
|
-
Headless anchored panel, non-modal. side, align, arrow, trigger (node),
|
|
176
|
-
|
|
193
|
+
Headless anchored panel, non-modal. side, align, arrow, trigger (node), dismissible.
|
|
194
|
+
Anchor without a trigger: pass anchor - anything with getBoundingClientRect (a DOMRect source, an
|
|
195
|
+
element) - and drive open yourself. Pass a new object to re-place a moving anchor.
|
|
196
|
+
<Popover side="bottom" align="start" trigger={<Button>Actions</Button>}><menu>...</menu></Popover>
|
|
197
|
+
<Popover open={!!spot} onOpenChange={dismiss} anchor={spot} side="right"><menu>...</menu></Popover>
|
|
177
198
|
|
|
178
199
|
Sheet - @zyncat/ui/sheet
|
|
179
|
-
Edge-docked modal panel, drag-to-dismiss. side right|bottom, trigger (node),
|
|
180
|
-
|
|
200
|
+
Edge-docked modal panel, drag-to-dismiss. side right|bottom, trigger (node), dismissible.
|
|
201
|
+
container docks it inside a positioned element instead of the viewport; scrim, scroll lock and
|
|
202
|
+
inert then scope to that box and the rest of the page stays live.
|
|
203
|
+
<Sheet side="right" trigger={<Button>Filters</Button>}><div role="dialog">...</div></Sheet>
|
|
204
|
+
<Sheet side="bottom" container={panel} open={o} onOpenChange={setO}><div role="dialog">...</div></Sheet>
|
|
181
205
|
|
|
182
206
|
============================================================ DEV TOOLS
|
|
183
207
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zyncat/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A React 19 design system: accessible, animated UI components built on a small, closed CSS design-token vocabulary. No Tailwind, no CSS-in-JS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -181,6 +181,11 @@
|
|
|
181
181
|
"types": "./dist/dialog.d.ts",
|
|
182
182
|
"import": "./dist/dialog.js"
|
|
183
183
|
},
|
|
184
|
+
"./modal": {
|
|
185
|
+
"source": "./src/components/composites/modal/Modal.tsx",
|
|
186
|
+
"types": "./dist/modal.d.ts",
|
|
187
|
+
"import": "./dist/modal.js"
|
|
188
|
+
},
|
|
184
189
|
"./tooltip": {
|
|
185
190
|
"source": "./src/components/composites/tooltip/Tooltip.tsx",
|
|
186
191
|
"types": "./dist/tooltip.d.ts",
|
|
@@ -230,15 +235,6 @@
|
|
|
230
235
|
"next.mjs",
|
|
231
236
|
"next.d.mts"
|
|
232
237
|
],
|
|
233
|
-
"scripts": {
|
|
234
|
-
"build": "tsup",
|
|
235
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.node.json",
|
|
236
|
-
"check:css": "node scripts/check-css-graph.mjs",
|
|
237
|
-
"check:llms": "node scripts/check-llms.mjs",
|
|
238
|
-
"format": "prettier --write .",
|
|
239
|
-
"format:check": "prettier --check .",
|
|
240
|
-
"prepublishOnly": "pnpm build"
|
|
241
|
-
},
|
|
242
238
|
"peerDependencies": {
|
|
243
239
|
"react": "^19",
|
|
244
240
|
"react-dom": "^19"
|
|
@@ -254,5 +250,14 @@
|
|
|
254
250
|
"react-dom": "^19.2.7",
|
|
255
251
|
"tsup": "^8.5.0",
|
|
256
252
|
"typescript": "~6.0.3"
|
|
253
|
+
},
|
|
254
|
+
"scripts": {
|
|
255
|
+
"build": "NODE_OPTIONS=--max-old-space-size=8192 tsup",
|
|
256
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.node.json",
|
|
257
|
+
"check:css": "node scripts/check-css-graph.mjs",
|
|
258
|
+
"check:llms": "node scripts/check-llms.mjs",
|
|
259
|
+
"format": "prettier --write .",
|
|
260
|
+
"format:check": "prettier --check .",
|
|
261
|
+
"dev": "cd playground && pnpm dev"
|
|
257
262
|
}
|
|
258
|
-
}
|
|
263
|
+
}
|
|
@@ -40,6 +40,9 @@ export interface DialogProps {
|
|
|
40
40
|
icon?: ReactNode;
|
|
41
41
|
/** Close button + backdrop/Esc dismissal. Default true. */
|
|
42
42
|
dismissible?: boolean;
|
|
43
|
+
/** Center inside this element instead of the viewport - it must be positioned (`position: relative` or similar).
|
|
44
|
+
* Scrim, scroll lock and inert scope to it; the rest of the page stays interactive. */
|
|
45
|
+
container?: HTMLElement | null;
|
|
43
46
|
/** Action row - a node, or a render fn `(close) => node` so uncontrolled dialogs can dismiss. */
|
|
44
47
|
footer?: ReactNode | ((close: () => void) => ReactNode);
|
|
45
48
|
/** Dialog body - scrolls when tall; its scroll edges flag the header lift + footer divider. */
|
|
@@ -100,30 +103,32 @@ function DialogSurface({
|
|
|
100
103
|
aria-labelledby={title ? titleId : undefined}
|
|
101
104
|
aria-describedby={description ? descId : undefined}
|
|
102
105
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<div className="dialog__heading">
|
|
110
|
-
{title && (
|
|
111
|
-
<h2 className="dialog__title" id={titleId}>
|
|
112
|
-
{title}
|
|
113
|
-
</h2>
|
|
106
|
+
{(icon || title || description || dismissible) && (
|
|
107
|
+
<header className="dialog__header">
|
|
108
|
+
{icon && (
|
|
109
|
+
<span className="dialog__icon">
|
|
110
|
+
<IconSlot size="md">{icon}</IconSlot>
|
|
111
|
+
</span>
|
|
114
112
|
)}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
|
|
113
|
+
<div className="dialog__heading">
|
|
114
|
+
{title && (
|
|
115
|
+
<h2 className="dialog__title" id={titleId}>
|
|
116
|
+
{title}
|
|
117
|
+
</h2>
|
|
118
|
+
)}
|
|
119
|
+
{description && (
|
|
120
|
+
<p className="dialog__desc" id={descId}>
|
|
121
|
+
{description}
|
|
122
|
+
</p>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
{dismissible && (
|
|
126
|
+
<button type="button" className="dialog__close" aria-label="Close dialog" onClick={requestClose}>
|
|
127
|
+
<Icon name="x" size="sm" weight="bold" />
|
|
128
|
+
</button>
|
|
119
129
|
)}
|
|
120
|
-
</
|
|
121
|
-
|
|
122
|
-
<button type="button" className="dialog__close" aria-label="Close dialog" onClick={requestClose}>
|
|
123
|
-
<Icon name="x" size="sm" weight="bold" />
|
|
124
|
-
</button>
|
|
125
|
-
)}
|
|
126
|
-
</header>
|
|
130
|
+
</header>
|
|
131
|
+
)}
|
|
127
132
|
|
|
128
133
|
<div className="dialog__body" ref={bodyRef}>
|
|
129
134
|
{children}
|
|
@@ -145,6 +150,7 @@ export function Dialog({
|
|
|
145
150
|
tone = 'default',
|
|
146
151
|
icon = null,
|
|
147
152
|
dismissible = true,
|
|
153
|
+
container = null,
|
|
148
154
|
footer = null,
|
|
149
155
|
children,
|
|
150
156
|
id,
|
|
@@ -162,7 +168,7 @@ export function Dialog({
|
|
|
162
168
|
return (
|
|
163
169
|
<Fragment>
|
|
164
170
|
{ovCloneTrigger(trigger, { open, onPress: () => setOpen(true), panelId, haspopup: 'dialog', triggerRef })}
|
|
165
|
-
<OverlayPortal>
|
|
171
|
+
<OverlayPortal container={container}>
|
|
166
172
|
<Presence>
|
|
167
173
|
{open && (
|
|
168
174
|
<ModalShell
|
|
@@ -173,6 +179,7 @@ export function Dialog({
|
|
|
173
179
|
panelClass="overlay-panel--dialog"
|
|
174
180
|
panelId={panelId}
|
|
175
181
|
dismissible={dismissible}
|
|
182
|
+
container={container}
|
|
176
183
|
requestClose={close}
|
|
177
184
|
>
|
|
178
185
|
<DialogSurface
|
|
@@ -136,6 +136,9 @@
|
|
|
136
136
|
color: var(--text-body);
|
|
137
137
|
font: var(--type-body);
|
|
138
138
|
}
|
|
139
|
+
.dialog__body:first-child {
|
|
140
|
+
padding-top: var(--space-5);
|
|
141
|
+
}
|
|
139
142
|
.dialog:has(.dialog__icon) .dialog__body {
|
|
140
143
|
padding-left: calc(var(--space-5) + 2.25rem + var(--space-3));
|
|
141
144
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import './modal.css';
|
|
4
|
+
import { Fragment, useId, useRef, type HTMLAttributes, type ReactElement, type ReactNode } from 'react';
|
|
5
|
+
import { Presence } from '../../../motion/presence';
|
|
6
|
+
import { resolveMotionTiming } from '../../../motion/motion-timing';
|
|
7
|
+
import type { DisableableAnimation } from '../../../motion/timing';
|
|
8
|
+
import { useControllable } from '../../internal/hooks/use-controllable';
|
|
9
|
+
import { ovCloneTrigger, OverlayPortal } from '../../internal/overlay/layer';
|
|
10
|
+
import { ModalShell } from '../../internal/overlay/modal';
|
|
11
|
+
import type { DataAttributes } from '../../../dom-props';
|
|
12
|
+
|
|
13
|
+
const MODAL_TIMING = {
|
|
14
|
+
open: { duration: 'slow', ease: 'entrance' },
|
|
15
|
+
close: { duration: 'base', ease: 'standard' },
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export interface ModalProps {
|
|
19
|
+
/** Controlled open state. Omit to stay uncontrolled. */
|
|
20
|
+
open?: boolean;
|
|
21
|
+
/** Initial state when uncontrolled. Default false. */
|
|
22
|
+
defaultOpen?: boolean;
|
|
23
|
+
/** Fires whenever the open state changes. Pair with `open` for controlled use. */
|
|
24
|
+
onOpenChange?: (open: boolean) => void;
|
|
25
|
+
|
|
26
|
+
/** Cloned to open the modal on click. */
|
|
27
|
+
trigger?: ReactElement | null;
|
|
28
|
+
|
|
29
|
+
/** Scrim/Esc dismissal. Default true. */
|
|
30
|
+
dismissible?: boolean;
|
|
31
|
+
|
|
32
|
+
/** Mount inside this element instead of the viewport - it must be positioned (`position: relative` or similar).
|
|
33
|
+
* Scrim, scroll lock and inert scope to it; the rest of the page stays interactive. */
|
|
34
|
+
container?: HTMLElement | null;
|
|
35
|
+
|
|
36
|
+
/** Base id for the panel; drives the trigger's `aria-controls`. Auto-generated when omitted. */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Standard attributes (className, style, data-*, ...) forwarded to the panel - this is where your paint goes.
|
|
39
|
+
* Centered by default; `width: 100%; height: 100%` makes it full-bleed. */
|
|
40
|
+
htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
|
|
41
|
+
/** Standard attributes forwarded to the layer behind the panel - set `--bg-overlay` here to retint the scrim. */
|
|
42
|
+
layerProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
|
|
43
|
+
/** Open/close timing - motion tokens only, or `null` to disable. @default open 'slow'/'entrance', close 'base'/'standard' */
|
|
44
|
+
animation?: DisableableAnimation;
|
|
45
|
+
/** The ENTIRE surface - paint AND semantics (`role="dialog"`, a label). Nothing is painted for you. */
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function Modal({
|
|
50
|
+
open: controlledOpen,
|
|
51
|
+
defaultOpen = false,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
trigger = null,
|
|
54
|
+
dismissible = true,
|
|
55
|
+
container = null,
|
|
56
|
+
id,
|
|
57
|
+
htmlProps,
|
|
58
|
+
layerProps,
|
|
59
|
+
animation,
|
|
60
|
+
children,
|
|
61
|
+
}: ModalProps) {
|
|
62
|
+
const [open, setOpen] = useControllable(controlledOpen, defaultOpen, onOpenChange);
|
|
63
|
+
const triggerRef = useRef<HTMLElement>(null);
|
|
64
|
+
const autoId = useId();
|
|
65
|
+
const panelId = id || 'modal-' + autoId;
|
|
66
|
+
const close = () => setOpen(false);
|
|
67
|
+
const timings = resolveMotionTiming(animation, MODAL_TIMING);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Fragment>
|
|
71
|
+
{ovCloneTrigger(trigger, { open, onPress: () => setOpen(true), panelId, haspopup: 'dialog', triggerRef })}
|
|
72
|
+
<OverlayPortal container={container}>
|
|
73
|
+
<Presence>
|
|
74
|
+
{open && (
|
|
75
|
+
<ModalShell
|
|
76
|
+
key="modal"
|
|
77
|
+
animate={{ opacity: [0, 1], scale: [0.98, 1], timing: timings.open }}
|
|
78
|
+
exit={{ opacity: [0], scale: [0.98], timing: timings.close }}
|
|
79
|
+
layerClass="overlay-layer--modal"
|
|
80
|
+
panelClass="overlay-panel--modal"
|
|
81
|
+
panelId={panelId}
|
|
82
|
+
dismissible={dismissible}
|
|
83
|
+
container={container}
|
|
84
|
+
requestClose={close}
|
|
85
|
+
panelProps={htmlProps}
|
|
86
|
+
layerProps={layerProps}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</ModalShell>
|
|
90
|
+
)}
|
|
91
|
+
</Presence>
|
|
92
|
+
</OverlayPortal>
|
|
93
|
+
</Fragment>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* modal.css - the unpainted modal surface: the shell layer centers the panel and
|
|
2
|
+
constrains it to the layer box. Everything else is the consumer's paint. */
|
|
3
|
+
.overlay-layer--modal {
|
|
4
|
+
place-items: center;
|
|
5
|
+
}
|
|
6
|
+
.overlay-panel--modal {
|
|
7
|
+
max-width: 100%;
|
|
8
|
+
max-height: 100%;
|
|
9
|
+
}
|
|
@@ -9,10 +9,12 @@ import type { DisableableAnimation } from '../../../motion/timing';
|
|
|
9
9
|
import { useControllable } from '../../internal/hooks/use-controllable';
|
|
10
10
|
import { ovCloneTrigger, useOverlayEntry, useOutsidePress, OverlayPortal } from '../../internal/overlay/layer';
|
|
11
11
|
import { useReturnFocus } from '../../internal/overlay/focus';
|
|
12
|
-
import { useAnchorPosition } from '../../internal/overlay/position';
|
|
12
|
+
import { useAnchorPosition, type VirtualAnchor } from '../../internal/overlay/position';
|
|
13
13
|
import type { DataAttributes } from '../../../dom-props';
|
|
14
14
|
import { cx } from '../../internal/utils/cx';
|
|
15
15
|
|
|
16
|
+
export type { VirtualAnchor };
|
|
17
|
+
|
|
16
18
|
const POPOVER_TIMING = {
|
|
17
19
|
open: { duration: 'base', ease: 'entrance' },
|
|
18
20
|
close: { duration: 'fast', ease: 'exit' },
|
|
@@ -26,8 +28,11 @@ export interface PopoverProps {
|
|
|
26
28
|
/** Fires whenever the open state changes. Pair with `open` for controlled use. */
|
|
27
29
|
onOpenChange?: (open: boolean) => void;
|
|
28
30
|
|
|
29
|
-
/** Cloned to toggle the panel
|
|
31
|
+
/** Cloned to toggle the panel, and used as the anchor unless `anchor` is set. */
|
|
30
32
|
trigger?: ReactElement | null;
|
|
33
|
+
/** Anchor to an arbitrary rect instead of the trigger - any `{ getBoundingClientRect() }`, which an element also satisfies.
|
|
34
|
+
* Drive `open` yourself; pass a new object to re-place a moving anchor. */
|
|
35
|
+
anchor?: VirtualAnchor | null;
|
|
31
36
|
|
|
32
37
|
/** Preferred side; flips to the opposite side when cramped. Default 'bottom'. */
|
|
33
38
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
@@ -56,6 +61,7 @@ function PopoverPanel({
|
|
|
56
61
|
arrow,
|
|
57
62
|
dismissible,
|
|
58
63
|
requestClose,
|
|
64
|
+
anchor,
|
|
59
65
|
triggerRef,
|
|
60
66
|
htmlProps,
|
|
61
67
|
animate,
|
|
@@ -68,6 +74,7 @@ function PopoverPanel({
|
|
|
68
74
|
arrow: boolean;
|
|
69
75
|
dismissible: boolean;
|
|
70
76
|
requestClose: () => void;
|
|
77
|
+
anchor?: VirtualAnchor | null;
|
|
71
78
|
triggerRef: RefObject<HTMLElement>;
|
|
72
79
|
htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
|
|
73
80
|
children: ReactNode;
|
|
@@ -76,7 +83,7 @@ function PopoverPanel({
|
|
|
76
83
|
const entry = useOverlayEntry({ nodeRef: panelRef, dismissible, requestClose });
|
|
77
84
|
useMotion(panelRef, { animate, exit });
|
|
78
85
|
useReturnFocus(panelRef);
|
|
79
|
-
useAnchorPosition({ side, align, arrow, triggerRef, panelRef });
|
|
86
|
+
useAnchorPosition({ side, align, arrow, anchor, triggerRef, panelRef });
|
|
80
87
|
useOutsidePress({ entry, refs: [triggerRef, panelRef], enabled: dismissible, onPress: requestClose });
|
|
81
88
|
return (
|
|
82
89
|
<div
|
|
@@ -96,6 +103,7 @@ export function Popover({
|
|
|
96
103
|
defaultOpen = false,
|
|
97
104
|
onOpenChange,
|
|
98
105
|
trigger = null,
|
|
106
|
+
anchor = null,
|
|
99
107
|
side = 'bottom',
|
|
100
108
|
align = 'start',
|
|
101
109
|
arrow = false,
|
|
@@ -128,6 +136,7 @@ export function Popover({
|
|
|
128
136
|
arrow={arrow}
|
|
129
137
|
dismissible={dismissible}
|
|
130
138
|
requestClose={close}
|
|
139
|
+
anchor={anchor}
|
|
131
140
|
triggerRef={triggerRef}
|
|
132
141
|
htmlProps={htmlProps}
|
|
133
142
|
>
|
|
@@ -51,24 +51,20 @@ export function SelectTrigger({
|
|
|
51
51
|
...htmlProps,
|
|
52
52
|
}}
|
|
53
53
|
onClick={() => (open ? requestClose() : show())}
|
|
54
|
-
iconRight={
|
|
55
|
-
<span className="select__caret">
|
|
56
|
-
<Icon name="caret-down" size="sm" />
|
|
57
|
-
</span>
|
|
58
|
-
}
|
|
59
|
-
iconLeft={
|
|
60
|
-
leading && (
|
|
61
|
-
<span className="select__leading">
|
|
62
|
-
<IconSlot size="sm">{leading}</IconSlot>
|
|
63
|
-
</span>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
54
|
{...rest}
|
|
67
55
|
>
|
|
56
|
+
{leading && (
|
|
57
|
+
<span className="select__leading">
|
|
58
|
+
<IconSlot size="sm">{leading}</IconSlot>
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
68
61
|
<span className="select__value" data-placeholder={isPlaceholder ? 'true' : undefined}>
|
|
69
62
|
{text}
|
|
70
63
|
</span>
|
|
71
64
|
{count != null && count > 0 && <span className="select__count">+{count}</span>}
|
|
65
|
+
<span className="select__caret">
|
|
66
|
+
<Icon name="caret-down" size="sm" />
|
|
67
|
+
</span>
|
|
72
68
|
</Button>
|
|
73
69
|
);
|
|
74
70
|
}
|
|
@@ -35,6 +35,10 @@ export interface SheetProps {
|
|
|
35
35
|
/** Scrim/Esc dismissal + drag-to-edge. Default true. */
|
|
36
36
|
dismissible?: boolean;
|
|
37
37
|
|
|
38
|
+
/** Dock inside this element instead of the viewport - it must be positioned (`position: relative` or similar).
|
|
39
|
+
* Scrim, scroll lock and inert scope to it; the rest of the page stays interactive. */
|
|
40
|
+
container?: HTMLElement | null;
|
|
41
|
+
|
|
38
42
|
/** Base id for the panel; drives the trigger's `aria-controls`. Auto-generated when omitted. */
|
|
39
43
|
id?: string;
|
|
40
44
|
/** Standard attributes (className, style, data-*, ...) forwarded to the sheet panel. */
|
|
@@ -54,6 +58,7 @@ function SheetShell({
|
|
|
54
58
|
side,
|
|
55
59
|
panelId,
|
|
56
60
|
dismissible,
|
|
61
|
+
container,
|
|
57
62
|
requestClose,
|
|
58
63
|
htmlProps,
|
|
59
64
|
animate,
|
|
@@ -63,6 +68,7 @@ function SheetShell({
|
|
|
63
68
|
side: 'right' | 'bottom';
|
|
64
69
|
panelId: string;
|
|
65
70
|
dismissible: boolean;
|
|
71
|
+
container?: HTMLElement | null;
|
|
66
72
|
requestClose: () => void;
|
|
67
73
|
htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
|
|
68
74
|
children: ReactNode;
|
|
@@ -77,6 +83,7 @@ function SheetShell({
|
|
|
77
83
|
panelClass={'overlay-panel--sheet-' + side}
|
|
78
84
|
panelId={panelId}
|
|
79
85
|
dismissible={dismissible}
|
|
86
|
+
container={container}
|
|
80
87
|
requestClose={requestClose}
|
|
81
88
|
panelRef={panelRef}
|
|
82
89
|
panelProps={{ ...htmlProps, ...dragProps }}
|
|
@@ -93,6 +100,7 @@ export function Sheet({
|
|
|
93
100
|
trigger = null,
|
|
94
101
|
side = 'right',
|
|
95
102
|
dismissible = true,
|
|
103
|
+
container = null,
|
|
96
104
|
id,
|
|
97
105
|
htmlProps,
|
|
98
106
|
animation,
|
|
@@ -108,7 +116,7 @@ export function Sheet({
|
|
|
108
116
|
return (
|
|
109
117
|
<Fragment>
|
|
110
118
|
{ovCloneTrigger(trigger, { open, onPress: () => setOpen(true), panelId, haspopup: 'dialog', triggerRef })}
|
|
111
|
-
<OverlayPortal>
|
|
119
|
+
<OverlayPortal container={container}>
|
|
112
120
|
<Presence>
|
|
113
121
|
{open && (
|
|
114
122
|
<SheetShell
|
|
@@ -122,6 +130,7 @@ export function Sheet({
|
|
|
122
130
|
side={side}
|
|
123
131
|
panelId={panelId}
|
|
124
132
|
dismissible={dismissible}
|
|
133
|
+
container={container}
|
|
125
134
|
requestClose={close}
|
|
126
135
|
htmlProps={htmlProps}
|
|
127
136
|
>
|
|
@@ -129,7 +129,7 @@ function ovCloneTrigger(
|
|
|
129
129
|
} as Partial<typeof trigger.props>);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function OverlayPortal({ children }: { children: ReactNode }) {
|
|
132
|
+
function OverlayPortal({ container, children }: { container?: HTMLElement | null; children: ReactNode }) {
|
|
133
133
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
134
134
|
if (typeof document !== 'undefined' && !hostRef.current) {
|
|
135
135
|
hostRef.current = document.createElement('div');
|
|
@@ -138,9 +138,9 @@ function OverlayPortal({ children }: { children: ReactNode }) {
|
|
|
138
138
|
useLayoutEffect(() => {
|
|
139
139
|
const el = hostRef.current;
|
|
140
140
|
if (!el) return;
|
|
141
|
-
document.body.appendChild(el);
|
|
141
|
+
(container ?? document.body).appendChild(el);
|
|
142
142
|
return () => el.remove();
|
|
143
|
-
}, []);
|
|
143
|
+
}, [container]);
|
|
144
144
|
if (!hostRef.current) return null;
|
|
145
145
|
return createPortal(children, hostRef.current);
|
|
146
146
|
}
|