panelui-web 0.1.0 → 0.2.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.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Card — a surface that holds one thing: a plan, a setting, a summary.
3
+ *
4
+ * ```tsx
5
+ * <Card>
6
+ * <Card.Header>
7
+ * <Card.Title>Storage</Card.Title>
8
+ * <Card.Description>Northwind Studio workspace</Card.Description>
9
+ * <Card.Action><Button variant="ghost">Manage</Button></Card.Action>
10
+ * </Card.Header>
11
+ * <Card.Content>…</Card.Content>
12
+ * <Card.Footer variant="panel">…</Card.Footer>
13
+ * </Card>
14
+ * ```
15
+ *
16
+ * ## The padding is on the parts
17
+ *
18
+ * The card itself draws a surface and nothing inside it, and every part
19
+ * carries its own padding. That is what lets a picture reach the card's
20
+ * corners with nothing but `overflow-hidden` on the card, and what lets a
21
+ * footer be a band set into the bottom of it rather than one more row.
22
+ *
23
+ * ## The action sits beside the heading
24
+ *
25
+ * `Card.Action` is placed in the header's top trailing corner whatever order
26
+ * it is written in, and the title and description keep the rest of the width.
27
+ * A button that belongs to the whole card goes there; one that belongs to the
28
+ * content goes in the content.
29
+ *
30
+ * ## It renders on the server
31
+ *
32
+ * No state, no effects, no handlers of its own: everything that changes with
33
+ * `size` or with the parts present is done in CSS, so a page of cards costs no
34
+ * JavaScript.
35
+ */
36
+ import type { ComponentProps } from 'react';
37
+ export interface CardProps extends ComponentProps<'div'> {
38
+ /** `sm` tightens the padding and the title, for a card in a dense grid. Default `default`. */
39
+ size?: 'default' | 'sm';
40
+ /**
41
+ * The element to render. `article` for a card that stands on its own, `li`
42
+ * for one in a list, `section` for one that groups a part of a page.
43
+ */
44
+ as?: 'div' | 'article' | 'section' | 'li';
45
+ }
46
+ declare function CardRoot({ size, as: Element, className, ...props }: CardProps): import("react").JSX.Element;
47
+ declare namespace CardRoot {
48
+ var displayName: string;
49
+ }
50
+ export interface CardHeaderProps extends ComponentProps<'div'> {
51
+ }
52
+ /** The title, the line under it, and the action beside them. */
53
+ declare function CardHeader({ className, ...props }: CardHeaderProps): import("react").JSX.Element;
54
+ declare namespace CardHeader {
55
+ var displayName: string;
56
+ }
57
+ export interface CardTitleProps extends ComponentProps<'h3'> {
58
+ /** The heading level, to fit the page around the card. Default `h3`. */
59
+ as?: 'h2' | 'h3' | 'h4' | 'div';
60
+ }
61
+ /** What the card is about. A heading, so the page outline includes it. */
62
+ declare function CardTitle({ as: Element, className, ...props }: CardTitleProps): import("react").JSX.Element;
63
+ declare namespace CardTitle {
64
+ var displayName: string;
65
+ }
66
+ export interface CardDescriptionProps extends ComponentProps<'p'> {
67
+ }
68
+ /** A line under the title. */
69
+ declare function CardDescription({ className, ...props }: CardDescriptionProps): import("react").JSX.Element;
70
+ declare namespace CardDescription {
71
+ var displayName: string;
72
+ }
73
+ export interface CardActionProps extends ComponentProps<'div'> {
74
+ }
75
+ /** A control for the whole card, held in the header's top trailing corner. */
76
+ declare function CardAction({ className, ...props }: CardActionProps): import("react").JSX.Element;
77
+ declare namespace CardAction {
78
+ var displayName: string;
79
+ }
80
+ export interface CardContentProps extends ComponentProps<'div'> {
81
+ }
82
+ /** The body. */
83
+ declare function CardContent({ className, ...props }: CardContentProps): import("react").JSX.Element;
84
+ declare namespace CardContent {
85
+ var displayName: string;
86
+ }
87
+ export interface CardFooterProps extends ComponentProps<'div'> {
88
+ /**
89
+ * `plain` continues the card. `panel` is a darker band set into its bottom,
90
+ * for actions. Default `plain`.
91
+ */
92
+ variant?: 'plain' | 'panel';
93
+ }
94
+ /** The row at the bottom, usually the card's actions. */
95
+ declare function CardFooter({ variant, className, ...props }: CardFooterProps): import("react").JSX.Element;
96
+ declare namespace CardFooter {
97
+ var displayName: string;
98
+ }
99
+ export declare const Card: typeof CardRoot & {
100
+ Header: typeof CardHeader;
101
+ Title: typeof CardTitle;
102
+ Description: typeof CardDescription;
103
+ Action: typeof CardAction;
104
+ Content: typeof CardContent;
105
+ Footer: typeof CardFooter;
106
+ };
107
+ export { CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { tv } from 'tailwind-variants';
3
+ const cardVariants = tv({
4
+ slots: {
5
+ root: 'group/card flex flex-col rounded-2xl border bg-card text-card-foreground shadow-xs',
6
+ header: [
7
+ 'grid auto-rows-min grid-cols-[minmax(0,1fr)] items-start gap-x-4 gap-y-1.5 p-6',
8
+ 'has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]',
9
+ 'group-data-[size=sm]/card:gap-y-1 group-data-[size=sm]/card:p-4',
10
+ ],
11
+ title: 'text-lg font-semibold leading-tight tracking-[-0.01em] group-data-[size=sm]/card:text-base',
12
+ description: 'text-sm text-muted-foreground',
13
+ // Its own column, across both of the heading's rows, so a long title wraps
14
+ // beside it instead of underneath it.
15
+ action: 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
16
+ // `first:` for a card with no header, which would otherwise start flush.
17
+ content: 'px-6 pb-6 first:pt-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:pb-4 group-data-[size=sm]/card:first:pt-4',
18
+ footer: 'flex items-center gap-2',
19
+ },
20
+ variants: {
21
+ footer: {
22
+ /** Part of the card: same surface, continuing the content's padding. */
23
+ plain: {
24
+ footer: 'px-6 pb-6 first:pt-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:pb-4 group-data-[size=sm]/card:first:pt-4',
25
+ },
26
+ /**
27
+ * A band set into the bottom of the card: a rule across its top, a step
28
+ * darker, and the card's own bottom corners. For a footer that is what
29
+ * somebody does with the card, not more of what it says.
30
+ */
31
+ panel: {
32
+ footer: 'mt-auto rounded-b-[calc(var(--radius-2xl)-1px)] border-t bg-muted/60 px-6 py-4 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:py-3',
33
+ },
34
+ },
35
+ },
36
+ });
37
+ function CardRoot({ size = 'default', as: Element = 'div', className, ...props }) {
38
+ const slots = cardVariants();
39
+ const Tag = Element;
40
+ return _jsx(Tag, { "data-slot": "card", "data-size": size, className: slots.root({ className }), ...props });
41
+ }
42
+ CardRoot.displayName = 'Card';
43
+ /** The title, the line under it, and the action beside them. */
44
+ function CardHeader({ className, ...props }) {
45
+ const slots = cardVariants();
46
+ return _jsx("div", { "data-slot": "card-header", className: slots.header({ className }), ...props });
47
+ }
48
+ CardHeader.displayName = 'Card.Header';
49
+ /** What the card is about. A heading, so the page outline includes it. */
50
+ function CardTitle({ as: Element = 'h3', className, ...props }) {
51
+ const slots = cardVariants();
52
+ const Tag = Element;
53
+ return _jsx(Tag, { "data-slot": "card-title", className: slots.title({ className }), ...props });
54
+ }
55
+ CardTitle.displayName = 'Card.Title';
56
+ /** A line under the title. */
57
+ function CardDescription({ className, ...props }) {
58
+ const slots = cardVariants();
59
+ return _jsx("p", { "data-slot": "card-description", className: slots.description({ className }), ...props });
60
+ }
61
+ CardDescription.displayName = 'Card.Description';
62
+ /** A control for the whole card, held in the header's top trailing corner. */
63
+ function CardAction({ className, ...props }) {
64
+ const slots = cardVariants();
65
+ return _jsx("div", { "data-slot": "card-action", className: slots.action({ className }), ...props });
66
+ }
67
+ CardAction.displayName = 'Card.Action';
68
+ /** The body. */
69
+ function CardContent({ className, ...props }) {
70
+ const slots = cardVariants();
71
+ return _jsx("div", { "data-slot": "card-content", className: slots.content({ className }), ...props });
72
+ }
73
+ CardContent.displayName = 'Card.Content';
74
+ /** The row at the bottom, usually the card's actions. */
75
+ function CardFooter({ variant = 'plain', className, ...props }) {
76
+ const slots = cardVariants({ footer: variant });
77
+ return _jsx("div", { "data-slot": "card-footer", className: slots.footer({ className }), ...props });
78
+ }
79
+ CardFooter.displayName = 'Card.Footer';
80
+ export const Card = Object.assign(CardRoot, {
81
+ Header: CardHeader,
82
+ Title: CardTitle,
83
+ Description: CardDescription,
84
+ Action: CardAction,
85
+ Content: CardContent,
86
+ Footer: CardFooter,
87
+ });
88
+ export { CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Frame — a widget shell: a panel of rows sitting in a tray, with the tray's
3
+ * one exposed strip along the top carrying the title.
4
+ *
5
+ * ```tsx
6
+ * <Frame>
7
+ * <Frame.Header>
8
+ * <Frame.Title>Deploys</Frame.Title>
9
+ * <Frame.Action>Last 24 hours</Frame.Action>
10
+ * </Frame.Header>
11
+ * <Frame.Panel>
12
+ * <Frame.Row>
13
+ * <Frame.Media><RocketIcon /></Frame.Media>
14
+ * <Frame.Content>
15
+ * <Frame.Title>main · a41c2e9</Frame.Title>
16
+ * <Frame.Description>Production · 2 minutes ago</Frame.Description>
17
+ * </Frame.Content>
18
+ * <Frame.Actions><Badge>Ready</Badge></Frame.Actions>
19
+ * </Frame.Row>
20
+ * </Frame.Panel>
21
+ * </Frame>
22
+ * ```
23
+ *
24
+ * ## Two surfaces, one exposed edge
25
+ *
26
+ * The panel sits flush against the shell's left, right and bottom, so the
27
+ * shell reads as something the panel is set into rather than as a border
28
+ * round it — and the strip left at the top is the header, which is why the
29
+ * header needs no rule under it and no background of its own. The panel's
30
+ * bottom corners are not drawn at all: the shell clips them to its own radius.
31
+ *
32
+ * ## Four ways to nest them
33
+ *
34
+ * `default` is the tray. `band` holds a bordered card in a thin muted band,
35
+ * with the band's top strip naming what is inside — the frame for a widget
36
+ * with one thing to show, a figure or a chart, rather than a list of rows.
37
+ * `inset` floats the panel clear of the shell on all four sides, at the radius
38
+ * that keeps the two curves concentric, and the band left round it is a recess
39
+ * — darker than the panel in a light theme and a dark one alike — that
40
+ * `Frame.Footer` puts its actions in. `plain` drops the shell, for a frame
41
+ * inside something that already draws an edge, where a second one just inside
42
+ * it reads as a double line.
43
+ *
44
+ * ## Rows divide themselves
45
+ *
46
+ * The panel draws the hairline between its children, so no row has to know
47
+ * whether it is first. A row with `href` is a link and one with `onClick` is a
48
+ * button, each with the hover and focus a pressable row should have, and
49
+ * `chevron` marks one that leads somewhere.
50
+ *
51
+ * ## It renders on the server
52
+ *
53
+ * The variant reaches the parts through a data attribute and CSS rather than
54
+ * through context, so a frame has no client JavaScript of its own.
55
+ */
56
+ import type { ComponentProps, MouseEventHandler, ReactNode } from 'react';
57
+ export type FrameVariant = 'default' | 'band' | 'inset' | 'plain';
58
+ export interface FrameProps extends ComponentProps<'div'> {
59
+ /**
60
+ * `default` sets the panel in a tray. `band` holds a bordered card in a
61
+ * thin muted band, for a widget showing one figure or chart. `inset` floats
62
+ * the panel in a recessed band and gives `Frame.Footer` somewhere to sit.
63
+ * `plain` drops the shell, for a frame inside a container that already draws
64
+ * its own edge.
65
+ */
66
+ variant?: FrameVariant;
67
+ }
68
+ declare function FrameRoot({ variant, className, ...props }: FrameProps): import("react").JSX.Element;
69
+ declare namespace FrameRoot {
70
+ var displayName: string;
71
+ }
72
+ export interface FrameHeaderProps extends ComponentProps<'div'> {
73
+ }
74
+ /**
75
+ * The strip of shell above the panel: a title, an optional description under
76
+ * it, and `Frame.Action` on the trailing side.
77
+ */
78
+ declare function FrameHeader({ className, ...props }: FrameHeaderProps): import("react").JSX.Element;
79
+ declare namespace FrameHeader {
80
+ var displayName: string;
81
+ }
82
+ export interface FrameTitleProps extends ComponentProps<'h3'> {
83
+ /** The heading level in a header. Default `h3`; a row title is always a `p`. */
84
+ as?: 'h2' | 'h3' | 'h4' | 'p';
85
+ }
86
+ /** A caption in the header; the subject line inside `Frame.Content`. */
87
+ declare function FrameTitle({ as: Tag, className, ...props }: FrameTitleProps): import("react").JSX.Element;
88
+ declare namespace FrameTitle {
89
+ var displayName: string;
90
+ }
91
+ export interface FrameDescriptionProps extends ComponentProps<'p'> {
92
+ }
93
+ /** A line under a title, in the header or in a row. */
94
+ declare function FrameDescription({ className, ...props }: FrameDescriptionProps): import("react").JSX.Element;
95
+ declare namespace FrameDescription {
96
+ var displayName: string;
97
+ }
98
+ export interface FrameActionProps extends ComponentProps<'div'> {
99
+ }
100
+ /** The header's trailing side: a short status, a link, a small button. */
101
+ declare function FrameAction({ className, ...props }: FrameActionProps): import("react").JSX.Element;
102
+ declare namespace FrameAction {
103
+ var displayName: string;
104
+ }
105
+ export interface FramePanelProps extends ComponentProps<'div'> {
106
+ }
107
+ /** The card the rows sit on. It draws the hairline between its children. */
108
+ declare function FramePanel({ className, ...props }: FramePanelProps): import("react").JSX.Element;
109
+ declare namespace FramePanel {
110
+ var displayName: string;
111
+ }
112
+ export interface FrameRowProps extends Omit<ComponentProps<'div'>, 'onClick'> {
113
+ /** Make the row a link to this address. */
114
+ href?: string;
115
+ /** Where a link row opens, as on an `<a>`. */
116
+ target?: string;
117
+ /** The relationship of a link row's target, as on an `<a>`. */
118
+ rel?: string;
119
+ /** Make the row a button that calls this. Ignored when `href` is set. */
120
+ onClick?: MouseEventHandler<HTMLElement>;
121
+ /** A trailing chevron, for a row that leads somewhere. */
122
+ chevron?: boolean;
123
+ }
124
+ /**
125
+ * One line of the panel: `Frame.Media`, `Frame.Content` and `Frame.Actions`
126
+ * side by side. With `href` it is a link and with `onClick` a button.
127
+ */
128
+ declare function FrameRow({ className, children, chevron, href, target, rel, ...props }: FrameRowProps): import("react").JSX.Element;
129
+ declare namespace FrameRow {
130
+ var displayName: string;
131
+ }
132
+ export interface FrameMediaProps extends ComponentProps<'div'> {
133
+ }
134
+ /** A row's leading icon, avatar or picture. Holds its size. */
135
+ declare function FrameMedia({ className, ...props }: FrameMediaProps): import("react").JSX.Element;
136
+ declare namespace FrameMedia {
137
+ var displayName: string;
138
+ }
139
+ export interface FrameContentProps extends ComponentProps<'div'> {
140
+ }
141
+ /**
142
+ * The flexible middle of a row, usually a title over a description. It takes
143
+ * what the media and actions leave and may shrink past its content, which is
144
+ * what keeps a long line from pushing the rest of the row off the edge.
145
+ */
146
+ declare function FrameContent({ className, ...props }: FrameContentProps): import("react").JSX.Element;
147
+ declare namespace FrameContent {
148
+ var displayName: string;
149
+ }
150
+ export interface FrameActionsProps extends ComponentProps<'div'> {
151
+ }
152
+ /** A row's trailing side: a badge, a value, a switch, a small button. Holds its size. */
153
+ declare function FrameActions({ className, ...props }: FrameActionsProps): import("react").JSX.Element;
154
+ declare namespace FrameActions {
155
+ var displayName: string;
156
+ }
157
+ export interface FrameSectionProps extends Omit<ComponentProps<'section'>, 'title'> {
158
+ /** The heading over the section's rows. */
159
+ title?: ReactNode;
160
+ }
161
+ /** A labelled group of rows, for a panel holding more than one. */
162
+ declare function FrameSection({ title, className, children, ...props }: FrameSectionProps): import("react").JSX.Element;
163
+ declare namespace FrameSection {
164
+ var displayName: string;
165
+ }
166
+ export interface FrameFooterProps extends ComponentProps<'div'> {
167
+ }
168
+ /**
169
+ * The strip below the panel. Under `inset` its children become equal pills in
170
+ * the recessed band, the shape a row of decisions takes.
171
+ */
172
+ declare function FrameFooter({ className, ...props }: FrameFooterProps): import("react").JSX.Element;
173
+ declare namespace FrameFooter {
174
+ var displayName: string;
175
+ }
176
+ export declare const Frame: typeof FrameRoot & {
177
+ Header: typeof FrameHeader;
178
+ Title: typeof FrameTitle;
179
+ Description: typeof FrameDescription;
180
+ Action: typeof FrameAction;
181
+ Panel: typeof FramePanel;
182
+ Row: typeof FrameRow;
183
+ Media: typeof FrameMedia;
184
+ Content: typeof FrameContent;
185
+ Actions: typeof FrameActions;
186
+ Section: typeof FrameSection;
187
+ Footer: typeof FrameFooter;
188
+ };
189
+ export { FrameHeader, FrameTitle, FrameDescription, FrameAction, FramePanel, FrameRow, FrameMedia, FrameContent, FrameActions, FrameSection, FrameFooter, };
@@ -0,0 +1,181 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tv } from 'tailwind-variants';
3
+ /*
4
+ * `inset` geometry. The panel's radius is the shell's less the shell's padding,
5
+ * so the two curves are concentric — a panel radius that ignored the band
6
+ * would leave a crescent of shell thicker at the corners than along the sides.
7
+ */
8
+ const frameVariants = tv({
9
+ slots: {
10
+ root: 'group/frame flex flex-col text-foreground',
11
+ header: [
12
+ 'grid grid-cols-[minmax(0,1fr)] items-center gap-x-3 gap-y-0.5 px-4 pt-2.5 pb-3',
13
+ 'has-data-[slot=frame-action]:grid-cols-[minmax(0,1fr)_auto]',
14
+ 'group-data-[variant=inset]/frame:px-3 group-data-[variant=inset]/frame:pt-1 group-data-[variant=inset]/frame:pb-2.5',
15
+ 'group-data-[variant=plain]/frame:px-1',
16
+ 'group-data-[variant=band]/frame:px-3.5 group-data-[variant=band]/frame:py-2.5',
17
+ ],
18
+ /*
19
+ * One part in two places. In the header it labels the tray, so it stays
20
+ * quiet; inside a row's content it is the row's subject, so it takes the
21
+ * foreground and the weight, and truncates rather than pushing the row's
22
+ * trailing slot out of view.
23
+ */
24
+ title: [
25
+ 'text-sm font-medium text-muted-foreground',
26
+ 'in-data-[slot=frame-content]:truncate in-data-[slot=frame-content]:text-foreground',
27
+ // A band names one thing rather than captioning a list, so its title reads first.
28
+ 'group-data-[variant=band]/frame:text-foreground',
29
+ ],
30
+ description: 'text-sm text-muted-foreground in-data-[slot=frame-content]:truncate',
31
+ action: 'col-start-2 row-span-2 row-start-1 text-sm text-muted-foreground',
32
+ // The panel's shape follows the root's variant, read from its data attribute.
33
+ panel: [
34
+ 'flex flex-col divide-y overflow-hidden bg-card text-card-foreground',
35
+ 'group-data-[variant=default]/frame:rounded-t-2xl group-data-[variant=default]/frame:border-t',
36
+ 'group-data-[variant=plain]/frame:rounded-3xl group-data-[variant=plain]/frame:border group-data-[variant=plain]/frame:shadow-xs',
37
+ 'group-data-[variant=inset]/frame:rounded-[30px] group-data-[variant=inset]/frame:bg-popover group-data-[variant=inset]/frame:text-popover-foreground group-data-[variant=inset]/frame:shadow-xs',
38
+ // 18px is the band's 24 less its 6px of padding: one corner, two curves.
39
+ 'group-data-[variant=band]/frame:rounded-[1.125rem] group-data-[variant=band]/frame:border group-data-[variant=band]/frame:shadow-xs',
40
+ ],
41
+ row: 'flex w-full items-center gap-3 px-4 py-3 text-start',
42
+ rowPressable: [
43
+ 'cursor-pointer outline-none transition-colors hover:bg-muted/60',
44
+ 'focus-visible:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
45
+ ],
46
+ chevron: 'size-4 shrink-0 text-muted-foreground/70 rtl:-scale-x-100',
47
+ media: 'flex shrink-0 items-center justify-center',
48
+ content: 'flex min-w-0 flex-1 flex-col gap-0.5',
49
+ actions: 'flex shrink-0 items-center gap-1.5',
50
+ section: 'flex flex-col divide-y',
51
+ sectionTitle: 'bg-muted/40 px-4 pt-2.5 pb-1.5 text-xs font-medium uppercase tracking-wider text-muted-foreground',
52
+ footer: [
53
+ 'flex items-center gap-2 px-4 pt-3 pb-3.5',
54
+ // Under `inset` the band is a row of equal decisions: equal widths, full pills.
55
+ 'group-data-[variant=inset]/frame:gap-3.5 group-data-[variant=inset]/frame:px-[18px] group-data-[variant=inset]/frame:pt-4 group-data-[variant=inset]/frame:pb-2.5',
56
+ 'group-data-[variant=inset]/frame:*:h-11 group-data-[variant=inset]/frame:*:flex-1 group-data-[variant=inset]/frame:*:rounded-full',
57
+ 'group-data-[variant=plain]/frame:px-1',
58
+ 'group-data-[variant=band]/frame:px-3.5 group-data-[variant=band]/frame:pt-2.5 group-data-[variant=band]/frame:pb-1',
59
+ ],
60
+ },
61
+ variants: {
62
+ variant: {
63
+ default: {
64
+ // `overflow-hidden` is what gives the panel the shell's bottom corners.
65
+ root: 'overflow-hidden rounded-3xl border bg-muted',
66
+ },
67
+ plain: {},
68
+ band: {
69
+ root: 'rounded-3xl bg-muted p-1.5',
70
+ },
71
+ inset: {
72
+ // The band is the popover surface with black mixed in, so it is the
73
+ // darker of the two in either theme, which a step on a grey scale
74
+ // cannot promise: those run darker one way and lighter the other.
75
+ root: [
76
+ 'rounded-[38px] p-2 ring-1 ring-foreground/[0.06]',
77
+ 'bg-[color-mix(in_oklab,var(--popover),black_5%)] dark:bg-[color-mix(in_oklab,var(--popover),black_14%)]',
78
+ ],
79
+ },
80
+ },
81
+ },
82
+ defaultVariants: { variant: 'default' },
83
+ });
84
+ function FrameRoot({ variant = 'default', className, ...props }) {
85
+ const slots = frameVariants({ variant });
86
+ return _jsx("div", { "data-slot": "frame", "data-variant": variant, className: slots.root({ className }), ...props });
87
+ }
88
+ FrameRoot.displayName = 'Frame';
89
+ /**
90
+ * The strip of shell above the panel: a title, an optional description under
91
+ * it, and `Frame.Action` on the trailing side.
92
+ */
93
+ function FrameHeader({ className, ...props }) {
94
+ return _jsx("div", { "data-slot": "frame-header", className: frameVariants().header({ className }), ...props });
95
+ }
96
+ FrameHeader.displayName = 'Frame.Header';
97
+ /** A caption in the header; the subject line inside `Frame.Content`. */
98
+ function FrameTitle({ as: Tag = 'h3', className, ...props }) {
99
+ return _jsx(Tag, { "data-slot": "frame-title", className: frameVariants().title({ className }), ...props });
100
+ }
101
+ FrameTitle.displayName = 'Frame.Title';
102
+ /** A line under a title, in the header or in a row. */
103
+ function FrameDescription({ className, ...props }) {
104
+ return (_jsx("p", { "data-slot": "frame-description", className: frameVariants().description({ className }), ...props }));
105
+ }
106
+ FrameDescription.displayName = 'Frame.Description';
107
+ /** The header's trailing side: a short status, a link, a small button. */
108
+ function FrameAction({ className, ...props }) {
109
+ return _jsx("div", { "data-slot": "frame-action", className: frameVariants().action({ className }), ...props });
110
+ }
111
+ FrameAction.displayName = 'Frame.Action';
112
+ /** The card the rows sit on. It draws the hairline between its children. */
113
+ function FramePanel({ className, ...props }) {
114
+ return _jsx("div", { "data-slot": "frame-panel", className: frameVariants().panel({ className }), ...props });
115
+ }
116
+ FramePanel.displayName = 'Frame.Panel';
117
+ const Chevron = ({ className }) => (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: className, fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m9 6 6 6-6 6" }) }));
118
+ /**
119
+ * One line of the panel: `Frame.Media`, `Frame.Content` and `Frame.Actions`
120
+ * side by side. With `href` it is a link and with `onClick` a button.
121
+ */
122
+ function FrameRow({ className, children, chevron = false, href, target, rel, ...props }) {
123
+ const slots = frameVariants();
124
+ const inside = (_jsxs(_Fragment, { children: [children, chevron ? _jsx(Chevron, { className: slots.chevron() }) : null] }));
125
+ if (href !== undefined) {
126
+ return (_jsx("a", { "data-slot": "frame-row", href: href, target: target, rel: rel, className: slots.row({ className: [slots.rowPressable(), className] }), ...props, children: inside }));
127
+ }
128
+ // `onClick` stays in `props`, so it reaches whichever element is rendered.
129
+ if (props.onClick !== undefined) {
130
+ return (_jsx("button", { type: "button", "data-slot": "frame-row", className: slots.row({ className: [slots.rowPressable(), className] }), ...props, children: inside }));
131
+ }
132
+ return (_jsx("div", { "data-slot": "frame-row", className: slots.row({ className }), ...props, children: inside }));
133
+ }
134
+ FrameRow.displayName = 'Frame.Row';
135
+ /** A row's leading icon, avatar or picture. Holds its size. */
136
+ function FrameMedia({ className, ...props }) {
137
+ return _jsx("div", { "data-slot": "frame-media", className: frameVariants().media({ className }), ...props });
138
+ }
139
+ FrameMedia.displayName = 'Frame.Media';
140
+ /**
141
+ * The flexible middle of a row, usually a title over a description. It takes
142
+ * what the media and actions leave and may shrink past its content, which is
143
+ * what keeps a long line from pushing the rest of the row off the edge.
144
+ */
145
+ function FrameContent({ className, ...props }) {
146
+ return _jsx("div", { "data-slot": "frame-content", className: frameVariants().content({ className }), ...props });
147
+ }
148
+ FrameContent.displayName = 'Frame.Content';
149
+ /** A row's trailing side: a badge, a value, a switch, a small button. Holds its size. */
150
+ function FrameActions({ className, ...props }) {
151
+ return _jsx("div", { "data-slot": "frame-actions", className: frameVariants().actions({ className }), ...props });
152
+ }
153
+ FrameActions.displayName = 'Frame.Actions';
154
+ /** A labelled group of rows, for a panel holding more than one. */
155
+ function FrameSection({ title, className, children, ...props }) {
156
+ const slots = frameVariants();
157
+ return (_jsxs("section", { "data-slot": "frame-section", className: slots.section({ className }), ...props, children: [title !== undefined ? _jsx("h4", { className: slots.sectionTitle(), children: title }) : null, children] }));
158
+ }
159
+ FrameSection.displayName = 'Frame.Section';
160
+ /**
161
+ * The strip below the panel. Under `inset` its children become equal pills in
162
+ * the recessed band, the shape a row of decisions takes.
163
+ */
164
+ function FrameFooter({ className, ...props }) {
165
+ return _jsx("div", { "data-slot": "frame-footer", className: frameVariants().footer({ className }), ...props });
166
+ }
167
+ FrameFooter.displayName = 'Frame.Footer';
168
+ export const Frame = Object.assign(FrameRoot, {
169
+ Header: FrameHeader,
170
+ Title: FrameTitle,
171
+ Description: FrameDescription,
172
+ Action: FrameAction,
173
+ Panel: FramePanel,
174
+ Row: FrameRow,
175
+ Media: FrameMedia,
176
+ Content: FrameContent,
177
+ Actions: FrameActions,
178
+ Section: FrameSection,
179
+ Footer: FrameFooter,
180
+ });
181
+ export { FrameHeader, FrameTitle, FrameDescription, FrameAction, FramePanel, FrameRow, FrameMedia, FrameContent, FrameActions, FrameSection, FrameFooter, };
@@ -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, };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ export { cn } from './lib/cn.js';
2
2
  export { RollingNumber, type RollingNumberProps } from './components/rolling-number/index.js';
3
3
  export { Planet, PlanetGlobe, PlanetMarker, PlanetControls, type PlanetProps, type PlanetHandle, type PlanetRotation, type PlanetGlobeProps, type PlanetMarkerProps, type PlanetMarkerColor, type PlanetControlsProps, } from './components/planet/index.js';
4
4
  export { Feedback, FeedbackTrigger, FeedbackContent, FeedbackPanel, FeedbackTitle, FeedbackClose, FeedbackField, FeedbackFooter, FeedbackCancel, FeedbackSubmit, type FeedbackProps, type FeedbackTriggerProps, type FeedbackContentProps, type FeedbackPanelProps, type FeedbackTitleProps, type FeedbackCloseProps, type FeedbackFieldProps, type FeedbackFooterProps, type FeedbackActionProps, type FeedbackSubmitProps, } from './components/feedback/index.js';
5
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter, type CardProps, type CardHeaderProps, type CardTitleProps, type CardDescriptionProps, type CardActionProps, type CardContentProps, type CardFooterProps, } from './components/card/index.js';
6
+ export { Frame, FrameHeader, FrameTitle, FrameDescription, FrameAction, FramePanel, FrameRow, FrameMedia, FrameContent, FrameActions, FrameSection, FrameFooter, type FrameProps, type FrameVariant, type FrameHeaderProps, type FrameTitleProps, type FrameDescriptionProps, type FrameActionProps, type FramePanelProps, type FrameRowProps, type FrameMediaProps, type FrameContentProps, type FrameActionsProps, type FrameSectionProps, type FrameFooterProps, } from './components/frame/index.js';
7
+ export { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, type SheetProps, type SheetSide, type SheetTriggerProps, type SheetContentProps, type SheetHeaderProps, type SheetTitleProps, type SheetDescriptionProps, type SheetBodyProps, type SheetFooterProps, type SheetCloseProps, type SheetStageProps, } from './components/sheet/index.js';
5
8
  export { DayGrid, DayGridLegend, type DayGridProps, type DayGridLegendProps, type DayGridColor, type DayCell, type DayLevel, } from './components/day-grid/index.js';
