@planningcenter/sweetest-alert 1.8.0 → 2.0.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
@@ -21,7 +21,7 @@ This package requires the following peer dependencies:
21
21
 
22
22
  - `react` ^18.3.1 || ^19.0.0
23
23
  - `react-dom` ^18.3.1 || ^19.0.0
24
- - `@planningcenter/tapestry` ^2.10.1 || ^3.0.0
24
+ - `@planningcenter/tapestry` >=4
25
25
  - `@planningcenter/icons` ^15.29.1
26
26
 
27
27
  ## Usage
@@ -95,6 +95,49 @@ if (isConfirmed) {
95
95
  }
96
96
  ```
97
97
 
98
+ ### Async Confirm
99
+
100
+ If `onConfirm` returns a promise (or other thenable), the dialog stays open with a loading confirm button and a disabled cancel button until it settles. Resolving closes the dialog and adds the resolved value as `value` on the result. Escape and `close()` do nothing while it's pending — `cleanup()` is the only way to force the dialog closed mid-flight.
101
+
102
+ ```jsx
103
+ const { isConfirmed, value } = await SweetestAlert({
104
+ type: "warning",
105
+ title: "Save changes?",
106
+ content: "The dialog stays open, with a spinner, while the save is in flight.",
107
+ confirmButton: "Save",
108
+ onConfirm: async () => {
109
+ await saveChanges()
110
+ return { savedAt: new Date().toISOString() }
111
+ },
112
+ })
113
+
114
+ if (isConfirmed) {
115
+ console.log("saved", value)
116
+ }
117
+ ```
118
+
119
+ If it rejects, the dialog's content is replaced with a generic error message and a single "Okay" button. Dismissing that button — the promise doesn't resolve on rejection itself — resolves `{ isConfirmed: false, isDismissed: true }`. **The returned promise never rejects**, so a failed confirm can't be mistaken for a success by a caller using `.then()`/`await` without a `try`/`catch`:
120
+
121
+ ```jsx
122
+ SweetestAlert({
123
+ type: "danger",
124
+ title: "Delete this list?",
125
+ content: "This can't be undone.",
126
+ confirmButton: "Delete",
127
+ onConfirm: () => deleteList(),
128
+ })
129
+ ```
130
+
131
+ `isDismissed: true` now also covers a failed confirm, not just cancel/Escape — if you need to distinguish "the user backed out" from "the action failed," check `isConfirmed` first, since both cases share `isConfirmed: false`.
132
+
133
+ If you don't want the dialog to wait on the returned promise, don't return it:
134
+
135
+ ```jsx
136
+ onConfirm: () => {
137
+ void saveChanges()
138
+ }
139
+ ```
140
+
98
141
  ### Alert Types
99
142
 
100
143
  The component supports five visual types:
@@ -157,6 +200,42 @@ SweetestAlert({
157
200
  })
158
201
  ```
159
202
 
203
+ ### Confirmation Input
204
+
205
+ > Reserve this for destructive, hard-to-undo actions — deleting an organization, wiping a list, removing a person. The typing friction is the whole point, and it only stays meaningful if it's rare. Use discretion: on a routine confirm it's just an obstacle, and if people meet it often they learn to type past it without reading. If a plain confirm dialog would do, use a plain confirm dialog.
206
+
207
+ Pass `confirmation` to require the user to type an exact phrase before the confirm button enables:
208
+
209
+ ```jsx
210
+ SweetestAlert({
211
+ type: "danger",
212
+ title: `Delete ${list.name}?`,
213
+ content: "This permanently deletes the list and everything in it.",
214
+ confirmButton: "Delete list",
215
+ confirmation: { match: list.name },
216
+ onConfirm: () => destroyList(list),
217
+ })
218
+ ```
219
+
220
+ The confirm button stays disabled until the input's value — trimmed, and compared case-sensitively — equals `confirmation.match`. The input autofocuses when the dialog opens, and pressing Enter in it confirms once it matches. Blurring the field with a non-empty mismatch shows an inline error; it clears as soon as the value matches. This works with any `type` and composes with `hideCancel`.
221
+
222
+ Customize the label or helper text with `confirmation.label`/`confirmation.description`:
223
+
224
+ ```jsx
225
+ SweetestAlert({
226
+ type: "danger",
227
+ title: "Delete this organization?",
228
+ content: "Every list, person, and integration underneath it goes too.",
229
+ confirmButton: "Delete organization",
230
+ confirmation: {
231
+ match: organization.name,
232
+ label: "Organization name",
233
+ description: "Case sensitive.",
234
+ },
235
+ onConfirm: () => destroyOrganization(organization),
236
+ })
237
+ ```
238
+
160
239
  ## API
161
240
 
162
241
  ### Parameters
