@planningcenter/sweetest-alert 1.9.0 → 2.0.1

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,9 +21,51 @@ 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
+ ### Using with React 17
28
+
29
+ The peer dependency range is `react`/`react-dom` `^18.3.1 || ^19.0.0` because sweetest-alert statically imports `createRoot` from `react-dom/client`, which doesn't exist on React 17. If your app is still on React 17 (as `giving` was — see [planningcenter/giving#8757](https://github.com/planningcenter/giving/pull/8757)), you can't upgrade react-dom just for this package, but you can shim the import your bundler resolves instead of the real module.
30
+
31
+ Add a shim implementing the subset of the `react-dom/client` API sweetest-alert actually uses (`createRoot(container).render()`/`.unmount()`), backed by React 17's real APIs:
32
+
33
+ ```js
34
+ // app/javascript/utils/react_dom_client_shim.js
35
+ import ReactDOM from "react-dom"
36
+
37
+ // react-dom/client doesn't exist on React 17. sweetest-alert statically
38
+ // imports createRoot from it, which fails at build time otherwise. This
39
+ // shim implements just the subset it actually uses so the import resolves
40
+ // and behaves the same as it would on React 18. Remove once on React 18
41
+ // and this can be dropped in favor of the real react-dom/client.
42
+ export function createRoot(container) {
43
+ return {
44
+ render: (element) => {
45
+ ReactDOM.render(element, container)
46
+ },
47
+ unmount: () => ReactDOM.unmountComponentAtNode(container),
48
+ }
49
+ }
50
+ ```
51
+
52
+ Then alias `react-dom/client` to it in your bundler config. For Vite:
53
+
54
+ ```js
55
+ // vite.config.mjs
56
+ resolve: {
57
+ alias: [
58
+ // ...your other aliases,
59
+ {
60
+ find: "react-dom/client",
61
+ replacement: resolve(__dirname, "app/javascript/utils/react_dom_client_shim.js"),
62
+ },
63
+ ],
64
+ },
65
+ ```
66
+
67
+ If you also run tests through Vitest, add the same alias to its config (or to whatever `resolve.alias` it merges from your Vite config) so component tests resolve the shim too.
68
+
27
69
  ## Usage
28
70
 
29
71
  ### Basic Example
@@ -200,6 +242,42 @@ SweetestAlert({
200
242
  })
201
243
  ```
202
244
 
245
+ ### Confirmation Input
246
+
247
+ > 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.
248
+
249
+ Pass `confirmation` to require the user to type an exact phrase before the confirm button enables:
250
+
251
+ ```jsx
252
+ SweetestAlert({
253
+ type: "danger",
254
+ title: `Delete ${list.name}?`,
255
+ content: "This permanently deletes the list and everything in it.",
256
+ confirmButton: "Delete list",
257
+ confirmation: { match: list.name },
258
+ onConfirm: () => destroyList(list),
259
+ })
260
+ ```
261
+
262
+ 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`.
263
+
264
+ Customize the label or helper text with `confirmation.label`/`confirmation.description`:
265
+
266
+ ```jsx
267
+ SweetestAlert({
268
+ type: "danger",
269
+ title: "Delete this organization?",
270
+ content: "Every list, person, and integration underneath it goes too.",
271
+ confirmButton: "Delete organization",
272
+ confirmation: {
273
+ match: organization.name,
274
+ label: "Organization name",
275
+ description: "Case sensitive.",
276
+ },
277
+ onConfirm: () => destroyOrganization(organization),
278
+ })
279
+ ```
280
+
203
281
  ## API
204
282
 
205
283
  ### Parameters
