@propellerads/dialog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # Dialog
2
+
3
+ `Dialog` is the blocking window: the page stays where it is, the dialog takes the
4
+ focus and the keyboard, and nothing else answers until it is done. It is built
5
+ the shadcn way — one stylesheet, variants picked by
6
+ [class-variance-authority](https://cva.style/), the dialog itself from
7
+ [Base UI](https://base-ui.com/react/components/dialog) — and every value in the
8
+ stylesheet comes from [@propellerads/tokens](../Tokens/README.md).
9
+
10
+ It replaces [`@propellerads/popup`](../Popup/README.md), which is the same window
11
+ built on styled-components and a hand-rolled portal. See
12
+ [From Popup](#from-popup).
13
+
14
+ [NPM](https://www.npmjs.com/package/@propellerads/dialog) |
15
+ [Github](https://github.com/propellerads/ui-components/tree/master/Components/Dialog)
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ bun add @propellerads/dialog
21
+ ```
22
+
23
+ The dialog brings its own stylesheet: the built entry imports it, so importing the
24
+ component is the whole of it. The tokens are the app's, not the dialog's — they
25
+ carry the brand, and only the app knows which brand it is:
26
+
27
+ ```ts
28
+ import '@propellerads/tokens/common.css';
29
+ import '@propellerads/tokens/propellerads.css'; // or monetag / propush / zeydoo
30
+ ```
31
+
32
+ The tokens are not optional: no `var()` in the stylesheet carries a fallback, so
33
+ without them the dialog has no colours, no spacing and no type. That is on
34
+ purpose — a page with no design system on it should look like one, not like a
35
+ dialog that almost works. The stylesheet is also exported as
36
+ `@propellerads/dialog/index.css`, for a consumer that extracts CSS by hand.
37
+
38
+ ## Use
39
+
40
+ ```tsx
41
+ import Dialog from '@propellerads/dialog';
42
+ import Button, {ButtonVariant} from '@propellerads/button';
43
+
44
+ <Dialog isOpen={isOpen} onClose={close} title="Title" footer={
45
+ <>
46
+ <Button variant={ButtonVariant.Secondary} onClick={close}>Cancel</Button>
47
+ <Button onClick={save}>OK</Button>
48
+ </>
49
+ }>
50
+ What the dialog is about.
51
+ </Dialog>
52
+ ```
53
+
54
+ Three parts, in the order the design file stacks them: a header with the title
55
+ and the cross, the content, and a footer whose buttons end at the right edge. A
56
+ part that is not passed is not rendered — a dialog with no footer has no empty row
57
+ at the bottom, and no gap where one would have been.
58
+
59
+ The open state is the product's. Pass `isOpen` and close it yourself from
60
+ `onClose`; nothing closes behind your back. For a dialog whose state nobody else
61
+ needs, hand it a `trigger` instead and let it keep its own:
62
+
63
+ ```tsx
64
+ <Dialog trigger={<Button>Open</Button>} title="Title">…</Dialog>
65
+ ```
66
+
67
+ ### Blocking
68
+
69
+ Dismissable is the default: the cross, Escape and the backdrop all close the
70
+ dialog. They are one decision rather than three — a cross that is drawn but does
71
+ not close, or an Escape that works while the cross is gone, is a dialog lying
72
+ about itself. Turn it off for a choice that has to be taken, and the cross goes
73
+ with it:
74
+
75
+ ```tsx
76
+ <Dialog isOpen={isOpen} isDismissable={false} title="Delete the campaign?" footer={…}>
77
+ ```
78
+
79
+ The way out is then whatever the footer offers.
80
+
81
+ ### Width
82
+
83
+ | Width | Max | Where it comes from |
84
+ | --- | --- | --- |
85
+ | `minimum` | 296px | The design file's own size. The default. |
86
+ | `small` | 400px | Popup v4's `small` |
87
+ | `medium` | 600px | Popup v4's `medium` |
88
+ | `big` | 1080px | Popup v4's `big` |
89
+
90
+ A step is the width the box holds. It gives way to a narrower viewport, and below
91
+ the mobile breakpoint every step is the width of the screen — but it does not
92
+ shrink to its content: a ceiling alone would do nothing, because what is inside a
93
+ dialog stretches to the box rather than the other way round.
94
+
95
+ When none of the four is the answer, set the custom property instead of reaching
96
+ for a fifth name:
97
+
98
+ ```css
99
+ .onboarding .dialog { --dialog-width: 720px; }
100
+ ```
101
+
102
+ ## Props
103
+
104
+ | Prop | Type | Default | What it does |
105
+ | --- | --- | --- | --- |
106
+ | `isOpen` | `boolean` | — | Whether the dialog is open. Leave it out for an uncontrolled dialog driven by `trigger`. |
107
+ | `defaultOpen` | `boolean` | `false` | The state an uncontrolled dialog starts in. |
108
+ | `onOpenChange` | `(isOpen: boolean) => void` | — | Called whenever the dialog opens or closes, whoever asked for it. |
109
+ | `onClose` | `() => void` | — | Called when the dialog asks to be closed. The shorthand for `onOpenChange`. |
110
+ | `title` | `ReactNode` | — | The heading, and the dialog's accessible name. |
111
+ | `children` | `ReactNode` | — | What the dialog is about. The only part that scrolls. |
112
+ | `footer` | `ReactNode` | — | The row at the bottom. Buttons end at the right edge. |
113
+ | `width` | `DialogWidth` | `'minimum'` | How wide the box may grow. |
114
+ | `isDismissable` | `boolean` | `true` | Whether the dialog can be dismissed at all: the cross, Escape and the backdrop together. |
115
+ | `closeLabel` | `string` | `'Close'` | The cross's accessible name. |
116
+ | `closeIcon` | `ReactNode` | the pack's `Close` | Replaces the glyph inside the close button. |
117
+ | `trigger` | `ReactElement` | — | What opens the dialog. The element itself becomes the trigger. |
118
+ | `render` | `ReactElement \| fn` | — | Replaces the box with another element. See [As a form](#as-a-form). |
119
+ | `className` | `string` | — | Goes on the box. |
120
+ | `backdropClassName` | `string` | — | Goes on the backdrop. |
121
+ | `initialFocus` | `boolean \| RefObject \| fn` | `true` | Where the focus lands when it opens. |
122
+ | `finalFocus` | `boolean \| RefObject \| fn` | `true` | Where the focus returns when it closes. |
123
+ | `aria-label` | `string` | — | Required when there is no `title`: a dialog has to say what it is. |
124
+
125
+ ## As a form
126
+
127
+ A submit button in the footer cannot reach a `<form>` that lives in the content —
128
+ they are siblings. Render the box as the form instead, and the header, the content
129
+ and the footer are all inside it:
130
+
131
+ ```tsx
132
+ <Dialog
133
+ isOpen={isOpen}
134
+ onClose={close}
135
+ title="New domain pool"
136
+ render={<form onSubmit={save} />}
137
+ footer={
138
+ <>
139
+ <Button variant={ButtonVariant.Secondary} onClick={close}>Cancel</Button>
140
+ <Button type="submit">Save</Button>
141
+ </>
142
+ }
143
+ >
144
+ <InputText name="name" />
145
+ </Dialog>
146
+ ```
147
+
148
+ The dialog keeps everything it had — `role="dialog"`, the focus trap, the scroll
149
+ lock — and gains what a form gives: the submit button works, and Enter in a field
150
+ submits.
151
+
152
+ ## Styling
153
+
154
+ The custom properties at the top of `.dialog` are the override API. They need
155
+ no theme provider — any ancestor of the portal can set them, and so can the
156
+ dialog's own `className`:
157
+
158
+ | Property | Default | What it is |
159
+ | --- | --- | --- |
160
+ | `--dialog-width` | `296px` | What the width variant sets. |
161
+ | `--dialog-bg` | `--color-shade-white-inverter` | The surface. Flips with the theme. |
162
+ | `--dialog-fg` | `--color-shade-black-inverter` | The text. |
163
+ | `--dialog-padding` | `--space-24` | Inside the box. One step less below the mobile breakpoint. |
164
+ | `--dialog-gap` | `--space-24` | Between header, content and footer. |
165
+ | `--dialog-radius` | `--radius-24` | The corner. |
166
+ | `--dialog-shadow` | `--shadow-big` | The lift off the backdrop. |
167
+ | `--dialog-backdrop` | `--over-fullscreen` | The dimming behind it. |
168
+ | `--dialog-backdrop-blur` | `4px` | How far out of focus the page behind goes. `0` takes the blur off. |
169
+ | `--dialog-layer` | `3000` | The stacking order, as Popup v4 had it. |
170
+
171
+ The cross is `@propellerads/button` — a ghost, icon-only button — repainted by
172
+ `.dialog__close.button { --button-accent: var(--color-quiet) }`. It is the
173
+ quietest thing in the dialog and the action colour belongs to the footer, and the
174
+ button has no neutral tone yet. Two classes, because the tone class sets that
175
+ variable on the element itself, so an ancestor cannot reach it.
176
+
177
+ `dialogVariants` is exported for a product that renders Base UI's parts itself and
178
+ only wants the look:
179
+
180
+ ```tsx
181
+ <Dialog.Popup className={dialogVariants({width: 'medium'})} />
182
+ ```
183
+
184
+ ## Screenshots
185
+
186
+ The Storybook story at 2×: the playground, then the five shapes — a question,
187
+ something to agree to, a form, more than fits, and wide.
188
+
189
+ | Light | Dark |
190
+ |---|---|
191
+ | <img src="screenshots/dialog-light.png" width="420"> | <img src="screenshots/dialog-dark.png" width="420"> |
192
+
193
+ ## Where this differs from the design file
194
+
195
+ Three values are deliberately not what the Figma frame draws. They are listed
196
+ here so nobody "fixes" them back:
197
+
198
+ | | The file | Here | Why |
199
+ | --- | --- | --- | --- |
200
+ | `big` width | 800px (`Popup old/Large`) | 1080px | What Popup v4 shipped, and the screens are built against it. |
201
+ | Corner | 8px | `--radius-24` | Decided after the frame was drawn. |
202
+ | Shadow | `Shadow/Middle` | `--shadow-big` | The token for large containers standing over an overlay. |
203
+
204
+ ## From Popup
205
+
206
+ `@propellerads/popup` is the same window with a different shape. The move is a
207
+ rename of most props:
208
+
209
+ | Popup v4 | Dialog | Note |
210
+ | --- | --- | --- |
211
+ | `header` | `title` | The heading is a heading now — `<h2>`, read out as the dialog's name. |
212
+ | `children` | `children` | Unchanged. |
213
+ | `footer` | `footer` | Unchanged. |
214
+ | `size` | `width` | Same three sizes under the same names, plus `minimum`. |
215
+ | `closePopupHandler` | `onClose` | Takes no argument: it is only ever called with `false`. |
216
+ | `closePopupHandler={undefined}` | `isDismissable={false}` | No cross meant no way to close it. Now that is one decision, and it takes Escape and the backdrop with it. |
217
+ | `wrapperHandler` | `isDismissable` / `onClose` | A click on the backdrop closes the dialog; `isDismissable={false}` makes it do nothing. |
218
+ | `theme` | — | Dropped. The backdrop follows the page's theme through the tokens. |
219
+ | `popupCustomStyles` | `className` | A class, or one of the custom properties above. |
220
+ | — | `isOpen` | Popup was mounted to be shown; the dialog is told. |
221
+
222
+ Popup rendered only when it was mounted, so the calling code carried an
223
+ `{isVisible && <Popup />}`. The dialog takes `isOpen` and handles its own mounting,
224
+ which is what gives it the closing animation, the focus return and the scroll
225
+ lock.
package/dist/index.css ADDED
@@ -0,0 +1 @@
1
+ .dialog__backdrop{--dialog-backdrop-blur:4px;z-index:var(--dialog-layer,3000);background-color:var(--dialog-backdrop,var(--over-fullscreen));-webkit-backdrop-filter:blur(var(--dialog-backdrop-blur));backdrop-filter:blur(var(--dialog-backdrop-blur));transition:opacity var(--duration-100) var(--ease-flat);position:fixed;inset:0}.dialog__viewport{z-index:var(--dialog-layer,3000);padding:var(--space-24);overscroll-behavior:contain;justify-content:center;align-items:center;display:flex;position:fixed;inset:0;overflow:auto}.dialog{--dialog-bg:var(--color-light-inversion);--dialog-fg:var(--color-dark-inversion);--dialog-padding:var(--space-24);--dialog-gap:var(--space-24);--dialog-radius:var(--radius-24);--dialog-shadow:var(--shadow-big);--dialog-width:296px;box-sizing:border-box;gap:var(--dialog-gap);width:min(var(--dialog-width), 100%);max-height:100%;padding:var(--dialog-padding);border-radius:var(--dialog-radius);background-color:var(--dialog-bg);box-shadow:var(--dialog-shadow);color:var(--dialog-fg);font:var(--font-text);font-family:var(--font-family);--dialog-ease:var(--ease-enter);transition:opacity var(--duration-100) var(--ease-flat), transform var(--duration-100) var(--dialog-ease);outline:none;flex-direction:column;display:flex;position:relative}.dialog--minimum{--dialog-width:296px}.dialog--small{--dialog-width:400px}.dialog--medium{--dialog-width:600px}.dialog--big{--dialog-width:1080px}.dialog__header{align-items:flex-start;gap:var(--space-8);display:flex}.dialog__title{min-width:0;margin:var(--space-0);font:var(--font-h2);font-family:var(--font-family);word-break:break-word;flex:auto}.dialog__header:has(.dialog__close) .dialog__title{padding-block-start:calc(var(--space-4) + var(--space-2))}.dialog__close.button{--button-accent:var(--color-quiet);flex:none}.dialog__close svg{width:var(--space-24);height:var(--space-24);fill:currentColor;display:block}.dialog__content{margin-inline:calc(-1 * var(--space-4));padding-inline:var(--space-4);overscroll-behavior:contain;word-break:break-word;flex:0 auto;overflow-y:auto}.dialog__footer{justify-content:flex-end;align-items:center;gap:var(--space-12);flex-wrap:wrap;flex:none;display:flex}.dialog__backdrop[data-starting-style],.dialog__backdrop[data-ending-style]{opacity:0}.dialog[data-starting-style],.dialog[data-ending-style]{opacity:0;transform:scale(.95)}.dialog[data-ending-style]{--dialog-ease:var(--ease-exit)}@media (width<=480px){.dialog{--dialog-padding:var(--space-16);--dialog-gap:var(--space-16);--dialog-width:100%}.dialog__viewport{padding:var(--space-16)}}
@@ -0,0 +1,102 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Dialog as Dialog$1 } from '@base-ui-components/react/dialog';
3
+ import { ReactNode, ReactElement, ComponentPropsWithoutRef, Ref } from 'react';
4
+ import * as class_variance_authority_types from 'class-variance-authority/types';
5
+
6
+ /**
7
+ * How wide the box is allowed to grow. It is a maximum, not a width: the dialog
8
+ * shrinks with the viewport, and below the mobile breakpoint every step is the
9
+ * same — the screen decides.
10
+ *
11
+ * `Minimum` is the design file's own size and the default. The three above it
12
+ * carry the sizes v4 called `small`, `medium` and `big`, so a popup that names
13
+ * its size keeps it through the move.
14
+ */
15
+ declare const DialogWidth: {
16
+ readonly Minimum: "minimum";
17
+ readonly Small: "small";
18
+ readonly Medium: "medium";
19
+ readonly Big: "big";
20
+ };
21
+ type DialogWidth = (typeof DialogWidth)[keyof typeof DialogWidth];
22
+ type DialogPopupProps = ComponentPropsWithoutRef<typeof Dialog$1.Popup>;
23
+ type DialogProps = {
24
+ /** Whether the dialog is open. Leave it out for an uncontrolled dialog driven by `trigger`. */
25
+ isOpen?: boolean;
26
+ /** The open state an uncontrolled dialog starts in. @default false */
27
+ defaultOpen?: boolean;
28
+ /** Called whenever the dialog opens or closes, whoever asked for it. */
29
+ onOpenChange?: (isOpen: boolean) => void;
30
+ /**
31
+ * Called when the dialog asks to be closed — the close button, the backdrop,
32
+ * Escape. The shorthand for `onOpenChange` when only the closing matters.
33
+ */
34
+ onClose?: () => void;
35
+ /**
36
+ * The heading. Read out as the dialog's accessible name, so a dialog without
37
+ * one has to carry an `aria-label` instead.
38
+ */
39
+ title?: ReactNode;
40
+ /** The row at the bottom. Buttons go here, and they end at the right edge. */
41
+ footer?: ReactNode;
42
+ /** What the dialog is about. */
43
+ children?: ReactNode;
44
+ /** How wide the box may grow. @default 'minimum' */
45
+ width?: DialogWidth;
46
+ /**
47
+ * Whether the dialog can be dismissed at all — the cross, Escape and the
48
+ * backdrop are one decision, not three. Turn it off for a choice that has to be
49
+ * taken: the cross goes with them, so the dialog does not offer a way out it
50
+ * will not honour, and it then closes only through what its own content does.
51
+ * @default true
52
+ */
53
+ isDismissable?: boolean;
54
+ /** The cross's accessible name. @default 'Close' */
55
+ closeLabel?: string;
56
+ /** Replaces the glyph inside the close button. */
57
+ closeIcon?: ReactNode;
58
+ /**
59
+ * What opens the dialog. The element itself becomes the trigger — Base UI's
60
+ * polymorphism, so a `<Button>` here stays a single button rather than being
61
+ * wrapped in one.
62
+ */
63
+ trigger?: ReactElement<Record<string, unknown>>;
64
+ /**
65
+ * Replaces the box with another element — Base UI's polymorphism, in place of
66
+ * Radix's `asChild`. The header, the content and the footer are then inside
67
+ * whatever you pass, which is how a dialog becomes a form:
68
+ * `render={<form onSubmit={save} />}`.
69
+ */
70
+ render?: DialogPopupProps['render'];
71
+ /** Goes on the box. */
72
+ className?: string;
73
+ /** Goes on the backdrop, for a product that dims its own way. */
74
+ backdropClassName?: string;
75
+ /** Where the focus lands when the dialog opens. @default true — the first tabbable element */
76
+ initialFocus?: DialogPopupProps['initialFocus'];
77
+ /** Where the focus returns when it closes. @default true — the trigger */
78
+ finalFocus?: DialogPopupProps['finalFocus'];
79
+ ref?: Ref<HTMLDivElement>;
80
+ /** Required when there is no `title`: a dialog has to say what it is. */
81
+ 'aria-label'?: string;
82
+ };
83
+
84
+ /**
85
+ * The class names the stylesheet defines.
86
+ *
87
+ * Width is the only axis. Everything else about a dialog — the padding, the
88
+ * corner, the shadow — is the same whatever is inside it, and the parts are
89
+ * plain element classes rather than variants.
90
+ *
91
+ * `dialogVariants` is exported so a product that renders Base UI's parts itself
92
+ * can still wear the dialog's look:
93
+ * `<Dialog.Popup className={dialogVariants({width: 'medium'})} />`.
94
+ */
95
+ declare const dialogVariants: (props?: ({
96
+ width?: "minimum" | "small" | "medium" | "big" | null | undefined;
97
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
98
+
99
+ declare const Dialog: ({ isOpen, defaultOpen, onOpenChange, onClose, title, footer, children, width, isDismissable, closeLabel, closeIcon, trigger, render, className, backdropClassName, initialFocus, finalFocus, ref, "aria-label": ariaLabel, }: DialogProps) => react_jsx_runtime.JSX.Element;
100
+
101
+ export { Dialog, DialogWidth, Dialog as default, dialogVariants };
102
+ export type { DialogProps };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import { Dialog as e } from "@base-ui-components/react/dialog";
2
+ import t, { ButtonVariant as n } from "@propellerads/button";
3
+ import { clsx as r } from "clsx";
4
+ import { cva as i } from "class-variance-authority";
5
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
+ import './index.css';//#region src/cn.ts
7
+ var s = (...e) => r(e), c = {
8
+ Minimum: "minimum",
9
+ Small: "small",
10
+ Medium: "medium",
11
+ Big: "big"
12
+ }, l = i("dialog", {
13
+ variants: { width: {
14
+ minimum: "dialog--minimum",
15
+ small: "dialog--small",
16
+ medium: "dialog--medium",
17
+ big: "dialog--big"
18
+ } },
19
+ defaultVariants: { width: "minimum" }
20
+ }), u = () => /* @__PURE__ */ a("svg", {
21
+ viewBox: "0 0 24 24",
22
+ "aria-hidden": "true",
23
+ focusable: "false",
24
+ children: /* @__PURE__ */ a("path", { d: "M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z" })
25
+ }), d = ({ isOpen: r, defaultOpen: i, onOpenChange: d, onClose: f, title: p, footer: m, children: h, width: g = c.Minimum, isDismissable: _ = !0, closeLabel: v = "Close", closeIcon: y, trigger: b, render: x, className: S, backdropClassName: C, initialFocus: w, finalFocus: T, ref: E, "aria-label": D }) => {
26
+ let O = (e, t) => {
27
+ if (!e && !_ && t.reason !== "close-press") {
28
+ t.cancel();
29
+ return;
30
+ }
31
+ d?.(e), e || f?.();
32
+ }, k = p !== void 0 || _;
33
+ return /* @__PURE__ */ o(e.Root, {
34
+ open: r,
35
+ defaultOpen: i,
36
+ disablePointerDismissal: !_,
37
+ onOpenChange: O,
38
+ children: [b !== void 0 && /* @__PURE__ */ a(e.Trigger, { render: b }), /* @__PURE__ */ o(e.Portal, { children: [/* @__PURE__ */ a(e.Backdrop, { className: s("dialog__backdrop", C) }), /* @__PURE__ */ a(e.Viewport, {
39
+ className: "dialog__viewport",
40
+ children: /* @__PURE__ */ o(e.Popup, {
41
+ ref: E,
42
+ render: x,
43
+ className: s(l({ width: g }), S),
44
+ initialFocus: w,
45
+ finalFocus: T,
46
+ "aria-label": p === void 0 ? D : void 0,
47
+ children: [
48
+ k && /* @__PURE__ */ o("header", {
49
+ className: "dialog__header",
50
+ children: [p !== void 0 && /* @__PURE__ */ a(e.Title, {
51
+ className: "dialog__title",
52
+ children: p
53
+ }), _ && /* @__PURE__ */ a(e.Close, { render: /* @__PURE__ */ a(t, {
54
+ variant: n.Ghost,
55
+ className: "dialog__close",
56
+ icon: y ?? /* @__PURE__ */ a(u, {}),
57
+ "aria-label": v
58
+ }) })]
59
+ }),
60
+ h !== void 0 && /* @__PURE__ */ a("div", {
61
+ className: "dialog__content",
62
+ children: h
63
+ }),
64
+ m !== void 0 && /* @__PURE__ */ a("footer", {
65
+ className: "dialog__footer",
66
+ children: m
67
+ })
68
+ ]
69
+ })
70
+ })] })]
71
+ });
72
+ };
73
+ //#endregion
74
+ export { d as Dialog, d as default, c as DialogWidth, l as dialogVariants };
75
+
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/cn.ts","../src/types.ts","../src/variants.ts","../src/index.tsx"],"sourcesContent":["import {type ClassValue, clsx} from 'clsx';\n\n/**\n * Joins class names. There is no tailwind-merge here on purpose: the dialog ships\n * its own stylesheet instead of utility classes, so there is nothing to\n * deduplicate.\n */\nconst cn = (...inputs: ClassValue[]): string => clsx(inputs);\n\nexport {cn};\n","import type {Dialog} from '@base-ui-components/react/dialog';\nimport type {ComponentPropsWithoutRef, ReactElement, ReactNode, Ref} from 'react';\n\n/**\n * How wide the box is allowed to grow. It is a maximum, not a width: the dialog\n * shrinks with the viewport, and below the mobile breakpoint every step is the\n * same — the screen decides.\n *\n * `Minimum` is the design file's own size and the default. The three above it\n * carry the sizes v4 called `small`, `medium` and `big`, so a popup that names\n * its size keeps it through the move.\n */\nexport const DialogWidth = {\n Minimum: 'minimum',\n Small: 'small',\n Medium: 'medium',\n Big: 'big',\n} as const;\nexport type DialogWidth = (typeof DialogWidth)[keyof typeof DialogWidth];\n\ntype DialogPopupProps = ComponentPropsWithoutRef<typeof Dialog.Popup>;\n\nexport type DialogProps = {\n /** Whether the dialog is open. Leave it out for an uncontrolled dialog driven by `trigger`. */\n isOpen?: boolean;\n /** The open state an uncontrolled dialog starts in. @default false */\n defaultOpen?: boolean;\n /** Called whenever the dialog opens or closes, whoever asked for it. */\n onOpenChange?: (isOpen: boolean) => void;\n /**\n * Called when the dialog asks to be closed — the close button, the backdrop,\n * Escape. The shorthand for `onOpenChange` when only the closing matters.\n */\n onClose?: () => void;\n /**\n * The heading. Read out as the dialog's accessible name, so a dialog without\n * one has to carry an `aria-label` instead.\n */\n title?: ReactNode;\n /** The row at the bottom. Buttons go here, and they end at the right edge. */\n footer?: ReactNode;\n /** What the dialog is about. */\n children?: ReactNode;\n /** How wide the box may grow. @default 'minimum' */\n width?: DialogWidth;\n /**\n * Whether the dialog can be dismissed at all — the cross, Escape and the\n * backdrop are one decision, not three. Turn it off for a choice that has to be\n * taken: the cross goes with them, so the dialog does not offer a way out it\n * will not honour, and it then closes only through what its own content does.\n * @default true\n */\n isDismissable?: boolean;\n /** The cross's accessible name. @default 'Close' */\n closeLabel?: string;\n /** Replaces the glyph inside the close button. */\n closeIcon?: ReactNode;\n /**\n * What opens the dialog. The element itself becomes the trigger — Base UI's\n * polymorphism, so a `<Button>` here stays a single button rather than being\n * wrapped in one.\n */\n trigger?: ReactElement<Record<string, unknown>>;\n /**\n * Replaces the box with another element — Base UI's polymorphism, in place of\n * Radix's `asChild`. The header, the content and the footer are then inside\n * whatever you pass, which is how a dialog becomes a form:\n * `render={<form onSubmit={save} />}`.\n */\n render?: DialogPopupProps['render'];\n /** Goes on the box. */\n className?: string;\n /** Goes on the backdrop, for a product that dims its own way. */\n backdropClassName?: string;\n /** Where the focus lands when the dialog opens. @default true — the first tabbable element */\n initialFocus?: DialogPopupProps['initialFocus'];\n /** Where the focus returns when it closes. @default true — the trigger */\n finalFocus?: DialogPopupProps['finalFocus'];\n ref?: Ref<HTMLDivElement>;\n /** Required when there is no `title`: a dialog has to say what it is. */\n 'aria-label'?: string;\n};\n","import {cva} from 'class-variance-authority';\n\n/**\n * The class names the stylesheet defines.\n *\n * Width is the only axis. Everything else about a dialog — the padding, the\n * corner, the shadow — is the same whatever is inside it, and the parts are\n * plain element classes rather than variants.\n *\n * `dialogVariants` is exported so a product that renders Base UI's parts itself\n * can still wear the dialog's look:\n * `<Dialog.Popup className={dialogVariants({width: 'medium'})} />`.\n */\nconst dialogVariants = cva('dialog', {\n variants: {\n width: {\n minimum: 'dialog--minimum',\n small: 'dialog--small',\n medium: 'dialog--medium',\n big: 'dialog--big',\n },\n },\n defaultVariants: {\n width: 'minimum',\n },\n});\n\nexport {dialogVariants};\n","import {Dialog as BaseDialog} from '@base-ui-components/react/dialog';\nimport Button, {ButtonVariant} from '@propellerads/button';\nimport './dialog.css';\nimport {cn} from './cn';\nimport {type DialogProps, DialogWidth} from './types';\nimport {dialogVariants} from './variants';\n\n/** The same glyph @propellerads/icon exports as `Close`, inlined so the dialog\n * does not pull styled-components in for one cross. `closeIcon` replaces it. */\nconst CloseGlyph = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z\" />\n </svg>\n);\n\nconst Dialog = ({\n isOpen,\n defaultOpen,\n onOpenChange,\n onClose,\n title,\n footer,\n children,\n width = DialogWidth.Minimum,\n isDismissable = true,\n closeLabel = 'Close',\n closeIcon,\n trigger,\n render,\n className,\n backdropClassName,\n initialFocus,\n finalFocus,\n ref,\n 'aria-label': ariaLabel,\n}: DialogProps) => {\n const handleOpenChange = (open: boolean, details: BaseDialog.Root.ChangeEventDetails): void => {\n // A dialog that is not dismissable closes through what it renders itself and\n // nothing else: the backdrop is already handled by `disablePointerDismissal`,\n // and this turns away Escape and anything else that asks from the outside.\n if (!open && !isDismissable && details.reason !== 'close-press') {\n details.cancel();\n return;\n }\n\n onOpenChange?.(open);\n\n if (!open) {\n onClose?.();\n }\n };\n\n // The cross is the visible half of being dismissable.\n const hasHeader = title !== undefined || isDismissable;\n\n return (\n <BaseDialog.Root\n open={isOpen}\n defaultOpen={defaultOpen}\n disablePointerDismissal={!isDismissable}\n onOpenChange={handleOpenChange}\n >\n {trigger !== undefined && <BaseDialog.Trigger render={trigger} />}\n <BaseDialog.Portal>\n <BaseDialog.Backdrop className={cn('dialog__backdrop', backdropClassName)} />\n <BaseDialog.Viewport className=\"dialog__viewport\">\n <BaseDialog.Popup\n ref={ref}\n render={render}\n className={cn(dialogVariants({width}), className)}\n initialFocus={initialFocus}\n finalFocus={finalFocus}\n aria-label={title === undefined ? ariaLabel : undefined}\n >\n {hasHeader && (\n <header className=\"dialog__header\">\n {title !== undefined && <BaseDialog.Title className=\"dialog__title\">{title}</BaseDialog.Title>}\n {isDismissable && (\n // The cross is a ghost icon button like any other, repainted by\n // the class: it is the quietest thing in the dialog, and the\n // action colour belongs to the footer.\n <BaseDialog.Close\n render={\n <Button\n variant={ButtonVariant.Ghost}\n className=\"dialog__close\"\n icon={closeIcon ?? <CloseGlyph />}\n aria-label={closeLabel}\n />\n }\n />\n )}\n </header>\n )}\n {children !== undefined && <div className=\"dialog__content\">{children}</div>}\n {footer !== undefined && <footer className=\"dialog__footer\">{footer}</footer>}\n </BaseDialog.Popup>\n </BaseDialog.Viewport>\n </BaseDialog.Portal>\n </BaseDialog.Root>\n );\n};\n\nexport default Dialog;\n\nexport type {DialogProps};\nexport {Dialog, DialogWidth, dialogVariants};\n"],"mappings":";;;;;;AAOA,IAAM,KAAM,GAAG,MAAiC,EAAK,EAAO,ECK/C,IAAc;CACzB,SAAS;CACT,OAAO;CACP,QAAQ;CACR,KAAK;CACN,ECJK,IAAiB,EAAI,UAAU;CACnC,UAAU,EACR,OAAO;EACL,SAAS;EACT,OAAO;EACP,QAAQ;EACR,KAAK;EACN,EACF;CACD,iBAAiB,EACf,OAAO,WACR;CACF,CAAC,EChBI,UACJ,kBAAC,OAAD;CAAK,SAAQ;CAAY,eAAY;CAAO,WAAU;WACpD,kBAAC,QAAD,EAAM,GAAE,mRAAoR,CAAA;CACxR,CAAA,EAGF,KAAU,EACd,WACA,gBACA,iBACA,YACA,UACA,WACA,aACA,WAAQ,EAAY,SACpB,mBAAgB,IAChB,gBAAa,SACb,cACA,YACA,WACA,cACA,sBACA,iBACA,eACA,QACA,cAAc,QACG;CACjB,IAAM,KAAoB,GAAe,MAAsD;EAI7F,IAAI,CAAC,KAAQ,CAAC,KAAiB,EAAQ,WAAW,eAAe;GAC/D,EAAQ,QAAQ;GAChB;;EAKF,AAFA,IAAe,EAAK,EAEf,KACH,KAAW;IAKT,IAAY,MAAU,KAAA,KAAa;CAEzC,OACE,kBAAC,EAAW,MAAZ;EACE,MAAM;EACO;EACb,yBAAyB,CAAC;EAC1B,cAAc;YAJhB,CAMG,MAAY,KAAA,KAAa,kBAAC,EAAW,SAAZ,EAAoB,QAAQ,GAAW,CAAA,EACjE,kBAAC,EAAW,QAAZ,EAAA,UAAA,CACE,kBAAC,EAAW,UAAZ,EAAqB,WAAW,EAAG,oBAAoB,EAAkB,EAAI,CAAA,EAC7E,kBAAC,EAAW,UAAZ;GAAqB,WAAU;aAC7B,kBAAC,EAAW,OAAZ;IACO;IACG;IACR,WAAW,EAAG,EAAe,EAAC,UAAM,CAAC,EAAE,EAAU;IACnC;IACF;IACZ,cAAY,MAAU,KAAA,IAAY,IAAY,KAAA;cANhD;KAQG,KACC,kBAAC,UAAD;MAAQ,WAAU;gBAAlB,CACG,MAAU,KAAA,KAAa,kBAAC,EAAW,OAAZ;OAAkB,WAAU;iBAAiB;OAAyB,CAAA,EAC7F,KAIC,kBAAC,EAAW,OAAZ,EACE,QACE,kBAAC,GAAD;OACE,SAAS,EAAc;OACvB,WAAU;OACV,MAAM,KAAa,kBAAC,GAAD,EAAc,CAAA;OACjC,cAAY;OACZ,CAAA,EAEJ,CAAA,CAEG;;KAEV,MAAa,KAAA,KAAa,kBAAC,OAAD;MAAK,WAAU;MAAmB;MAAe,CAAA;KAC3E,MAAW,KAAA,KAAa,kBAAC,UAAD;MAAQ,WAAU;gBAAkB;MAAgB,CAAA;KAC5D;;GACC,CAAA,CACJ,EAAA,CAAA,CACJ"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@propellerads/dialog",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "types": "dist/index.d.ts",
6
+ "style": "dist/index.css",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./index.css": "./dist/index.css"
13
+ },
14
+ "sideEffects": [
15
+ "**/*.css"
16
+ ],
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "repository": "https://github.com/propellerads/ui-components.git",
21
+ "author": "v.bykov@propellerads.net",
22
+ "license": "MIT",
23
+ "bugs": {
24
+ "url": "https://github.com/propellerads/ui-components/issues"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc && vite build && bunx rollup -c rollup.dts.config.mjs"
28
+ },
29
+ "homepage": "https://github.com/propellerads/ui-components#readme",
30
+ "private": false,
31
+ "dependencies": {
32
+ "@propellerads/button": "^11.0.0",
33
+ "@base-ui-components/react": "1.0.0-rc.0",
34
+ "class-variance-authority": "^0.7.1",
35
+ "clsx": "^2.1.1"
36
+ },
37
+ "peerDependencies": {
38
+ "@propellerads/tokens": ">=0.0.1",
39
+ "react": "^18.0.0 || ^19.0.0",
40
+ "react-dom": "^18.0.0 || ^19.0.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@propellerads/tokens": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@propellerads/tokens": "workspace:^",
49
+ "@types/react": "^19.2.14",
50
+ "@vitejs/plugin-react": "^6.0.1",
51
+ "rollup": "^4.60.3",
52
+ "rollup-plugin-dts": "^6.4.1",
53
+ "typescript": "^6.0.3",
54
+ "vite": "^8.0.10",
55
+ "vite-plugin-lib-inject-css": "^2.2.2"
56
+ }
57
+ }