@planningcenter/sweetest-alert 0.1.0 → 0.2.3

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/package.json CHANGED
@@ -1,9 +1,24 @@
1
1
  {
2
2
  "name": "@planningcenter/sweetest-alert",
3
- "version": "0.1.0",
3
+ "version": "0.2.3",
4
4
  "description": "The sweetest alert ever",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@1.22.22",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./style.css": "./dist/style.css"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
7
22
  "repository": {
8
23
  "type": "git",
9
24
  "url": "git+https://github.com/planningcenter/sweetest-alert.git"
@@ -14,24 +29,26 @@
14
29
  "url": "https://github.com/planningcenter/sweetest-alert/issues"
15
30
  },
16
31
  "scripts": {
17
- "dev": "vite",
18
- "build": "vite build",
19
- "preview": "vite preview",
32
+ "dev": "vite --config vite.config.docs.ts",
33
+ "build": "vite build --config vite.config.docs.ts",
34
+ "build:lib": "vite build --config vite.config.lib.ts && tsc --project tsconfig.lib.json && cp src/sweetest_alert.css dist/style.css",
35
+ "build:docs": "vite build --config vite.config.docs.ts",
36
+ "preview": "vite preview --config vite.config.docs.ts",
20
37
  "test": "vitest",
21
38
  "lint:js": "eslint .",
22
39
  "lint:css": "stylelint \"**/*.css\""
23
40
  },
24
41
  "peerDependencies": {
25
- "@planningcenter/icons": "^15.25.0",
26
- "@planningcenter/tapestry": "^2.3.0",
42
+ "@planningcenter/icons": "^15.26.0",
43
+ "@planningcenter/tapestry": "^2.5.0",
27
44
  "react": "^18.3.1 <19",
28
45
  "react-dom": "^18.3.1"
29
46
  },
30
47
  "devDependencies": {
31
48
  "@eslint/js": "^9.37.0",
32
- "@planningcenter/icons": "^15.25.0",
49
+ "@planningcenter/icons": "^15.26.0",
33
50
  "@planningcenter/stylelint-config-pco": "^0.0.5",
34
- "@planningcenter/tapestry": "^2.3.0",
51
+ "@planningcenter/tapestry": "^2.5.0",
35
52
  "@testing-library/dom": "^10.4.1",
36
53
  "@testing-library/jest-dom": "^6.9.1",
37
54
  "@testing-library/react": "^16.3.0",
package/.editorconfig DELETED
@@ -1,8 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- charset = utf-8
5
- end_of_line = lf
6
- indent_size = 2
7
- indent_style = space
8
- insert_final_newline = true
@@ -1 +0,0 @@
1
- * @starzonmyarmz
@@ -1,24 +0,0 @@
1
- name: PCO-Release - Release Automation
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- permissions:
9
- contents: write
10
- pull-requests: write
11
-
12
- jobs:
13
- release-automation:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-node@v4
18
- with:
19
- node-version: "20"
20
- cache: "yarn"
21
- - uses: planningcenter/pco-release-action/release-by-pr@v1
22
- with:
23
- app-id: ${{ secrets.PCO_DEPENDENCIES_APP_ID }}
24
- private-key: ${{ secrets.PCO_DEPENDENCIES_PRIVATE_KEY}}
@@ -1,17 +0,0 @@
1
- name: PCO-Release - Create Release on Merge
2
-
3
- on:
4
- pull_request:
5
- types: [closed]
6
- branches:
7
- - main
8
-
9
- jobs:
10
- create-release:
11
- runs-on: ubuntu-latest
12
- if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'pco-release-pending')
13
- steps:
14
- - uses: planningcenter/pco-release-action/create-release-on-merge@v1
15
- with:
16
- app-id: ${{ secrets.PCO_DEPENDENCIES_APP_ID }}
17
- private-key: ${{ secrets.PCO_DEPENDENCIES_PRIVATE_KEY}}
@@ -1,15 +0,0 @@
1
- name: PCO-Release - Dependabot Automation (Update changelog)
2
-
3
- on: pull_request
4
-
5
- jobs:
6
- update-dependabot-pr-changelog:
7
- if: github.actor == 'dependabot[bot]'
8
- runs-on: ubuntu-latest
9
- permissions:
10
- contents: write
11
- steps:
12
- - uses: actions/checkout@v4
13
- with:
14
- ref: ${{ github.event.pull_request.head.ref }}
15
- - uses: planningcenter/pco-release-action/dependabot-automation@v1
@@ -1,22 +0,0 @@
1
- name: PCO-Release - Require Changelog Update
2
-
3
- on:
4
- pull_request:
5
- branches: [main]
6
-
7
- jobs:
8
- require-changelog-update:
9
- runs-on: ubuntu-latest
10
- permissions:
11
- pull-requests: read
12
- contents: read
13
- steps:
14
- - uses: actions/checkout@v4
15
- - id: changed-files
16
- uses: tj-actions/changed-files@v44
17
- with:
18
- files: CHANGELOG.md
19
- - if: steps.changed-files.outputs.any_changed == 'false'
20
- run: |
21
- echo "Pull Requests require an update to the CHANGELOG.md file."
22
- exit 1
@@ -1,18 +0,0 @@
1
- on:
2
- pull_request:
3
- types: [labeled]
4
-
5
- permissions:
6
- contents: write
7
- pull-requests: write
8
-
9
- name: PCO-Release - Sync With Labels
10
-
11
- jobs:
12
- sync-with-labels:
13
- if: ${{ github.event.pull_request.head.ref == 'pco-release--internal' && (github.event.label.name == 'pco-release-patch' || github.event.label.name == 'pco-release-minor' || github.event.label.name == 'pco-release-major') }}
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: planningcenter/pco-release-action/sync-with-labels@v1
17
- with:
18
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/.stylelintrc.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "@planningcenter/stylelint-config-pco",
3
- "rules": {
4
- "number-leading-zero": "always",
5
- "order/properties-order": null,
6
- "order/properties-alphabetical-order": true
7
- }
8
- }
package/CHANGELOG.md DELETED
@@ -1,6 +0,0 @@
1
-
2
-
3
- ### Dependencies
4
-
5
- - Bump vite from 7.1.10 to 7.1.11
6
-
package/docs/demo.css DELETED
@@ -1,198 +0,0 @@
1
- * {
2
- box-sizing: border-box;
3
- margin: 0;
4
- padding: 0;
5
- }
6
-
7
- #root {
8
- display: contents;
9
- }
10
-
11
- body {
12
- background: linear-gradient(135deg, var(--t-fill-color-tag-blue-040) 0%, var(--t-fill-color-tag-purple-030) 100%);
13
- color: var(--t-text-color-default-primary);
14
- display: grid;
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
16
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
17
- sans-serif;
18
- line-height: 1.4;
19
- min-height: 100vh;
20
- padding: var(--t-spacing-3);
21
- place-items: center;
22
- text-wrap: pretty;
23
- gap:32px;
24
- }
25
-
26
- .container {
27
- background: var(--t-surface-color-card);
28
- border-radius: var(--t-border-radius-xl);
29
- box-shadow: 0 var(--t-spacing-3) var(--t-spacing-6) var(--t-fill-color-transparency-dark-030);
30
- max-width: 960px;
31
- padding: var(--t-spacing-6);
32
- width: 100%;
33
- }
34
-
35
- h1 {
36
- color: var(--t-text-color-default-headline);
37
- font-size: 2.5rem;
38
- margin: 0;
39
- text-align: center;
40
- }
41
-
42
- h2 {
43
- color: var(--t-text-color-default-secondary);
44
- font-size: 1.2rem;
45
- font-weight: var(--t-font-weight-normal);
46
- margin-bottom: var(--t-spacing-5);
47
- text-align: center;
48
- }
49
-
50
- ul {
51
- margin: 1rem;
52
- padding-left: 2ch;
53
- }
54
-
55
- table {
56
- border-collapse: collapse;
57
- }
58
-
59
- th, td {
60
- border-bottom: var(--t-border-width-default) solid var(--t-border-color-default-base);
61
- text-align: left;
62
- padding: var(--t-spacing-1);
63
- }
64
-
65
- tr:last-child :is(th, td) {
66
- border: 0;
67
- }
68
-
69
- .button-grid {
70
- display: grid;
71
- gap: var(--t-spacing-2);
72
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
73
- margin-bottom: var(--t-spacing-4);
74
- }
75
-
76
- .demo-button {
77
- background: var(--t-surface-color-card);
78
- border: var(--t-border-width-thick) solid var(--t-border-color-default-base);
79
- border-radius: 12px;
80
- color: var(--t-text-color-default-secondary);
81
- cursor: pointer;
82
- display: flex;
83
- flex-direction: column;
84
- font-family: inherit;
85
- font-size: 1rem;
86
- gap: var(--t-spacing-half);
87
- padding: var(--t-spacing-3);
88
- text-align: left;
89
- transition: all .2s ease;
90
-
91
- &:hover {
92
- box-shadow: 0 var(--t-spacing-half) var(--t-spacing-1) var(--t-fill-color-transparency-dark-010);
93
- translate: 0 -2px;
94
- }
95
-
96
- &:active {
97
- translate: 0;
98
- }
99
-
100
- h3 {
101
- color: var(--t-text-color-default-headline);
102
- font-size: 1.1rem;
103
- font-weight: var(--t-font-weight-semibold);
104
- }
105
- }
106
-
107
- .info {
108
- border-color: var(--t-border-color-status-info);
109
-
110
- &:hover {
111
- background: var(--t-fill-color-status-info-dim);
112
- border-color: var(--t-border-color-status-info);
113
- }
114
- }
115
-
116
- .success {
117
- border-color: var(--t-border-color-status-success);
118
-
119
- &:hover {
120
- background: var(--t-fill-color-status-success-ghost);
121
- border-color: var(--t-border-color-status-success);
122
- }
123
- }
124
-
125
- .warning {
126
- border-color: var(--t-border-color-status-warning);
127
-
128
- &:hover {
129
- background: var(--t-fill-color-status-warning-ghost);
130
- border-color: var(--t-border-color-status-warning);
131
- }
132
- }
133
-
134
- .error, .danger {
135
- border-color: var(--t-border-color-status-error);
136
-
137
- &:hover {
138
- background: var(--t-fill-color-status-error-ghost);
139
- border-color: var(--t-border-color-status-error);
140
- }
141
- }
142
-
143
- .custom {
144
- border-color: var(--t-border-color-default-darker);
145
-
146
- &:hover {
147
- background: var(--t-fill-color-tag-purple-010);
148
- border-color: var(--t-border-color-default-darkest);
149
- }
150
- }
151
-
152
- .tip {
153
- background: var(--t-fill-color-neutral-090);
154
- border: var(--t-border-width-default) solid var(--t-border-color-default-base);
155
- border-radius: var(--t-border-radius-lg);
156
- color: var(--t-text-color-default-secondary);
157
- font-size: 1rem;
158
- padding: var(--t-spacing-2) var(--t-spacing-3);
159
-
160
- strong {
161
- color: var(--t-text-color-default-primary);
162
- }
163
- }
164
-
165
- .codeblock {
166
- background: #2d2d2d;
167
- border-radius: var(--t-border-radius-lg);
168
- margin: var(--t-spacing-3) 0 var(--t-spacing-6);
169
- overflow-x: auto;
170
- padding: 0;
171
-
172
- pre {
173
- margin: 0;
174
- padding: 0;
175
- }
176
-
177
- code {
178
- display: block;
179
- font-family: 'Fira Code', 'Monaco', 'Menlo', 'Courier New', monospace;
180
- font-size: 0.9rem;
181
- line-height: 1.5;
182
- padding: var(--t-spacing-3);
183
- }
184
- }
185
-
186
- @media (max-width: 640px) {
187
- .demo-container {
188
- padding: var(--t-spacing-4) var(--t-spacing-3);
189
- }
190
-
191
- h1 {
192
- font-size: 2rem;
193
- }
194
-
195
- .button-grid {
196
- grid-template-columns: 1fr;
197
- }
198
- }
package/docs/demo.tsx DELETED
@@ -1,306 +0,0 @@
1
- /* eslint-disable no-console */
2
-
3
- import { createRoot } from "react-dom/client"
4
- import { useEffect, useRef } from "react"
5
- import { SweetestAlert } from "../src/index"
6
- import Prism from "prismjs"
7
- import "prismjs/themes/prism-tomorrow.css"
8
- import "prismjs/components/prism-jsx"
9
- import "prismjs/components/prism-tsx"
10
- import "prismjs/components/prism-bash"
11
-
12
- import "../node_modules/@planningcenter/tapestry/dist/index.css"
13
- import "../src/sweetest_alert.css"
14
- import "./demo.css"
15
-
16
- function CodeBlock({ code, language = "jsx" }: { code: string; language?: string }) {
17
- const codeRef = useRef<HTMLElement>(null)
18
-
19
- useEffect(() => {
20
- if (codeRef.current) {
21
- Prism.highlightElement(codeRef.current)
22
- }
23
- }, [code, language])
24
-
25
- return (
26
- <div className="codeblock">
27
- <pre>
28
- <code ref={codeRef} className={`language-${language}`}>
29
- {code}
30
- </code>
31
- </pre>
32
- </div>
33
- )
34
- }
35
-
36
- function Demo() {
37
- const showInfoAlert = () => {
38
- SweetestAlert({
39
- type: "info",
40
- title: "Information",
41
- content: "This is an informational alert to let you know something.",
42
- hideCancel: true,
43
- })
44
- }
45
-
46
- const showSuccessAlert = () => {
47
- SweetestAlert({
48
- type: "success",
49
- title: "Success!",
50
- content: "Your operation completed successfully.",
51
- hideCancel: true,
52
- })
53
- }
54
-
55
- const showWarningAlert = () => {
56
- SweetestAlert({
57
- type: "warning",
58
- title: "Warning",
59
- content: "Are you sure you want to proceed with this action?",
60
- confirm: () => console.log("User confirmed warning"),
61
- cancel: () => console.log("User cancelled warning"),
62
- })
63
- }
64
-
65
- const showErrorAlert = () => {
66
- SweetestAlert({
67
- type: "error",
68
- title: "Error Occurred",
69
- content: "Something went wrong. Please try again.",
70
- hideCancel: true,
71
- })
72
- }
73
-
74
- const showDangerAlert = () => {
75
- SweetestAlert({
76
- type: "danger",
77
- title: "Delete Item",
78
- content: "Are you sure you want to delete this item? This action cannot be undone.",
79
- confirmButton: "Delete",
80
- confirm: () => console.log("User confirmed deletion"),
81
- cancel: () => console.log("User cancelled deletion"),
82
- })
83
- }
84
-
85
- const showCustomContentAlert = () => {
86
- SweetestAlert({
87
- type: "info",
88
- title: "Custom Content",
89
- content: (
90
- <>
91
- <p>You can pass custom React components as content.</p>
92
- <ul>
93
- <li>Item one</li>
94
- <li>Item two</li>
95
- <li>Item three</li>
96
- </ul>
97
- <p>This allows for rich, formatted content in your alerts.</p>
98
- </>
99
- ),
100
- confirm: () => console.log("Custom content confirmed"),
101
- cancel: () => console.log("Custom content cancelled"),
102
- })
103
- }
104
-
105
- return (
106
- <>
107
- <div className="container">
108
- <h1>Sweetest Alert Demo</h1>
109
- <h2>Click the buttons below to test different alert types</h2>
110
-
111
- <div className="button-grid">
112
- <button className="demo-button info" onClick={showInfoAlert}>
113
- <h3>Info Alert</h3>
114
- Simple notification
115
- </button>
116
-
117
- <button className="demo-button success" onClick={showSuccessAlert}>
118
- <h3>Success Alert</h3>
119
- Success message
120
- </button>
121
-
122
- <button className="demo-button warning" onClick={showWarningAlert}>
123
- <h3>Warning Alert</h3>
124
- With cancel option
125
- </button>
126
-
127
- <button className="demo-button error" onClick={showErrorAlert}>
128
- <h3>Error Alert</h3>
129
- Error notification
130
- </button>
131
-
132
- <button className="demo-button danger" onClick={showDangerAlert}>
133
- <h3>Danger Alert</h3>
134
- Destructive action
135
- </button>
136
-
137
- <button className="demo-button custom" onClick={showCustomContentAlert}>
138
- <h3>Custom Content</h3>
139
- Rich content example
140
- </button>
141
- </div>
142
-
143
- <div className="tip">
144
- <strong>Tip:</strong> Open your browser console to see the callback logs when you confirm or cancel alerts.
145
- </div>
146
- </div>
147
-
148
- <div className="container">
149
- <h1>Documentation</h1>
150
-
151
- <h2>Installation</h2>
152
- <CodeBlock code="yarn add @planningcenter/sweetest-alert" language="bash" />
153
-
154
- <h3>Peer Dependencies</h3>
155
- <p>This package requires the following peer dependencies:</p>
156
- <ul>
157
- <li>react ^18.3.1</li>
158
- <li>react-dom ^18.3.1</li>
159
- <li>@planningcenter/tapestry ^2.3.0</li>
160
- <li>@planningcenter/icons ^15.25.0</li>
161
- </ul>
162
-
163
- <h2>Usage</h2>
164
-
165
- <h3>Basic Example</h3>
166
- <CodeBlock code={`import { SweetestAlert } from '@planningcenter/sweetest-alert'
167
-
168
- // Simple notification
169
- SweetestAlert({
170
- title: "Warning!",
171
- content: "Watch out, he's coming to get you!",
172
- })`} />
173
-
174
- <h3>Confirmation Dialog</h3>
175
- <CodeBlock code={`SweetestAlert({
176
- type: "danger",
177
- title: "Delete Item",
178
- content: "Are you sure you want to delete this item?",
179
- confirm: () => console.log("Confirmed"),
180
- cancel: () => console.log("Cancelled")
181
- })`} />
182
-
183
- <h3>Alert Types</h3>
184
- <p>The component supports five visual types:</p>
185
- <CodeBlock code={`// Info alert
186
- SweetestAlert({
187
- type: "info",
188
- title: "Information",
189
- content: "This is an informational message."
190
- })
191
-
192
- // Success alert
193
- SweetestAlert({
194
- type: "success",
195
- title: "Success!",
196
- content: "Operation completed successfully."
197
- })
198
-
199
- // Warning alert (default)
200
- SweetestAlert({
201
- type: "warning",
202
- title: "Warning",
203
- content: "Please proceed with caution."
204
- })
205
-
206
- // Error alert
207
- SweetestAlert({
208
- type: "error",
209
- title: "Error",
210
- content: "Something went wrong."
211
- })
212
-
213
- // Danger alert (for destructive actions)
214
- SweetestAlert({
215
- type: "danger",
216
- title: "Delete Account",
217
- content: "This action cannot be undone.",
218
- confirmButton: "Delete"
219
- })`} />
220
-
221
- <h3>Custom Content</h3>
222
- <p>You can pass React components as content for rich formatting:</p>
223
- <CodeBlock code={`SweetestAlert({
224
- title: "Custom Content",
225
- content: (
226
- <>
227
- <p>You can include any React elements:</p>
228
- <ul>
229
- <li>Lists</li>
230
- <li>Links</li>
231
- <li>Formatted text</li>
232
- </ul>
233
- </>
234
- )
235
- })`} />
236
-
237
- <h2>API</h2>
238
- <table>
239
- <thead>
240
- <tr>
241
- <th>Parameter</th>
242
- <th>Type</th>
243
- <th>Required</th>
244
- <th>Default</th>
245
- <th>Description</th>
246
- </tr>
247
- </thead>
248
- <tbody>
249
- <tr>
250
- <td><code>title</code></td>
251
- <td><code>string</code></td>
252
- <td>Yes</td>
253
- <td>-</td>
254
- <td>The main heading text displayed at the top of the modal</td>
255
- </tr>
256
- <tr>
257
- <td><code>content</code></td>
258
- <td><code>string | React.ReactNode</code></td>
259
- <td>Yes</td>
260
- <td>-</td>
261
- <td>The body content. Accepts plain text or React components</td>
262
- </tr>
263
- <tr>
264
- <td><code>type</code></td>
265
- <td><code>&quot;info&quot; | &quot;success&quot; | &quot;error&quot; | &quot;danger&quot; | &quot;warning&quot;</code></td>
266
- <td>No</td>
267
- <td>&quot;warning&quot;</td>
268
- <td>The visual type of alert, affects icon and styling</td>
269
- </tr>
270
- <tr>
271
- <td><code>confirm</code></td>
272
- <td><code>() =&gt; void</code></td>
273
- <td>No</td>
274
- <td>-</td>
275
- <td>Callback executed when confirm button is clicked</td>
276
- </tr>
277
- <tr>
278
- <td><code>cancel</code></td>
279
- <td><code>() =&gt; void</code></td>
280
- <td>No</td>
281
- <td>-</td>
282
- <td>Callback executed when cancel button is clicked</td>
283
- </tr>
284
- <tr>
285
- <td><code>confirmButton</code></td>
286
- <td><code>string</code></td>
287
- <td>No</td>
288
- <td>&quot;Okay&quot;</td>
289
- <td>Custom text for the confirm button</td>
290
- </tr>
291
- <tr>
292
- <td><code>hideCancel</code></td>
293
- <td><code>boolean</code></td>
294
- <td>No</td>
295
- <td>false</td>
296
- <td>When true, hides the cancel button for simple notifications</td>
297
- </tr>
298
- </tbody>
299
- </table>
300
- </div>
301
- </>
302
- )
303
- }
304
-
305
- const root = createRoot(document.getElementById("root")!)
306
- root.render(<Demo />)
package/docs/index.html DELETED
@@ -1,13 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍬</text></svg>" />
7
- <title>Sweetest Alert Demo</title>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="./demo.tsx"></script>
12
- </body>
13
- </html>
package/eslint.config.js DELETED
@@ -1,56 +0,0 @@
1
- import js from '@eslint/js';
2
- import tseslint from 'typescript-eslint';
3
- import react from 'eslint-plugin-react';
4
- import reactHooks from 'eslint-plugin-react-hooks';
5
-
6
- export default tseslint.config(
7
- js.configs.recommended,
8
- ...tseslint.configs.recommended,
9
- {
10
- files: ['**/*.{js,jsx,ts,tsx}'],
11
- plugins: {
12
- react,
13
- 'react-hooks': reactHooks,
14
- },
15
- languageOptions: {
16
- parserOptions: {
17
- ecmaFeatures: {
18
- jsx: true,
19
- },
20
- },
21
- },
22
- settings: {
23
- react: {
24
- version: 'detect',
25
- },
26
- },
27
- rules: {
28
- ...react.configs.recommended.rules,
29
- ...react.configs['jsx-runtime'].rules,
30
- ...reactHooks.configs.recommended.rules,
31
- 'eqeqeq': ['error', 'always'],
32
- 'no-console': 'warn',
33
- 'no-underscore-dangle': 'error',
34
- 'no-unused-vars': 'off',
35
- '@typescript-eslint/no-unused-vars': ['error', {
36
- 'argsIgnorePattern': '^_',
37
- 'varsIgnorePattern': '^_',
38
- }],
39
- 'react/no-deprecated': 'error',
40
- 'semi': ['error', 'never'],
41
- },
42
- },
43
- {
44
- files: ['tests/**/*.{js,jsx,ts,tsx}'],
45
- languageOptions: {
46
- globals: {
47
- document: 'readonly',
48
- window: 'readonly',
49
- Event: 'readonly',
50
- },
51
- },
52
- },
53
- {
54
- ignores: ['dist/', 'node_modules/', '*.config.ts', '*.config.js'],
55
- }
56
- );
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export { SweetestAlert } from "./sweetest_alert.tsx"
@@ -1,62 +0,0 @@
1
- .pco-sweetest-alert {
2
- background: var(--t-surface-color-card);
3
- border: 0;
4
- border-radius: var(--t-border-radius-lg);
5
- box-shadow: 0 0 var(--t-spacing-3) var(--t-fill-color-transparency-dark-020);
6
- font-size: var(--t-font-size-md);
7
- margin: auto;
8
- max-width: 600px;
9
- padding: var(--t-spacing-4) var(--t-spacing-4) 0;
10
- text-align: center;
11
-
12
- &::backdrop {
13
- background: var(--t-fill-color-transparency-dark-040);
14
- }
15
- }
16
-
17
- .pco-sweetest-alert__icon {
18
- height: var(--t-element-size-3xl);
19
- margin-block: var(--t-spacing-1);
20
- width: var(--t-element-size-3xl);
21
-
22
- &.pco-sweetest-alert--info {
23
- fill: var(--t-icon-color-status-info-primary);
24
- }
25
-
26
- &.pco-sweetest-alert--success {
27
- fill: var(--t-icon-color-status-success-primary);
28
- }
29
-
30
- &.pco-sweetest-alert--warning {
31
- fill: var(--t-icon-color-status-warning-primary);
32
- }
33
-
34
- &:is(.pco-sweetest-alert--danger, .pco-sweetest-alert--error) {
35
- fill: var(--t-icon-color-status-error-primary);
36
- }
37
- }
38
-
39
- .pco-sweetest-alert__title {
40
- color: var(--t-text-color-default-headline);
41
- font-size: var(--t-font-size-2xl);
42
- font-weight: var(--t-font-weight-semibold);
43
- margin-block: var(--t-spacing-2);
44
- }
45
-
46
- .pco-sweetest-alert__body {
47
- color: var(--t-text-color-default-primary);
48
- font-weight: var(--t-font-weight-normal);
49
- line-height: 1.4;
50
- margin-bottom: var(--t-spacing-4);
51
- }
52
-
53
- .pco-sweetest-alert__actions {
54
- align-items: center;
55
- border-top: var(--t-border-width-default) solid var(--t-border-color-default-base);
56
- display: flex;
57
- flex-wrap: wrap;
58
- gap: var(--t-spacing-1);
59
- justify-content: center;
60
- margin-inline: calc(-1 * var(--t-spacing-4));
61
- padding: var(--t-spacing-3) var(--t-spacing-4);
62
- }
@@ -1,130 +0,0 @@
1
- import React from "react"
2
- import { createRoot } from "react-dom/client"
3
- import { Button } from "@planningcenter/tapestry"
4
- import {
5
- checkCircle,
6
- xCircle,
7
- infoCircle,
8
- exclamationTriangle,
9
- } from "@planningcenter/icons/paths/general"
10
-
11
- // A modal dialog component that replaces SweetAlert2 with a custom implementation.
12
- // Creates and manages its own DOM dialog element with React rendering.
13
- //
14
- // Basic Usage
15
- //
16
- // SweetestAlert({
17
- // title: "Delete Item",
18
- // content: "Are you sure you want to delete this item?",
19
- // confirm: () => console.log("Confirmed"),
20
- // cancel: () => console.log("Cancelled")
21
- // });
22
-
23
- type AlertTypes = "info" | "success" | "error" | "danger" | "warning"
24
-
25
- export function SweetestAlert({
26
- // Optional: Callback function executed when cancel button is clicked
27
- cancel,
28
-
29
- // Optional: Callback function executed when confirm button is clicked
30
- confirm,
31
-
32
- // Optional: Text for the confirm button. Defaults to "Okay".
33
- confirmButton = "Okay",
34
-
35
- // Required: The body content explaining the alert.
36
- // Accepts a React fragment or a string of text.
37
- content,
38
-
39
- // Optional: When true, hides the cancel button for simple notifications.
40
- // Defaults to "false".
41
- hideCancel = false,
42
-
43
- // Required: The main heading text displayed at the top of the modal
44
- title,
45
-
46
- // Optional: The visual type of alert, affects icon and styling.
47
- // Values can be "info", "success", "error", "danger", or "warning" (default).
48
- type = "warning",
49
- }: {
50
- cancel?: () => void
51
- confirm?: () => void
52
- confirmButton?: string
53
- content: string | React.ReactNode
54
- hideCancel?: boolean
55
- title: string
56
- type?: AlertTypes
57
- }) {
58
- const dialog = document.createElement("dialog")
59
- dialog.className = "pco-sweetest-alert"
60
- document.body.appendChild(dialog)
61
- const root = createRoot(dialog)
62
-
63
- const cleanup = () => {
64
- root.unmount()
65
- document.body.removeChild(dialog)
66
- }
67
-
68
- const handleConfirm = () => {
69
- cleanup()
70
- confirm?.()
71
- }
72
-
73
- const handleCancel = () => {
74
- cleanup()
75
- cancel?.()
76
- }
77
-
78
- const iconPath = () => {
79
- switch (type) {
80
- case "success":
81
- return checkCircle
82
- case "error":
83
- return xCircle
84
- case "info":
85
- return infoCircle
86
- case "danger":
87
- case "warning":
88
- return exclamationTriangle
89
- }
90
- }
91
-
92
- root.render(
93
- <>
94
- <svg
95
- className={`pco-sweetest-alert__icon pco-sweetest-alert--${type}`}
96
- width="48"
97
- height="48"
98
- viewBox="0 0 16 16"
99
- aria-hidden="true"
100
- >
101
- <path d={iconPath()} />
102
- </svg>
103
-
104
- <h2 className="pco-sweetest-alert__title">{title}</h2>
105
-
106
- <div className="pco-sweetest-alert__body">
107
- {typeof content === "string" ? <p>{content}</p> : content}
108
- </div>
109
-
110
- <div className="pco-sweetest-alert__actions">
111
- {!hideCancel && <Button label="Cancel" kind="ghost" onClick={handleCancel} />}
112
- <Button
113
- label={confirmButton}
114
- kind={type === "danger" ? "delete" : "primary"}
115
- onClick={handleConfirm}
116
- />
117
- </div>
118
- </>
119
- )
120
-
121
- dialog.showModal()
122
-
123
- dialog.addEventListener("close", handleCancel)
124
-
125
- return {
126
- show: () => dialog.showModal(),
127
- close: () => dialog.close(),
128
- cleanup,
129
- }
130
- }
@@ -1,254 +0,0 @@
1
- import React from "react"
2
- import { screen, fireEvent, waitFor, act } from "@testing-library/react"
3
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
4
- import { SweetestAlert } from "../src/sweetest_alert.tsx"
5
-
6
- describe("SweetestAlert", () => {
7
- const mockConfirm = vi.fn()
8
- const mockCancel = vi.fn()
9
-
10
- beforeEach(() => {
11
- vi.clearAllMocks()
12
- document.querySelectorAll("dialog.pco-sweetest-alert").forEach((dialog) => dialog.remove())
13
- })
14
-
15
- afterEach(() => {
16
- document.querySelectorAll("dialog.pco-sweetest-alert").forEach((dialog) => dialog.remove())
17
- })
18
-
19
- describe("Initial Render", () => {
20
- beforeEach(async () => {
21
- await act(async () => {
22
- SweetestAlert({
23
- title: "Test Title",
24
- content: "Test content",
25
- confirm: mockConfirm,
26
- cancel: mockCancel,
27
- })
28
- })
29
- })
30
-
31
- it("creates and displays a modal dialog", () => {
32
- const dialog = document.querySelector("dialog.pco-sweetest-alert")
33
- expect(dialog).toBeInTheDocument()
34
- expect(dialog).toHaveAttribute("open")
35
- })
36
-
37
- it("displays the provided title as a heading", () => {
38
- expect(screen.getByRole("heading", { name: "Test Title" })).toBeInTheDocument()
39
- })
40
-
41
- it("displays the content text in the dialog body", () => {
42
- expect(screen.getByText("Test content")).toBeInTheDocument()
43
- })
44
-
45
- it("displays a cancel button", () => {
46
- expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument()
47
- })
48
-
49
- it("displays a confirm button with default label", () => {
50
- expect(screen.getByRole("button", { name: "Okay" })).toBeInTheDocument()
51
- })
52
-
53
- it("displays a warning icon when no type is specified", () => {
54
- const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--warning")
55
- expect(icon).toBeInTheDocument()
56
- expect(icon.querySelector("path")).toBeInTheDocument()
57
- })
58
- })
59
-
60
- describe("Alert Types", () => {
61
- it("displays success icon when type is set to success", async () => {
62
- await act(async () => {
63
- SweetestAlert({
64
- title: "Success",
65
- content: "Success content",
66
- type: "success",
67
- })
68
- })
69
-
70
- const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--success")
71
- expect(icon).toBeInTheDocument()
72
- expect(icon.querySelector("path")).toBeInTheDocument()
73
- })
74
-
75
- it("displays error icon when type is set to error", async () => {
76
- await act(async () => {
77
- SweetestAlert({
78
- title: "Error",
79
- content: "Error content",
80
- type: "error",
81
- })
82
- })
83
-
84
- const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--error")
85
- expect(icon).toBeInTheDocument()
86
- expect(icon.querySelector("path")).toBeInTheDocument()
87
- })
88
-
89
- it("displays info icon when type is set to info", async () => {
90
- await act(async () => {
91
- SweetestAlert({
92
- title: "Info",
93
- content: "Info content",
94
- type: "info",
95
- })
96
- })
97
-
98
- const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--info")
99
- expect(icon).toBeInTheDocument()
100
- expect(icon.querySelector("path")).toBeInTheDocument()
101
- })
102
-
103
- it("displays red warning icon and delete button kind when type is set to danger", async () => {
104
- await act(async () => {
105
- SweetestAlert({
106
- title: "Danger",
107
- content: "Danger content",
108
- type: "danger",
109
- })
110
- })
111
-
112
- const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--danger")
113
- expect(icon).toBeInTheDocument()
114
- expect(icon.querySelector("path")).toBeInTheDocument()
115
-
116
- const button = document.querySelector(".tds-btn.tds-btn--delete")
117
- expect(button).toBeInTheDocument()
118
- })
119
- })
120
-
121
- describe("Button Interactions", () => {
122
- beforeEach(async () => {
123
- await act(async () => {
124
- SweetestAlert({
125
- title: "Interactive Test",
126
- content: "Test content",
127
- confirm: mockConfirm,
128
- cancel: mockCancel,
129
- })
130
- })
131
- })
132
-
133
- it("executes confirm callback and removes dialog when confirm button is clicked", async () => {
134
- const confirmButton = screen.getByRole("button", { name: "Okay" })
135
- fireEvent.click(confirmButton)
136
-
137
- await waitFor(() => {
138
- expect(mockConfirm).toHaveBeenCalledTimes(1)
139
- })
140
-
141
- // Dialog should be removed from DOM
142
- await waitFor(() => {
143
- expect(document.querySelector("dialog.pco-sweetest-alert")).not.toBeInTheDocument()
144
- })
145
- })
146
-
147
- it("executes cancel callback and removes dialog when cancel button is clicked", async () => {
148
- const cancelButton = screen.getByRole("button", { name: "Cancel" })
149
- fireEvent.click(cancelButton)
150
-
151
- await waitFor(() => {
152
- expect(mockCancel).toHaveBeenCalledTimes(1)
153
- })
154
-
155
- // Dialog should be removed from DOM
156
- await waitFor(() => {
157
- expect(document.querySelector("dialog.pco-sweetest-alert")).not.toBeInTheDocument()
158
- })
159
- })
160
-
161
- it("executes cancel callback and removes dialog when escape key is pressed", async () => {
162
- const dialog = document.querySelector("dialog.pco-sweetest-alert")
163
-
164
- // Simulate pressing Escape key which triggers the dialog's close event
165
- fireEvent.keyDown(dialog, { key: "Escape", code: "Escape" })
166
-
167
- // Manually trigger the close event since jsdom doesn't handle native dialog behavior
168
- fireEvent(dialog, new Event("close"))
169
-
170
- await waitFor(() => {
171
- expect(mockCancel).toHaveBeenCalledTimes(1)
172
- })
173
-
174
- // Dialog should be removed from DOM
175
- await waitFor(() => {
176
- expect(document.querySelector("dialog.pco-sweetest-alert")).not.toBeInTheDocument()
177
- })
178
- })
179
- })
180
-
181
- describe("Custom Button Labels", () => {
182
- beforeEach(async () => {
183
- await act(async () => {
184
- SweetestAlert({
185
- title: "Custom Buttons",
186
- content: "Test content",
187
- confirmButton: "Yes please",
188
- confirm: mockConfirm,
189
- cancel: mockCancel,
190
- })
191
- })
192
- })
193
-
194
- it("displays custom confirm button text when confirmButton prop is provided", () => {
195
- expect(screen.getByRole("button", { name: "Yes please" })).toBeInTheDocument()
196
- })
197
- })
198
-
199
- describe("Hide Cancel Button", () => {
200
- beforeEach(async () => {
201
- await act(async () => {
202
- SweetestAlert({
203
- title: "Custom Buttons",
204
- content: "Test content",
205
- confirm: mockConfirm,
206
- cancel: mockCancel,
207
- hideCancel: true,
208
- })
209
- })
210
- })
211
-
212
- it("does not display cancel button when hideCancel is true", () => {
213
- expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument()
214
- })
215
- })
216
-
217
- describe("Renders HTML instead of text content", () => {
218
- it("renders content property as HTML", async () => {
219
- await act(async () => {
220
- const HtmlFrag = () => {
221
- return <strong>Test content</strong>
222
- }
223
-
224
- SweetestAlert({
225
- title: "Custom HTML",
226
- content: <HtmlFrag />,
227
- confirm: mockConfirm,
228
- cancel: mockCancel,
229
- })
230
- })
231
-
232
- expect(screen.getByText("Test content")).toBeInTheDocument()
233
- expect(screen.getByText("Test content").tagName).toBe("STRONG")
234
- })
235
-
236
- it("does not render content property as HTML", async () => {
237
- await act(async () => {
238
- SweetestAlert({
239
- title: "Custom HTML",
240
- content: "<strong>Test content</strong>",
241
- confirm: mockConfirm,
242
- cancel: mockCancel,
243
- })
244
- })
245
-
246
- // Test that escaped HTML is displayed
247
- expect(screen.getByText("<strong>Test content</strong>")).toBeInTheDocument()
248
-
249
- // Test that actual HTML element is NOT created by checking there's no strong element
250
- const strongElement = document.querySelector("strong")
251
- expect(strongElement).not.toBeInTheDocument()
252
- })
253
- })
254
- })
@@ -1,13 +0,0 @@
1
- import '@testing-library/jest-dom'
2
- import { vi, beforeAll } from 'vitest'
3
-
4
- // Mock dialog.showModal() method which is not available in jsdom
5
- beforeAll(() => {
6
- HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) {
7
- this.open = true
8
- })
9
-
10
- HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
11
- this.open = false
12
- })
13
- })
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2024",
4
- "module": "esnext",
5
- "lib": ["es2024", "DOM", "DOM.Iterable"],
6
- "jsx": "react-jsx",
7
- "moduleResolution": "bundler",
8
- "resolveJsonModule": true,
9
- "allowImportingTsExtensions": true,
10
- "allowJs": true,
11
- "checkJs": false,
12
- "strict": true,
13
- "noUnusedLocals": true,
14
- "noUnusedParameters": true,
15
- "noFallthroughCasesInSwitch": true,
16
- "noEmit": true,
17
- "esModuleInterop": true,
18
- "skipLibCheck": true,
19
- "forceConsistentCasingInFileNames": true
20
- },
21
- "include": ["src/**/*", "tests/sweetest_alert.test.jsx", "tests/test-setup.ts"],
22
- "exclude": ["node_modules"]
23
- }
package/vite.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'vite'
2
- import react from '@vitejs/plugin-react'
3
-
4
- export default defineConfig({
5
- plugins: [react()],
6
- root: './docs',
7
- server: {
8
- port: 3000,
9
- open: true
10
- }
11
- })
package/vitest.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
- import react from '@vitejs/plugin-react'
3
-
4
- export default defineConfig({
5
- plugins: [react()],
6
- test: {
7
- globals: true,
8
- environment: 'jsdom',
9
- setupFiles: './tests/test-setup.ts',
10
- },
11
- })