panelui-web 0.1.0 → 0.3.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/dist/components/card/index.d.ts +107 -0
- package/dist/components/card/index.js +88 -0
- package/dist/components/command-dock/index.d.ts +23 -2
- package/dist/components/command-dock/index.js +22 -2
- package/dist/components/day-grid/index.d.ts +45 -1
- package/dist/components/day-grid/index.js +40 -1
- package/dist/components/feedback/index.d.ts +50 -1
- package/dist/components/feedback/index.js +43 -1
- package/dist/components/flow/index.d.ts +61 -3
- package/dist/components/flow/index.js +65 -9
- package/dist/components/frame/index.d.ts +191 -0
- package/dist/components/frame/index.js +186 -0
- package/dist/components/kpi/index.d.ts +3 -1
- package/dist/components/kpi/index.js +6 -3
- package/dist/components/lane-board/index.d.ts +9 -1
- package/dist/components/lane-board/index.js +8 -1
- package/dist/components/planet/index.d.ts +24 -1
- package/dist/components/planet/index.js +27 -6
- package/dist/components/range-meter/index.d.ts +30 -2
- package/dist/components/range-meter/index.js +27 -2
- package/dist/components/sheet/index.d.ts +148 -0
- package/dist/components/sheet/index.js +330 -0
- package/dist/components/stat/index.d.ts +106 -0
- package/dist/components/stat/index.js +72 -0
- package/dist/components/tilt-card/index.d.ts +23 -2
- package/dist/components/tilt-card/index.js +19 -2
- package/dist/index.d.ts +12 -8
- package/dist/index.js +12 -8
- package/package.json +1 -1
|
@@ -80,6 +80,11 @@ const planetVariants = tv({
|
|
|
80
80
|
controls: 'inline-flex items-stretch divide-x divide-border overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm',
|
|
81
81
|
control: 'inline-flex size-9 cursor-pointer items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground active:bg-muted',
|
|
82
82
|
caption: 'flex items-center px-3 text-sm font-medium',
|
|
83
|
+
// The places on the globe again, as a list beside it, ruled off from what is above.
|
|
84
|
+
list: 'grid grid-cols-2 gap-1 border-t pt-3',
|
|
85
|
+
listItem: 'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-start text-sm text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-muted aria-pressed:text-foreground',
|
|
86
|
+
listDot: 'size-2 shrink-0 rounded-full bg-[var(--pu-color)]',
|
|
87
|
+
listName: 'truncate',
|
|
83
88
|
},
|
|
84
89
|
variants: {
|
|
85
90
|
/** The colour of the light at the globe's edge. */
|
|
@@ -99,11 +104,11 @@ const planetVariants = tv({
|
|
|
99
104
|
},
|
|
100
105
|
},
|
|
101
106
|
color: {
|
|
102
|
-
primary: { marker: '[--pu-color:var(--primary)]' },
|
|
103
|
-
info: { marker: '[--pu-color:var(--info)]' },
|
|
104
|
-
success: { marker: '[--pu-color:var(--success)]' },
|
|
105
|
-
warning: { marker: '[--pu-color:var(--warning)]' },
|
|
106
|
-
destructive: { marker: '[--pu-color:var(--destructive)]' },
|
|
107
|
+
primary: { marker: '[--pu-color:var(--primary)]', listDot: '[--pu-color:var(--primary)]' },
|
|
108
|
+
info: { marker: '[--pu-color:var(--info)]', listDot: '[--pu-color:var(--info)]' },
|
|
109
|
+
success: { marker: '[--pu-color:var(--success)]', listDot: '[--pu-color:var(--success)]' },
|
|
110
|
+
warning: { marker: '[--pu-color:var(--warning)]', listDot: '[--pu-color:var(--warning)]' },
|
|
111
|
+
destructive: { marker: '[--pu-color:var(--destructive)]', listDot: '[--pu-color:var(--destructive)]' },
|
|
107
112
|
},
|
|
108
113
|
},
|
|
109
114
|
defaultVariants: { color: 'primary', glow: 'primary' },
|
|
@@ -638,10 +643,26 @@ function PlanetControls({ step = 60, className, children, ...props }) {
|
|
|
638
643
|
return (_jsxs("div", { role: "group", "aria-label": "Turn the globe", className: slots.controls({ className }), ...props, children: [_jsx("button", { type: "button", "aria-label": "Turn west", className: slots.control(), onClick: () => planet.rotateBy(-step), children: _jsx(Arrow, { flipped: true }) }), children !== undefined && children !== null ? (_jsx("span", { className: slots.caption(), children: children })) : null, _jsx("button", { type: "button", "aria-label": "Turn east", className: slots.control(), onClick: () => planet.rotateBy(step), children: _jsx(Arrow, {}) })] }));
|
|
639
644
|
}
|
|
640
645
|
PlanetControls.displayName = 'Planet.Controls';
|
|
646
|
+
/**
|
|
647
|
+
* The places on the globe as a list, in two columns, for picking one by name.
|
|
648
|
+
* Put it beside or under the globe and turn the globe to whatever is picked.
|
|
649
|
+
*/
|
|
650
|
+
function PlanetList({ className, ...props }) {
|
|
651
|
+
return _jsx("ul", { "data-slot": "planet-list", className: planetVariants().list({ className }), ...props });
|
|
652
|
+
}
|
|
653
|
+
PlanetList.displayName = 'Planet.List';
|
|
654
|
+
/** One place in `Planet.List`: a dot in its marker's colour, and its name. A button. */
|
|
655
|
+
function PlanetListItem({ color = 'primary', selected = false, className, children, ...props }) {
|
|
656
|
+
const slots = planetVariants({ color });
|
|
657
|
+
return (_jsx("li", { "data-slot": "planet-list-item", children: _jsxs("button", { type: "button", "aria-pressed": selected, className: slots.listItem({ className }), ...props, children: [_jsx("span", { "aria-hidden": "true", className: slots.listDot() }), _jsx("span", { className: slots.listName(), children: children })] }) }));
|
|
658
|
+
}
|
|
659
|
+
PlanetListItem.displayName = 'Planet.ListItem';
|
|
641
660
|
export const Planet = Object.assign(PlanetRoot, {
|
|
642
661
|
Globe: PlanetGlobe,
|
|
643
662
|
Marker: PlanetMarker,
|
|
644
663
|
Controls: PlanetControls,
|
|
664
|
+
List: PlanetList,
|
|
665
|
+
ListItem: PlanetListItem,
|
|
645
666
|
});
|
|
646
667
|
/*
|
|
647
668
|
* The parts again, by name. `Planet.Globe` is a property of a component, and a
|
|
@@ -650,4 +671,4 @@ export const Planet = Object.assign(PlanetRoot, {
|
|
|
650
671
|
* Server Component writes `<PlanetGlobe>`; anywhere else the dotted form is
|
|
651
672
|
* the same thing.
|
|
652
673
|
*/
|
|
653
|
-
export { PlanetGlobe, PlanetMarker, PlanetControls };
|
|
674
|
+
export { PlanetGlobe, PlanetMarker, PlanetControls, PlanetList, PlanetListItem };
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* ruler of ticks that rises under the value.
|
|
4
4
|
*
|
|
5
5
|
* ```tsx
|
|
6
|
+
* <RangeMeter.Label>Response time</RangeMeter.Label>
|
|
6
7
|
* <RangeMeter min={80} max={600} value={time} onValueChange={setTime} marker={300} />
|
|
8
|
+
* <RangeMeter.Scale start="80 ms" end="600 ms" />
|
|
7
9
|
* ```
|
|
8
10
|
*
|
|
9
11
|
* ## The ruler swells where the reading is
|
|
@@ -64,7 +66,33 @@ export interface RangeMeterProps extends Omit<ComponentProps<'div'>, 'onChange'
|
|
|
64
66
|
/** Drawn in the tag before the number: an icon. */
|
|
65
67
|
children?: ReactNode;
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
declare function RangeMeterRoot({ min, max, value, onValueChange, step, marker, ticks, color, formatValue, label, valueText, className, children, ...props }: RangeMeterProps): import("react").JSX.Element;
|
|
70
|
+
declare namespace RangeMeterRoot {
|
|
69
71
|
var displayName: string;
|
|
70
72
|
}
|
|
73
|
+
export interface RangeMeterLabelProps extends ComponentProps<'p'> {
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The meter's name, written over it. The meter's own `label` is what a screen
|
|
77
|
+
* reader says; this is what everyone else reads.
|
|
78
|
+
*/
|
|
79
|
+
declare function RangeMeterLabel({ className, ...props }: RangeMeterLabelProps): import("react").JSX.Element;
|
|
80
|
+
declare namespace RangeMeterLabel {
|
|
81
|
+
var displayName: string;
|
|
82
|
+
}
|
|
83
|
+
export interface RangeMeterScaleProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
84
|
+
/** The word or figure at the low end. */
|
|
85
|
+
start: ReactNode;
|
|
86
|
+
/** The word or figure at the high end. */
|
|
87
|
+
end: ReactNode;
|
|
88
|
+
}
|
|
89
|
+
/** What the two ends of the ruler stand for, written under it at either side. */
|
|
90
|
+
declare function RangeMeterScale({ start, end, className, ...props }: RangeMeterScaleProps): import("react").JSX.Element;
|
|
91
|
+
declare namespace RangeMeterScale {
|
|
92
|
+
var displayName: string;
|
|
93
|
+
}
|
|
94
|
+
export declare const RangeMeter: typeof RangeMeterRoot & {
|
|
95
|
+
Label: typeof RangeMeterLabel;
|
|
96
|
+
Scale: typeof RangeMeterScale;
|
|
97
|
+
};
|
|
98
|
+
export { RangeMeterLabel, RangeMeterScale };
|
|
@@ -5,7 +5,9 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
5
5
|
* ruler of ticks that rises under the value.
|
|
6
6
|
*
|
|
7
7
|
* ```tsx
|
|
8
|
+
* <RangeMeter.Label>Response time</RangeMeter.Label>
|
|
8
9
|
* <RangeMeter min={80} max={600} value={time} onValueChange={setTime} marker={300} />
|
|
10
|
+
* <RangeMeter.Scale start="80 ms" end="600 ms" />
|
|
9
11
|
* ```
|
|
10
12
|
*
|
|
11
13
|
* ## The ruler swells where the reading is
|
|
@@ -44,6 +46,9 @@ const rangeMeterVariants = tv({
|
|
|
44
46
|
ruler: 'relative flex h-10 items-end justify-between',
|
|
45
47
|
tick: 'w-[3px] rounded-full transition-[height,background-color,opacity,box-shadow] duration-200 ease-out motion-reduce:transition-none',
|
|
46
48
|
marker: 'pointer-events-none absolute -bottom-2.5 size-1.5 -translate-x-1/2 rotate-45 rounded-[1px] bg-foreground rtl:translate-x-1/2',
|
|
49
|
+
// Siblings of the meter, written beside it rather than inside it.
|
|
50
|
+
label: 'text-sm font-medium',
|
|
51
|
+
scale: 'flex justify-between text-xs text-muted-foreground',
|
|
47
52
|
tag: 'pointer-events-none absolute top-0 flex h-7 items-center gap-1 whitespace-nowrap rounded-full bg-[var(--pu-color)] px-2.5 text-xs font-semibold tabular-nums text-[var(--pu-on-color)] shadow-[0_6px_18px_-4px_color-mix(in_oklab,var(--pu-color)_70%,transparent)] transition-[background-color,box-shadow,scale] duration-200 motion-reduce:transition-none [&_svg]:size-3.5 [&_svg]:shrink-0',
|
|
48
53
|
},
|
|
49
54
|
variants: {
|
|
@@ -85,7 +90,7 @@ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
|
85
90
|
const SPREAD = 2.6;
|
|
86
91
|
/** A resting tick's height, as a share of the ruler's. */
|
|
87
92
|
const REST = 0.34;
|
|
88
|
-
|
|
93
|
+
function RangeMeterRoot({ min = 0, max = 100, value, onValueChange, step = 1, marker, ticks = 41, color = 'primary', formatValue, label = 'Reading', valueText, className, children, ...props }) {
|
|
89
94
|
const interactive = onValueChange !== undefined;
|
|
90
95
|
const slots = rangeMeterVariants({ color, interactive, marked: marker !== undefined });
|
|
91
96
|
const ruler = useRef(null);
|
|
@@ -167,4 +172,24 @@ export function RangeMeter({ min = 0, max = 100, value, onValueChange, step = 1,
|
|
|
167
172
|
} }, index));
|
|
168
173
|
}), marker !== undefined ? (_jsx("span", { className: slots.marker(), style: { insetInlineStart: `${(clamp((marker - min) / span, 0, 1) * 100).toFixed(2)}%` } })) : null] })] }));
|
|
169
174
|
}
|
|
170
|
-
|
|
175
|
+
RangeMeterRoot.displayName = 'RangeMeter';
|
|
176
|
+
/**
|
|
177
|
+
* The meter's name, written over it. The meter's own `label` is what a screen
|
|
178
|
+
* reader says; this is what everyone else reads.
|
|
179
|
+
*/
|
|
180
|
+
function RangeMeterLabel({ className, ...props }) {
|
|
181
|
+
return _jsx("p", { "data-slot": "range-meter-label", className: rangeMeterVariants().label({ className }), ...props });
|
|
182
|
+
}
|
|
183
|
+
RangeMeterLabel.displayName = 'RangeMeter.Label';
|
|
184
|
+
/** What the two ends of the ruler stand for, written under it at either side. */
|
|
185
|
+
function RangeMeterScale({ start, end, className, ...props }) {
|
|
186
|
+
return (_jsxs("div", { "data-slot": "range-meter-scale", className: rangeMeterVariants().scale({ className }), ...props, children: [_jsx("span", { children: start }), _jsx("span", { children: end })] }));
|
|
187
|
+
}
|
|
188
|
+
RangeMeterScale.displayName = 'RangeMeter.Scale';
|
|
189
|
+
export const RangeMeter = Object.assign(RangeMeterRoot, {
|
|
190
|
+
Label: RangeMeterLabel,
|
|
191
|
+
Scale: RangeMeterScale,
|
|
192
|
+
});
|
|
193
|
+
/* By name as well, for a Server Component, which cannot dot into a client
|
|
194
|
+
module. */
|
|
195
|
+
export { RangeMeterLabel, RangeMeterScale };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
/** Where the sheet rises from, which is also the direction it leaves in. */
|
|
3
|
+
export type SheetSide = 'bottom' | 'top' | 'left' | 'right';
|
|
4
|
+
export interface SheetProps {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
/** Whether the sheet is open, when the caller holds that. */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
/** Called when the sheet asks to open or close. */
|
|
9
|
+
onOpenChange?: (open: boolean) => void;
|
|
10
|
+
/** Whether it starts open, when the caller does not hold it. */
|
|
11
|
+
defaultOpen?: boolean;
|
|
12
|
+
/** The edge it rises from and is swiped back to. Default `bottom`. */
|
|
13
|
+
side?: SheetSide;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a click outside, a swipe or Escape closes it. Turn it off for a
|
|
16
|
+
* sheet that must be answered; `Sheet.Close` still works.
|
|
17
|
+
*/
|
|
18
|
+
dismissible?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function SheetRoot({ children, open, onOpenChange, defaultOpen, side, dismissible, }: SheetProps): import("react").JSX.Element;
|
|
21
|
+
declare namespace SheetRoot {
|
|
22
|
+
var displayName: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SheetTriggerProps extends Omit<ComponentProps<'button'>, 'children'> {
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* Your own element to be the trigger, such as a styled button. It is given
|
|
28
|
+
* the click handler and the attributes that tie it to the sheet.
|
|
29
|
+
*/
|
|
30
|
+
render?: ReactElement;
|
|
31
|
+
}
|
|
32
|
+
/** What opens the sheet. A plain button unless `render` supplies one. */
|
|
33
|
+
declare function SheetTrigger({ render, children, ...props }: SheetTriggerProps): import("react").JSX.Element;
|
|
34
|
+
declare namespace SheetTrigger {
|
|
35
|
+
var displayName: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SheetContentProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Stand clear of the screen edges with every corner rounded. On by default;
|
|
41
|
+
* off, the sheet meets the edges it rises from.
|
|
42
|
+
*/
|
|
43
|
+
detached?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* How tall a top or bottom sheet is. `auto` fits the content; `half` and
|
|
46
|
+
* `full` take that share of the screen, and `full` meets the edges.
|
|
47
|
+
*/
|
|
48
|
+
size?: 'auto' | 'half' | 'full';
|
|
49
|
+
/** Show the grabber. On by default for a top or bottom sheet. */
|
|
50
|
+
showGrabber?: boolean;
|
|
51
|
+
/** Show the round close button in the top trailing corner. Default true. */
|
|
52
|
+
showClose?: boolean;
|
|
53
|
+
/** Frost the page behind the sheet instead of only dimming it. */
|
|
54
|
+
blur?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Draw the sheet in the page instead of over it: no backdrop, no trigger,
|
|
57
|
+
* always there. For previews and for a layout that shows the sheet as part
|
|
58
|
+
* of a screen.
|
|
59
|
+
*/
|
|
60
|
+
inline?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** The sheet itself, with the backdrop behind it. */
|
|
63
|
+
declare function SheetContent({ children, detached, size, showGrabber, showClose, blur, inline, className, ...props }: SheetContentProps): import("react").JSX.Element;
|
|
64
|
+
declare namespace SheetContent {
|
|
65
|
+
var displayName: string;
|
|
66
|
+
}
|
|
67
|
+
export interface SheetHeaderProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
68
|
+
/** The heading. In an open sheet it is also what the sheet is announced as. */
|
|
69
|
+
title?: ReactNode;
|
|
70
|
+
/** A line under the heading. */
|
|
71
|
+
description?: ReactNode;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The heading block. It leaves room for the close button beside the title,
|
|
75
|
+
* so the two share the top line instead of stacking.
|
|
76
|
+
*/
|
|
77
|
+
declare function SheetHeader({ title, description, className, children, ...props }: SheetHeaderProps): import("react").JSX.Element;
|
|
78
|
+
declare namespace SheetHeader {
|
|
79
|
+
var displayName: string;
|
|
80
|
+
}
|
|
81
|
+
export interface SheetTitleProps extends ComponentProps<'h2'> {
|
|
82
|
+
}
|
|
83
|
+
/** The sheet's name, announced when it opens. */
|
|
84
|
+
declare function SheetTitle({ className, ...props }: SheetTitleProps): import("react").JSX.Element;
|
|
85
|
+
declare namespace SheetTitle {
|
|
86
|
+
var displayName: string;
|
|
87
|
+
}
|
|
88
|
+
export interface SheetDescriptionProps extends ComponentProps<'p'> {
|
|
89
|
+
}
|
|
90
|
+
/** A sentence under the title, read out after it. */
|
|
91
|
+
declare function SheetDescription({ className, ...props }: SheetDescriptionProps): import("react").JSX.Element;
|
|
92
|
+
declare namespace SheetDescription {
|
|
93
|
+
var displayName: string;
|
|
94
|
+
}
|
|
95
|
+
export interface SheetBodyProps extends ComponentProps<'div'> {
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The part that scrolls. The header and footer stay put while it moves, and
|
|
99
|
+
* the scrollbar is a thin drawn thumb that shows while the content is moving.
|
|
100
|
+
*/
|
|
101
|
+
declare function SheetBody({ className, children, ...props }: SheetBodyProps): import("react").JSX.Element;
|
|
102
|
+
declare namespace SheetBody {
|
|
103
|
+
var displayName: string;
|
|
104
|
+
}
|
|
105
|
+
export interface SheetFooterProps extends ComponentProps<'div'> {
|
|
106
|
+
}
|
|
107
|
+
/** The actions, stacked full width at the bottom of the sheet. */
|
|
108
|
+
declare function SheetFooter({ className, ...props }: SheetFooterProps): import("react").JSX.Element;
|
|
109
|
+
declare namespace SheetFooter {
|
|
110
|
+
var displayName: string;
|
|
111
|
+
}
|
|
112
|
+
export interface SheetCloseProps extends ComponentProps<'button'> {
|
|
113
|
+
/**
|
|
114
|
+
* Your own element to close the sheet with, such as a Cancel button.
|
|
115
|
+
* Without it, and without children, this is the round close button.
|
|
116
|
+
*/
|
|
117
|
+
render?: ReactElement;
|
|
118
|
+
/** Draw the cross glyph. Set for you on the built-in close button. */
|
|
119
|
+
icon?: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** Closes the sheet. Works even when `dismissible` is off. */
|
|
122
|
+
declare function SheetClose({ render, icon, children, ...props }: SheetCloseProps): import("react").JSX.Element;
|
|
123
|
+
declare namespace SheetClose {
|
|
124
|
+
var displayName: string;
|
|
125
|
+
}
|
|
126
|
+
export interface SheetStageProps extends ComponentProps<'div'> {
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Wrap the app — or the part of the page a sheet belongs to — in this, and
|
|
130
|
+
* the page steps back slightly while a sheet is open. Sheets opened anywhere
|
|
131
|
+
* inside it take part.
|
|
132
|
+
*/
|
|
133
|
+
declare function SheetStage({ className, children, ...props }: SheetStageProps): import("react").JSX.Element;
|
|
134
|
+
declare namespace SheetStage {
|
|
135
|
+
var displayName: string;
|
|
136
|
+
}
|
|
137
|
+
export declare const Sheet: typeof SheetRoot & {
|
|
138
|
+
Trigger: typeof SheetTrigger;
|
|
139
|
+
Content: typeof SheetContent;
|
|
140
|
+
Header: typeof SheetHeader;
|
|
141
|
+
Title: typeof SheetTitle;
|
|
142
|
+
Description: typeof SheetDescription;
|
|
143
|
+
Body: typeof SheetBody;
|
|
144
|
+
Footer: typeof SheetFooter;
|
|
145
|
+
Close: typeof SheetClose;
|
|
146
|
+
Stage: typeof SheetStage;
|
|
147
|
+
};
|
|
148
|
+
export { SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Sheet — a panel that rises over the page from one of its edges, floating
|
|
5
|
+
* clear of them, and leaves with a swipe.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <Sheet>
|
|
9
|
+
* <Sheet.Trigger render={<Button />}>Edit profile</Sheet.Trigger>
|
|
10
|
+
* <Sheet.Content>
|
|
11
|
+
* <Sheet.Header title="Edit profile" description="Shown on your public page." />
|
|
12
|
+
* <Sheet.Body>…</Sheet.Body>
|
|
13
|
+
* <Sheet.Footer>
|
|
14
|
+
* <Button>Save</Button>
|
|
15
|
+
* <Sheet.Close render={<Button variant="ghost" />}>Cancel</Sheet.Close>
|
|
16
|
+
* </Sheet.Footer>
|
|
17
|
+
* </Sheet.Content>
|
|
18
|
+
* </Sheet>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## It floats
|
|
22
|
+
*
|
|
23
|
+
* A sheet that holds a task for the page is a card laid over it, so by
|
|
24
|
+
* default it stands a few pixels off every edge with all four corners rounded.
|
|
25
|
+
* The corners are large and concentric with the gap around them, which is what
|
|
26
|
+
* makes the card read as belonging to the screen it sits in rather than as a
|
|
27
|
+
* box dropped onto it. `size="full"` or `detached={false}` gives the gap up and
|
|
28
|
+
* meets the edges, for a sheet that needs every pixel.
|
|
29
|
+
*
|
|
30
|
+
* ## Sheets stack
|
|
31
|
+
*
|
|
32
|
+
* A sheet opened from inside another slides over it from the same edge, and
|
|
33
|
+
* the one underneath steps back: a little smaller, pushed inward so its edge
|
|
34
|
+
* still shows, and dimmed. Only the first sheet darkens the page, so two open
|
|
35
|
+
* sheets do not stack two shades of black.
|
|
36
|
+
*
|
|
37
|
+
* ## The hand decides
|
|
38
|
+
*
|
|
39
|
+
* Every sheet follows a drag and leaves on a flick toward its own edge. The
|
|
40
|
+
* grabber on a top or bottom sheet says so before anybody tries.
|
|
41
|
+
*/
|
|
42
|
+
import { Drawer } from '@base-ui/react/drawer';
|
|
43
|
+
import { ScrollArea } from '@base-ui/react/scroll-area';
|
|
44
|
+
import { createContext, useContext, useMemo, } from 'react';
|
|
45
|
+
import { tv } from 'tailwind-variants';
|
|
46
|
+
/*
|
|
47
|
+
* One curve for every movement: the sheet arriving, the one behind it stepping
|
|
48
|
+
* back, the page receding. A fast start that eases into place reads as weight,
|
|
49
|
+
* where a symmetric ease reads as a slide show.
|
|
50
|
+
*/
|
|
51
|
+
const MOVE = '[transition:transform_450ms_cubic-bezier(0.32,0.72,0,1),border-radius_450ms_cubic-bezier(0.32,0.72,0,1),margin_450ms_cubic-bezier(0.32,0.72,0,1)]';
|
|
52
|
+
const sheetVariants = tv({
|
|
53
|
+
slots: {
|
|
54
|
+
backdrop: [
|
|
55
|
+
'fixed inset-0 z-50 bg-black opacity-[calc(var(--sheet-dim)*(1-var(--drawer-swipe-progress,0)))]',
|
|
56
|
+
'[--sheet-dim:0.28] dark:[--sheet-dim:0.5]',
|
|
57
|
+
'transition-opacity duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:duration-0',
|
|
58
|
+
'data-starting-style:opacity-0 data-ending-style:opacity-0',
|
|
59
|
+
'data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] motion-reduce:transition-none',
|
|
60
|
+
],
|
|
61
|
+
/*
|
|
62
|
+
* The viewport holds the gap. The sheet is laid out inside it, so the
|
|
63
|
+
* distance to every edge is one number, and the corner radius below is
|
|
64
|
+
* that number's companion.
|
|
65
|
+
*/
|
|
66
|
+
viewport: 'pointer-events-none fixed inset-0 z-50 flex [--sheet-gap:0.5rem] sm:[--sheet-gap:0.75rem]',
|
|
67
|
+
popup: [
|
|
68
|
+
'group/sheet pointer-events-auto relative flex min-h-0 flex-col bg-popover text-popover-foreground outline-none',
|
|
69
|
+
'shadow-[0_24px_64px_-16px_rgb(0_0_0/0.3)] ring-1 ring-foreground/[0.06] dark:ring-foreground/10',
|
|
70
|
+
MOVE,
|
|
71
|
+
'data-swiping:select-none data-swiping:duration-0 data-nested-drawer-swiping:duration-0',
|
|
72
|
+
'data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*400ms)]',
|
|
73
|
+
'motion-reduce:transition-none',
|
|
74
|
+
/*
|
|
75
|
+
* The stack. Each sheet behind the front one is 4% smaller and pushed
|
|
76
|
+
* 14px inward from its edge, so a strip of it shows past the one in front
|
|
77
|
+
* and the depth can be counted. It scales about its inner edge, so the
|
|
78
|
+
* strip stays put while it shrinks.
|
|
79
|
+
*/
|
|
80
|
+
'[--sheet-scale:calc(1-var(--nested-drawers,0)*0.04)] [--sheet-push:calc(var(--nested-drawers,0)*14px)]',
|
|
81
|
+
// A veil in the sheet's own colour: what is on the sheet behind fades, the
|
|
82
|
+
// same way in either theme, instead of going grey under a black tint.
|
|
83
|
+
"after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-popover after:opacity-0 after:transition-opacity after:duration-[450ms] after:content-['']",
|
|
84
|
+
'data-nested-drawer-open:after:opacity-50',
|
|
85
|
+
],
|
|
86
|
+
grabber: [
|
|
87
|
+
'pointer-events-none absolute inset-x-0 z-10 mx-auto flex h-6 w-24 justify-center',
|
|
88
|
+
'before:h-[5px] before:w-9 before:rounded-full before:bg-muted-foreground/35',
|
|
89
|
+
],
|
|
90
|
+
close: [
|
|
91
|
+
'absolute end-4 top-4 z-10 inline-flex size-8 cursor-pointer items-center justify-center rounded-full',
|
|
92
|
+
'bg-muted text-muted-foreground outline-none transition-[background-color,color,scale] duration-150',
|
|
93
|
+
'after:absolute after:-inset-1.5 hover:bg-foreground/10 hover:text-foreground active:scale-95',
|
|
94
|
+
'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',
|
|
95
|
+
],
|
|
96
|
+
header: 'flex shrink-0 flex-col gap-1 px-6 pt-6 pb-3',
|
|
97
|
+
title: 'text-[19px] font-semibold leading-6 tracking-[-0.01em]',
|
|
98
|
+
description: 'text-[15px] leading-5 text-muted-foreground',
|
|
99
|
+
body: 'relative flex min-h-0 flex-1 flex-col',
|
|
100
|
+
bodyViewport: 'min-h-0 flex-1 overscroll-contain outline-none',
|
|
101
|
+
bodyContent: 'px-6 pb-2',
|
|
102
|
+
scrollbar: 'my-3 me-1 flex w-1.5 opacity-0 transition-opacity delay-300 data-hovering:opacity-100 data-hovering:delay-0 data-scrolling:opacity-100 data-scrolling:delay-0',
|
|
103
|
+
thumb: 'relative flex-1 rounded-full bg-foreground/20',
|
|
104
|
+
footer: 'flex shrink-0 flex-col gap-2 px-6 pt-3 pb-6',
|
|
105
|
+
/*
|
|
106
|
+
* The page behind steps back by 3%, not more: enough to read as a layer
|
|
107
|
+
* underneath, not so much that the page looks like it shrank. It keeps its
|
|
108
|
+
* own colours and draws no outline of its own: the only edge is the strip
|
|
109
|
+
* of the surface it steps back onto, which is the muted tone of the theme
|
|
110
|
+
* rather than black, so it is quiet in light and dark alike. A rim on top
|
|
111
|
+
* of that strip doubles every border the page sits inside.
|
|
112
|
+
*
|
|
113
|
+
* It shrinks about its centre, so the strip is the same width on every
|
|
114
|
+
* side, and its corners take `--stage-radius`: set that to the radius of
|
|
115
|
+
* whatever the stage sits in, less the strip, and the two outlines are
|
|
116
|
+
* concentric instead of a second, tighter curve inside the first.
|
|
117
|
+
*/
|
|
118
|
+
stage: 'relative isolate overflow-hidden [--stage-radius:1rem]',
|
|
119
|
+
stageBackground: 'absolute inset-0 -z-10 bg-muted',
|
|
120
|
+
stagePage: [
|
|
121
|
+
'relative h-full origin-center bg-background will-change-transform',
|
|
122
|
+
'transition-[transform,border-radius] duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none',
|
|
123
|
+
'[--stage-progress:calc(1-var(--drawer-swipe-progress,0))]',
|
|
124
|
+
'data-active:[transform:scale(calc(1-0.03*var(--stage-progress)))]',
|
|
125
|
+
'data-active:overflow-hidden data-active:rounded-[calc(var(--stage-radius)*var(--stage-progress))]',
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
variants: {
|
|
129
|
+
side: {
|
|
130
|
+
bottom: {
|
|
131
|
+
grabber: 'top-0 items-start pt-1.5',
|
|
132
|
+
viewport: 'items-end justify-center px-(--sheet-gap) pt-12 pb-[max(var(--sheet-gap),env(safe-area-inset-bottom))]',
|
|
133
|
+
popup: [
|
|
134
|
+
'max-h-full w-full max-w-lg origin-[50%_0] rounded-[32px]',
|
|
135
|
+
'[transform:translateY(calc(var(--drawer-swipe-movement-y,0px)-var(--sheet-push)))_scale(var(--sheet-scale))]',
|
|
136
|
+
'data-starting-style:[transform:translateY(calc(100%+var(--sheet-gap)+2rem))]',
|
|
137
|
+
'data-ending-style:[transform:translateY(calc(100%+var(--sheet-gap)+2rem))]',
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
top: {
|
|
141
|
+
// Swiped up, so the grabber sits on the edge the hand pushes.
|
|
142
|
+
grabber: 'bottom-0 items-end pb-1.5',
|
|
143
|
+
viewport: 'items-start justify-center px-(--sheet-gap) pb-12 pt-[max(var(--sheet-gap),env(safe-area-inset-top))]',
|
|
144
|
+
popup: [
|
|
145
|
+
'max-h-full w-full max-w-lg origin-[50%_100%] rounded-[32px]',
|
|
146
|
+
'[transform:translateY(calc(var(--drawer-swipe-movement-y,0px)+var(--sheet-push)))_scale(var(--sheet-scale))]',
|
|
147
|
+
'data-starting-style:[transform:translateY(calc(-100%-var(--sheet-gap)-2rem))]',
|
|
148
|
+
'data-ending-style:[transform:translateY(calc(-100%-var(--sheet-gap)-2rem))]',
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
left: {
|
|
152
|
+
viewport: 'items-stretch justify-start p-(--sheet-gap)',
|
|
153
|
+
popup: [
|
|
154
|
+
'h-full w-[min(24rem,calc(100vw-2*var(--sheet-gap)))] origin-[100%_50%] rounded-[28px]',
|
|
155
|
+
'[transform:translateX(calc(var(--drawer-swipe-movement-x,0px)+var(--sheet-push)))_scale(var(--sheet-scale))]',
|
|
156
|
+
'data-starting-style:[transform:translateX(calc(-100%-var(--sheet-gap)-2rem))]',
|
|
157
|
+
'data-ending-style:[transform:translateX(calc(-100%-var(--sheet-gap)-2rem))]',
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
right: {
|
|
161
|
+
viewport: 'items-stretch justify-end p-(--sheet-gap)',
|
|
162
|
+
popup: [
|
|
163
|
+
'h-full w-[min(24rem,calc(100vw-2*var(--sheet-gap)))] origin-[0_50%] rounded-[28px]',
|
|
164
|
+
'[transform:translateX(calc(var(--drawer-swipe-movement-x,0px)-var(--sheet-push)))_scale(var(--sheet-scale))]',
|
|
165
|
+
'data-starting-style:[transform:translateX(calc(100%+var(--sheet-gap)+2rem))]',
|
|
166
|
+
'data-ending-style:[transform:translateX(calc(100%+var(--sheet-gap)+2rem))]',
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
size: {
|
|
171
|
+
auto: {},
|
|
172
|
+
half: { popup: 'h-[min(50dvh,100%)]' },
|
|
173
|
+
full: { popup: 'h-full' },
|
|
174
|
+
},
|
|
175
|
+
// Meets the edges it rises from instead of standing clear of them.
|
|
176
|
+
attached: {
|
|
177
|
+
true: {},
|
|
178
|
+
},
|
|
179
|
+
blur: {
|
|
180
|
+
true: { backdrop: 'backdrop-blur-sm [--sheet-dim:0.16] dark:[--sheet-dim:0.36]' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
compoundVariants: [
|
|
184
|
+
{
|
|
185
|
+
side: 'bottom',
|
|
186
|
+
attached: true,
|
|
187
|
+
class: {
|
|
188
|
+
popup: '-mx-(--sheet-gap) -mb-[max(var(--sheet-gap),env(safe-area-inset-bottom))] max-w-[calc(100%+2*var(--sheet-gap))] rounded-b-none pb-[env(safe-area-inset-bottom)] sm:mx-0 sm:max-w-2xl',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
side: 'top',
|
|
193
|
+
attached: true,
|
|
194
|
+
class: {
|
|
195
|
+
popup: '-mx-(--sheet-gap) -mt-[max(var(--sheet-gap),env(safe-area-inset-top))] max-w-[calc(100%+2*var(--sheet-gap))] rounded-t-none sm:mx-0 sm:max-w-2xl',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{ side: 'left', attached: true, class: { popup: '-my-(--sheet-gap) -ms-(--sheet-gap) rounded-s-none' } },
|
|
199
|
+
{ side: 'right', attached: true, class: { popup: '-my-(--sheet-gap) -me-(--sheet-gap) rounded-e-none' } },
|
|
200
|
+
],
|
|
201
|
+
defaultVariants: { side: 'bottom', size: 'auto' },
|
|
202
|
+
});
|
|
203
|
+
const SWIPE = {
|
|
204
|
+
bottom: 'down',
|
|
205
|
+
top: 'up',
|
|
206
|
+
left: 'left',
|
|
207
|
+
right: 'right',
|
|
208
|
+
};
|
|
209
|
+
const SheetContext = createContext(null);
|
|
210
|
+
function useSheet(part) {
|
|
211
|
+
const context = useContext(SheetContext);
|
|
212
|
+
if (!context)
|
|
213
|
+
throw new Error(`${part} must be used inside a <Sheet>.`);
|
|
214
|
+
return context;
|
|
215
|
+
}
|
|
216
|
+
function SheetRoot({ children, open, onOpenChange, defaultOpen = false, side = 'bottom', dismissible = true, }) {
|
|
217
|
+
// A sheet inside another sheet's content finds that sheet's context.
|
|
218
|
+
const parent = useContext(SheetContext);
|
|
219
|
+
const context = useMemo(() => ({ side, nested: parent !== null && !parent.inline, inline: false, showClose: true }), [side, parent]);
|
|
220
|
+
return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: open, defaultOpen: defaultOpen, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
|
|
221
|
+
// The primitive has a switch for a press outside and none for
|
|
222
|
+
// Escape or a swipe, so those are refused here.
|
|
223
|
+
if (!next &&
|
|
224
|
+
!dismissible &&
|
|
225
|
+
details.reason !== 'close-press' &&
|
|
226
|
+
details.reason !== 'imperative-action') {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
onOpenChange?.(next);
|
|
230
|
+
}, children: children }) }));
|
|
231
|
+
}
|
|
232
|
+
SheetRoot.displayName = 'Sheet';
|
|
233
|
+
/** What opens the sheet. A plain button unless `render` supplies one. */
|
|
234
|
+
function SheetTrigger({ render, children, ...props }) {
|
|
235
|
+
return (_jsx(Drawer.Trigger, { render: render, ...props, children: children }));
|
|
236
|
+
}
|
|
237
|
+
SheetTrigger.displayName = 'Sheet.Trigger';
|
|
238
|
+
/** The sheet itself, with the backdrop behind it. */
|
|
239
|
+
function SheetContent({ children, detached = true, size = 'auto', showGrabber, showClose = true, blur = false, inline = false, className, ...props }) {
|
|
240
|
+
const sheet = useSheet('Sheet.Content');
|
|
241
|
+
const vertical = sheet.side === 'bottom' || sheet.side === 'top';
|
|
242
|
+
const slots = sheetVariants({
|
|
243
|
+
side: sheet.side,
|
|
244
|
+
size: vertical ? size : undefined,
|
|
245
|
+
attached: !detached || (vertical && size === 'full'),
|
|
246
|
+
blur,
|
|
247
|
+
});
|
|
248
|
+
const context = useMemo(() => ({ ...sheet, inline, showClose }), [sheet, inline, showClose]);
|
|
249
|
+
const inside = (_jsxs(_Fragment, { children: [(showGrabber ?? vertical) ? _jsx("div", { "aria-hidden": true, className: slots.grabber() }) : null, showClose ? _jsx(SheetClose, { "aria-label": "Close", className: slots.close(), icon: true }) : null, children] }));
|
|
250
|
+
if (inline) {
|
|
251
|
+
return (_jsx(SheetContext.Provider, { value: context, children: _jsx("div", { className: slots.popup({ className: ['[transform:none]', className] }), ...props, children: inside }) }));
|
|
252
|
+
}
|
|
253
|
+
return (_jsxs(Drawer.Portal, { children: [sheet.nested ? null : _jsx(Drawer.Backdrop, { className: slots.backdrop() }), _jsx(Drawer.Viewport, { className: slots.viewport(), children: _jsx(Drawer.Popup, { className: slots.popup({ className }), ...props, children: _jsx(SheetContext.Provider, { value: context, children: inside }) }) })] }));
|
|
254
|
+
}
|
|
255
|
+
SheetContent.displayName = 'Sheet.Content';
|
|
256
|
+
/**
|
|
257
|
+
* The heading block. It leaves room for the close button beside the title,
|
|
258
|
+
* so the two share the top line instead of stacking.
|
|
259
|
+
*/
|
|
260
|
+
function SheetHeader({ title, description, className, children, ...props }) {
|
|
261
|
+
const { showClose } = useSheet('Sheet.Header');
|
|
262
|
+
const slots = sheetVariants();
|
|
263
|
+
return (_jsxs("div", { className: slots.header({ className: [showClose && 'pe-14', className] }), ...props, children: [title !== undefined ? _jsx(SheetTitle, { children: title }) : null, description !== undefined ? _jsx(SheetDescription, { children: description }) : null, children] }));
|
|
264
|
+
}
|
|
265
|
+
SheetHeader.displayName = 'Sheet.Header';
|
|
266
|
+
/** The sheet's name, announced when it opens. */
|
|
267
|
+
function SheetTitle({ className, ...props }) {
|
|
268
|
+
const { inline } = useSheet('Sheet.Title');
|
|
269
|
+
const slots = sheetVariants();
|
|
270
|
+
if (inline)
|
|
271
|
+
return _jsx("h2", { className: slots.title({ className }), ...props });
|
|
272
|
+
return _jsx(Drawer.Title, { className: slots.title({ className }), ...props });
|
|
273
|
+
}
|
|
274
|
+
SheetTitle.displayName = 'Sheet.Title';
|
|
275
|
+
/** A sentence under the title, read out after it. */
|
|
276
|
+
function SheetDescription({ className, ...props }) {
|
|
277
|
+
const { inline } = useSheet('Sheet.Description');
|
|
278
|
+
const slots = sheetVariants();
|
|
279
|
+
if (inline)
|
|
280
|
+
return _jsx("p", { className: slots.description({ className }), ...props });
|
|
281
|
+
return _jsx(Drawer.Description, { className: slots.description({ className }), ...props });
|
|
282
|
+
}
|
|
283
|
+
SheetDescription.displayName = 'Sheet.Description';
|
|
284
|
+
/**
|
|
285
|
+
* The part that scrolls. The header and footer stay put while it moves, and
|
|
286
|
+
* the scrollbar is a thin drawn thumb that shows while the content is moving.
|
|
287
|
+
*/
|
|
288
|
+
function SheetBody({ className, children, ...props }) {
|
|
289
|
+
const slots = sheetVariants();
|
|
290
|
+
return (_jsxs(ScrollArea.Root, { className: slots.body(), children: [_jsx(ScrollArea.Viewport, { className: slots.bodyViewport(), children: _jsx(ScrollArea.Content, { className: slots.bodyContent({ className }), ...props, children: children }) }), _jsx(ScrollArea.Scrollbar, { orientation: "vertical", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
|
|
291
|
+
}
|
|
292
|
+
SheetBody.displayName = 'Sheet.Body';
|
|
293
|
+
/** The actions, stacked full width at the bottom of the sheet. */
|
|
294
|
+
function SheetFooter({ className, ...props }) {
|
|
295
|
+
const slots = sheetVariants();
|
|
296
|
+
return _jsx("div", { className: slots.footer({ className }), ...props });
|
|
297
|
+
}
|
|
298
|
+
SheetFooter.displayName = 'Sheet.Footer';
|
|
299
|
+
/** Closes the sheet. Works even when `dismissible` is off. */
|
|
300
|
+
function SheetClose({ render, icon = false, children, ...props }) {
|
|
301
|
+
const { inline } = useSheet('Sheet.Close');
|
|
302
|
+
const content = icon ? (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) })) : (children);
|
|
303
|
+
if (inline) {
|
|
304
|
+
return (_jsx("button", { type: "button", ...props, children: content }));
|
|
305
|
+
}
|
|
306
|
+
return (_jsx(Drawer.Close, { render: render, ...props, children: content }));
|
|
307
|
+
}
|
|
308
|
+
SheetClose.displayName = 'Sheet.Close';
|
|
309
|
+
/**
|
|
310
|
+
* Wrap the app — or the part of the page a sheet belongs to — in this, and
|
|
311
|
+
* the page steps back slightly while a sheet is open. Sheets opened anywhere
|
|
312
|
+
* inside it take part.
|
|
313
|
+
*/
|
|
314
|
+
function SheetStage({ className, children, ...props }) {
|
|
315
|
+
const slots = sheetVariants();
|
|
316
|
+
return (_jsx(Drawer.Provider, { children: _jsxs("div", { className: slots.stage({ className }), ...props, children: [_jsx(Drawer.IndentBackground, { className: slots.stageBackground() }), _jsx(Drawer.Indent, { className: slots.stagePage(), children: children })] }) }));
|
|
317
|
+
}
|
|
318
|
+
SheetStage.displayName = 'Sheet.Stage';
|
|
319
|
+
export const Sheet = Object.assign(SheetRoot, {
|
|
320
|
+
Trigger: SheetTrigger,
|
|
321
|
+
Content: SheetContent,
|
|
322
|
+
Header: SheetHeader,
|
|
323
|
+
Title: SheetTitle,
|
|
324
|
+
Description: SheetDescription,
|
|
325
|
+
Body: SheetBody,
|
|
326
|
+
Footer: SheetFooter,
|
|
327
|
+
Close: SheetClose,
|
|
328
|
+
Stage: SheetStage,
|
|
329
|
+
});
|
|
330
|
+
export { SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
|