6
9
  export { RangeMeter, type RangeMeterProps, type RangeMeterColor, } from './components/range-meter/index.js';
7
10
  export { BarPicker, type BarPickerProps, type BarPickerBar, type BarPickerColor, } from './components/bar-picker/index.js';
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@ export { cn } from './lib/cn.js';
2
2
  export { RollingNumber } from './components/rolling-number/index.js';
3
3
  export { Planet, PlanetGlobe, PlanetMarker, PlanetControls, } from './components/planet/index.js';
4
4
  export { Feedback, FeedbackTrigger, FeedbackContent, FeedbackPanel, FeedbackTitle, FeedbackClose, FeedbackField, FeedbackFooter, FeedbackCancel, FeedbackSubmit, } from './components/feedback/index.js';
5
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter, } from './components/card/index.js';
6
+ export { Frame, FrameHeader, FrameTitle, FrameDescription, FrameAction, FramePanel, FrameRow, FrameMedia, FrameContent, FrameActions, FrameSection, FrameFooter, } from './components/frame/index.js';
7
+ export { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, } from './components/sheet/index.js';
5
8
  export { DayGrid, DayGridLegend, } from './components/day-grid/index.js';
6
9
  export { RangeMeter, } from './components/range-meter/index.js';
7
10
  export { BarPicker, } from './components/bar-picker/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panelui-web",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "PanelUI for the web: animated, data-rich React components styled with Tailwind CSS on shadcn tokens.",
5
5
  "type": "module",
6
6
  "sideEffects": false,