gbs-add-block 1.2.13 → 1.2.15

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.
Files changed (105) hide show
  1. package/README.md +4 -12
  2. package/index.cjs +9 -0
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/README.md +34 -0
  5. package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
  6. package/source/beta-components/breadcrumb/core/index.ts +4 -0
  7. package/source/beta-components/breadcrumb/core/trail.ts +42 -0
  8. package/source/beta-components/breadcrumb/core/types.ts +20 -0
  9. package/source/beta-components/breadcrumb/index.ts +5 -0
  10. package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
  11. package/source/beta-components/breadcrumb/react/locale.ts +6 -0
  12. package/source/beta-components/breadcrumb/react/props.ts +14 -0
  13. package/source/beta-components/breadcrumb/styles.css +139 -0
  14. package/source/beta-components/button/README.md +32 -0
  15. package/source/beta-components/button/__tests__/core.test.ts +31 -0
  16. package/source/beta-components/button/core/index.ts +3 -0
  17. package/source/beta-components/button/core/state.ts +16 -0
  18. package/source/beta-components/button/core/types.ts +28 -0
  19. package/source/beta-components/button/index.ts +5 -0
  20. package/source/beta-components/button/react/Button.tsx +173 -0
  21. package/source/beta-components/button/react/locale.ts +5 -0
  22. package/source/beta-components/button/react/props.ts +23 -0
  23. package/source/beta-components/button/styles.css +233 -0
  24. package/source/beta-components/checkbox/README.md +36 -0
  25. package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
  26. package/source/beta-components/checkbox/core/group.ts +30 -0
  27. package/source/beta-components/checkbox/core/index.ts +3 -0
  28. package/source/beta-components/checkbox/core/types.ts +17 -0
  29. package/source/beta-components/checkbox/index.ts +9 -0
  30. package/source/beta-components/checkbox/react/Checkbox.tsx +146 -0
  31. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
  32. package/source/beta-components/checkbox/react/context.ts +16 -0
  33. package/source/beta-components/checkbox/react/locale.ts +5 -0
  34. package/source/beta-components/checkbox/react/props.ts +6 -0
  35. package/source/beta-components/checkbox/styles.css +212 -0
  36. package/source/beta-components/dialog/README.md +39 -0
  37. package/source/beta-components/dialog/__tests__/core.test.ts +86 -0
  38. package/source/beta-components/dialog/core/api.ts +34 -0
  39. package/source/beta-components/dialog/core/dialog.ts +11 -0
  40. package/source/beta-components/dialog/core/index.ts +7 -0
  41. package/source/beta-components/dialog/core/store.ts +81 -0
  42. package/source/beta-components/dialog/core/types.ts +59 -0
  43. package/source/beta-components/dialog/index.ts +7 -0
  44. package/source/beta-components/dialog/react/Dialog.tsx +279 -0
  45. package/source/beta-components/dialog/react/DialogHost.tsx +45 -0
  46. package/source/beta-components/dialog/react/icons.tsx +51 -0
  47. package/source/beta-components/dialog/react/locale.ts +8 -0
  48. package/source/beta-components/dialog/react/props.ts +13 -0
  49. package/source/beta-components/dialog/styles.css +278 -0
  50. package/source/beta-components/input/README.md +44 -0
  51. package/source/beta-components/input/__tests__/core.test.ts +75 -0
  52. package/source/beta-components/input/core/count.ts +14 -0
  53. package/source/beta-components/input/core/index.ts +11 -0
  54. package/source/beta-components/input/core/otp.ts +64 -0
  55. package/source/beta-components/input/core/types.ts +14 -0
  56. package/source/beta-components/input/index.ts +8 -0
  57. package/source/beta-components/input/react/Input.tsx +219 -0
  58. package/source/beta-components/input/react/OtpInput.tsx +256 -0
  59. package/source/beta-components/input/react/dom.ts +10 -0
  60. package/source/beta-components/input/react/icons.tsx +35 -0
  61. package/source/beta-components/input/react/locale.ts +9 -0
  62. package/source/beta-components/input/react/props.ts +6 -0
  63. package/source/beta-components/input/styles.css +296 -0
  64. package/source/beta-components/modal/README.md +51 -0
  65. package/source/beta-components/modal/__tests__/core.test.ts +55 -0
  66. package/source/beta-components/modal/core/dismiss.ts +29 -0
  67. package/source/beta-components/modal/core/index.ts +3 -0
  68. package/source/beta-components/modal/core/types.ts +27 -0
  69. package/source/beta-components/modal/index.ts +5 -0
  70. package/source/beta-components/modal/react/Modal.tsx +238 -0
  71. package/source/beta-components/modal/react/icons.tsx +19 -0
  72. package/source/beta-components/modal/react/locale.ts +5 -0
  73. package/source/beta-components/modal/react/props.ts +17 -0
  74. package/source/beta-components/modal/styles.css +235 -0
  75. package/source/beta-components/spinner/README.md +29 -0
  76. package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
  77. package/source/beta-components/spinner/core/index.ts +3 -0
  78. package/source/beta-components/spinner/core/types.ts +28 -0
  79. package/source/beta-components/spinner/core/visibility.ts +34 -0
  80. package/source/beta-components/spinner/index.ts +6 -0
  81. package/source/beta-components/spinner/react/Spinner.tsx +96 -0
  82. package/source/beta-components/spinner/react/locale.ts +5 -0
  83. package/source/beta-components/spinner/react/props.ts +4 -0
  84. package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
  85. package/source/beta-components/spinner/styles.css +167 -0
  86. package/source/beta-components/tabs/README.md +32 -0
  87. package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
  88. package/source/beta-components/tabs/core/index.ts +3 -0
  89. package/source/beta-components/tabs/core/navigation.ts +54 -0
  90. package/source/beta-components/tabs/core/types.ts +16 -0
  91. package/source/beta-components/tabs/index.ts +5 -0
  92. package/source/beta-components/tabs/react/Tabs.tsx +260 -0
  93. package/source/beta-components/tabs/react/context.ts +21 -0
  94. package/source/beta-components/tabs/styles.css +257 -0
  95. package/source/beta-components/textarea/README.md +39 -0
  96. package/source/beta-components/textarea/__tests__/core.test.ts +38 -0
  97. package/source/beta-components/textarea/core/count.ts +14 -0
  98. package/source/beta-components/textarea/core/index.ts +4 -0
  99. package/source/beta-components/textarea/core/size.ts +23 -0
  100. package/source/beta-components/textarea/core/types.ts +17 -0
  101. package/source/beta-components/textarea/index.ts +5 -0
  102. package/source/beta-components/textarea/react/Textarea.tsx +209 -0
  103. package/source/beta-components/textarea/react/locale.ts +5 -0
  104. package/source/beta-components/textarea/react/props.ts +4 -0
  105. package/source/beta-components/textarea/styles.css +159 -0
