react-confirm-alert 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -16,20 +16,55 @@ npm install react-confirm-alert
16
16
 
17
17
  ```tsx
18
18
  import { confirm } from 'react-confirm-alert'
19
- import 'react-confirm-alert/src/react-confirm-alert.css'
19
+ import 'react-confirm-alert/style.css'
20
20
 
21
21
  if (await confirm({ title: 'Delete file?', message: 'This cannot be undone.', tone: 'danger' })) {
22
22
  remove(file)
23
23
  }
24
24
  ```
25
25
 
26
+ `style.css` needs 3.1.1 or later. The older path, `react-confirm-alert/src/react-confirm-alert.css`, still works.
27
+
26
28
  `confirm()` resolves `true` or `false`. `alert()` and `prompt()` work the same way, and `<DialogHost />` placed inside your providers lets dialogs read their context.
27
29
 
30
+ ## Custom UI
31
+
32
+ Put your own form, list or warning in a dialog. Plain HTML gets the default look, even under Tailwind v4 preflight, and any class you add wins:
33
+
34
+ ```jsx
35
+ import { DialogLayout, openDialog } from 'react-confirm-alert'
36
+
37
+ const colour = await openDialog({
38
+ render: ({ close, dismiss }) => (
39
+ <DialogLayout title="Pick a colour" actions={[{ label: 'Cancel', onClick: dismiss }, { label: 'Blue', variant: 'primary', onClick: () => close('blue') }]}>
40
+ <p>Used for the team badge.</p>
41
+ </DialogLayout>
42
+ )
43
+ }).result
44
+ ```
45
+
46
+ 3.0 `customUI` still works. Add `skin: true` for the new look:
47
+
48
+ ```jsx
49
+ confirmAlert({
50
+ skin: true,
51
+ customUI: ({ onClose }) => (
52
+ <>
53
+ <h1>Leave the board?</h1>
54
+ <p>You can join again from the invite link.</p>
55
+ <button onClick={onClose}>Stay</button>
56
+ </>
57
+ )
58
+ })
59
+ ```
60
+
61
+ `confirm`, `alert` and `prompt` take `content` for JSX between the message and the buttons. See [Custom UI](https://ga-mo.github.io/react-confirm-alert/docs/custom-ui/).
62
+
28
63
  ## 3.0 code keeps working
29
64
 
30
65
  ```jsx
31
66
  import { confirmAlert } from 'react-confirm-alert'
32
- import 'react-confirm-alert/src/react-confirm-alert.css'
67
+ import 'react-confirm-alert/style.css'
33
68
 
34
69
  confirmAlert({
35
70
  title: 'Confirm to submit',
@@ -47,7 +82,7 @@ Same options, DOM, class names and CSS path as 3.0.
47
82
 
48
83
  ```css
49
84
  @import 'tailwindcss';
50
- @import 'react-confirm-alert/src/react-confirm-alert.css' layer(components);
85
+ @import 'react-confirm-alert/style.css' layer(components);
51
86
  ```
52
87
 
53
88
  ```tsx
@@ -60,11 +95,11 @@ Importing the CSS into `layer(components)` lets Tailwind utilities win without `
60
95
 
61
96
  | Import | gzip |
62
97
  |---|---|
63
- | `import { confirmAlert }` | 3,571 B |
64
- | `import { confirm }` | 3,851 B |
65
- | CSS | 1,095 B |
98
+ | `import { confirmAlert }` | 3,769 B |
99
+ | `import { confirm }` | 4,042 B |
100
+ | CSS (the shipped file, not minified) | 2,909 B |
66
101
 
67
- Measured on 2026-10-23 with size-limit 14.3.0 (`npx size-limit`, config in `.size-limit.json`): the ESM build bundled and minified with React and React DOM excluded, then gzipped. 3.0.6 measured the same way is 2,396 B, including `prop-types`. The extra bytes are the native modal, focus handling, stacked and queued dialogs, and dialogs that render inside your providers.
102
+ Measured on 2026-10-23 with size-limit 14.3.0 (`npx size-limit`, config in `.size-limit.json`): the ESM build bundled and minified with React and React DOM excluded, then gzipped. 3.0.6 measured the same way is 2,396 B, including `prop-types`. The extra bytes are the native modal, focus handling, stacked and queued dialogs, dialogs that render inside your providers, and the default look for your own content.
68
103
 
69
104
  ## Older versions
70
105
 
package/dist/index.esm.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { Component, createContext, useContext, useEffect, useId, useLayoutEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
2
+ import { Component, Fragment, createContext, useContext, useEffect, useId, useLayoutEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { flushSync } from "react-dom";
4
4
  import { createRoot } from "react-dom/client";
5
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  // src/store.ts
7
7
  var emptyState = {
8
8
  entries: [],
@@ -159,6 +159,7 @@ var slotsFor = (layers) => (slot, extra) => ({
159
159
  });
160
160
  var SlotContext = createContext(slotsFor([]));
161
161
  var useSlot = () => useContext(SlotContext);
162
+ var FrameContext = createContext({});
162
163
  function configureDialogs(defaults) {
163
164
  runtime().defaults = defaults;
164
165
  }
@@ -171,7 +172,7 @@ function DialogStack({ target, host }) {
171
172
  if (!(host ? hosts[hosts.length - 1] === host : target !== null || hosts.length === 0)) return null;
172
173
  const openIds = entries.filter((e) => e.phase === "open").map((e) => e.id);
173
174
  const top = openIds[openIds.length - 1];
174
- return /* @__PURE__ */ jsx(Fragment, { children: entries.filter((e) => e.target === target && e.phase !== "queued").map((e) => /* @__PURE__ */ jsx(EntryView, {
175
+ return /* @__PURE__ */ jsx(Fragment$1, { children: entries.filter((e) => e.target === target && e.phase !== "queued").map((e) => /* @__PURE__ */ jsx(EntryView, {
175
176
  entry: e,
176
177
  depth: openIds.indexOf(e.id),
177
178
  topmost: e.id === top
@@ -213,6 +214,7 @@ function EntryView({ entry, depth, topmost }) {
213
214
  closeOnClickOutside: (_ref2 = (_options$closeOnClick = options.closeOnClickOutside) !== null && _options$closeOnClick !== void 0 ? _options$closeOnClick : defaults.closeOnClickOutside) !== null && _ref2 !== void 0 ? _ref2 : true,
214
215
  closeOnEscape: (_ref3 = (_options$closeOnEscap = options.closeOnEscape) !== null && _options$closeOnEscap !== void 0 ? _options$closeOnEscap : defaults.closeOnEscape) !== null && _ref3 !== void 0 ? _ref3 : true,
215
216
  legacy: !!options.legacy,
217
+ skin: options.skin,
216
218
  onClickOutside: options.onClickOutside,
217
219
  onDismiss: dismiss,
218
220
  children: /* @__PURE__ */ jsx(Render, {
@@ -221,6 +223,7 @@ function EntryView({ entry, depth, topmost }) {
221
223
  descriptionId,
222
224
  title: options.title,
223
225
  message: options.message,
226
+ content: options.content,
224
227
  buttons: (_options$buttons = options.buttons) !== null && _options$buttons !== void 0 ? _options$buttons : [],
225
228
  close,
226
229
  dismiss,
@@ -374,12 +377,15 @@ function animationsOf({ frame, panel }) {
374
377
  // src/DialogFrame.tsx
375
378
  var canShowModal = () => typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
376
379
  function DialogFrame(props) {
377
- var _props$tone, _props$depth, _props$topmost;
380
+ var _props$labelledBy, _props$tone, _props$depth, _props$topmost, _props$labelledBy2;
378
381
  const frame = useRef(null);
379
382
  const panel = useRef(null);
380
383
  const pressedOn = useRef(null);
381
384
  const stay = useRef(null);
382
385
  const native = props.modal && canShowModal();
386
+ const layoutTitleId = useId();
387
+ const [layoutTitles, setLayoutTitles] = useState(0);
388
+ const labelledBy = (_props$labelledBy = props.labelledBy) !== null && _props$labelledBy !== void 0 ? _props$labelledBy : props.label || layoutTitles === 0 ? void 0 : layoutTitleId;
383
389
  useLayoutEffect(() => {
384
390
  var _panel$current, _panel$current2, _panel$current3;
385
391
  const element = frame.current;
@@ -451,12 +457,12 @@ function DialogFrame(props) {
451
457
  ...slot("dialog", props.extraClass),
452
458
  role: props.role,
453
459
  "aria-label": props.label,
454
- "aria-labelledby": props.labelledBy,
460
+ "aria-labelledby": labelledBy,
455
461
  "aria-describedby": props.describedBy,
456
462
  "data-rca": "",
457
463
  "data-state": props.state,
458
464
  "data-kind": props.kind,
459
- "data-skin": props.legacy ? void 0 : "",
465
+ "data-skin": props.legacy && !props.skin ? void 0 : "",
460
466
  "data-tone": (_props$tone = props.tone) !== null && _props$tone !== void 0 ? _props$tone : "default",
461
467
  "data-depth": String((_props$depth = props.depth) !== null && _props$depth !== void 0 ? _props$depth : 0),
462
468
  "data-topmost": ((_props$topmost = props.topmost) !== null && _props$topmost !== void 0 ? _props$topmost : true) ? "" : void 0,
@@ -470,7 +476,14 @@ function DialogFrame(props) {
470
476
  tabIndex: -1,
471
477
  children: /* @__PURE__ */ jsx(SlotContext.Provider, {
472
478
  value: slot,
473
- children: props.children
479
+ children: /* @__PURE__ */ jsx(FrameContext.Provider, {
480
+ value: {
481
+ id: props.id,
482
+ titleId: (_props$labelledBy2 = props.labelledBy) !== null && _props$labelledBy2 !== void 0 ? _props$labelledBy2 : layoutTitleId,
483
+ countTitle: setLayoutTitles
484
+ },
485
+ children: props.children
486
+ })
474
487
  })
475
488
  });
476
489
  return native ? /* @__PURE__ */ jsx("dialog", {
@@ -498,9 +511,9 @@ var defaultButtons = [{
498
511
  onClick: () => null
499
512
  }];
500
513
  function CustomUIHost({ render, args }) {
501
- return /* @__PURE__ */ jsx(Fragment, { children: render(args) });
514
+ return /* @__PURE__ */ jsx(Fragment$1, { children: render(args) });
502
515
  }
503
- function LegacyContent({ props, titleId, close, id }) {
516
+ function LegacyContent({ props, titleId, close, id, skin }) {
504
517
  var _props$buttons;
505
518
  const latest = useRef({
506
519
  props,
@@ -514,6 +527,7 @@ function LegacyContent({ props, titleId, close, id }) {
514
527
  const keyboard = (event) => {
515
528
  var _topmost, _props$keyCodeForClos, _props$closeOnEscape, _props$onkeyPress;
516
529
  if (id && ((_topmost = topmost(store.get())) === null || _topmost === void 0 ? void 0 : _topmost.id) !== id) return;
530
+ if (id && runtime().busy.has(id)) return;
517
531
  const { props } = latest.current;
518
532
  const close = () => {
519
533
  event.preventDefault();
@@ -532,17 +546,30 @@ function LegacyContent({ props, titleId, close, id }) {
532
546
  return () => document.removeEventListener("keydown", keyboard, false);
533
547
  }, []);
534
548
  const slot = useSlot();
549
+ const frame = useContext(FrameContext);
535
550
  const { title, message, customUI, childrenElement } = props;
536
551
  const buttons = (_props$buttons = props.buttons) !== null && _props$buttons !== void 0 ? _props$buttons : defaultButtons;
537
- if (customUI) return /* @__PURE__ */ jsx(CustomUIHost, {
538
- render: customUI,
539
- args: {
540
- title,
541
- message,
542
- buttons,
543
- onClose: close
544
- }
545
- });
552
+ if (customUI) {
553
+ const ui = /* @__PURE__ */ jsx(CustomUIHost, {
554
+ render: customUI,
555
+ args: {
556
+ title,
557
+ message,
558
+ buttons,
559
+ onClose: close
560
+ }
561
+ });
562
+ return skin ? /* @__PURE__ */ jsx("div", {
563
+ ...slot("body"),
564
+ children: /* @__PURE__ */ jsx(FrameContext.Provider, {
565
+ value: {
566
+ ...frame,
567
+ inBody: true
568
+ },
569
+ children: ui
570
+ })
571
+ }) : ui;
572
+ }
546
573
  return /* @__PURE__ */ jsxs("div", {
547
574
  ...slot("body"),
548
575
  children: [
@@ -569,19 +596,21 @@ function LegacyContent({ props, titleId, close, id }) {
569
596
  });
570
597
  }
571
598
  function confirmAlert(properties) {
572
- var _ref, _properties$modal, _runtime$defaults$con, _properties$closeOnCl, _properties$closeOnEs, _properties$targetId, _ref2, _properties$strategy, _runtime$defaults$con2;
599
+ var _ref, _properties$skin, _runtime$defaults$con, _ref2, _properties$modal, _runtime$defaults$con2, _properties$closeOnCl, _properties$closeOnEs, _properties$targetId, _ref3, _properties$strategy, _runtime$defaults$con3;
600
+ const skin = (_ref = (_properties$skin = properties.skin) !== null && _properties$skin !== void 0 ? _properties$skin : (_runtime$defaults$con = runtime().defaults.confirmAlert) === null || _runtime$defaults$con === void 0 ? void 0 : _runtime$defaults$con.skin) !== null && _ref !== void 0 ? _ref : false;
573
601
  function Content({ id, titleId, dismiss }) {
574
602
  return /* @__PURE__ */ jsx(LegacyContent, {
575
603
  props: properties,
576
604
  titleId,
577
605
  close: dismiss,
578
- id
606
+ id,
607
+ skin
579
608
  });
580
609
  }
581
610
  openEntry({
582
611
  kind: "custom",
583
612
  title: properties.title,
584
- modal: (_ref = (_properties$modal = properties.modal) !== null && _properties$modal !== void 0 ? _properties$modal : (_runtime$defaults$con = runtime().defaults.confirmAlert) === null || _runtime$defaults$con === void 0 ? void 0 : _runtime$defaults$con.modal) !== null && _ref !== void 0 ? _ref : false,
613
+ modal: (_ref2 = (_properties$modal = properties.modal) !== null && _properties$modal !== void 0 ? _properties$modal : (_runtime$defaults$con2 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con2 === void 0 ? void 0 : _runtime$defaults$con2.modal) !== null && _ref2 !== void 0 ? _ref2 : false,
585
614
  unstyled: properties.unstyled,
586
615
  classNames: properties.classNames,
587
616
  closeOnClickOutside: (_properties$closeOnCl = properties.closeOnClickOutside) !== null && _properties$closeOnCl !== void 0 ? _properties$closeOnCl : true,
@@ -589,18 +618,20 @@ function confirmAlert(properties) {
589
618
  overlayClassName: properties.overlayClassName,
590
619
  onClickOutside: properties.onClickOutside,
591
620
  legacy: true,
621
+ skin,
592
622
  willUnmount: properties.willUnmount,
593
623
  afterClose: properties.afterClose,
594
624
  render: Content
595
- }, (_properties$targetId = properties.targetId) !== null && _properties$targetId !== void 0 ? _properties$targetId : null, (_ref2 = (_properties$strategy = properties.strategy) !== null && _properties$strategy !== void 0 ? _properties$strategy : (_runtime$defaults$con2 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con2 === void 0 ? void 0 : _runtime$defaults$con2.strategy) !== null && _ref2 !== void 0 ? _ref2 : "replace");
625
+ }, (_properties$targetId = properties.targetId) !== null && _properties$targetId !== void 0 ? _properties$targetId : null, (_ref3 = (_properties$strategy = properties.strategy) !== null && _properties$strategy !== void 0 ? _properties$strategy : (_runtime$defaults$con3 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con3 === void 0 ? void 0 : _runtime$defaults$con3.strategy) !== null && _ref3 !== void 0 ? _ref3 : "replace");
596
626
  }
597
627
  function DeclarativeDialog(props) {
598
- var _props$modal, _props$closeOnClickOu, _props$closeOnEscape2;
628
+ var _ref4, _props$skin, _runtime$defaults$con4, _props$modal, _props$closeOnClickOu, _props$closeOnEscape2;
599
629
  const titleId = useId();
600
630
  const close = () => {
601
631
  var _props$afterClose;
602
632
  return (_props$afterClose = props.afterClose) === null || _props$afterClose === void 0 ? void 0 : _props$afterClose.call(props);
603
633
  };
634
+ const skin = (_ref4 = (_props$skin = props.skin) !== null && _props$skin !== void 0 ? _props$skin : (_runtime$defaults$con4 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con4 === void 0 ? void 0 : _runtime$defaults$con4.skin) !== null && _ref4 !== void 0 ? _ref4 : false;
604
635
  return /* @__PURE__ */ jsx(DialogFrame, {
605
636
  modal: (_props$modal = props.modal) !== null && _props$modal !== void 0 ? _props$modal : false,
606
637
  state: "open",
@@ -614,13 +645,15 @@ function DeclarativeDialog(props) {
614
645
  closeOnClickOutside: (_props$closeOnClickOu = props.closeOnClickOutside) !== null && _props$closeOnClickOu !== void 0 ? _props$closeOnClickOu : true,
615
646
  closeOnEscape: (_props$closeOnEscape2 = props.closeOnEscape) !== null && _props$closeOnEscape2 !== void 0 ? _props$closeOnEscape2 : true,
616
647
  legacy: true,
648
+ skin,
617
649
  role: "dialog",
618
650
  onClickOutside: props.onClickOutside,
619
651
  onDismiss: close,
620
652
  children: /* @__PURE__ */ jsx(LegacyContent, {
621
653
  props,
622
654
  titleId,
623
- close
655
+ close,
656
+ skin
624
657
  })
625
658
  });
626
659
  }
@@ -656,7 +689,7 @@ function open(options, dismissValue, body) {
656
689
  (_options$signal = options.signal) === null || _options$signal === void 0 || _options$signal.removeEventListener("abort", onAbort);
657
690
  resolve(value === void 0 ? dismissValue : value);
658
691
  };
659
- const render = (props) => /* @__PURE__ */ jsx(Fragment, { children: body(props, controller.signal) });
692
+ const render = (props) => /* @__PURE__ */ jsx(Fragment$1, { children: body(props, controller.signal) });
660
693
  const onAbort = () => closeDialog$1(id);
661
694
  const id = openEntry({
662
695
  ...options,
@@ -671,6 +704,7 @@ function open(options, dismissValue, body) {
671
704
  }
672
705
  function Body({ props, children }) {
673
706
  const slot = useSlot();
707
+ const frame = useContext(FrameContext);
674
708
  return /* @__PURE__ */ jsxs("div", {
675
709
  ...slot("body"),
676
710
  children: [
@@ -679,22 +713,42 @@ function Body({ props, children }) {
679
713
  ...slot("title"),
680
714
  children: props.title
681
715
  }),
682
- props.message != null && /* @__PURE__ */ jsx("p", {
716
+ props.message != null && (typeof props.message === "object" ? /* @__PURE__ */ jsx("div", {
683
717
  id: props.descriptionId,
684
718
  ...slot("message"),
685
719
  children: props.message
720
+ }) : /* @__PURE__ */ jsx("p", {
721
+ id: props.descriptionId,
722
+ ...slot("message"),
723
+ children: props.message
724
+ })),
725
+ props.content != null && /* @__PURE__ */ jsx(FrameContext.Provider, {
726
+ value: {
727
+ ...frame,
728
+ inBody: true
729
+ },
730
+ children: props.content
686
731
  }),
687
732
  children
688
733
  ]
689
734
  });
690
735
  }
691
- function Buttons({ props, signal: getSignal, focus }) {
692
- const slot = useSlot();
736
+ function useBusy(id) {
693
737
  const [busy, setBusy] = useState(null);
694
- const markBusy = (on) => {
695
- var _runtime$frames$get;
696
- return (_runtime$frames$get = runtime().frames.get(props.id)) === null || _runtime$frames$get === void 0 ? void 0 : _runtime$frames$get.frame.toggleAttribute("data-busy", on);
738
+ const mark = (index) => {
739
+ if (id) {
740
+ var _runtime$frames$get;
741
+ (_runtime$frames$get = runtime().frames.get(id)) === null || _runtime$frames$get === void 0 || _runtime$frames$get.frame.toggleAttribute("data-busy", index !== null);
742
+ if (index === null) runtime().busy.delete(id);
743
+ else runtime().busy.add(id);
744
+ }
745
+ setBusy(index);
697
746
  };
747
+ return [busy, mark];
748
+ }
749
+ function Buttons({ props, signal: getSignal, focus }) {
750
+ const slot = useSlot();
751
+ const [busy, setBusy] = useBusy(props.id);
698
752
  const press = async (button, index) => {
699
753
  const signal = getSignal();
700
754
  let waiting = false;
@@ -711,16 +765,11 @@ function Buttons({ props, signal: getSignal, focus }) {
711
765
  }
712
766
  waiting = true;
713
767
  setBusy(index);
714
- markBusy(true);
715
- runtime().busy.add(props.id);
716
768
  if (await outcome !== false && !signal.aborted) props.close(button.value);
717
769
  } catch (error) {
718
770
  console.error(error);
719
771
  } finally {
720
- if (!waiting) return;
721
- runtime().busy.delete(props.id);
722
- markBusy(false);
723
- if (!signal.aborted) setBusy(null);
772
+ if (waiting) setBusy(null);
724
773
  }
725
774
  };
726
775
  return /* @__PURE__ */ jsx("div", {
@@ -992,6 +1041,7 @@ function ConfirmDialog(props) {
992
1041
  descriptionId,
993
1042
  title: props.title,
994
1043
  message: props.message,
1044
+ content: props.content,
995
1045
  dismiss: hide,
996
1046
  onClose: hide
997
1047
  };
@@ -1063,4 +1113,53 @@ function ConfirmDialog(props) {
1063
1113
  children: body
1064
1114
  });
1065
1115
  }
1066
- export { ConfirmDialog, Dialog, DialogHost, alert, closeAll, closeDialog, configureDialogs, confirm, confirmAlert, ReactConfirmAlert as default, openDialog, prompt, useDialog };
1116
+ // src/DialogLayout.tsx
1117
+ /** The default dialog layout for your own content: title, message, your children, then the actions. */
1118
+ function DialogLayout({ title, message, actions, children }) {
1119
+ const slot = useSlot();
1120
+ const frame = useContext(FrameContext);
1121
+ const [busy, setBusy] = useBusy(frame.id);
1122
+ const hasTitle = title != null;
1123
+ const { countTitle } = frame;
1124
+ useLayoutEffect(() => {
1125
+ if (!hasTitle || !countTitle) return;
1126
+ countTitle((n) => n + 1);
1127
+ return () => countTitle((n) => n - 1);
1128
+ }, [hasTitle, countTitle]);
1129
+ const Body = frame.inBody ? Fragment : "div";
1130
+ return /* @__PURE__ */ jsxs(Body, {
1131
+ ...frame.inBody ? {} : slot("body"),
1132
+ children: [
1133
+ title != null && /* @__PURE__ */ jsx("h1", {
1134
+ id: frame.titleId,
1135
+ ...slot("title"),
1136
+ children: title
1137
+ }),
1138
+ message != null && /* @__PURE__ */ jsx("div", {
1139
+ ...slot("message"),
1140
+ children: message
1141
+ }),
1142
+ children,
1143
+ actions && actions.length > 0 && /* @__PURE__ */ jsx("div", {
1144
+ ...slot("actions"),
1145
+ children: actions.map(({ label, variant = "secondary", onClick, disabled, className, ...rest }, i) => /* @__PURE__ */ jsx("button", {
1146
+ type: "button",
1147
+ ...rest,
1148
+ ...slot("button", className),
1149
+ "data-variant": variant,
1150
+ "data-busy": busy === i ? "" : void 0,
1151
+ "aria-busy": busy === i || void 0,
1152
+ disabled: disabled || busy !== null,
1153
+ onClick: (event) => {
1154
+ const outcome = onClick === null || onClick === void 0 ? void 0 : onClick(event);
1155
+ if (!(outcome instanceof Promise)) return;
1156
+ setBusy(i);
1157
+ outcome.catch((error) => console.error(error)).finally(() => setBusy(null));
1158
+ },
1159
+ children: label
1160
+ }, i))
1161
+ })
1162
+ ]
1163
+ });
1164
+ }
1165
+ export { ConfirmDialog, Dialog, DialogHost, DialogLayout, alert, closeAll, closeDialog, configureDialogs, confirm, confirmAlert, ReactConfirmAlert as default, openDialog, prompt, useDialog };
package/lib/index.js CHANGED
@@ -163,6 +163,7 @@ var slotsFor = (layers) => (slot, extra) => ({
163
163
  });
164
164
  var SlotContext = (0, react.createContext)(slotsFor([]));
165
165
  var useSlot = () => (0, react.useContext)(SlotContext);
166
+ var FrameContext = (0, react.createContext)({});
166
167
  function configureDialogs(defaults) {
167
168
  runtime().defaults = defaults;
168
169
  }
@@ -217,6 +218,7 @@ function EntryView({ entry, depth, topmost }) {
217
218
  closeOnClickOutside: (_ref2 = (_options$closeOnClick = options.closeOnClickOutside) !== null && _options$closeOnClick !== void 0 ? _options$closeOnClick : defaults.closeOnClickOutside) !== null && _ref2 !== void 0 ? _ref2 : true,
218
219
  closeOnEscape: (_ref3 = (_options$closeOnEscap = options.closeOnEscape) !== null && _options$closeOnEscap !== void 0 ? _options$closeOnEscap : defaults.closeOnEscape) !== null && _ref3 !== void 0 ? _ref3 : true,
219
220
  legacy: !!options.legacy,
221
+ skin: options.skin,
220
222
  onClickOutside: options.onClickOutside,
221
223
  onDismiss: dismiss,
222
224
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Render, {
@@ -225,6 +227,7 @@ function EntryView({ entry, depth, topmost }) {
225
227
  descriptionId,
226
228
  title: options.title,
227
229
  message: options.message,
230
+ content: options.content,
228
231
  buttons: (_options$buttons = options.buttons) !== null && _options$buttons !== void 0 ? _options$buttons : [],
229
232
  close,
230
233
  dismiss,
@@ -378,12 +381,15 @@ function animationsOf({ frame, panel }) {
378
381
  // src/DialogFrame.tsx
379
382
  var canShowModal = () => typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
380
383
  function DialogFrame(props) {
381
- var _props$tone, _props$depth, _props$topmost;
384
+ var _props$labelledBy, _props$tone, _props$depth, _props$topmost, _props$labelledBy2;
382
385
  const frame = (0, react.useRef)(null);
383
386
  const panel = (0, react.useRef)(null);
384
387
  const pressedOn = (0, react.useRef)(null);
385
388
  const stay = (0, react.useRef)(null);
386
389
  const native = props.modal && canShowModal();
390
+ const layoutTitleId = (0, react.useId)();
391
+ const [layoutTitles, setLayoutTitles] = (0, react.useState)(0);
392
+ const labelledBy = (_props$labelledBy = props.labelledBy) !== null && _props$labelledBy !== void 0 ? _props$labelledBy : props.label || layoutTitles === 0 ? void 0 : layoutTitleId;
387
393
  (0, react.useLayoutEffect)(() => {
388
394
  var _panel$current, _panel$current2, _panel$current3;
389
395
  const element = frame.current;
@@ -455,12 +461,12 @@ function DialogFrame(props) {
455
461
  ...slot("dialog", props.extraClass),
456
462
  role: props.role,
457
463
  "aria-label": props.label,
458
- "aria-labelledby": props.labelledBy,
464
+ "aria-labelledby": labelledBy,
459
465
  "aria-describedby": props.describedBy,
460
466
  "data-rca": "",
461
467
  "data-state": props.state,
462
468
  "data-kind": props.kind,
463
- "data-skin": props.legacy ? void 0 : "",
469
+ "data-skin": props.legacy && !props.skin ? void 0 : "",
464
470
  "data-tone": (_props$tone = props.tone) !== null && _props$tone !== void 0 ? _props$tone : "default",
465
471
  "data-depth": String((_props$depth = props.depth) !== null && _props$depth !== void 0 ? _props$depth : 0),
466
472
  "data-topmost": ((_props$topmost = props.topmost) !== null && _props$topmost !== void 0 ? _props$topmost : true) ? "" : void 0,
@@ -474,7 +480,14 @@ function DialogFrame(props) {
474
480
  tabIndex: -1,
475
481
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotContext.Provider, {
476
482
  value: slot,
477
- children: props.children
483
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FrameContext.Provider, {
484
+ value: {
485
+ id: props.id,
486
+ titleId: (_props$labelledBy2 = props.labelledBy) !== null && _props$labelledBy2 !== void 0 ? _props$labelledBy2 : layoutTitleId,
487
+ countTitle: setLayoutTitles
488
+ },
489
+ children: props.children
490
+ })
478
491
  })
479
492
  });
480
493
  return native ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
@@ -504,7 +517,7 @@ var defaultButtons = [{
504
517
  function CustomUIHost({ render, args }) {
505
518
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: render(args) });
506
519
  }
507
- function LegacyContent({ props, titleId, close, id }) {
520
+ function LegacyContent({ props, titleId, close, id, skin }) {
508
521
  var _props$buttons;
509
522
  const latest = (0, react.useRef)({
510
523
  props,
@@ -518,6 +531,7 @@ function LegacyContent({ props, titleId, close, id }) {
518
531
  const keyboard = (event) => {
519
532
  var _topmost, _props$keyCodeForClos, _props$closeOnEscape, _props$onkeyPress;
520
533
  if (id && ((_topmost = topmost(store.get())) === null || _topmost === void 0 ? void 0 : _topmost.id) !== id) return;
534
+ if (id && runtime().busy.has(id)) return;
521
535
  const { props } = latest.current;
522
536
  const close = () => {
523
537
  event.preventDefault();
@@ -536,17 +550,30 @@ function LegacyContent({ props, titleId, close, id }) {
536
550
  return () => document.removeEventListener("keydown", keyboard, false);
537
551
  }, []);
538
552
  const slot = useSlot();
553
+ const frame = (0, react.useContext)(FrameContext);
539
554
  const { title, message, customUI, childrenElement } = props;
540
555
  const buttons = (_props$buttons = props.buttons) !== null && _props$buttons !== void 0 ? _props$buttons : defaultButtons;
541
- if (customUI) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomUIHost, {
542
- render: customUI,
543
- args: {
544
- title,
545
- message,
546
- buttons,
547
- onClose: close
548
- }
549
- });
556
+ if (customUI) {
557
+ const ui = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomUIHost, {
558
+ render: customUI,
559
+ args: {
560
+ title,
561
+ message,
562
+ buttons,
563
+ onClose: close
564
+ }
565
+ });
566
+ return skin ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
567
+ ...slot("body"),
568
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FrameContext.Provider, {
569
+ value: {
570
+ ...frame,
571
+ inBody: true
572
+ },
573
+ children: ui
574
+ })
575
+ }) : ui;
576
+ }
550
577
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
551
578
  ...slot("body"),
552
579
  children: [
@@ -573,19 +600,21 @@ function LegacyContent({ props, titleId, close, id }) {
573
600
  });
574
601
  }
575
602
  function confirmAlert(properties) {
576
- var _ref, _properties$modal, _runtime$defaults$con, _properties$closeOnCl, _properties$closeOnEs, _properties$targetId, _ref2, _properties$strategy, _runtime$defaults$con2;
603
+ var _ref, _properties$skin, _runtime$defaults$con, _ref2, _properties$modal, _runtime$defaults$con2, _properties$closeOnCl, _properties$closeOnEs, _properties$targetId, _ref3, _properties$strategy, _runtime$defaults$con3;
604
+ const skin = (_ref = (_properties$skin = properties.skin) !== null && _properties$skin !== void 0 ? _properties$skin : (_runtime$defaults$con = runtime().defaults.confirmAlert) === null || _runtime$defaults$con === void 0 ? void 0 : _runtime$defaults$con.skin) !== null && _ref !== void 0 ? _ref : false;
577
605
  function Content({ id, titleId, dismiss }) {
578
606
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegacyContent, {
579
607
  props: properties,
580
608
  titleId,
581
609
  close: dismiss,
582
- id
610
+ id,
611
+ skin
583
612
  });
584
613
  }
585
614
  openEntry({
586
615
  kind: "custom",
587
616
  title: properties.title,
588
- modal: (_ref = (_properties$modal = properties.modal) !== null && _properties$modal !== void 0 ? _properties$modal : (_runtime$defaults$con = runtime().defaults.confirmAlert) === null || _runtime$defaults$con === void 0 ? void 0 : _runtime$defaults$con.modal) !== null && _ref !== void 0 ? _ref : false,
617
+ modal: (_ref2 = (_properties$modal = properties.modal) !== null && _properties$modal !== void 0 ? _properties$modal : (_runtime$defaults$con2 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con2 === void 0 ? void 0 : _runtime$defaults$con2.modal) !== null && _ref2 !== void 0 ? _ref2 : false,
589
618
  unstyled: properties.unstyled,
590
619
  classNames: properties.classNames,
591
620
  closeOnClickOutside: (_properties$closeOnCl = properties.closeOnClickOutside) !== null && _properties$closeOnCl !== void 0 ? _properties$closeOnCl : true,
@@ -593,18 +622,20 @@ function confirmAlert(properties) {
593
622
  overlayClassName: properties.overlayClassName,
594
623
  onClickOutside: properties.onClickOutside,
595
624
  legacy: true,
625
+ skin,
596
626
  willUnmount: properties.willUnmount,
597
627
  afterClose: properties.afterClose,
598
628
  render: Content
599
- }, (_properties$targetId = properties.targetId) !== null && _properties$targetId !== void 0 ? _properties$targetId : null, (_ref2 = (_properties$strategy = properties.strategy) !== null && _properties$strategy !== void 0 ? _properties$strategy : (_runtime$defaults$con2 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con2 === void 0 ? void 0 : _runtime$defaults$con2.strategy) !== null && _ref2 !== void 0 ? _ref2 : "replace");
629
+ }, (_properties$targetId = properties.targetId) !== null && _properties$targetId !== void 0 ? _properties$targetId : null, (_ref3 = (_properties$strategy = properties.strategy) !== null && _properties$strategy !== void 0 ? _properties$strategy : (_runtime$defaults$con3 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con3 === void 0 ? void 0 : _runtime$defaults$con3.strategy) !== null && _ref3 !== void 0 ? _ref3 : "replace");
600
630
  }
601
631
  function DeclarativeDialog(props) {
602
- var _props$modal, _props$closeOnClickOu, _props$closeOnEscape2;
632
+ var _ref4, _props$skin, _runtime$defaults$con4, _props$modal, _props$closeOnClickOu, _props$closeOnEscape2;
603
633
  const titleId = (0, react.useId)();
604
634
  const close = () => {
605
635
  var _props$afterClose;
606
636
  return (_props$afterClose = props.afterClose) === null || _props$afterClose === void 0 ? void 0 : _props$afterClose.call(props);
607
637
  };
638
+ const skin = (_ref4 = (_props$skin = props.skin) !== null && _props$skin !== void 0 ? _props$skin : (_runtime$defaults$con4 = runtime().defaults.confirmAlert) === null || _runtime$defaults$con4 === void 0 ? void 0 : _runtime$defaults$con4.skin) !== null && _ref4 !== void 0 ? _ref4 : false;
608
639
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogFrame, {
609
640
  modal: (_props$modal = props.modal) !== null && _props$modal !== void 0 ? _props$modal : false,
610
641
  state: "open",
@@ -618,13 +649,15 @@ function DeclarativeDialog(props) {
618
649
  closeOnClickOutside: (_props$closeOnClickOu = props.closeOnClickOutside) !== null && _props$closeOnClickOu !== void 0 ? _props$closeOnClickOu : true,
619
650
  closeOnEscape: (_props$closeOnEscape2 = props.closeOnEscape) !== null && _props$closeOnEscape2 !== void 0 ? _props$closeOnEscape2 : true,
620
651
  legacy: true,
652
+ skin,
621
653
  role: "dialog",
622
654
  onClickOutside: props.onClickOutside,
623
655
  onDismiss: close,
624
656
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LegacyContent, {
625
657
  props,
626
658
  titleId,
627
- close
659
+ close,
660
+ skin
628
661
  })
629
662
  });
630
663
  }
@@ -675,6 +708,7 @@ function open(options, dismissValue, body) {
675
708
  }
676
709
  function Body({ props, children }) {
677
710
  const slot = useSlot();
711
+ const frame = (0, react.useContext)(FrameContext);
678
712
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
679
713
  ...slot("body"),
680
714
  children: [
@@ -683,22 +717,42 @@ function Body({ props, children }) {
683
717
  ...slot("title"),
684
718
  children: props.title
685
719
  }),
686
- props.message != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
720
+ props.message != null && (typeof props.message === "object" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
687
721
  id: props.descriptionId,
688
722
  ...slot("message"),
689
723
  children: props.message
724
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
725
+ id: props.descriptionId,
726
+ ...slot("message"),
727
+ children: props.message
728
+ })),
729
+ props.content != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FrameContext.Provider, {
730
+ value: {
731
+ ...frame,
732
+ inBody: true
733
+ },
734
+ children: props.content
690
735
  }),
691
736
  children
692
737
  ]
693
738
  });
694
739
  }
695
- function Buttons({ props, signal: getSignal, focus }) {
696
- const slot = useSlot();
740
+ function useBusy(id) {
697
741
  const [busy, setBusy] = (0, react.useState)(null);
698
- const markBusy = (on) => {
699
- var _runtime$frames$get;
700
- return (_runtime$frames$get = runtime().frames.get(props.id)) === null || _runtime$frames$get === void 0 ? void 0 : _runtime$frames$get.frame.toggleAttribute("data-busy", on);
742
+ const mark = (index) => {
743
+ if (id) {
744
+ var _runtime$frames$get;
745
+ (_runtime$frames$get = runtime().frames.get(id)) === null || _runtime$frames$get === void 0 || _runtime$frames$get.frame.toggleAttribute("data-busy", index !== null);
746
+ if (index === null) runtime().busy.delete(id);
747
+ else runtime().busy.add(id);
748
+ }
749
+ setBusy(index);
701
750
  };
751
+ return [busy, mark];
752
+ }
753
+ function Buttons({ props, signal: getSignal, focus }) {
754
+ const slot = useSlot();
755
+ const [busy, setBusy] = useBusy(props.id);
702
756
  const press = async (button, index) => {
703
757
  const signal = getSignal();
704
758
  let waiting = false;
@@ -715,16 +769,11 @@ function Buttons({ props, signal: getSignal, focus }) {
715
769
  }
716
770
  waiting = true;
717
771
  setBusy(index);
718
- markBusy(true);
719
- runtime().busy.add(props.id);
720
772
  if (await outcome !== false && !signal.aborted) props.close(button.value);
721
773
  } catch (error) {
722
774
  console.error(error);
723
775
  } finally {
724
- if (!waiting) return;
725
- runtime().busy.delete(props.id);
726
- markBusy(false);
727
- if (!signal.aborted) setBusy(null);
776
+ if (waiting) setBusy(null);
728
777
  }
729
778
  };
730
779
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -996,6 +1045,7 @@ function ConfirmDialog(props) {
996
1045
  descriptionId,
997
1046
  title: props.title,
998
1047
  message: props.message,
1048
+ content: props.content,
999
1049
  dismiss: hide,
1000
1050
  onClose: hide
1001
1051
  };
@@ -1067,9 +1117,59 @@ function ConfirmDialog(props) {
1067
1117
  children: body
1068
1118
  });
1069
1119
  }
1120
+ // src/DialogLayout.tsx
1121
+ /** The default dialog layout for your own content: title, message, your children, then the actions. */
1122
+ function DialogLayout({ title, message, actions, children }) {
1123
+ const slot = useSlot();
1124
+ const frame = (0, react.useContext)(FrameContext);
1125
+ const [busy, setBusy] = useBusy(frame.id);
1126
+ const hasTitle = title != null;
1127
+ const { countTitle } = frame;
1128
+ (0, react.useLayoutEffect)(() => {
1129
+ if (!hasTitle || !countTitle) return;
1130
+ countTitle((n) => n + 1);
1131
+ return () => countTitle((n) => n - 1);
1132
+ }, [hasTitle, countTitle]);
1133
+ const Body = frame.inBody ? react.Fragment : "div";
1134
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Body, {
1135
+ ...frame.inBody ? {} : slot("body"),
1136
+ children: [
1137
+ title != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
1138
+ id: frame.titleId,
1139
+ ...slot("title"),
1140
+ children: title
1141
+ }),
1142
+ message != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1143
+ ...slot("message"),
1144
+ children: message
1145
+ }),
1146
+ children,
1147
+ actions && actions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1148
+ ...slot("actions"),
1149
+ children: actions.map(({ label, variant = "secondary", onClick, disabled, className, ...rest }, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1150
+ type: "button",
1151
+ ...rest,
1152
+ ...slot("button", className),
1153
+ "data-variant": variant,
1154
+ "data-busy": busy === i ? "" : void 0,
1155
+ "aria-busy": busy === i || void 0,
1156
+ disabled: disabled || busy !== null,
1157
+ onClick: (event) => {
1158
+ const outcome = onClick === null || onClick === void 0 ? void 0 : onClick(event);
1159
+ if (!(outcome instanceof Promise)) return;
1160
+ setBusy(i);
1161
+ outcome.catch((error) => console.error(error)).finally(() => setBusy(null));
1162
+ },
1163
+ children: label
1164
+ }, i))
1165
+ })
1166
+ ]
1167
+ });
1168
+ }
1070
1169
  exports.ConfirmDialog = ConfirmDialog;
1071
1170
  exports.Dialog = Dialog;
1072
1171
  exports.DialogHost = DialogHost;
1172
+ exports.DialogLayout = DialogLayout;
1073
1173
  exports.alert = alert;
1074
1174
  exports.closeAll = closeAll;
1075
1175
  exports.closeDialog = closeDialog;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-confirm-alert",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "react component confirm dialog.",
5
5
  "main": "lib/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -61,6 +61,7 @@
61
61
  "react": "^18.1.0",
62
62
  "react-dom": "^18.1.0",
63
63
  "size-limit": "^14.3.0",
64
+ "tailwindcss": "^4.3.3",
64
65
  "typescript": "^7.0.2",
65
66
  "vite": "^8.3.5",
66
67
  "vitest": "^5.0.5"
@@ -158,6 +158,7 @@ dialog.react-confirm-alert-overlay::backdrop {
158
158
  --_primary-fg: #18181b;
159
159
  --_ring: #71717a;
160
160
  --_error: #f87171;
161
+ color-scheme: dark;
161
162
  background: var(--rca-backdrop, rgba(0, 0, 0, 0.7));
162
163
  }
163
164
  .react-confirm-alert-overlay[data-skin] .react-confirm-alert {
@@ -184,7 +185,10 @@ dialog.react-confirm-alert-overlay::backdrop {
184
185
  .react-confirm-alert-overlay[data-skin][data-state=closing] .react-confirm-alert {
185
186
  animation: rca-pop-out 150ms ease-in both;
186
187
  }
187
- .react-confirm-alert-overlay[data-skin] [data-slot=title] {
188
+ /* Plain elements inside a skinned dialog look like the slots. Their selectors are type selectors under
189
+ :where(), so they beat a CSS reset such as Tailwind preflight and lose to any class you add. */
190
+ .react-confirm-alert-overlay[data-skin] [data-slot=title],
191
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(h1, h2, h3) {
188
192
  margin: 0 0 8px;
189
193
  font-size: 18px;
190
194
  font-weight: 600;
@@ -195,14 +199,17 @@ dialog.react-confirm-alert-overlay::backdrop {
195
199
  .react-confirm-alert-overlay[data-skin] [data-slot=field] {
196
200
  margin-top: 16px;
197
201
  }
198
- .react-confirm-alert-overlay[data-skin] [data-slot=label] {
202
+ .react-confirm-alert-overlay[data-skin] [data-slot=label],
203
+ :where(.react-confirm-alert-overlay[data-skin]) div label {
199
204
  display: block;
200
205
  margin-bottom: 6px;
201
206
  font-size: 13px;
202
207
  font-weight: 500;
203
208
  color: var(--rca-title, var(--_title));
204
209
  }
205
- .react-confirm-alert-overlay[data-skin] [data-slot=input] {
210
+ .react-confirm-alert-overlay[data-skin] [data-slot=input],
211
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(input:where(:not([type=checkbox], [type=radio], [type=range], [type=file], [type=color], [type=submit], [type=button], [type=reset], [type=image])), textarea, select) {
212
+ display: block;
206
213
  box-sizing: border-box;
207
214
  width: 100%;
208
215
  height: 36px;
@@ -215,14 +222,35 @@ dialog.react-confirm-alert-overlay::backdrop {
215
222
  outline: none;
216
223
  transition: border-color 150ms, box-shadow 150ms;
217
224
  }
218
- .react-confirm-alert-overlay[data-skin] [data-slot=input]:focus {
225
+ .react-confirm-alert-overlay[data-skin] textarea[data-slot=input],
226
+ :where(.react-confirm-alert-overlay[data-skin]) div textarea {
227
+ height: auto;
228
+ min-height: 80px;
229
+ padding: 8px 12px;
230
+ resize: vertical;
231
+ }
232
+ .react-confirm-alert-overlay[data-skin] [data-slot=input]:focus,
233
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(input, textarea, select):where(:focus) {
219
234
  border-color: var(--rca-ring, var(--_ring));
220
235
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--rca-ring, var(--_ring)) 50%, transparent);
221
236
  }
222
- .react-confirm-alert-overlay[data-skin] [data-slot=input][aria-invalid=true] {
237
+ .react-confirm-alert-overlay[data-skin] [data-slot=input][aria-invalid=true],
238
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(input, textarea, select):where([aria-invalid=true]) {
223
239
  border-color: var(--rca-danger, var(--_error));
224
240
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--rca-danger, var(--_error)) 20%, transparent);
225
241
  }
242
+ :where(.react-confirm-alert-overlay[data-skin]) div p {
243
+ margin: 0;
244
+ }
245
+ :where(.react-confirm-alert-overlay[data-skin]) div input:where([type=checkbox], [type=radio]) {
246
+ accent-color: var(--rca-button-bg, var(--_primary));
247
+ }
248
+ :where(.react-confirm-alert-overlay[data-skin]) div * + :is(p, label, input, textarea, select) {
249
+ margin-top: 12px;
250
+ }
251
+ :where(.react-confirm-alert-overlay[data-skin]) div label + :is(input, textarea, select) {
252
+ margin-top: 0;
253
+ }
226
254
  .react-confirm-alert-overlay[data-skin] [data-slot=error] {
227
255
  margin: 6px 0 0;
228
256
  font-size: 13px;
@@ -233,7 +261,8 @@ dialog.react-confirm-alert-overlay::backdrop {
233
261
  gap: var(--rca-gap, 8px);
234
262
  margin-top: 24px;
235
263
  }
236
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button {
264
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button,
265
+ :where(.react-confirm-alert-overlay[data-skin]) div button {
237
266
  display: inline-flex;
238
267
  align-items: center;
239
268
  justify-content: center;
@@ -252,38 +281,53 @@ dialog.react-confirm-alert-overlay::backdrop {
252
281
  white-space: nowrap;
253
282
  transition: background-color 150ms, transform 150ms;
254
283
  }
255
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:hover {
256
- background: var(--_hover);
257
- }
258
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=primary] {
284
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=primary],
285
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=primary]) {
259
286
  border-color: transparent;
260
287
  background: var(--rca-button-bg, var(--_primary));
261
288
  color: var(--rca-button-fg, var(--_primary-fg));
262
289
  }
263
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=danger] {
290
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=danger],
291
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=danger]) {
264
292
  border-color: transparent;
265
293
  background: var(--rca-danger, var(--_danger));
266
294
  color: #fff;
267
295
  }
268
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > :is([data-variant=primary],[data-variant=danger]):hover {
269
- opacity: 0.9;
296
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > :not([data-variant=primary],[data-variant=danger]):enabled:hover,
297
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:not([data-variant=primary],[data-variant=danger]):enabled:hover) {
298
+ background: var(--_hover);
299
+ }
300
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=primary]:enabled:hover,
301
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=primary]:enabled:hover) {
302
+ background: var(--rca-button-bg-hover, color-mix(in srgb, var(--rca-button-bg, var(--_primary)) 85%, #000));
270
303
  }
271
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:active {
304
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=danger]:enabled:hover,
305
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=danger]:enabled:hover) {
306
+ background: var(--rca-danger-hover, color-mix(in srgb, var(--rca-danger, var(--_danger)) 85%, #000));
307
+ }
308
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:active,
309
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:active) {
272
310
  transform: scale(0.97);
273
311
  }
274
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:focus-visible {
312
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:focus-visible,
313
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:focus-visible) {
275
314
  outline: var(--rca-focus-ring, 2px solid var(--_ring));
276
315
  outline-offset: 2px;
277
316
  }
278
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:disabled {
317
+ /* Not faded: white on a faded red is below 4.5:1. */
318
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:disabled:not([aria-busy=true]),
319
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:disabled:not([aria-busy=true])) {
279
320
  cursor: default;
280
- opacity: 0.6;
321
+ border-color: var(--rca-border, var(--_border));
322
+ background: var(--rca-bg, var(--_bg));
323
+ color: var(--rca-fg, var(--_fg));
281
324
  }
282
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true] {
325
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true],
326
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([aria-busy=true]) {
283
327
  cursor: progress;
284
- opacity: 1;
285
328
  }
286
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true]::before {
329
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true]::before,
330
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([aria-busy=true])::before {
287
331
  content: '';
288
332
  width: 12px;
289
333
  height: 12px;
package/style.css CHANGED
@@ -158,6 +158,7 @@ dialog.react-confirm-alert-overlay::backdrop {
158
158
  --_primary-fg: #18181b;
159
159
  --_ring: #71717a;
160
160
  --_error: #f87171;
161
+ color-scheme: dark;
161
162
  background: var(--rca-backdrop, rgba(0, 0, 0, 0.7));
162
163
  }
163
164
  .react-confirm-alert-overlay[data-skin] .react-confirm-alert {
@@ -184,7 +185,10 @@ dialog.react-confirm-alert-overlay::backdrop {
184
185
  .react-confirm-alert-overlay[data-skin][data-state=closing] .react-confirm-alert {
185
186
  animation: rca-pop-out 150ms ease-in both;
186
187
  }
187
- .react-confirm-alert-overlay[data-skin] [data-slot=title] {
188
+ /* Plain elements inside a skinned dialog look like the slots. Their selectors are type selectors under
189
+ :where(), so they beat a CSS reset such as Tailwind preflight and lose to any class you add. */
190
+ .react-confirm-alert-overlay[data-skin] [data-slot=title],
191
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(h1, h2, h3) {
188
192
  margin: 0 0 8px;
189
193
  font-size: 18px;
190
194
  font-weight: 600;
@@ -195,14 +199,17 @@ dialog.react-confirm-alert-overlay::backdrop {
195
199
  .react-confirm-alert-overlay[data-skin] [data-slot=field] {
196
200
  margin-top: 16px;
197
201
  }
198
- .react-confirm-alert-overlay[data-skin] [data-slot=label] {
202
+ .react-confirm-alert-overlay[data-skin] [data-slot=label],
203
+ :where(.react-confirm-alert-overlay[data-skin]) div label {
199
204
  display: block;
200
205
  margin-bottom: 6px;
201
206
  font-size: 13px;
202
207
  font-weight: 500;
203
208
  color: var(--rca-title, var(--_title));
204
209
  }
205
- .react-confirm-alert-overlay[data-skin] [data-slot=input] {
210
+ .react-confirm-alert-overlay[data-skin] [data-slot=input],
211
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(input:where(:not([type=checkbox], [type=radio], [type=range], [type=file], [type=color], [type=submit], [type=button], [type=reset], [type=image])), textarea, select) {
212
+ display: block;
206
213
  box-sizing: border-box;
207
214
  width: 100%;
208
215
  height: 36px;
@@ -215,14 +222,35 @@ dialog.react-confirm-alert-overlay::backdrop {
215
222
  outline: none;
216
223
  transition: border-color 150ms, box-shadow 150ms;
217
224
  }
218
- .react-confirm-alert-overlay[data-skin] [data-slot=input]:focus {
225
+ .react-confirm-alert-overlay[data-skin] textarea[data-slot=input],
226
+ :where(.react-confirm-alert-overlay[data-skin]) div textarea {
227
+ height: auto;
228
+ min-height: 80px;
229
+ padding: 8px 12px;
230
+ resize: vertical;
231
+ }
232
+ .react-confirm-alert-overlay[data-skin] [data-slot=input]:focus,
233
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(input, textarea, select):where(:focus) {
219
234
  border-color: var(--rca-ring, var(--_ring));
220
235
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--rca-ring, var(--_ring)) 50%, transparent);
221
236
  }
222
- .react-confirm-alert-overlay[data-skin] [data-slot=input][aria-invalid=true] {
237
+ .react-confirm-alert-overlay[data-skin] [data-slot=input][aria-invalid=true],
238
+ :where(.react-confirm-alert-overlay[data-skin]) div :is(input, textarea, select):where([aria-invalid=true]) {
223
239
  border-color: var(--rca-danger, var(--_error));
224
240
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--rca-danger, var(--_error)) 20%, transparent);
225
241
  }
242
+ :where(.react-confirm-alert-overlay[data-skin]) div p {
243
+ margin: 0;
244
+ }
245
+ :where(.react-confirm-alert-overlay[data-skin]) div input:where([type=checkbox], [type=radio]) {
246
+ accent-color: var(--rca-button-bg, var(--_primary));
247
+ }
248
+ :where(.react-confirm-alert-overlay[data-skin]) div * + :is(p, label, input, textarea, select) {
249
+ margin-top: 12px;
250
+ }
251
+ :where(.react-confirm-alert-overlay[data-skin]) div label + :is(input, textarea, select) {
252
+ margin-top: 0;
253
+ }
226
254
  .react-confirm-alert-overlay[data-skin] [data-slot=error] {
227
255
  margin: 6px 0 0;
228
256
  font-size: 13px;
@@ -233,7 +261,8 @@ dialog.react-confirm-alert-overlay::backdrop {
233
261
  gap: var(--rca-gap, 8px);
234
262
  margin-top: 24px;
235
263
  }
236
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button {
264
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button,
265
+ :where(.react-confirm-alert-overlay[data-skin]) div button {
237
266
  display: inline-flex;
238
267
  align-items: center;
239
268
  justify-content: center;
@@ -252,38 +281,53 @@ dialog.react-confirm-alert-overlay::backdrop {
252
281
  white-space: nowrap;
253
282
  transition: background-color 150ms, transform 150ms;
254
283
  }
255
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:hover {
256
- background: var(--_hover);
257
- }
258
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=primary] {
284
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=primary],
285
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=primary]) {
259
286
  border-color: transparent;
260
287
  background: var(--rca-button-bg, var(--_primary));
261
288
  color: var(--rca-button-fg, var(--_primary-fg));
262
289
  }
263
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=danger] {
290
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=danger],
291
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=danger]) {
264
292
  border-color: transparent;
265
293
  background: var(--rca-danger, var(--_danger));
266
294
  color: #fff;
267
295
  }
268
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > :is([data-variant=primary],[data-variant=danger]):hover {
269
- opacity: 0.9;
296
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > :not([data-variant=primary],[data-variant=danger]):enabled:hover,
297
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:not([data-variant=primary],[data-variant=danger]):enabled:hover) {
298
+ background: var(--_hover);
299
+ }
300
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=primary]:enabled:hover,
301
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=primary]:enabled:hover) {
302
+ background: var(--rca-button-bg-hover, color-mix(in srgb, var(--rca-button-bg, var(--_primary)) 85%, #000));
270
303
  }
271
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:active {
304
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > [data-variant=danger]:enabled:hover,
305
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([data-variant=danger]:enabled:hover) {
306
+ background: var(--rca-danger-hover, color-mix(in srgb, var(--rca-danger, var(--_danger)) 85%, #000));
307
+ }
308
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:active,
309
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:active) {
272
310
  transform: scale(0.97);
273
311
  }
274
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:focus-visible {
312
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:focus-visible,
313
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:focus-visible) {
275
314
  outline: var(--rca-focus-ring, 2px solid var(--_ring));
276
315
  outline-offset: 2px;
277
316
  }
278
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:disabled {
317
+ /* Not faded: white on a faded red is below 4.5:1. */
318
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button:disabled:not([aria-busy=true]),
319
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where(:disabled:not([aria-busy=true])) {
279
320
  cursor: default;
280
- opacity: 0.6;
321
+ border-color: var(--rca-border, var(--_border));
322
+ background: var(--rca-bg, var(--_bg));
323
+ color: var(--rca-fg, var(--_fg));
281
324
  }
282
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true] {
325
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true],
326
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([aria-busy=true]) {
283
327
  cursor: progress;
284
- opacity: 1;
285
328
  }
286
- .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true]::before {
329
+ .react-confirm-alert-overlay[data-skin] .react-confirm-alert-button-group > button[aria-busy=true]::before,
330
+ :where(.react-confirm-alert-overlay[data-skin]) div button:where([aria-busy=true])::before {
287
331
  content: '';
288
332
  width: 12px;
289
333
  height: 12px;
@@ -20,6 +20,8 @@ export interface DialogFrameProps {
20
20
  closeOnEscape: boolean;
21
21
  /** the v3 API reads Escape from document keydown and focuses the panel */
22
22
  legacy: boolean;
23
+ /** confirmAlert opted into the new default look */
24
+ skin?: boolean | undefined;
23
25
  onKeyDown?: ((event: KeyboardEvent<HTMLElement>, close: () => void) => void) | undefined;
24
26
  onClickOutside?: (() => void) | undefined;
25
27
  onDismiss: () => void;
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { DialogLayoutProps } from './types';
3
+ /** The default dialog layout for your own content: title, message, your children, then the actions. */
4
+ export declare function DialogLayout({ title, message, actions, children }: DialogLayoutProps): ReactElement;
package/typings/api.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare function Body({ props, children }: {
4
4
  props: DialogRenderProps<any>;
5
5
  children: ReactNode;
6
6
  }): ReactElement;
7
+ export declare function useBusy(id: DialogId | undefined): [number | null, (index: number | null) => void];
7
8
  export declare function Buttons<R>({ props, signal: getSignal, focus }: {
8
9
  props: DialogRenderProps<R>;
9
10
  signal: () => AbortSignal;
@@ -1,4 +1,4 @@
1
- import type { DialogClassNames, DialogDefaults, DialogSlot, StyleOptions } from './types';
1
+ import type { DialogClassNames, DialogDefaults, DialogId, DialogSlot, StyleOptions } from './types';
2
2
  /**
3
3
  * Style layers from widest to narrowest. confirmAlert keeps the v3 look unless the app sets
4
4
  * confirmAlert defaults, so app-wide unstyled or classNames never reach it by surprise.
@@ -12,5 +12,11 @@ export type SlotProps = (slot: DialogSlot, extra?: string) => {
12
12
  export declare const slotsFor: (layers: readonly StyleOptions[]) => SlotProps;
13
13
  export declare const SlotContext: import("react").Context<SlotProps>;
14
14
  export declare const useSlot: () => SlotProps;
15
+ export declare const FrameContext: import("react").Context<{
16
+ id?: DialogId | undefined;
17
+ titleId?: string | undefined;
18
+ inBody?: boolean;
19
+ countTitle?: (change: (count: number) => number) => void;
20
+ }>;
15
21
  export declare function configureDialogs(defaults: DialogDefaults): void;
16
22
  export type { DialogClassNames };
@@ -35,6 +35,8 @@ export interface ReactConfirmAlertProps {
35
35
  unstyled?: boolean;
36
36
  /** classes per slot, added after the app defaults */
37
37
  classNames?: DialogClassNames;
38
+ /** use the new default look instead of the 3.0 one; customUI then sits in the dialog body */
39
+ skin?: boolean;
38
40
  }
39
41
  export type ConfirmAlertOptions = ReactConfirmAlertProps;
40
42
  export declare function confirmAlert(properties: ReactConfirmAlertProps): void;
@@ -5,5 +5,6 @@ export { DialogHost, useDialog } from './DialogHost';
5
5
  export { configureDialogs } from './classNames';
6
6
  export type { DialogHostProps } from './DialogHost';
7
7
  export { ConfirmDialog, Dialog } from './Dialog';
8
+ export { DialogLayout } from './DialogLayout';
8
9
  export type { ConfirmDialogProps, DialogProps } from './Dialog';
9
- export type { AlertOptions, BaseOptions, ButtonContext, ButtonDataAttributes, DialogClassNames, DialogDataAttributes, DialogDefaults, DialogSlot, SlotDataAttributes, StyleOptions, ConfirmOptions, CustomOptions, DialogButton, DialogHandle, DialogId, DialogKind, DialogRenderProps, PromptOptions, Strategy } from './types';
10
+ export type { AlertOptions, BaseOptions, ButtonContext, ButtonDataAttributes, DialogAction, DialogClassNames, DialogLayoutProps, DialogDataAttributes, DialogDefaults, DialogSlot, SlotDataAttributes, StyleOptions, ConfirmOptions, CustomOptions, DialogButton, DialogHandle, DialogId, DialogKind, DialogRenderProps, PromptOptions, Strategy } from './types';
@@ -1,4 +1,4 @@
1
- import type { ButtonHTMLAttributes, ComponentType, InputHTMLAttributes, KeyboardEvent, ReactNode } from 'react';
1
+ import type { ButtonHTMLAttributes, ComponentType, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactNode } from 'react';
2
2
  export type DialogId = string & {
3
3
  readonly __brand: 'DialogId';
4
4
  };
@@ -22,6 +22,7 @@ export interface DialogRenderProps<R> {
22
22
  descriptionId: string;
23
23
  title?: ReactNode | undefined;
24
24
  message?: ReactNode | undefined;
25
+ content?: ReactNode | undefined;
25
26
  buttons: readonly DialogButton<R>[];
26
27
  close: (value: R) => void;
27
28
  dismiss: () => void;
@@ -53,6 +54,7 @@ export interface DialogDefaults extends StyleOptions {
53
54
  confirmAlert?: (StyleOptions & {
54
55
  modal?: boolean | undefined;
55
56
  strategy?: Strategy | undefined;
57
+ skin?: boolean | undefined;
56
58
  }) | undefined;
57
59
  }
58
60
  /** Attributes on the frame, for CSS and Tailwind data-* variants. A type only, kept in step by tests. */
@@ -75,6 +77,8 @@ export interface ButtonDataAttributes extends SlotDataAttributes {
75
77
  export interface BaseOptions extends StyleOptions {
76
78
  title?: ReactNode | undefined;
77
79
  message?: ReactNode | undefined;
80
+ /** your own content between the message and the buttons */
81
+ content?: ReactNode | undefined;
78
82
  closeOnEscape?: boolean | undefined;
79
83
  closeOnClickOutside?: boolean | undefined;
80
84
  strategy?: Strategy | undefined;
@@ -124,6 +128,8 @@ export interface EntryOptions extends BaseOptions {
124
128
  onClickOutside?: (() => void) | undefined;
125
129
  /** @internal the v3 API: Escape from document keydown, blur on body.children[0] */
126
130
  legacy?: boolean | undefined;
131
+ /** @internal confirmAlert opted into the new default look */
132
+ skin?: boolean | undefined;
127
133
  /** @internal confirm's tone, shown as data-tone on the frame */
128
134
  tone?: 'default' | 'danger' | undefined;
129
135
  /** @internal v3 callback, runs before afterClose when the dialog unmounts */
@@ -136,3 +142,15 @@ export interface DialogHandle<R> {
136
142
  dismiss: () => void;
137
143
  update: (patch: Partial<Omit<CustomOptions<R>, 'kind'>>) => void;
138
144
  }
145
+ export interface DialogAction extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'children'> {
146
+ label: ReactNode;
147
+ variant?: 'primary' | 'secondary' | 'danger' | undefined;
148
+ /** return a Promise to show the button as busy until it settles; the dialog stays open */
149
+ onClick?: ((event: MouseEvent<HTMLButtonElement>) => void | Promise<unknown>) | undefined;
150
+ }
151
+ export interface DialogLayoutProps {
152
+ title?: ReactNode | undefined;
153
+ message?: ReactNode | undefined;
154
+ actions?: readonly DialogAction[] | undefined;
155
+ children?: ReactNode | undefined;
156
+ }