@@ -166,20 +245,27 @@ SweetestAlert({
166
245
  | `title` | **Required.** The main heading text displayed at the top of the modal. |
167
246
  | `content` | **Required.** The body content. Accepts plain text or React components (`string \| React.ReactNode`). |
168
247
  | `type` | The visual type of alert (`info`, `success`, `error`, `danger`, `warning`), affects icon and styling. Defaults to "warning". |
169
- | `onConfirm` | Callback executed when confirm button is clicked. |
170
- | `onCancel` | Callback executed when cancel button is clicked. |
248
+ | `onConfirm` | Callback executed when confirm button is clicked. If it returns a promise, the dialog stays open with a loading confirm button until it settles — see [Async Confirm](#async-confirm). |
249
+ | `onCancel` | Callback executed when cancel button is clicked. Not called when `onConfirm` rejects. |
171
250
  | `confirmButton` | Custom text for the confirm button. Defaults to "Okay". |
172
251
  | `hideCancel` | When `true`, hides the cancel button for simple notifications. Defaults to `false`. |
252
+ | `confirmation` | When set (`{ match, label?, description? }`), requires the user to type `match` before the confirm button is enabled. Case-sensitive; surrounding whitespace is ignored. Pressing Enter in the box confirms. An empty or whitespace-only `match` means no requirement. Reserve for destructive actions — see [Confirmation Input](#confirmation-input). |
173
253
 
174
254
  ### Return Value
175
255
 
176
- `SweetestAlert` returns a promise that resolves to `{ isConfirmed, isDismissed }` when the dialog is closed (via confirm, cancel, or Escape), plus:
256
+ `SweetestAlert` returns a promise that resolves to `{ isConfirmed, isDismissed }` when the dialog is closed (via confirm, cancel, Escape, or dismissing the error state after a rejected `onConfirm`), plus:
257
+
258
+ | Property | Description |
259
+ | -------- | ------------------------------------------------------------------------------------------------------------------ |
260
+ | `value` | The value `onConfirm`'s promise resolved with. Only present when `onConfirm` returned a promise and it resolved. |
261
+
262
+ The returned object is also merged with imperative controls:
177
263
 
178
- | Property | Description |
179
- | --------- | ----------------------------------------------------------------------------------------------- |
180
- | `show` | Re-opens the dialog (calls the native `showModal()`). |
181
- | `close` | Closes the dialog (calls the native `close()`), resolving the promise with `isDismissed: true`. |
182
- | `cleanup` | Unmounts the React root and removes the dialog from the DOM immediately. |
264
+ | Property | Description |
265
+ | --------- | ------------------------------------------------------------------------------------------------------------ |
266
+ | `show` | Re-opens the dialog (calls the native `showModal()`). |
267
+ | `close` | Closes the dialog (calls the native `close()`), resolving the promise with `isDismissed: true`. Does nothing while `onConfirm` is pending. |
268
+ | `cleanup` | Unmounts the React root and removes the dialog from the DOM immediately, regardless of pending state. |
183
269
 
184
270
  ## Development
185
271
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require("react");let e=require("react-dom/client"),t=require("@planningcenter/tapestry"),n=require("react/jsx-runtime");var r=`M8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8M12.57,6.07l-1.14,-1.14l-4.43,4.44l-2.43,-2.44l-1.14,1.14l3.57,3.56z`,i=`M15.669,13.676c0.13528,0.2343 0.13541,0.52294 0.00036,0.75737c-0.13505,0.23443 -0.38481,0.37911 -0.65536,0.37963h-14.028c-0.27081,0.0001 -0.52105,-0.14447 -0.65623,-0.37913c-0.13518,-0.23466 -0.13471,-0.52366 0.00123,-0.75787l7.014,-12.115c0.13497,-0.23419 0.3847,-0.37849 0.655,-0.37849c0.2703,0 0.52003,0.14431 0.655,0.37849l7.014,12.115M8.757,11.028h-1.514v1.514h1.514zM8.757,6.485h-1.514v3.029h1.514z`,a=`M8.8,4h-1.6v1.6h1.6zM8.8,7.2h-1.6v4.8h1.6zM8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8`,o=`M11.395,10.262l-2.262,-2.262l2.262,-2.262l-1.132,-1.132l-2.262,2.262l-2.263,-2.262l-1.131,1.132l2.262,2.262l-2.262,2.262l1.131,1.132l2.263,-2.262l2.262,2.262zM13.659,2.343c3.12218,3.12514 3.12218,8.18886 0,11.314c-2.28834,2.28863 -5.73017,2.97305 -8.72,1.734c-2.99134,-1.23749 -4.94222,-4.15558 -4.94244,-7.39278c-0.00022,-3.2372 1.95027,-6.15555 4.94143,-7.39345c2.99117,-1.2379 6.43353,-0.55139 8.72101,1.73923`;function s(){let e;return{promise:new Promise(t=>{e=t}),resolve:e}}function c({onCancel:c,onConfirm:l,confirmButton:u=`Okay`,content:d,hideCancel:f=!1,title:p,type:m=`warning`}){let h=document.createElement(`dialog`);h.className=`pco-sweetest-alert`,document.body.appendChild(h);let g=(0,e.createRoot)(h),_=!1,{promise:v,resolve:y}=s(),b=()=>{_||(_=!0,y({isConfirmed:!1,isDismissed:!0}),g.unmount(),document.body.removeChild(h))},x=()=>{_||(y({isConfirmed:!0,isDismissed:!1}),b(),l?.())},S=()=>{_||(y({isConfirmed:!1,isDismissed:!0}),b(),c?.())};return g.render((0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(`svg`,{className:`pco-sweetest-alert__icon pco-sweetest-alert--${m}`,width:`48`,height:`48`,viewBox:`0 0 16 16`,"aria-hidden":`true`,children:(0,n.jsx)(`path`,{d:(()=>{switch(m){case`success`:return r;case`error`:return o;case`info`:return a;case`danger`:case`warning`:return i}})()})}),(0,n.jsx)(`h2`,{className:`pco-sweetest-alert__title`,children:p}),(0,n.jsx)(`div`,{className:`pco-sweetest-alert__body`,children:typeof d==`string`?(0,n.jsx)(`p`,{children:d}):d}),(0,n.jsxs)(`div`,{className:`pco-sweetest-alert__actions`,children:[!f&&(0,n.jsx)(t.Button,{label:`Cancel`,kind:`ghost`,onClick:S}),(0,n.jsx)(t.Button,{label:u,kind:m===`danger`?`delete`:`primary`,onClick:x})]})]})),h.showModal(),h.addEventListener(`close`,S),Object.assign(v,{show:()=>h.showModal(),close:()=>h.close(),cleanup:b})}exports.SweetestAlert=c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require("react");let e=require("react-dom/client"),t=require("@planningcenter/tapestry"),n=require("react/jsx-runtime");var r=`M8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8M12.57,6.07l-1.14,-1.14l-4.43,4.44l-2.43,-2.44l-1.14,1.14l3.57,3.56z`,i=`M15.669,13.676c0.13528,0.2343 0.13541,0.52294 0.00036,0.75737c-0.13505,0.23443 -0.38481,0.37911 -0.65536,0.37963h-14.028c-0.27081,0.0001 -0.52105,-0.14447 -0.65623,-0.37913c-0.13518,-0.23466 -0.13471,-0.52366 0.00123,-0.75787l7.014,-12.115c0.13497,-0.23419 0.3847,-0.37849 0.655,-0.37849c0.2703,0 0.52003,0.14431 0.655,0.37849l7.014,12.115M8.757,11.028h-1.514v1.514h1.514zM8.757,6.485h-1.514v3.029h1.514z`,a=`M8.8,4h-1.6v1.6h1.6zM8.8,7.2h-1.6v4.8h1.6zM8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8`,o=`M11.395,10.262l-2.262,-2.262l2.262,-2.262l-1.132,-1.132l-2.262,2.262l-2.263,-2.262l-1.131,1.132l2.262,2.262l-2.262,2.262l1.131,1.132l2.263,-2.262l2.262,2.262zM13.659,2.343c3.12218,3.12514 3.12218,8.18886 0,11.314c-2.28834,2.28863 -5.73017,2.97305 -8.72,1.734c-2.99134,-1.23749 -4.94222,-4.15558 -4.94244,-7.39278c-0.00022,-3.2372 1.95027,-6.15555 4.94143,-7.39345c2.99117,-1.2379 6.43353,-0.55139 8.72101,1.73923`,s=`Uh oh!`,c=`Something went wrong, please try again. If the error continues, please contact support.`,l=e=>`Doesn’t match. Type ${e} exactly, including capitalization.`;function u(){let e;return{promise:new Promise(t=>{e=t}),resolve:e}}function d(e){return typeof(e!==null&&(typeof e==`object`||typeof e==`function`)?e.then:void 0)==`function`}function f({onCancel:f,onConfirm:p,confirmButton:m=`Okay`,confirmation:h,content:g,hideCancel:_=!1,title:v,type:y=`warning`}){let b=document.createElement(`dialog`);b.className=`pco-sweetest-alert`,document.body.appendChild(b);let x=(0,e.createRoot)(b),S=h?.match.trim()??``,C=S.length>0,w=!1,T=`idle`,E=``,D=!1,{promise:O,resolve:k}=u(),A=()=>{w||(w=!0,T=`idle`,k({isConfirmed:!1,isDismissed:!0}),x.unmount(),b.remove())},j=e=>{w||(k({isConfirmed:!1,isDismissed:!0}),A(),e&&f?.())},M=async e=>{T=`pending`,H();let t;try{t=await e}catch{if(w)return;T=`error`,H();return}w||(k({isConfirmed:!0,isDismissed:!1,value:t}),A())},N=()=>E.trim()===S,P=()=>{if(w||T!==`idle`||C&&!N())return;let e=p?.();if(!d(e)){k({isConfirmed:!0,isDismissed:!1}),A();return}M(e)},F=e=>{let t=N();E=e.target.value;let n=D&&E.trim()!==``&&!N(),r=N()!==t||n!==D;D=n,r&&H()},I=()=>{let e=E.trim()!==``&&!N();e!==D&&(D=e,H())},L=e=>{e.preventDefault(),P()},R=e=>{T===`pending`&&e.preventDefault()},z=()=>{w||T===`pending`||j(T!==`error`)},B=e=>{switch(e){case`success`:return r;case`error`:return o;case`info`:return a;case`danger`:case`warning`:return i}},V=()=>{let e=T===`error`,r=T===`pending`,i=e?`error`:y;return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(`svg`,{className:`pco-sweetest-alert__icon pco-sweetest-alert--${i}`,width:`48`,height:`48`,viewBox:`0 0 16 16`,"aria-hidden":`true`,children:(0,n.jsx)(`path`,{d:B(i)})}),(0,n.jsx)(`h2`,{className:`pco-sweetest-alert__title`,children:e?s:v}),(0,n.jsx)(`div`,{className:`pco-sweetest-alert__body`,children:e?(0,n.jsx)(`p`,{children:c}):typeof g==`string`?(0,n.jsx)(`p`,{children:g}):g}),!e&&C&&(0,n.jsx)(`form`,{className:`pco-sweetest-alert__confirmation`,onSubmit:L,children:(0,n.jsx)(t.Input,{autoCapitalize:`none`,autoComplete:`off`,autoFocus:!0,description:D?l(S):h.description,disabled:r,invalid:D,label:h.label??(0,n.jsxs)(n.Fragment,{children:[`Type `,(0,n.jsx)(`strong`,{children:S}),` to confirm`]}),onBlur:I,onChange:F,spellCheck:!1})}),(0,n.jsxs)(`div`,{className:`pco-sweetest-alert__actions`,children:[!e&&!_&&(0,n.jsx)(t.Button,{label:`Cancel`,kind:`ghost`,disabled:r,onClick:()=>j(!0)}),(0,n.jsx)(t.Button,{label:e?`Okay`:m,kind:!e&&y===`danger`?`delete`:`primary`,loading:r,disabled:!e&&C&&!N(),onClick:e?()=>j(!1):P})]})]})},H=()=>x.render(V());return H(),b.showModal(),b.addEventListener(`cancel`,R),b.addEventListener(`close`,z),Object.assign(O,{show:()=>b.showModal(),close:()=>{T!==`pending`&&b.close()},cleanup:A})}exports.SweetestAlert=f;
package/dist/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import "react";
2
2
  import { createRoot as e } from "react-dom/client";