@@ -0,0 +1,11 @@
1
+ import { createDialogApi } from "./api";
2
+ import { createDialogStore } from "./store";
3
+
4
+ /** The store behind `dialog`, and behind any `<DialogHost />` without a `store` prop. */
5
+ export const dialogStore = createDialogStore();
6
+
7
+ /**
8
+ * Ask the user something from anywhere and `await` the answer. Where there is
9
+ * no document (a server render), calls settle at once as a cancel.
10
+ */
11
+ export const dialog = createDialogApi(dialogStore, () => typeof document !== "undefined");
@@ -0,0 +1,7 @@
1
+ // Framework-free: the dialog queue and the promise API run without React.
2
+ export { createDialogApi } from "./api";
3
+ export type { DialogApi } from "./api";
4
+ export { createDialogStore, resultFor } from "./store";
5
+ export type { DialogStore } from "./store";
6
+ export { dialog, dialogStore } from "./dialog";
7
+ export type * from "./types";
@@ -0,0 +1,81 @@
1
+ import type {
2
+ AlertOptions,
3
+ ConfirmOptions,
4
+ DialogKind,
5
+ DialogRequest,
6
+ DialogSnapshot,
7
+ PromptOptions,
8
+ } from "./types";
9
+
10
+ export interface DialogStore {
11
+ subscribe(listener: () => void): () => void;
12
+ getSnapshot(): DialogSnapshot;
13
+ getServerSnapshot(): DialogSnapshot;
14
+ open(kind: "alert", options: AlertOptions): Promise<void>;
15
+ open(kind: "confirm", options: ConfirmOptions): Promise<boolean>;
16
+ open(kind: "prompt", options: PromptOptions): Promise<string | null>;
17
+ /** Closes a request and settles its promise. */
18
+ resolve(id: string, confirmed: boolean, value?: string): void;
19
+ /** Cancels every open and waiting dialog. */
20
+ dismissAll(): void;
21
+ }
22
+
23
+ const EMPTY: DialogSnapshot = { queue: [] };
24
+
25
+ /** What each kind's promise resolves with. */
26
+ export function resultFor(kind: DialogKind, confirmed: boolean, value = ""): boolean | string | null | undefined {
27
+ if (kind === "confirm") return confirmed;
28
+ if (kind === "prompt") return confirmed ? value : null;
29
+ return undefined;
30
+ }
31
+
32
+ /**
33
+ * A queue of dialogs, one on screen at a time, with no React. Each request is a
34
+ * promise that settles when the user answers, so callers can simply `await` it.
35
+ */
36
+ export function createDialogStore(): DialogStore {
37
+ let snapshot: DialogSnapshot = EMPTY;
38
+ let counter = 0;
39
+ const listeners = new Set<() => void>();
40
+ const settlers = new Map<string, (result: unknown) => void>();
41
+
42
+ function commit(queue: readonly DialogRequest[]) {
43
+ snapshot = { queue };
44
+ for (const listener of listeners) listener();
45
+ }
46
+
47
+ function open(kind: DialogKind, options: AlertOptions | ConfirmOptions | PromptOptions) {
48
+ const id = `dialog-${++counter}`;
49
+ return new Promise<unknown>((resolve) => {
50
+ settlers.set(id, resolve);
51
+ commit([...snapshot.queue, { id, kind, options } as DialogRequest]);
52
+ });
53
+ }
54
+
55
+ function resolve(id: string, confirmed: boolean, value?: string) {
56
+ const request = snapshot.queue.find((item) => item.id === id);
57
+ const settle = settlers.get(id);
58
+ if (!request || !settle) return;
59
+ settlers.delete(id);
60
+ commit(snapshot.queue.filter((item) => item.id !== id));
61
+ settle(resultFor(request.kind, confirmed, value));
62
+ }
63
+
64
+ function dismissAll() {
65
+ for (const request of snapshot.queue) resolve(request.id, false);
66
+ }
67
+
68
+ return {
69
+ subscribe(listener) {
70
+ listeners.add(listener);
71
+ return () => {
72
+ listeners.delete(listener);
73
+ };
74
+ },
75
+ getSnapshot: () => snapshot,
76
+ getServerSnapshot: () => EMPTY,
77
+ open: open as DialogStore["open"],
78
+ resolve,
79
+ dismissAll,
80
+ };
81
+ }
@@ -0,0 +1,59 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type DialogKind = "alert" | "confirm" | "prompt";
4
+ export type DialogIntent = "default" | "info" | "success" | "warning" | "danger";
5
+ export type DialogSize = "sm" | "md";
6
+
7
+ export interface DialogBaseOptions {
8
+ title: ReactNode;
9
+ description?: ReactNode;
10
+ /** Sets the icon and the confirm button's color. `danger` also focuses Cancel first. */
11
+ intent?: DialogIntent;
12
+ /** Replaces the intent's icon; `null` hides it. */
13
+ icon?: ReactNode;
14
+ confirmLabel?: ReactNode;
15
+ /** Escape and a backdrop click count as Cancel. Default true. */
16
+ dismissible?: boolean;
17
+ size?: DialogSize;
18
+ }
19
+
20
+ export interface AlertOptions extends DialogBaseOptions {
21
+ /** Runs on OK. The dialog waits while it runs and shows its error if it throws. */
22
+ onConfirm?(): void | Promise<void>;
23
+ }
24
+
25
+ export interface ConfirmOptions extends DialogBaseOptions {
26
+ cancelLabel?: ReactNode;
27
+ /** Runs on Confirm. The dialog waits while it runs and shows its error if it throws. */
28
+ onConfirm?(): void | Promise<void>;
29
+ }
30
+
31
+ export interface PromptOptions extends DialogBaseOptions {
32
+ cancelLabel?: ReactNode;
33
+ defaultValue?: string;
34
+ placeholder?: string;
35
+ inputLabel?: ReactNode;
36
+ inputType?: "text" | "email" | "password" | "number" | "url" | "tel";
37
+ required?: boolean;
38
+ /** Return a message to block confirming. */
39
+ validate?(value: string): string | null | undefined;
40
+ /** Runs with the value on Confirm. The dialog waits while it runs and shows its error if it throws. */
41
+ onConfirm?(value: string): void | Promise<void>;
42
+ }
43
+
44
+ export type DialogRequest =
45
+ | { id: string; kind: "alert"; options: AlertOptions }
46
+ | { id: string; kind: "confirm"; options: ConfirmOptions }
47
+ | { id: string; kind: "prompt"; options: PromptOptions };
48
+
49
+ export interface DialogSnapshot {
50
+ /** The first request is on screen; the rest wait their turn. */
51
+ queue: readonly DialogRequest[];
52
+ }
53
+
54
+ export interface DialogLocaleText {
55
+ ok: string;
56
+ confirm: string;
57
+ cancel: string;
58
+ required: string;
59
+ }
@@ -0,0 +1,7 @@
1
+ export { Dialog } from "./react/Dialog";
2
+ export type { DialogAction, DialogProps } from "./react/Dialog";
3
+ export { DialogHost } from "./react/DialogHost";
4
+ export type { DialogHostProps } from "./react/DialogHost";
5
+ export { defaultDialogText } from "./react/locale";
6
+ export type { DialogSlot } from "./react/props";
7
+ export * from "./core";
@@ -0,0 +1,279 @@
1
+ "use client";
2
+
3
+ import {
4
+ useEffect,
5
+ useEffectEvent,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type CSSProperties,
11
+ type MouseEvent,
12
+ type ReactNode,
13
+ } from "react";
14
+ import type {
15
+ DialogIntent,
16
+ DialogKind,
17
+ DialogLocaleText,
18
+ PromptOptions,
19
+ } from "../core/types";
20
+ import { DangerIcon, InfoIcon, SpinnerIcon, SuccessIcon, WarningIcon } from "./icons";
21
+ import { defaultDialogText } from "./locale";
22
+ import { cx, type DialogSlot } from "./props";
23
+
24
+ /** Matches the CSS transition, so content stays mounted while the dialog animates out. */
25
+ const EXIT_DURATION = 200;
26
+
27
+ const INTENT_ICONS: Record<DialogIntent, ReactNode> = {
28
+ default: null,
29
+ info: <InfoIcon />,
30
+ success: <SuccessIcon />,
31
+ warning: <WarningIcon />,
32
+ danger: <DangerIcon />,
33
+ };
34
+
35
+ export type DialogAction = "confirm" | "cancel";
36
+
37
+ export interface DialogProps extends Omit<PromptOptions, "onConfirm"> {
38
+ open: boolean;
39
+ /** `alert` has one button, `confirm` two, `prompt` adds a text field. Default `confirm`. */
40
+ kind?: DialogKind;
41
+ /**
42
+ * Runs on Confirm, with the prompt's value. While its promise is pending the
43
+ * buttons show progress; if it throws, the error message is shown and the
44
+ * dialog stays open.
45
+ */
46
+ onConfirm?(value: string): void | Promise<void>;
47
+ /** The user answered. Set `open` to false here. */
48
+ onClose?(action: DialogAction, value: string): void;
49
+ /** The exit animation finished. */
50
+ onExited?(): void;
51
+ id?: string;
52
+ className?: string;
53
+ classNames?: Partial<Record<DialogSlot, string>>;
54
+ style?: CSSProperties;
55
+ localeText?: Partial<DialogLocaleText>;
56
+ }
57
+
58
+ const outside = (event: MouseEvent<HTMLDialogElement>) => {
59
+ if (event.target !== event.currentTarget) return false;
60
+ const rect = event.currentTarget.getBoundingClientRect();
61
+ const { clientX: x, clientY: y } = event;
62
+ return x < rect.left || x > rect.right || y < rect.top || y > rect.bottom;
63
+ };
64
+
65
+ /** An alert, confirm or prompt dialog. For the promise API, mount `<DialogHost />` and call `dialog.confirm()`. */
66
+ export function Dialog(props: DialogProps) {
67
+ const {
68
+ open,
69
+ kind = "confirm",
70
+ title,
71
+ description,
72
+ intent = "default",
73
+ icon,
74
+ confirmLabel,
75
+ cancelLabel,
76
+ dismissible = true,
77
+ size = "sm",
78
+ defaultValue = "",
79
+ placeholder,
80
+ inputLabel,
81
+ inputType = "text",
82
+ required = false,
83
+ validate,
84
+ onConfirm,
85
+ onClose,
86
+ onExited,
87
+ id: idProp,
88
+ className,
89
+ classNames,
90
+ style,
91
+ localeText,
92
+ } = props;
93
+
94
+ const reactId = useId();
95
+ const id = idProp ?? reactId;
96
+ const text = useMemo(() => ({ ...defaultDialogText, ...localeText }), [localeText]);
97
+ const dialogRef = useRef<HTMLDialogElement>(null);
98
+ const pressStartedOutside = useRef(false);
99
+
100
+ const [value, setValue] = useState(defaultValue);
101
+ const [error, setError] = useState<string | null>(null);
102
+ const [pending, setPending] = useState(false);
103
+
104
+ // Reset each time it opens, and keep content mounted through the exit transition.
105
+ const [previousOpen, setPreviousOpen] = useState(open);
106
+ const [closing, setClosing] = useState(false);
107
+ if (previousOpen !== open) {
108
+ setPreviousOpen(open);
109
+ setClosing(!open);
110
+ if (open) {
111
+ setValue(defaultValue);
112
+ setError(null);
113
+ setPending(false);
114
+ }
115
+ }
116
+
117
+ const exited = useEffectEvent(() => onExited?.());
118
+ useEffect(() => {
119
+ if (!closing) return;
120
+ const timer = setTimeout(() => {
121
+ setClosing(false);
122
+ exited();
123
+ }, EXIT_DURATION);
124
+ return () => clearTimeout(timer);
125
+ }, [closing]);
126
+
127
+ useEffect(() => {
128
+ const element = dialogRef.current;
129
+ if (!element) return;
130
+ if (open && !element.open) {
131
+ element.showModal();
132
+ element.querySelector<HTMLElement>("[data-autofocus]")?.focus();
133
+ } else if (!open && element.open) {
134
+ element.close();
135
+ }
136
+ }, [open]);
137
+
138
+ const confirm = async () => {
139
+ if (pending || !open) return;
140
+ if (kind === "prompt") {
141
+ const message = required && !value.trim() ? text.required : validate?.(value);
142
+ if (message) {
143
+ setError(message);
144
+ return;
145
+ }
146
+ }
147
+ if (onConfirm) {
148
+ setPending(true);
149
+ setError(null);
150
+ try {
151
+ await onConfirm(value);
152
+ } catch (reason) {
153
+ setError(reason instanceof Error ? reason.message : String(reason));
154
+ setPending(false);
155
+ return;
156
+ }
157
+ setPending(false);
158
+ }
159
+ onClose?.("confirm", value);
160
+ };
161
+
162
+ const cancel = () => {
163
+ if (pending || !open) return;
164
+ onClose?.("cancel", value);
165
+ };
166
+
167
+ const mounted = open || closing;
168
+ const resolvedIcon = icon !== undefined ? icon : INTENT_ICONS[intent];
169
+ // A destructive confirm starts on Cancel, so a stray Enter doesn't delete anything.
170
+ const focusCancel = kind === "confirm" && intent === "danger";
171
+ const errorId = `${id}-error`;
172
+
173
+ return (
174
+ <dialog
175
+ ref={dialogRef}
176
+ id={id}
177
+ role="alertdialog"
178
+ aria-labelledby={`${id}-title`}
179
+ aria-describedby={description ? `${id}-description` : undefined}
180
+ className={cx("dl-root", classNames?.root, className)}
181
+ style={style}
182
+ data-intent={intent}
183
+ data-size={size}
184
+ data-state={open ? "open" : "closed"}
185
+ onCancel={(event) => {
186
+ event.preventDefault();
187
+ if (dismissible) cancel();
188
+ }}
189
+ onPointerDown={(event) => {
190
+ pressStartedOutside.current = outside(event);
191
+ }}
192
+ onClick={(event) => {
193
+ if (dismissible && pressStartedOutside.current && outside(event)) cancel();
194
+ pressStartedOutside.current = false;
195
+ }}
196
+ >
197
+ {mounted && (
198
+ <form
199
+ className="dl-form"
200
+ noValidate
201
+ onSubmit={(event) => {
202
+ event.preventDefault();
203
+ void confirm();
204
+ }}
205
+ >
206
+ <div className={cx("dl-body", classNames?.body)}>
207
+ {resolvedIcon != null && (
208
+ <span className={cx("dl-icon", classNames?.icon)} aria-hidden="true">
209
+ {resolvedIcon}
210
+ </span>
211
+ )}
212
+ <div className="dl-text">
213
+ <h2 id={`${id}-title`} className={cx("dl-title", classNames?.title)}>
214
+ {title}
215
+ </h2>
216
+ {description && (
217
+ <div id={`${id}-description`} className={cx("dl-description", classNames?.description)}>
218
+ {description}
219
+ </div>
220
+ )}
221
+ {kind === "prompt" && (
222
+ <label className="dl-field">
223
+ {inputLabel && <span className="dl-label">{inputLabel}</span>}
224
+ <input
225
+ className={cx("dl-input", classNames?.input)}
226
+ type={inputType}
227
+ value={value}
228
+ placeholder={placeholder}
229
+ required={required}
230
+ readOnly={pending}
231
+ data-autofocus=""
232
+ aria-invalid={error ? true : undefined}
233
+ aria-describedby={error ? errorId : undefined}
234
+ onChange={(event) => {
235
+ setValue(event.target.value);
236
+ if (error) setError(null);
237
+ }}
238
+ />
239
+ </label>
240
+ )}
241
+ {error && (
242
+ <p id={errorId} className="dl-error" role="alert">
243
+ {error}
244
+ </p>
245
+ )}
246
+ </div>
247
+ </div>
248
+
249
+ <div className={cx("dl-actions", classNames?.actions)}>
250
+ {kind !== "alert" && (
251
+ <button
252
+ type="button"
253
+ className={cx("dl-button", classNames?.cancel)}
254
+ data-variant="secondary"
255
+ data-autofocus={focusCancel ? "" : undefined}
256
+ aria-disabled={pending || undefined}
257
+ onClick={cancel}
258
+ >
259
+ {cancelLabel ?? text.cancel}
260
+ </button>
261
+ )}
262
+ <button
263
+ type="submit"
264
+ className={cx("dl-button", classNames?.confirm)}
265
+ data-variant={intent === "danger" ? "danger" : "primary"}
266
+ data-autofocus={!focusCancel && kind !== "prompt" ? "" : undefined}
267
+ // aria-disabled rather than disabled, so focus stays on the button while it works.
268
+ aria-disabled={pending || undefined}
269
+ aria-busy={pending || undefined}
270
+ >
271
+ {pending && <SpinnerIcon />}
272
+ {confirmLabel ?? (kind === "alert" ? text.ok : text.confirm)}
273
+ </button>
274
+ </div>
275
+ </form>
276
+ )}
277
+ </dialog>
278
+ );
279
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useState, useSyncExternalStore, type CSSProperties } from "react";
4
+ import { dialogStore } from "../core/dialog";
5
+ import type { DialogStore } from "../core/store";
6
+ import type { DialogLocaleText, DialogRequest } from "../core/types";
7
+ import { Dialog } from "./Dialog";
8
+ import type { DialogSlot } from "./props";
9
+
10
+ export interface DialogHostProps {
11
+ /** A store from `createDialogStore()`. Defaults to the one behind `dialog`. */
12
+ store?: DialogStore;
13
+ /** Applied to every dialog this host shows. */
14
+ className?: string;
15
+ classNames?: Partial<Record<DialogSlot, string>>;
16
+ style?: CSSProperties;
17
+ localeText?: Partial<DialogLocaleText>;
18
+ }
19
+
20
+ /** Mount once. Shows the dialogs requested through `dialog.alert/confirm/prompt`, one at a time. */
21
+ export function DialogHost({ store = dialogStore, ...shared }: DialogHostProps) {
22
+ const { queue } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
23
+ const current = queue[0];
24
+
25
+ // Keep the last dialog rendered while it animates out.
26
+ const [shown, setShown] = useState<DialogRequest | undefined>(current);
27
+ if (current && current !== shown) setShown(current);
28
+
29
+ if (!shown) return null;
30
+
31
+ return (
32
+ <Dialog
33
+ key={shown.id}
34
+ {...shown.options}
35
+ {...shared}
36
+ open={shown === current}
37
+ kind={shown.kind}
38
+ onConfirm={shown.options.onConfirm}
39
+ onClose={(action, value) => store.resolve(shown.id, action === "confirm", value)}
40
+ onExited={() => {
41
+ if (store.getSnapshot().queue.length === 0) setShown(undefined);
42
+ }}
43
+ />
44
+ );
45
+ }
@@ -0,0 +1,51 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ type IconProps = SVGProps<SVGSVGElement>;
4
+
5
+ function Svg(props: IconProps) {
6
+ return (
7
+ <svg
8
+ width="20"
9
+ height="20"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ aria-hidden="true"
17
+ focusable="false"
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export const InfoIcon = (p: IconProps) => (
24
+ <Svg {...p}>
25
+ <circle cx="12" cy="12" r="9" />
26
+ <path d="M12 11v5M12 7.5h.01" />
27
+ </Svg>
28
+ );
29
+ export const SuccessIcon = (p: IconProps) => (
30
+ <Svg {...p}>
31
+ <circle cx="12" cy="12" r="9" />
32
+ <path d="m8 12.5 2.5 2.5L16 9.5" />
33
+ </Svg>
34
+ );
35
+ export const WarningIcon = (p: IconProps) => (
36
+ <Svg {...p}>
37
+ <path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
38
+ <path d="M12 9v4M12 17h.01" />
39
+ </Svg>
40
+ );
41
+ export const DangerIcon = (p: IconProps) => (
42
+ <Svg {...p}>
43
+ <circle cx="12" cy="12" r="9" />
44
+ <path d="M12 7.5v5.5M12 16.5h.01" />
45
+ </Svg>
46
+ );
47
+ export const SpinnerIcon = (p: IconProps) => (
48
+ <Svg width={14} height={14} {...p} className="dl-spinner">
49
+ <path d="M12 3a9 9 0 1 0 9 9" />
50
+ </Svg>
51
+ );
@@ -0,0 +1,8 @@
1
+ import type { DialogLocaleText } from "../core/types";
2
+
3
+ export const defaultDialogText: DialogLocaleText = {
4
+ ok: "OK",
5
+ confirm: "Confirm",
6
+ cancel: "Cancel",
7
+ required: "Please fill in this field",
8
+ };
@@ -0,0 +1,13 @@
1
+ export type DialogSlot =
2
+ | "root"
3
+ | "body"
4
+ | "icon"
5
+ | "title"
6
+ | "description"
7
+ | "input"
8
+ | "actions"
9
+ | "confirm"
10
+ | "cancel";
11
+
12
+ export const cx = (...names: (string | false | null | undefined)[]) =>
13
+ names.filter(Boolean).join(" ");