@@ -213,6 +291,7 @@ SweetestAlert({
213
291
  | `onCancel` | Callback executed when cancel button is clicked. Not called when `onConfirm` rejects. |
214
292
  | `confirmButton` | Custom text for the confirm button. Defaults to "Okay". |
215
293
  | `hideCancel` | When `true`, hides the cancel button for simple notifications. Defaults to `false`. |
294
+ | `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). |
216
295
 
217
296
  ### Return Value
218
297
 
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`,s=`Uh oh!`,c=`Something went wrong, please try again. If the error continues, please contact support.`;function l(){let e;return{promise:new Promise(t=>{e=t}),resolve:e}}function u(e){return typeof(e!==null&&(typeof e==`object`||typeof e==`function`)?e.then:void 0)==`function`}function d({onCancel:d,onConfirm:f,confirmButton:p=`Okay`,content:m,hideCancel:h=!1,title:g,type:_=`warning`}){let v=document.createElement(`dialog`);v.className=`pco-sweetest-alert`,document.body.appendChild(v);let y=(0,e.createRoot)(v),b=!1,x=`idle`,{promise:S,resolve:C}=l(),w=()=>{b||(b=!0,x=`idle`,C({isConfirmed:!1,isDismissed:!0}),y.unmount(),v.remove())},T=e=>{b||(C({isConfirmed:!1,isDismissed:!0}),w(),e&&d?.())},E=async e=>{x=`pending`,M();let t;try{t=await e}catch{if(b)return;x=`error`,M();return}b||(C({isConfirmed:!0,isDismissed:!1,value:t}),w())},D=()=>{if(b||x!==`idle`)return;let e=f?.();if(!u(e)){C({isConfirmed:!0,isDismissed:!1}),w();return}E(e)},O=e=>{x===`pending`&&e.preventDefault()},k=()=>{b||x===`pending`||T(x!==`error`)},A=e=>{switch(e){case`success`:return r;case`error`:return o;case`info`:return a;case`danger`:case`warning`:return i}},j=()=>{let e=x===`error`,r=x===`pending`,i=e?`error`:_;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:A(i)})}),(0,n.jsx)(`h2`,{className:`pco-sweetest-alert__title`,children:e?s:g}),(0,n.jsx)(`div`,{className:`pco-sweetest-alert__body`,children:e?(0,n.jsx)(`p`,{children:c}):typeof m==`string`?(0,n.jsx)(`p`,{children:m}):m}),(0,n.jsxs)(`div`,{className:`pco-sweetest-alert__actions`,children:[!e&&!h&&(0,n.jsx)(t.Button,{label:`Cancel`,kind:`ghost`,disabled:r,onClick:()=>T(!0)}),(0,n.jsx)(t.Button,{label:e?`Okay`:p,kind:!e&&_===`danger`?`delete`:`primary`,loading:r,onClick:e?()=>T(!1):D})]})]})},M=()=>y.render(j());return M(),v.showModal(),v.addEventListener(`cancel`,O),v.addEventListener(`close`,k),Object.assign(S,{show:()=>v.showModal(),close:()=>{x!==`pending`&&v.close()},cleanup:w})}exports.SweetestAlert=d;
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,10 +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", l = "Uh oh!", u = "Something went wrong, please try again. If the error continues, please contact support.";
7
- function d() {
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() {
8
8
  let e;
9
9
  return {
10
10
  promise: new Promise((t) => {
@@ -13,102 +13,133 @@ function d() {
13
13
  resolve: e
14
14
  };
15
15
  }
16
- function f(e) {
16
+ function m(e) {
17
17
  return typeof (e !== null && (typeof e == "object" || typeof e == "function") ? e.then : void 0) == "function";
18
18
  }
19
- function p({ onCancel: p, onConfirm: m, confirmButton: h = "Okay", content: g, hideCancel: _ = !1, title: v, type: y = "warning" }) {
20
- let b = document.createElement("dialog");
21
- b.className = "pco-sweetest-alert", document.body.appendChild(b);
22
- let x = e(b), S = !1, C = "idle", { promise: w, resolve: T } = d(), E = () => {
23
- S || (S = !0, C = "idle", T({
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({
24
24
  isConfirmed: !1,
25
25
  isDismissed: !0
26
- }), x.unmount(), b.remove());
27
- }, D = (e) => {
28
- S || (T({
26
+ }), w.unmount(), C.remove());
27
+ }, P = (e) => {
28
+ D || (M({
29
29
  isConfirmed: !1,
30
30
  isDismissed: !0
31
- }), E(), e && p?.());
32
- }, O = async (e) => {
33
- C = "pending", P();
31
+ }), N(), e && h?.());
32
+ }, F = async (e) => {
33
+ O = "pending", G();
34
34
  let t;
35
35
  try {
36
36
  t = await e;
37
37
  } catch {
38
- if (S) return;
39
- C = "error", P();
38
+ if (D) return;
39
+ O = "error", G();
40
40
  return;
41
41
  }
42
- S || (T({
42
+ D || (M({
43
43
  isConfirmed: !0,
44
44
  isDismissed: !1,
45
45
  value: t
46
- }), E());
47
- }, k = () => {
48
- if (S || C !== "idle") return;
49
- let e = m?.();
50
- if (!f(e)) {
51
- 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
52
  isConfirmed: !0,
53
53
  isDismissed: !1
54
- }), E();
54
+ }), N();
55
55
  return;
56
56
  }
57
- O(e);
58
- }, A = (e) => {
59
- C === "pending" && e.preventDefault();
60
- }, j = () => {
61
- S || C === "pending" || D(C !== "error");
62
- }, M = (e) => {
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) => {
63
73
  switch (e) {
64
- case "success": return a;
65
- case "error": return c;
66
- case "info": return s;
74
+ case "success": return o;
75
+ case "error": return l;
76
+ case "info": return c;
67
77
  case "danger":
68
- case "warning": return o;
78
+ case "warning": return s;
69
79
  }
70
- }, N = () => {
71
- let e = C === "error", a = C === "pending", o = e ? "error" : y;
72
- return /* @__PURE__ */ i(n, { children: [
73
- /* @__PURE__ */ r("svg", {
74
- className: `pco-sweetest-alert__icon pco-sweetest-alert--${o}`,
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}`,
75
85
  width: "48",
76
86
  height: "48",
77
87
  viewBox: "0 0 16 16",
78
88
  "aria-hidden": "true",
79
- children: /* @__PURE__ */ r("path", { d: M(o) })
89
+ children: /* @__PURE__ */ i("path", { d: U(s) })
80
90
  }),
81
- /* @__PURE__ */ r("h2", {
91
+ /* @__PURE__ */ i("h2", {
82
92
  className: "pco-sweetest-alert__title",
83
- children: e ? l : v
93
+ children: e ? u : x
84
94
  }),
85
- /* @__PURE__ */ r("div", {
95
+ /* @__PURE__ */ i("div", {
86
96
  className: "pco-sweetest-alert__body",
87
- children: e ? /* @__PURE__ */ r("p", { children: u }) : typeof g == "string" ? /* @__PURE__ */ r("p", { children: g }) : g
97
+ children: e ? /* @__PURE__ */ i("p", { children: d }) : typeof y == "string" ? /* @__PURE__ */ i("p", { children: y }) : y
88
98
  }),
89
- /* @__PURE__ */ i("div", {
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", {
90
120
  className: "pco-sweetest-alert__actions",
91
- children: [!e && !_ && /* @__PURE__ */ r(t, {
121
+ children: [!e && !b && /* @__PURE__ */ i(t, {
92
122
  label: "Cancel",
93
123
  kind: "ghost",
94
- disabled: a,
95
- onClick: () => D(!0)
96
- }), /* @__PURE__ */ r(t, {
97
- label: e ? "Okay" : h,
98
- kind: !e && y === "danger" ? "delete" : "primary",
99
- loading: a,
100
- onClick: e ? () => D(!1) : k
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
101
132
  })]
102
133
  })
103
134
  ] });
104
- }, P = () => x.render(N());
105
- return P(), b.showModal(), b.addEventListener("cancel", A), b.addEventListener("close", j), Object.assign(w, {
106
- show: () => b.showModal(),
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(),
107
138
  close: () => {
108
- C !== "pending" && b.close();
139
+ O !== "pending" && C.close();
109
140
  },
110
- cleanup: E
141
+ cleanup: N
111
142
  });
112
143
  }
113
144
  //#endregion
114
- export { p 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);
@@ -14,6 +14,26 @@ type AlertResult = {
14
14
  */
15
15
  value?: unknown;
16
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;
36
+ };
17
37
  /** Options for {@link SweetestAlert}. */
18
38
  type AlertOptions = {
19
39
  /** Callback executed when the cancel button is clicked, the dialog is dismissed via Escape, or `close()` is called. Not called when `onConfirm` rejects. */
@@ -62,6 +82,33 @@ type AlertOptions = {
62
82
  title: string;
63
83
  /** Visual type of alert; affects icon and styling. Defaults to `"warning"`. */
64
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;
65
112
  };
66
113
  /**
67
114
  * Shows a modal alert/confirmation dialog using a native `<dialog>` element.
@@ -90,7 +137,7 @@ type AlertOptions = {
90
137
  * - `cleanup()` — unmounts the React root and removes the dialog from the
91
138
  * DOM immediately, regardless of pending state
92
139
  */
93
- 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> & {
94
141
  show: () => void;
95
142
  close: () => void;
96
143
  cleanup: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/sweetest-alert",
3
- "version": "1.9.0",
3
+ "version": "2.0.1",
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",