panelui-web 0.6.0 → 0.8.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,138 @@
1
+ /**
2
+ * Alert — a message set into the page, which stays there until it has been
3
+ * dealt with: a payment that failed, a trial running out, a form that needs
4
+ * another look.
5
+ *
6
+ * ```tsx
7
+ * <Alert tone="destructive">
8
+ * <Alert.Icon />
9
+ * <Alert.Title>Your payment did not go through</Alert.Title>
10
+ * <Alert.Description>The card ending 4242 was declined.</Alert.Description>
11
+ * <Alert.Actions>
12
+ * <Button size="sm">Update card</Button>
13
+ * </Alert.Actions>
14
+ * <Alert.Close />
15
+ * </Alert>
16
+ * ```
17
+ *
18
+ * ## A card in a tray of its tone
19
+ *
20
+ * By default the message is a card set into a tray tinted in its tone, and
21
+ * the actions sit in the strip of tray left under the card. What the alert
22
+ * says and what can be done about it are two surfaces, so a row of buttons
23
+ * never crowds the sentence it answers. `plain` is a single tinted box, for
24
+ * inside a form or a dialog that already draws an edge; `banner` is one line
25
+ * across the top of a page or a panel.
26
+ *
27
+ * ## It folds away
28
+ *
29
+ * Closing it, with `Alert.Close` or `open`, folds it to no height instead of
30
+ * removing it, so what is under it slides up rather than jumping. Nothing in
31
+ * a closed alert can be focused.
32
+ *
33
+ * ## Interrupting is for trouble
34
+ *
35
+ * A `destructive` or `warning` alert is announced the moment it appears; the
36
+ * others are read when the reader gets to them, because a note about a trial
37
+ * should not cut across what a screen reader is in the middle of saying.
38
+ */
39
+ import { type ComponentProps, type ReactNode } from 'react';
40
+ /** The theme role an alert is drawn in. */
41
+ export type AlertTone = 'neutral' | 'info' | 'success' | 'warning' | 'destructive';
42
+ /** `default` is the card in a tray, `plain` one tinted box, `banner` a strip across a page. */
43
+ export type AlertVariant = 'default' | 'plain' | 'banner';
44
+ export interface AlertProps extends ComponentProps<'div'> {
45
+ /** The theme role it is drawn in, which also says how urgent it is. Default `neutral`. */
46
+ tone?: AlertTone;
47
+ /**
48
+ * `default` sets the message in a card inside a tray of its tone, with the
49
+ * actions in the tray under it. `plain` is one tinted box, for inside a form
50
+ * or a dialog. `banner` is one line across the top of a page or panel.
51
+ */
52
+ variant?: AlertVariant;
53
+ /** Whether it is shown, when the caller holds that. It folds away when it is not. */
54
+ open?: boolean;
55
+ /** Whether it starts shown, when the caller does not hold it. Default `true`. */
56
+ defaultOpen?: boolean;
57
+ /** Called when `Alert.Close` asks for it to close. */
58
+ onOpenChange?: (open: boolean) => void;
59
+ }
60
+ declare function AlertRoot({ tone, variant, open, defaultOpen, onOpenChange, role, className, children, ...props }: AlertProps): import("react").JSX.Element;
61
+ declare namespace AlertRoot {
62
+ var displayName: string;
63
+ }
64
+ export interface AlertIconProps extends ComponentProps<'span'> {
65
+ }
66
+ /** The glyph at the start, in the tone's colour. Without children it draws the tone's own. */
67
+ declare function AlertIcon({ className, children, ...props }: AlertIconProps): import("react").JSX.Element;
68
+ declare namespace AlertIcon {
69
+ var displayName: string;
70
+ }
71
+ export interface AlertTitleProps extends ComponentProps<'div'> {
72
+ }
73
+ /** What happened, in a few words. */
74
+ declare function AlertTitle({ className, ...props }: AlertTitleProps): import("react").JSX.Element;
75
+ declare namespace AlertTitle {
76
+ var displayName: string;
77
+ }
78
+ export interface AlertDescriptionProps extends ComponentProps<'div'> {
79
+ }
80
+ /** The sentence under the title: why, and what follows from it. A link in it is underlined. */
81
+ declare function AlertDescription({ className, ...props }: AlertDescriptionProps): import("react").JSX.Element;
82
+ declare namespace AlertDescription {
83
+ var displayName: string;
84
+ }
85
+ export interface AlertActionsProps extends ComponentProps<'div'> {
86
+ }
87
+ /**
88
+ * The buttons. In the default variant they sit in the tray under the card, at
89
+ * its end; in `plain` under the words; in `banner` at the end of the line.
90
+ * Keep it a direct child of `Alert`, so the tray can find it.
91
+ */
92
+ declare function AlertActions({ className, ...props }: AlertActionsProps): import("react").JSX.Element;
93
+ declare namespace AlertActions {
94
+ var displayName: string;
95
+ }
96
+ export interface AlertCloseProps extends ComponentProps<'button'> {
97
+ }
98
+ /** A round cross at the end that folds the alert away. */
99
+ declare function AlertClose({ className, onClick, 'aria-label': label, ...props }: AlertCloseProps): import("react").JSX.Element;
100
+ declare namespace AlertClose {
101
+ var displayName: string;
102
+ }
103
+ export interface AlertDetailsProps extends Omit<ComponentProps<'div'>, 'title'> {
104
+ /** The line that opens the list: "Show the 3 fields". */
105
+ label: ReactNode;
106
+ /** Whether the list is open, when the caller holds that. */
107
+ open?: boolean;
108
+ /** Whether the list starts open, when the caller does not hold it. Default `false`. */
109
+ defaultOpen?: boolean;
110
+ /** Called when the line is pressed to open or close the list. */
111
+ onOpenChange?: (open: boolean) => void;
112
+ }
113
+ /**
114
+ * A line under the description that opens into a list of `Alert.Item`s, for
115
+ * an alert that sums up several things: the fields a form still needs, the
116
+ * files an upload skipped.
117
+ */
118
+ declare function AlertDetails({ label, open, defaultOpen, onOpenChange, className, children, ...props }: AlertDetailsProps): import("react").JSX.Element;
119
+ declare namespace AlertDetails {
120
+ var displayName: string;
121
+ }
122
+ export interface AlertItemProps extends ComponentProps<'li'> {
123
+ }
124
+ /** One entry in `Alert.Details`, marked with a dot in the alert's tone. */
125
+ declare function AlertItem({ className, ...props }: AlertItemProps): import("react").JSX.Element;
126
+ declare namespace AlertItem {
127
+ var displayName: string;
128
+ }
129
+ export declare const Alert: typeof AlertRoot & {
130
+ Icon: typeof AlertIcon;
131
+ Title: typeof AlertTitle;
132
+ Description: typeof AlertDescription;
133
+ Actions: typeof AlertActions;
134
+ Close: typeof AlertClose;
135
+ Details: typeof AlertDetails;
136
+ Item: typeof AlertItem;
137
+ };
138
+ export { AlertIcon, AlertTitle, AlertDescription, AlertActions, AlertClose, AlertDetails, AlertItem };
@@ -0,0 +1,279 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Alert — a message set into the page, which stays there until it has been
5
+ * dealt with: a payment that failed, a trial running out, a form that needs
6
+ * another look.
7
+ *
8
+ * ```tsx
9
+ * <Alert tone="destructive">
10
+ * <Alert.Icon />
11
+ * <Alert.Title>Your payment did not go through</Alert.Title>
12
+ * <Alert.Description>The card ending 4242 was declined.</Alert.Description>
13
+ * <Alert.Actions>
14
+ * <Button size="sm">Update card</Button>
15
+ * </Alert.Actions>
16
+ * <Alert.Close />
17
+ * </Alert>
18
+ * ```
19
+ *
20
+ * ## A card in a tray of its tone
21
+ *
22
+ * By default the message is a card set into a tray tinted in its tone, and
23
+ * the actions sit in the strip of tray left under the card. What the alert
24
+ * says and what can be done about it are two surfaces, so a row of buttons
25
+ * never crowds the sentence it answers. `plain` is a single tinted box, for
26
+ * inside a form or a dialog that already draws an edge; `banner` is one line
27
+ * across the top of a page or a panel.
28
+ *
29
+ * ## It folds away
30
+ *
31
+ * Closing it, with `Alert.Close` or `open`, folds it to no height instead of
32
+ * removing it, so what is under it slides up rather than jumping. Nothing in
33
+ * a closed alert can be focused.
34
+ *
35
+ * ## Interrupting is for trouble
36
+ *
37
+ * A `destructive` or `warning` alert is announced the moment it appears; the
38
+ * others are read when the reader gets to them, because a note about a trial
39
+ * should not cut across what a screen reader is in the middle of saying.
40
+ */
41
+ import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
42
+ import { tv } from 'tailwind-variants';
43
+ /** The fold every opening and closing here uses, the one the Prompt's bands use. */
44
+ const FOLD = 'grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none';
45
+ const alertVariants = tv({
46
+ slots: {
47
+ root: ['group/alert', FOLD],
48
+ clip: 'min-h-0 overflow-hidden',
49
+ /*
50
+ * The tray. The card fills it, so its one hairline is the alert's only
51
+ * edge; a rim of tint round the card read as a heavy frame. The tint
52
+ * shows in the strip under the card when there are actions, and at the
53
+ * card's lower corners, which round off over it.
54
+ */
55
+ tray: 'flex flex-col overflow-hidden rounded-[20px] border',
56
+ /*
57
+ * The parts are laid out by column, not by wrapper: the icon spans the
58
+ * rows at the start, the words run down the middle, and the close button
59
+ * holds the top of the end column. So the parts can be written in a flat
60
+ * list and still land where they belong.
61
+ */
62
+ body: [
63
+ 'grid grid-cols-[auto_minmax(0,1fr)_auto] items-start text-sm',
64
+ // 19px is the tray's 20 less its hairline: one corner, two curves.
65
+ 'group-data-[variant=default]/alert:rounded-[19px] group-data-[variant=default]/alert:bg-card group-data-[variant=default]/alert:px-3.5 group-data-[variant=default]/alert:py-3 group-data-[variant=default]/alert:text-card-foreground group-data-[variant=default]/alert:shadow-xs',
66
+ 'group-data-[variant=plain]/alert:rounded-2xl group-data-[variant=plain]/alert:border group-data-[variant=plain]/alert:px-3.5 group-data-[variant=plain]/alert:py-3',
67
+ // One line: the parts sit in a row and wrap under each other when narrow.
68
+ 'group-data-[variant=banner]/alert:flex group-data-[variant=banner]/alert:flex-wrap group-data-[variant=banner]/alert:items-center group-data-[variant=banner]/alert:gap-x-3 group-data-[variant=banner]/alert:gap-y-1 group-data-[variant=banner]/alert:border-b group-data-[variant=banner]/alert:px-4 group-data-[variant=banner]/alert:py-2.5',
69
+ ],
70
+ icon: [
71
+ 'col-start-1 row-span-3 row-start-1 me-3 flex shrink-0 items-center justify-center text-[var(--pu-tone)] [&_svg]:shrink-0',
72
+ // In the card the glyph sits in a disc of its tone, the alert's one patch of colour.
73
+ 'group-data-[variant=default]/alert:size-8 group-data-[variant=default]/alert:rounded-full group-data-[variant=default]/alert:bg-[color-mix(in_oklab,var(--pu-tone)_14%,transparent)] group-data-[variant=default]/alert:[&_svg:not([class*=size-])]:size-4',
74
+ // Elsewhere it is the glyph alone, centred on the title's line.
75
+ 'group-data-[variant=plain]/alert:h-5 group-data-[variant=plain]/alert:[&_svg:not([class*=size-])]:size-4',
76
+ 'group-data-[variant=banner]/alert:me-0 group-data-[variant=banner]/alert:[&_svg:not([class*=size-])]:size-4',
77
+ ],
78
+ title: [
79
+ 'col-start-2 font-medium text-foreground',
80
+ // Beside the disc, the first line of the title sits on the disc's middle,
81
+ // and a title that wraps simply runs on below it.
82
+ 'leading-5 group-data-[variant=default]/alert:pt-1.5',
83
+ // On its own it takes the line and wraps beside the icon; beside a description it keeps its width.
84
+ 'group-data-[variant=banner]/alert:min-w-0 group-data-[variant=banner]/alert:flex-1 group-data-[variant=banner]/alert:basis-40 group-data-[variant=banner]/alert:group-has-[[data-slot=alert-description]]/alert:flex-none',
85
+ ],
86
+ description: [
87
+ 'col-start-2 text-muted-foreground [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4',
88
+ // Alone beside the disc its first line sits on the disc's middle, as a title's would.
89
+ 'group-data-[variant=default]/alert:pt-1.5 [[data-slot=alert-title]~&]:mt-0.5 group-data-[variant=default]/alert:[[data-slot=alert-title]~&]:pt-0',
90
+ 'group-data-[variant=banner]/alert:min-w-0 group-data-[variant=banner]/alert:flex-1 group-data-[variant=banner]/alert:basis-48',
91
+ ],
92
+ actions: [
93
+ 'flex flex-wrap items-center gap-2',
94
+ // In the tray's strip, at its end: what can be done follows what was said.
95
+ 'group-data-[variant=default]/alert:justify-end group-data-[variant=default]/alert:p-2.5',
96
+ 'group-data-[variant=plain]/alert:col-start-2 group-data-[variant=plain]/alert:mt-2.5',
97
+ 'group-data-[variant=banner]/alert:ms-auto',
98
+ ],
99
+ close: [
100
+ 'relative col-start-3 row-start-1 ms-2 inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground outline-none',
101
+ 'transition-[background-color,color,scale] duration-150 after:absolute after:-inset-1.5 hover:bg-foreground/10 hover:text-foreground active:scale-95',
102
+ 'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-3.5',
103
+ 'group-data-[variant=default]/alert:-me-1 group-data-[variant=default]/alert:mt-0.5',
104
+ 'group-data-[variant=plain]/alert:-me-1 group-data-[variant=plain]/alert:-mt-1',
105
+ 'group-data-[variant=banner]/alert:-me-1.5',
106
+ ],
107
+ details: 'col-start-2 mt-2 flex flex-col',
108
+ detailsToggle: [
109
+ 'inline-flex w-fit cursor-pointer items-center gap-1 rounded-md font-medium text-foreground outline-none',
110
+ 'transition-colors hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring',
111
+ ],
112
+ detailsChevron: 'size-4 transition-transform duration-200 motion-reduce:transition-none',
113
+ detailsList: 'flex flex-col gap-1.5 pt-2',
114
+ item: [
115
+ 'relative ps-4 text-muted-foreground',
116
+ // A dot in the tone, centred on the first line of the item.
117
+ "before:absolute before:start-1 before:top-[0.5lh] before:size-1.5 before:-translate-y-1/2 before:rounded-full before:bg-[var(--pu-tone)] before:content-['']",
118
+ ],
119
+ },
120
+ variants: {
121
+ tone: {
122
+ neutral: { root: '[--pu-tone:var(--muted-foreground)]' },
123
+ info: { root: '[--pu-tone:var(--info)]' },
124
+ success: { root: '[--pu-tone:var(--success)]' },
125
+ warning: { root: '[--pu-tone:var(--warning)]' },
126
+ destructive: { root: '[--pu-tone:var(--destructive)]' },
127
+ },
128
+ variant: {
129
+ default: {},
130
+ plain: {},
131
+ banner: {},
132
+ },
133
+ shown: {
134
+ true: { root: 'grid-rows-[1fr] opacity-100' },
135
+ false: { root: 'grid-rows-[0fr] opacity-0' },
136
+ },
137
+ },
138
+ compoundVariants: [
139
+ // The tray takes a little of the tone into the muted surface; neutral is the muted surface itself.
140
+ { variant: 'default', tone: 'neutral', className: { tray: 'bg-muted' } },
141
+ {
142
+ variant: 'default',
143
+ tone: ['info', 'success', 'warning', 'destructive'],
144
+ className: {
145
+ tray: 'border-[color-mix(in_oklab,var(--pu-tone)_30%,var(--border))] bg-[color-mix(in_oklab,var(--pu-tone)_10%,var(--muted))]',
146
+ },
147
+ },
148
+ { variant: 'plain', tone: 'neutral', className: { body: 'bg-muted/50' } },
149
+ {
150
+ variant: 'plain',
151
+ tone: ['info', 'success', 'warning', 'destructive'],
152
+ className: {
153
+ body: 'border-[color-mix(in_oklab,var(--pu-tone)_28%,var(--border))] bg-[color-mix(in_oklab,var(--pu-tone)_6%,var(--background))]',
154
+ },
155
+ },
156
+ { variant: 'banner', tone: 'neutral', className: { body: 'bg-muted' } },
157
+ {
158
+ variant: 'banner',
159
+ tone: ['info', 'success', 'warning', 'destructive'],
160
+ className: {
161
+ body: 'border-[color-mix(in_oklab,var(--pu-tone)_24%,var(--border))] bg-[color-mix(in_oklab,var(--pu-tone)_10%,var(--background))]',
162
+ },
163
+ },
164
+ ],
165
+ defaultVariants: { tone: 'neutral', variant: 'default', shown: true },
166
+ });
167
+ const AlertContext = createContext(null);
168
+ function useAlert(part) {
169
+ const alert = useContext(AlertContext);
170
+ if (!alert)
171
+ throw new Error(`${part} must be used inside an <Alert>.`);
172
+ return alert;
173
+ }
174
+ const isActions = (child) => isValidElement(child) && child.type === AlertActions;
175
+ function AlertRoot({ tone = 'neutral', variant = 'default', open, defaultOpen = true, onOpenChange, role, className, children, ...props }) {
176
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
177
+ const shown = open ?? internalOpen;
178
+ const setOpen = useCallback((next) => {
179
+ if (open === undefined)
180
+ setInternalOpen(next);
181
+ onOpenChange?.(next);
182
+ }, [open, onOpenChange]);
183
+ const clip = useRef(null);
184
+ // Nothing in a closed alert can be reached. Set on the element: how React
185
+ // writes a boolean `inert` differs between versions.
186
+ useEffect(() => {
187
+ clip.current?.toggleAttribute('inert', !shown);
188
+ }, [shown]);
189
+ const slots = alertVariants({ tone, variant, shown });
190
+ const context = useMemo(() => ({ tone, setOpen }), [tone, setOpen]);
191
+ // In the tray the actions leave the card for the strip under it.
192
+ const all = Children.toArray(children);
193
+ const actions = variant === 'default' ? all.filter(isActions) : [];
194
+ const rest = variant === 'default' ? all.filter((child) => !isActions(child)) : all;
195
+ const body = (_jsx("div", { "data-slot": "alert-body", className: slots.body(), children: rest }));
196
+ return (_jsx(AlertContext.Provider, { value: context, children: _jsx("div", { "data-slot": "alert", "data-tone": tone, "data-variant": variant, "data-open": shown ? '' : undefined, role: role ?? (tone === 'destructive' || tone === 'warning' ? 'alert' : 'status'), className: slots.root({ className }), ...props, children: _jsx("div", { ref: clip, className: slots.clip(), children: variant === 'default' ? (_jsxs("div", { "data-slot": "alert-tray", className: slots.tray(), children: [body, actions] })) : (body) }) }) }));
197
+ }
198
+ AlertRoot.displayName = 'Alert';
199
+ /* The glyph each tone draws when `Alert.Icon` is given none of its own. */
200
+ const GLYPHS = {
201
+ neutral: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 11v5M12 7.5h.01" })] })),
202
+ info: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 11v5M12 7.5h.01" })] })),
203
+ success: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] })),
204
+ warning: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M10.3 4.2 2.9 17a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0Z" }), _jsx("path", { d: "M12 9.5v4M12 17h.01" })] })),
205
+ destructive: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 7.5v5M12 16h.01" })] })),
206
+ };
207
+ /** The glyph at the start, in the tone's colour. Without children it draws the tone's own. */
208
+ function AlertIcon({ className, children, ...props }) {
209
+ const { tone } = useAlert('Alert.Icon');
210
+ return (_jsx("span", { "data-slot": "alert-icon", "aria-hidden": "true", className: alertVariants().icon({ className }), ...props, children: children ?? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: GLYPHS[tone] })) }));
211
+ }
212
+ AlertIcon.displayName = 'Alert.Icon';
213
+ /** What happened, in a few words. */
214
+ function AlertTitle({ className, ...props }) {
215
+ return _jsx("div", { "data-slot": "alert-title", className: alertVariants().title({ className }), ...props });
216
+ }
217
+ AlertTitle.displayName = 'Alert.Title';
218
+ /** The sentence under the title: why, and what follows from it. A link in it is underlined. */
219
+ function AlertDescription({ className, ...props }) {
220
+ return _jsx("div", { "data-slot": "alert-description", className: alertVariants().description({ className }), ...props });
221
+ }
222
+ AlertDescription.displayName = 'Alert.Description';
223
+ /**
224
+ * The buttons. In the default variant they sit in the tray under the card, at
225
+ * its end; in `plain` under the words; in `banner` at the end of the line.
226
+ * Keep it a direct child of `Alert`, so the tray can find it.
227
+ */
228
+ function AlertActions({ className, ...props }) {
229
+ return _jsx("div", { "data-slot": "alert-actions", className: alertVariants().actions({ className }), ...props });
230
+ }
231
+ AlertActions.displayName = 'Alert.Actions';
232
+ /** A round cross at the end that folds the alert away. */
233
+ function AlertClose({ className, onClick, 'aria-label': label = 'Dismiss', ...props }) {
234
+ const { setOpen } = useAlert('Alert.Close');
235
+ return (_jsx("button", { type: "button", "data-slot": "alert-close", "aria-label": label, className: alertVariants().close({ className }), onClick: (event) => {
236
+ onClick?.(event);
237
+ if (!event.defaultPrevented)
238
+ setOpen(false);
239
+ }, ...props, children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) }));
240
+ }
241
+ AlertClose.displayName = 'Alert.Close';
242
+ /**
243
+ * A line under the description that opens into a list of `Alert.Item`s, for
244
+ * an alert that sums up several things: the fields a form still needs, the
245
+ * files an upload skipped.
246
+ */
247
+ function AlertDetails({ label, open, defaultOpen = false, onOpenChange, className, children, ...props }) {
248
+ useAlert('Alert.Details');
249
+ const slots = alertVariants();
250
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
251
+ const expanded = open ?? internalOpen;
252
+ const listId = useId();
253
+ const clip = useRef(null);
254
+ useEffect(() => {
255
+ clip.current?.toggleAttribute('inert', !expanded);
256
+ }, [expanded]);
257
+ const toggle = () => {
258
+ if (open === undefined)
259
+ setInternalOpen(!expanded);
260
+ onOpenChange?.(!expanded);
261
+ };
262
+ return (_jsxs("div", { "data-slot": "alert-details", "data-open": expanded ? '' : undefined, className: slots.details({ className }), ...props, children: [_jsxs("button", { type: "button", "aria-expanded": expanded, "aria-controls": listId, onClick: toggle, className: slots.detailsToggle(), children: [label, _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: slots.detailsChevron({ className: expanded ? 'rotate-180' : undefined }), children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), _jsx("div", { className: [FOLD, expanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr] opacity-0'].join(' '), children: _jsx("div", { ref: clip, className: "min-h-0 overflow-hidden", children: _jsx("ul", { id: listId, className: slots.detailsList(), children: children }) }) })] }));
263
+ }
264
+ AlertDetails.displayName = 'Alert.Details';
265
+ /** One entry in `Alert.Details`, marked with a dot in the alert's tone. */
266
+ function AlertItem({ className, ...props }) {
267
+ return _jsx("li", { "data-slot": "alert-item", className: alertVariants().item({ className }), ...props });
268
+ }
269
+ AlertItem.displayName = 'Alert.Item';
270
+ export const Alert = Object.assign(AlertRoot, {
271
+ Icon: AlertIcon,
272
+ Title: AlertTitle,
273
+ Description: AlertDescription,
274
+ Actions: AlertActions,
275
+ Close: AlertClose,
276
+ Details: AlertDetails,
277
+ Item: AlertItem,
278
+ });
279
+ export { AlertIcon, AlertTitle, AlertDescription, AlertActions, AlertClose, AlertDetails, AlertItem };
@@ -0,0 +1,128 @@
1
+ import { type ComponentProps, type MouseEvent, type ReactElement, type ReactNode } from 'react';
2
+ import { type ButtonProps } from '../button/index.js';
3
+ /** The theme role the dialog's media is drawn in, which also sets its action's default colour. */
4
+ export type AlertDialogTone = 'neutral' | 'info' | 'success' | 'warning' | 'destructive';
5
+ export interface AlertDialogProps {
6
+ children?: ReactNode;
7
+ /** Whether it is open, when the caller holds that. */
8
+ open?: boolean;
9
+ /** Whether it starts open, when the caller does not hold it. */
10
+ defaultOpen?: boolean;
11
+ /** Called when it opens or closes. */
12
+ onOpenChange?: (open: boolean) => void;
13
+ /** Whether a press outside it closes it, as Cancel would. Escape always does. Default `true`. */
14
+ dismissible?: boolean;
15
+ }
16
+ declare function AlertDialogRoot({ children, open, defaultOpen, onOpenChange, dismissible, }: AlertDialogProps): import("react").JSX.Element;
17
+ declare namespace AlertDialogRoot {
18
+ var displayName: string;
19
+ }
20
+ export interface AlertDialogTriggerProps extends Omit<ComponentProps<'button'>, 'children'> {
21
+ children?: ReactNode;
22
+ /**
23
+ * Your own element to be the trigger, such as a styled button. It is given
24
+ * the click handler and the attributes that tie it to the dialog.
25
+ */
26
+ render?: ReactElement;
27
+ }
28
+ /** What opens the dialog. A plain button unless `render` supplies one. */
29
+ declare function AlertDialogTrigger({ render, children, ...props }: AlertDialogTriggerProps): import("react").JSX.Element;
30
+ declare namespace AlertDialogTrigger {
31
+ var displayName: string;
32
+ }
33
+ export interface AlertDialogContentProps extends Omit<ComponentProps<'div'>, 'children'> {
34
+ children?: ReactNode;
35
+ /**
36
+ * The theme role it is drawn in: the colour of `AlertDialog.Media`, and of
37
+ * an action that is not given its own. `destructive` makes the action red.
38
+ * Default `neutral`.
39
+ */
40
+ tone?: AlertDialogTone;
41
+ /** `sm` is narrower, with the question centred over the answers. Default `default`. */
42
+ size?: 'default' | 'sm';
43
+ /** Frost the page behind the dialog instead of only dimming it. */
44
+ blur?: boolean;
45
+ /**
46
+ * Draw it in the page instead of over it: no backdrop, no trigger, always
47
+ * there. For previews; `open` is ignored.
48
+ */
49
+ inline?: boolean;
50
+ }
51
+ /** The dialog: the panel with the question, in a band that holds the answers. */
52
+ declare function AlertDialogContent({ tone, size, blur, inline, className, children, ...props }: AlertDialogContentProps): import("react").JSX.Element;
53
+ declare namespace AlertDialogContent {
54
+ var displayName: string;
55
+ }
56
+ export interface AlertDialogMediaProps extends ComponentProps<'div'> {
57
+ }
58
+ /** A glyph in a disc of the dialog's tone, above the title. */
59
+ declare function AlertDialogMedia({ className, ...props }: AlertDialogMediaProps): import("react").JSX.Element;
60
+ declare namespace AlertDialogMedia {
61
+ var displayName: string;
62
+ }
63
+ export interface AlertDialogTitleProps extends ComponentProps<'h2'> {
64
+ }
65
+ /** The question. It is what the dialog is announced as. */
66
+ declare function AlertDialogTitle({ className, ...props }: AlertDialogTitleProps): import("react").JSX.Element;
67
+ declare namespace AlertDialogTitle {
68
+ var displayName: string;
69
+ }
70
+ export interface AlertDialogDescriptionProps extends ComponentProps<'p'> {
71
+ }
72
+ /** What follows from each answer, read out after the title. */
73
+ declare function AlertDialogDescription({ className, ...props }: AlertDialogDescriptionProps): import("react").JSX.Element;
74
+ declare namespace AlertDialogDescription {
75
+ var displayName: string;
76
+ }
77
+ export interface AlertDialogFooterProps extends ComponentProps<'div'> {
78
+ }
79
+ /**
80
+ * The answers, side by side at equal widths in the band under the panel.
81
+ * Keep it a direct child of `AlertDialog.Content`, so the band can find it.
82
+ */
83
+ declare function AlertDialogFooter({ className, ...props }: AlertDialogFooterProps): import("react").JSX.Element;
84
+ declare namespace AlertDialogFooter {
85
+ var displayName: string;
86
+ }
87
+ export interface AlertDialogCancelProps extends Omit<ButtonProps, 'variant' | 'size'> {
88
+ }
89
+ /** Closes the dialog without doing anything. It reads "Cancel" unless given children. */
90
+ declare function AlertDialogCancel({ className, children, disabled, ...props }: AlertDialogCancelProps): import("react").JSX.Element;
91
+ declare namespace AlertDialogCancel {
92
+ var displayName: string;
93
+ }
94
+ export interface AlertDialogActionProps extends Omit<ButtonProps, 'variant' | 'size' | 'onClick'> {
95
+ /**
96
+ * What it does. When it returns a promise, the button shows a spinner and
97
+ * the dialog stays open, and cannot be closed, until it settles: it closes
98
+ * when the promise resolves and stays open when it rejects. Call
99
+ * `event.preventDefault()` to keep the dialog open after a plain handler.
100
+ */
101
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void | Promise<unknown>;
102
+ /** Its colour. Default: `destructive` in a destructive dialog, `primary` otherwise. */
103
+ tone?: 'primary' | 'destructive';
104
+ /** `solid` is filled; `soft` is a tint of its colour, for an answer that is not the main one. Default `solid`. */
105
+ variant?: 'solid' | 'soft';
106
+ /**
107
+ * Makes it an action that must be pressed and held, for this many
108
+ * milliseconds, while a fill runs across it. Letting go early drains the
109
+ * fill. Space and Enter can be held the same way.
110
+ */
111
+ hold?: number;
112
+ }
113
+ /** Does what the dialog asks about, then closes it. */
114
+ declare function AlertDialogAction({ onClick, tone, variant, hold, loading, disabled, className, children, ...props }: AlertDialogActionProps): import("react").JSX.Element;
115
+ declare namespace AlertDialogAction {
116
+ var displayName: string;
117
+ }
118
+ export declare const AlertDialog: typeof AlertDialogRoot & {
119
+ Trigger: typeof AlertDialogTrigger;
120
+ Content: typeof AlertDialogContent;
121
+ Media: typeof AlertDialogMedia;
122
+ Title: typeof AlertDialogTitle;
123
+ Description: typeof AlertDialogDescription;
124
+ Footer: typeof AlertDialogFooter;
125
+ Cancel: typeof AlertDialogCancel;
126
+ Action: typeof AlertDialogAction;
127
+ };
128
+ export { AlertDialogTrigger, AlertDialogContent, AlertDialogMedia, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, };