3
- import { Button as t } from "@planningcenter/tapestry";
4
- import { Fragment as n, jsx as r, jsxs as i } from "react/jsx-runtime";
3
+ import { Button as t, Input as n } from "@planningcenter/tapestry";
4
+ import { Fragment as r, jsx as i, jsxs as a } from "react/jsx-runtime";
5
5
  //#region node_modules/@planningcenter/icons/paths/general.mjs
6
- var a = "M8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8M12.57,6.07l-1.14,-1.14l-4.43,4.44l-2.43,-2.44l-1.14,1.14l3.57,3.56z", o = "M15.669,13.676c0.13528,0.2343 0.13541,0.52294 0.00036,0.75737c-0.13505,0.23443 -0.38481,0.37911 -0.65536,0.37963h-14.028c-0.27081,0.0001 -0.52105,-0.14447 -0.65623,-0.37913c-0.13518,-0.23466 -0.13471,-0.52366 0.00123,-0.75787l7.014,-12.115c0.13497,-0.23419 0.3847,-0.37849 0.655,-0.37849c0.2703,0 0.52003,0.14431 0.655,0.37849l7.014,12.115M8.757,11.028h-1.514v1.514h1.514zM8.757,6.485h-1.514v3.029h1.514z", s = "M8.8,4h-1.6v1.6h1.6zM8.8,7.2h-1.6v4.8h1.6zM8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8", c = "M11.395,10.262l-2.262,-2.262l2.262,-2.262l-1.132,-1.132l-2.262,2.262l-2.263,-2.262l-1.131,1.132l2.262,2.262l-2.262,2.262l1.131,1.132l2.263,-2.262l2.262,2.262zM13.659,2.343c3.12218,3.12514 3.12218,8.18886 0,11.314c-2.28834,2.28863 -5.73017,2.97305 -8.72,1.734c-2.99134,-1.23749 -4.94222,-4.15558 -4.94244,-7.39278c-0.00022,-3.2372 1.95027,-6.15555 4.94143,-7.39345c2.99117,-1.2379 6.43353,-0.55139 8.72101,1.73923";
7
- //#endregion
8
- //#region src/sweetest_alert.tsx
9
- function l() {
6
+ var o = "M8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8M12.57,6.07l-1.14,-1.14l-4.43,4.44l-2.43,-2.44l-1.14,1.14l3.57,3.56z", s = "M15.669,13.676c0.13528,0.2343 0.13541,0.52294 0.00036,0.75737c-0.13505,0.23443 -0.38481,0.37911 -0.65536,0.37963h-14.028c-0.27081,0.0001 -0.52105,-0.14447 -0.65623,-0.37913c-0.13518,-0.23466 -0.13471,-0.52366 0.00123,-0.75787l7.014,-12.115c0.13497,-0.23419 0.3847,-0.37849 0.655,-0.37849c0.2703,0 0.52003,0.14431 0.655,0.37849l7.014,12.115M8.757,11.028h-1.514v1.514h1.514zM8.757,6.485h-1.514v3.029h1.514z", c = "M8.8,4h-1.6v1.6h1.6zM8.8,7.2h-1.6v4.8h1.6zM8,0c4.41828,0 8,3.58172 8,8c0,4.41828 -3.58172,8 -8,8c-4.41828,0 -8,-3.58172 -8,-8c0,-4.41828 3.58172,-8 8,-8", l = "M11.395,10.262l-2.262,-2.262l2.262,-2.262l-1.132,-1.132l-2.262,2.262l-2.263,-2.262l-1.131,1.132l2.262,2.262l-2.262,2.262l1.131,1.132l2.263,-2.262l2.262,2.262zM13.659,2.343c3.12218,3.12514 3.12218,8.18886 0,11.314c-2.28834,2.28863 -5.73017,2.97305 -8.72,1.734c-2.99134,-1.23749 -4.94222,-4.15558 -4.94244,-7.39278c-0.00022,-3.2372 1.95027,-6.15555 4.94143,-7.39345c2.99117,-1.2379 6.43353,-0.55139 8.72101,1.73923", u = "Uh oh!", d = "Something went wrong, please try again. If the error continues, please contact support.", f = (e) => `Doesn’t match. Type ${e} exactly, including capitalization.`;
7
+ function p() {
10
8
  let e;
11
9
  return {
12
10
  promise: new Promise((t) => {
@@ -15,67 +13,133 @@ function l() {
15
13
  resolve: e
16
14
  };
17
15
  }
18
- function u({ onCancel: u, onConfirm: d, confirmButton: f = "Okay", content: p, hideCancel: m = !1, title: h, type: g = "warning" }) {
19
- let _ = document.createElement("dialog");
20
- _.className = "pco-sweetest-alert", document.body.appendChild(_);
21
- let v = e(_), y = !1, { promise: b, resolve: x } = l(), S = () => {
22
- y || (y = !0, x({
16
+ function m(e) {
17
+ return typeof (e !== null && (typeof e == "object" || typeof e == "function") ? e.then : void 0) == "function";
18
+ }
19
+ function h({ onCancel: h, onConfirm: g, confirmButton: _ = "Okay", confirmation: v, content: y, hideCancel: b = !1, title: x, type: S = "warning" }) {
20
+ let C = document.createElement("dialog");
21
+ C.className = "pco-sweetest-alert", document.body.appendChild(C);
22
+ let w = e(C), T = v?.match.trim() ?? "", E = T.length > 0, D = !1, O = "idle", k = "", A = !1, { promise: j, resolve: M } = p(), N = () => {
23
+ D || (D = !0, O = "idle", M({
23
24
  isConfirmed: !1,
24
25
  isDismissed: !0
25
- }), v.unmount(), document.body.removeChild(_));
26
- }, C = () => {
27
- y || (x({
28
- isConfirmed: !0,
29
- isDismissed: !1
30
- }), S(), d?.());
31
- }, w = () => {
32
- y || (x({
26
+ }), w.unmount(), C.remove());
27
+ }, P = (e) => {
28
+ D || (M({
33
29
  isConfirmed: !1,
34
30
  isDismissed: !0
35
- }), S(), u?.());
36
- };
37
- return v.render(/* @__PURE__ */ i(n, { children: [
38
- /* @__PURE__ */ r("svg", {
39
- className: `pco-sweetest-alert__icon pco-sweetest-alert--${g}`,
40
- width: "48",
41
- height: "48",
42
- viewBox: "0 0 16 16",
43
- "aria-hidden": "true",
44
- children: /* @__PURE__ */ r("path", { d: (() => {
45
- switch (g) {
46
- case "success": return a;
47
- case "error": return c;
48
- case "info": return s;
49
- case "danger":
50
- case "warning": return o;
51
- }
52
- })() })
53
- }),
54
- /* @__PURE__ */ r("h2", {
55
- className: "pco-sweetest-alert__title",
56
- children: h
57
- }),
58
- /* @__PURE__ */ r("div", {
59
- className: "pco-sweetest-alert__body",
60
- children: typeof p == "string" ? /* @__PURE__ */ r("p", { children: p }) : p
61
- }),
62
- /* @__PURE__ */ i("div", {
63
- className: "pco-sweetest-alert__actions",
64
- children: [!m && /* @__PURE__ */ r(t, {
65
- label: "Cancel",
66
- kind: "ghost",
67
- onClick: w
68
- }), /* @__PURE__ */ r(t, {
69
- label: f,
70
- kind: g === "danger" ? "delete" : "primary",
71
- onClick: C
72
- })]
73
- })
74
- ] })), _.showModal(), _.addEventListener("close", w), Object.assign(b, {
75
- show: () => _.showModal(),
76
- close: () => _.close(),
77
- cleanup: S
31
+ }), N(), e && h?.());
32
+ }, F = async (e) => {
33
+ O = "pending", G();
34
+ let t;
35
+ try {
36
+ t = await e;
37
+ } catch {
38
+ if (D) return;
39
+ O = "error", G();
40
+ return;
41
+ }
42
+ D || (M({
43
+ isConfirmed: !0,
44
+ isDismissed: !1,
45
+ value: t
46
+ }), N());
47
+ }, I = () => k.trim() === T, L = () => {
48
+ if (D || O !== "idle" || E && !I()) return;
49
+ let e = g?.();
50
+ if (!m(e)) {
51
+ M({
52
+ isConfirmed: !0,
53
+ isDismissed: !1
54
+ }), N();
55
+ return;
56
+ }
57
+ F(e);
58
+ }, R = (e) => {
59
+ let t = I();
60
+ k = e.target.value;
61
+ let n = A && k.trim() !== "" && !I(), r = I() !== t || n !== A;
62
+ A = n, r && G();
63
+ }, z = () => {
64
+ let e = k.trim() !== "" && !I();
65
+ e !== A && (A = e, G());
66
+ }, B = (e) => {
67
+ e.preventDefault(), L();
68
+ }, V = (e) => {
69
+ O === "pending" && e.preventDefault();
70
+ }, H = () => {
71
+ D || O === "pending" || P(O !== "error");
72
+ }, U = (e) => {
73
+ switch (e) {
74
+ case "success": return o;
75
+ case "error": return l;
76
+ case "info": return c;
77
+ case "danger":
78
+ case "warning": return s;
79
+ }
80
+ }, W = () => {
81
+ let e = O === "error", o = O === "pending", s = e ? "error" : S;
82
+ return /* @__PURE__ */ a(r, { children: [
83
+ /* @__PURE__ */ i("svg", {
84
+ className: `pco-sweetest-alert__icon pco-sweetest-alert--${s}`,
85
+ width: "48",
86
+ height: "48",
87
+ viewBox: "0 0 16 16",
88
+ "aria-hidden": "true",
89
+ children: /* @__PURE__ */ i("path", { d: U(s) })
90
+ }),
91
+ /* @__PURE__ */ i("h2", {
92
+ className: "pco-sweetest-alert__title",
93
+ children: e ? u : x
94
+ }),
95
+ /* @__PURE__ */ i("div", {
96
+ className: "pco-sweetest-alert__body",
97
+ children: e ? /* @__PURE__ */ i("p", { children: d }) : typeof y == "string" ? /* @__PURE__ */ i("p", { children: y }) : y
98
+ }),
99
+ !e && E && /* @__PURE__ */ i("form", {
100
+ className: "pco-sweetest-alert__confirmation",
101
+ onSubmit: B,
102
+ children: /* @__PURE__ */ i(n, {
103
+ autoCapitalize: "none",
104
+ autoComplete: "off",
105
+ autoFocus: !0,
106
+ description: A ? f(T) : v.description,
107
+ disabled: o,
108
+ invalid: A,
109
+ label: v.label ?? /* @__PURE__ */ a(r, { children: [
110
+ "Type ",
111
+ /* @__PURE__ */ i("strong", { children: T }),
112
+ " to confirm"
113
+ ] }),
114
+ onBlur: z,
115
+ onChange: R,
116
+ spellCheck: !1
117
+ })
118
+ }),
119
+ /* @__PURE__ */ a("div", {
120
+ className: "pco-sweetest-alert__actions",
121
+ children: [!e && !b && /* @__PURE__ */ i(t, {
122
+ label: "Cancel",
123
+ kind: "ghost",
124
+ disabled: o,
125
+ onClick: () => P(!0)
126
+ }), /* @__PURE__ */ i(t, {
127
+ label: e ? "Okay" : _,
128
+ kind: !e && S === "danger" ? "delete" : "primary",
129
+ loading: o,
130
+ disabled: !e && E && !I(),
131
+ onClick: e ? () => P(!1) : L
132
+ })]
133
+ })
134
+ ] });
135
+ }, G = () => w.render(W());
136
+ return G(), C.showModal(), C.addEventListener("cancel", V), C.addEventListener("close", H), Object.assign(j, {
137
+ show: () => C.showModal(),
138
+ close: () => {
139
+ O !== "pending" && C.close();
140
+ },
141
+ cleanup: N
78
142
  });
79
143
  }
80
144
  //#endregion
81
- export { u as SweetestAlert };
145
+ export { h as SweetestAlert };
package/dist/style.css CHANGED
@@ -52,6 +52,13 @@
52
52
  margin-bottom: var(--t-spacing-4);
53
53
  }
54
54
 
55
+ .pco-sweetest-alert__confirmation {
56
+ contain: inline-size;
57
+ margin-bottom: var(--t-spacing-4);
58
+ overflow-wrap: break-word;
59
+ text-align: left;
60
+ }
61
+
55
62
  .pco-sweetest-alert__actions {
56
63
  align-items: center;
57
64
  border-top: var(--t-border-width) solid var(--t-border-color);
@@ -3,17 +3,75 @@ import React from "react";
3
3
  type AlertTypes = "info" | "success" | "error" | "danger" | "warning";
4
4
  /** Resolution value of the promise returned by {@link SweetestAlert}. */
5
5
  type AlertResult = {
6
- /** `true` when the user clicked the confirm button. */
6
+ /** `true` when the user clicked the confirm button (and `onConfirm`, if it returned a promise, resolved). */
7
7
  isConfirmed: boolean;
8
- /** `true` when the dialog was dismissed via cancel, Escape, `close()`, or `cleanup()`. */
8
+ /** `true` when the dialog was dismissed via cancel, Escape, `close()`, `cleanup()`, or a rejected `onConfirm`. */
9
9
  isDismissed: boolean;
10
+ /**
11
+ * The value `onConfirm` resolved with, when `onConfirm` returned a promise
12
+ * (or other thenable). Omitted entirely — not `undefined` — when `onConfirm`
13
+ * didn't return one, so the result shape for existing callers is unchanged.
14
+ */
15
+ value?: unknown;
16
+ };
17
+ /** Options for the type-to-confirm input, for consequential actions. */
18
+ type AlertConfirmation = {
19
+ /**
20
+ * The text the user must type before the confirm button enables. Compared
21
+ * after trimming both sides, and case-sensitively. An empty or
22
+ * whitespace-only value means no requirement, so a caller can pass a name
23
+ * that may not have loaded yet without shipping an unconfirmable dialog.
24
+ */
25
+ match: string;
26
+ /**
27
+ * Label for the input. Defaults to `Type <strong>{match}</strong> to
28
+ * confirm`.
29
+ */
30
+ label?: React.ReactNode;
31
+ /**
32
+ * Helper text below the input. Replaced by the mismatch message while the
33
+ * input is in its invalid state.
34
+ */
35
+ description?: React.ReactNode;
10
36
  };
11
37
  /** Options for {@link SweetestAlert}. */
12
38
  type AlertOptions = {
13
- /** Callback executed when the cancel button is clicked, the dialog is dismissed via Escape, or `close()` is called. */
39
+ /** Callback executed when the cancel button is clicked, the dialog is dismissed via Escape, or `close()` is called. Not called when `onConfirm` rejects. */
14
40
  onCancel?: () => void;
15
- /** Callback executed when the confirm button is clicked. */
16
- onConfirm?: () => void;
41
+ /**
42
+ * Callback executed when the confirm button is clicked.
43
+ *
44
+ * If it returns a promise (or other thenable), the dialog stays open with a
45
+ * loading confirm button and a disabled cancel button until it settles:
46
+ * - Resolves → the dialog closes and the returned promise resolves with
47
+ * `{ isConfirmed: true, isDismissed: false, value }`.
48
+ * - Rejects → the dialog stays open and its content is replaced with a
49
+ * generic error message and a single "Okay" button. Dismissing it
50
+ * resolves `{ isConfirmed: false, isDismissed: true }` — the returned
51
+ * promise never rejects, so a failed confirm can't be mistaken for a
52
+ * success by a caller using `.then()`/`await` without a `try`/`catch`.
53
+ *
54
+ * While pending, Escape and `close()` do nothing; `cleanup()` remains the
55
+ * only way to force the dialog closed.
56
+ *
57
+ * Only a rejected promise is treated as a failure — a synchronous throw is
58
+ * not caught, so it skips both the resolve and reject paths above. The
59
+ * dialog is left open and interactive (a user can still Cancel/Escape out
60
+ * of it), but the promise `SweetestAlert()` returned never settles, so an
61
+ * `await` on it hangs until then. Reject the returned promise instead of
62
+ * throwing.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const { isConfirmed, value } = await SweetestAlert({
67
+ * type: "danger",
68
+ * title: "Delete Item",
69
+ * content: "Are you sure you want to delete this item?",
70
+ * onConfirm: () => deleteItem(),
71
+ * })
72
+ * ```
73
+ */
74
+ onConfirm?: () => unknown;
17
75
  /** Text for the confirm button. Defaults to `"Okay"`. */
18
76
  confirmButton?: string;
19
77
  /** Required. The body content explaining the alert. Accepts a string or React node. */
@@ -24,6 +82,33 @@ type AlertOptions = {
24
82
  title: string;
25
83
  /** Visual type of alert; affects icon and styling. Defaults to `"warning"`. */
26
84
  type?: AlertTypes;
85
+ /**
86
+ * When set, requires the user to type `confirmation.match` before the
87
+ * confirm button becomes clickable — the "type the name to confirm"
88
+ * pattern. Works with any `type` and with `hideCancel`.
89
+ *
90
+ * Renders a focused, labeled text input between the content and the
91
+ * buttons. Pressing Enter in the box confirms, once it matches.
92
+ *
93
+ * Reserve this for destructive, hard-to-undo actions — deleting an
94
+ * organization, wiping a list, removing a person. The friction is the
95
+ * point, and it only reads as meaningful if it's rare. Use discretion: on
96
+ * a routine confirm it's just an obstacle, and it trains people to type
97
+ * past it.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const { isConfirmed } = await SweetestAlert({
102
+ * type: "danger",
103
+ * title: `Delete ${list.name}?`,
104
+ * content: "This permanently deletes the list and everything in it.",
105
+ * confirmButton: "Delete list",
106
+ * confirmation: { match: list.name },
107
+ * onConfirm: () => destroyList(list),
108
+ * })
109
+ * ```
110
+ */
111
+ confirmation?: AlertConfirmation;
27
112
  };
28
113
  /**
29
114
  * Shows a modal alert/confirmation dialog using a native `<dialog>` element.
@@ -44,14 +129,15 @@ type AlertOptions = {
44
129
  * ```
45
130
  *
46
131
  * @returns A promise that resolves to {@link AlertResult} when the dialog
47
- * closes (confirm, cancel, or Escape), merged with:
132
+ * closes (confirm, cancel, Escape, or a rejected `onConfirm`), merged with:
48
133
  * - `show()` — re-opens the dialog (calls the native `showModal()`)
49
134
  * - `close()` — closes the dialog (calls the native `close()`), resolving
50
- * the promise with `isDismissed: true` and invoking `onCancel` if provided
135
+ * the promise with `isDismissed: true` and invoking `onCancel` if provided.
136
+ * Does nothing while `onConfirm` is pending.
51
137
  * - `cleanup()` — unmounts the React root and removes the dialog from the
52
- * DOM immediately
138
+ * DOM immediately, regardless of pending state
53
139
  */
54
- export declare function SweetestAlert({ onCancel, onConfirm, confirmButton, content, hideCancel, title, type, }: AlertOptions): Promise<AlertResult> & {
140
+ export declare function SweetestAlert({ onCancel, onConfirm, confirmButton, confirmation, content, hideCancel, title, type, }: AlertOptions): Promise<AlertResult> & {
55
141
  show: () => void;
56
142
  close: () => void;
57
143
  cleanup: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/sweetest-alert",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "The sweetest alert ever",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.14.1+sha512.64df448055b2d37ba269d7db535a469b8da93f8ef1140c25fd7a83c00a8fbaacb214ca0e02553b92a2c54cef78bb67d0b4817fab02001df0e24fac0faccc3b42",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@planningcenter/icons": "^15.29.1",
45
- "@planningcenter/tapestry": ">=2.10.1",
45
+ "@planningcenter/tapestry": ">=4",
46
46
  "react": "^18.3.1 || ^19.0.0",
47
47
  "react-dom": "^18.3.1 || ^19.0.0"
48
48
  },
@@ -50,7 +50,7 @@
50
50
  "@eslint/js": "^9.37.0",
51
51
  "@planningcenter/icons": "^15.29.1",
52
52
  "@planningcenter/stylelint-config-pco": "^1.0.0",
53
- "@planningcenter/tapestry": ">=3.4.0",
53
+ "@planningcenter/tapestry": ">=4",
54
54
  "@testing-library/dom": "^10.4.1",
55
55
  "@testing-library/jest-dom": "^6.9.1",
56
56
  "@testing-library/react": "^16.3.0",