@opengovsg/confetti 0.0.0 → 0.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 +94 -0
- package/build/confetti.css +51 -8
- package/build/index.d.ts +89 -31
- package/build/index.js +236 -131
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @opengovsg/confetti
|
|
2
|
+
|
|
3
|
+
Embeddable React survey widget for collecting user feedback.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@opengovsg/confetti)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# npm
|
|
11
|
+
npm install @opengovsg/confetti
|
|
12
|
+
|
|
13
|
+
# yarn
|
|
14
|
+
yarn add @opengovsg/confetti
|
|
15
|
+
|
|
16
|
+
# pnpm
|
|
17
|
+
pnpm add @opengovsg/confetti
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Peer Dependencies
|
|
21
|
+
|
|
22
|
+
This package requires React 18.2.0+ or React 19:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install react@^18.2.0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
1. Import the CSS file in your application entry point:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import '@opengovsg/confetti/confetti.css'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. Add the widget to your page:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { PopoverConfetti } from '@opengovsg/confetti'
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
return (
|
|
43
|
+
<div style={{ position: 'fixed', bottom: '1rem', right: '1rem' }}>
|
|
44
|
+
<PopoverConfetti
|
|
45
|
+
surveyId="your-survey-id"
|
|
46
|
+
publishableKey="your-publishable-key"
|
|
47
|
+
metadata={{}}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Note:** You'll need the survey ID and publishable key from your Confetti dashboard. Make sure to whitelist your domain in your team settings.
|
|
55
|
+
|
|
56
|
+
3. Add Confetti to your CSP headers:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
connect-src https://confetti.hack2026.gov.sg;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Template Components
|
|
63
|
+
|
|
64
|
+
Choose the component that best fits your use case:
|
|
65
|
+
|
|
66
|
+
| Component | Use Case | Behavior |
|
|
67
|
+
| ------------------ | --------------------------------- | --------------------------------------------------------- |
|
|
68
|
+
| `EmbeddedConfetti` | Inline surveys, feedback sections | Shows questions in an inline manner with a submit button |
|
|
69
|
+
| `PopoverConfetti` | Floating feedback widgets | Dismissible popover, step-by-step questions, auto-submits |
|
|
70
|
+
| `StepperConfetti` | Guided surveys | One question at a time, auto-submits after last question |
|
|
71
|
+
|
|
72
|
+
## Visibility Hooks
|
|
73
|
+
|
|
74
|
+
Control when your survey appears with these hooks:
|
|
75
|
+
|
|
76
|
+
### useVisibleAfterDelay
|
|
77
|
+
|
|
78
|
+
Show the survey after a delay:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { useVisibleAfterDelay } from '@opengovsg/confetti'
|
|
82
|
+
|
|
83
|
+
const { isVisible } = useVisibleAfterDelay({ delay: 5000 }) // 5 seconds
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### useVisibleAfterScroll
|
|
87
|
+
|
|
88
|
+
Show the survey after scrolling a certain distance:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useVisibleAfterScroll } from '@opengovsg/confetti'
|
|
92
|
+
|
|
93
|
+
const { isVisible } = useVisibleAfterScroll({ threshold: 500 }) // 500px
|
|
94
|
+
```
|
package/build/confetti.css
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
--confetti-border-bottom: 1.5px solid var(--confetti-border-color);
|
|
11
11
|
--confetti-border-radius: 12px;
|
|
12
12
|
--confetti-background-color: #ffffff;
|
|
13
|
+
--confetti-hover-background-color: #f5f5f5;
|
|
13
14
|
--confetti-box-shadow:
|
|
14
15
|
0 4px 6px -4px rgba(16, 24, 40, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
15
16
|
--confetti-focus-shadow: 0 0 0 2px rgba(9, 9, 11, 0.3);
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
.confetti-dialog {
|
|
42
|
+
max-width: var(--confetti-max-width);
|
|
41
43
|
background: transparent;
|
|
42
44
|
position: relative;
|
|
43
45
|
|
|
@@ -87,7 +89,6 @@
|
|
|
87
89
|
color: var(--confetti-text-primary-color);
|
|
88
90
|
font-size: 16px;
|
|
89
91
|
width: 100%;
|
|
90
|
-
max-width: var(--confetti-max-width);
|
|
91
92
|
background: var(--confetti-background-color);
|
|
92
93
|
|
|
93
94
|
box-shadow: var(--confetti-box-shadow);
|
|
@@ -104,9 +105,7 @@
|
|
|
104
105
|
font-weight: 500;
|
|
105
106
|
line-height: 20px;
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
transform: scale(1.05);
|
|
109
|
-
}
|
|
108
|
+
transition: background 200ms ease-out;
|
|
110
109
|
|
|
111
110
|
&:focus-visible {
|
|
112
111
|
outline: 2px solid var(--confetti-outline-color);
|
|
@@ -118,6 +117,22 @@
|
|
|
118
117
|
cursor: not-allowed;
|
|
119
118
|
transform: none;
|
|
120
119
|
}
|
|
120
|
+
|
|
121
|
+
&:hover {
|
|
122
|
+
background: color-mix(
|
|
123
|
+
in srgb,
|
|
124
|
+
white 10%,
|
|
125
|
+
var(--confetti-submit-button-color)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&:active {
|
|
130
|
+
background: color-mix(
|
|
131
|
+
in srgb,
|
|
132
|
+
white 5%,
|
|
133
|
+
var(--confetti-submit-button-color)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
.sr-only {
|
|
@@ -138,13 +153,40 @@
|
|
|
138
153
|
gap: 12px;
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
.cf-choice {
|
|
157
|
+
font-size: 16px;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
padding: 8px 10px;
|
|
160
|
+
border-radius: 6px;
|
|
161
|
+
border: 1px solid var(--confetti-border-color);
|
|
162
|
+
|
|
163
|
+
transition: background 200ms ease-out;
|
|
164
|
+
|
|
165
|
+
&:hover {
|
|
166
|
+
background: var(--confetti-hover-background-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.cf-choice-label {
|
|
170
|
+
gap: 8px;
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.cf-choice-description {
|
|
176
|
+
font-size: 0.875em;
|
|
177
|
+
margin-left: 26px;
|
|
178
|
+
color: var(--confetti-text-subtle-color);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
.cf-radio,
|
|
142
183
|
.cf-checkbox {
|
|
184
|
+
cursor: pointer;
|
|
143
185
|
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
144
187
|
/* This is needed so the hidden input is positioned correctly */
|
|
145
188
|
position: relative;
|
|
146
|
-
align-items:
|
|
147
|
-
gap: 8px;
|
|
189
|
+
align-items: start;
|
|
148
190
|
color: var(--confetti-text-primary-color);
|
|
149
191
|
forced-color-adjust: none;
|
|
150
192
|
}
|
|
@@ -153,7 +195,7 @@
|
|
|
153
195
|
--checkmark-color: var(--confetti-indicator-active-dot-color);
|
|
154
196
|
|
|
155
197
|
.cf-checkbox-indicator {
|
|
156
|
-
background:
|
|
198
|
+
background: white;
|
|
157
199
|
border: 1px solid var(--confetti-border-color);
|
|
158
200
|
width: 18px;
|
|
159
201
|
height: 18px;
|
|
@@ -217,6 +259,7 @@
|
|
|
217
259
|
border-radius: 50%;
|
|
218
260
|
transition: color 200ms ease-out;
|
|
219
261
|
border: 1px solid var(--confetti-border-color);
|
|
262
|
+
background: white;
|
|
220
263
|
|
|
221
264
|
&::after {
|
|
222
265
|
content: '';
|
|
@@ -303,7 +346,7 @@
|
|
|
303
346
|
.cf-others-choice {
|
|
304
347
|
display: flex;
|
|
305
348
|
flex-direction: column;
|
|
306
|
-
gap:
|
|
349
|
+
gap: 8px;
|
|
307
350
|
}
|
|
308
351
|
}
|
|
309
352
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as react0 from "react";
|
|
2
2
|
import { HTMLAttributes, PropsWithChildren } from "react";
|
|
3
3
|
import { z } from "zod/v4/mini";
|
|
4
|
-
import * as
|
|
4
|
+
import * as react_jsx_runtime3 from "react/jsx-runtime";
|
|
5
5
|
|
|
6
6
|
//#region ../validators/src/questions/properties.d.ts
|
|
7
|
+
|
|
7
8
|
declare const ratingQuestionSchema: z.ZodMiniObject<{
|
|
8
9
|
type: z.ZodMiniLiteral<"rating">;
|
|
9
10
|
scale: z.ZodMiniNumber<number>;
|
|
@@ -22,24 +23,26 @@ type TextQuestion = z.infer<typeof textQuestionSchema>;
|
|
|
22
23
|
declare const singleSelectQuestionSchema: z.ZodMiniObject<{
|
|
23
24
|
type: z.ZodMiniLiteral<"single-select">;
|
|
24
25
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
25
|
-
label: z.ZodMiniString<string>;
|
|
26
26
|
value: z.ZodMiniString<string>;
|
|
27
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
27
28
|
}, z.core.$strip>>;
|
|
28
29
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
29
30
|
label: z.ZodMiniString<string>;
|
|
30
31
|
max: z.ZodMiniNumber<number>;
|
|
32
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
31
33
|
}, z.core.$strip>>;
|
|
32
34
|
}, z.core.$strip>;
|
|
33
35
|
type SingleSelectQuestion = z.infer<typeof singleSelectQuestionSchema>;
|
|
34
36
|
declare const multiSelectQuestionSchema: z.ZodMiniObject<{
|
|
35
37
|
type: z.ZodMiniLiteral<"multi-select">;
|
|
36
38
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
37
|
-
label: z.ZodMiniString<string>;
|
|
38
39
|
value: z.ZodMiniString<string>;
|
|
40
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
39
41
|
}, z.core.$strip>>;
|
|
40
42
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
41
43
|
label: z.ZodMiniString<string>;
|
|
42
44
|
max: z.ZodMiniNumber<number>;
|
|
45
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
43
46
|
}, z.core.$strip>>;
|
|
44
47
|
max: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
45
48
|
}, z.core.$strip>;
|
|
@@ -66,12 +69,13 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
|
|
|
66
69
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
67
70
|
type: z.ZodMiniLiteral<"single-select">;
|
|
68
71
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
69
|
-
label: z.ZodMiniString<string>;
|
|
70
72
|
value: z.ZodMiniString<string>;
|
|
73
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
71
74
|
}, z.core.$strip>>;
|
|
72
75
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
73
76
|
label: z.ZodMiniString<string>;
|
|
74
77
|
max: z.ZodMiniNumber<number>;
|
|
78
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
75
79
|
}, z.core.$strip>>;
|
|
76
80
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
77
81
|
type: z.ZodMiniLiteral<"open-ended">;
|
|
@@ -79,12 +83,13 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
|
|
|
79
83
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
80
84
|
type: z.ZodMiniLiteral<"multi-select">;
|
|
81
85
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
82
|
-
label: z.ZodMiniString<string>;
|
|
83
86
|
value: z.ZodMiniString<string>;
|
|
87
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
84
88
|
}, z.core.$strip>>;
|
|
85
89
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
86
90
|
label: z.ZodMiniString<string>;
|
|
87
91
|
max: z.ZodMiniNumber<number>;
|
|
92
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
88
93
|
}, z.core.$strip>>;
|
|
89
94
|
max: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
90
95
|
}, z.core.$strip>], "type">;
|
|
@@ -129,14 +134,14 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
|
|
|
129
134
|
}, z.core.$strip>;
|
|
130
135
|
type PublicGetSurveyOutput = z.infer<typeof publicGetSurveyOutputSchema>;
|
|
131
136
|
//#endregion
|
|
132
|
-
//#region src/types
|
|
133
|
-
type Question
|
|
137
|
+
//#region src/core/types.d.ts
|
|
138
|
+
type Question = {
|
|
134
139
|
visible: boolean;
|
|
135
140
|
error?: string;
|
|
136
141
|
} & PublicGetSurveyOutput['questions'][number];
|
|
137
142
|
type Answer = string | number | string[] | undefined;
|
|
138
143
|
interface ConfettiState {
|
|
139
|
-
questions: Question
|
|
144
|
+
questions: Question[];
|
|
140
145
|
current: number;
|
|
141
146
|
update: ({
|
|
142
147
|
question,
|
|
@@ -151,10 +156,10 @@ interface ConfettiState {
|
|
|
151
156
|
submit: () => void;
|
|
152
157
|
isCompleted: boolean;
|
|
153
158
|
isSubmitted: boolean;
|
|
154
|
-
errors: Record<Question
|
|
159
|
+
errors: Record<Question['id'], string>;
|
|
155
160
|
}
|
|
156
161
|
//#endregion
|
|
157
|
-
//#region src/
|
|
162
|
+
//#region src/core/ConfettiController.d.ts
|
|
158
163
|
interface ConfettiControllerProps {
|
|
159
164
|
children: (props: ConfettiState) => React.ReactElement;
|
|
160
165
|
/**
|
|
@@ -170,16 +175,16 @@ declare const ConfettiController: ({
|
|
|
170
175
|
shouldAutoSubmit,
|
|
171
176
|
onFinished,
|
|
172
177
|
children
|
|
173
|
-
}: ConfettiControllerProps) =>
|
|
178
|
+
}: ConfettiControllerProps) => react_jsx_runtime3.JSX.Element;
|
|
174
179
|
//#endregion
|
|
175
|
-
//#region src/
|
|
180
|
+
//#region src/core/ConfettiProvider.d.ts
|
|
176
181
|
interface ConfettiContextValue {
|
|
177
182
|
surveyId: string;
|
|
178
183
|
metadata: Record<string, string>;
|
|
179
184
|
submit: ({
|
|
180
185
|
answers
|
|
181
186
|
}: {
|
|
182
|
-
answers: Record<Question
|
|
187
|
+
answers: Record<Question['id'], Answer>;
|
|
183
188
|
}) => Promise<void>;
|
|
184
189
|
dismiss: () => Promise<void> | void;
|
|
185
190
|
survey: PublicGetSurveyOutput | undefined;
|
|
@@ -204,7 +209,7 @@ declare const ConfettiProvider: ({
|
|
|
204
209
|
metadata,
|
|
205
210
|
isSurveyVisible: isSurveyVisibleInput,
|
|
206
211
|
apiBaseUrl
|
|
207
|
-
}: ConfettiProviderProps) =>
|
|
212
|
+
}: ConfettiProviderProps) => react_jsx_runtime3.JSX.Element | null;
|
|
208
213
|
declare const useConfetti: () => ConfettiContextValue;
|
|
209
214
|
//#endregion
|
|
210
215
|
//#region src/components/Confetti.d.ts
|
|
@@ -213,7 +218,7 @@ declare const Confetti: ({
|
|
|
213
218
|
children,
|
|
214
219
|
shouldAutoSubmit,
|
|
215
220
|
...props
|
|
216
|
-
}: ConfettiProps & Pick<ConfettiControllerProps, "children" | "shouldAutoSubmit">) =>
|
|
221
|
+
}: ConfettiProps & Pick<ConfettiControllerProps, "children" | "shouldAutoSubmit">) => react_jsx_runtime3.JSX.Element;
|
|
217
222
|
//#endregion
|
|
218
223
|
//#region src/components/ThankYouMessage.d.ts
|
|
219
224
|
interface ThankYouMessageProps {
|
|
@@ -230,7 +235,7 @@ declare const ThankYouMessage: ({
|
|
|
230
235
|
title,
|
|
231
236
|
description,
|
|
232
237
|
classNames
|
|
233
|
-
}: ThankYouMessageProps) =>
|
|
238
|
+
}: ThankYouMessageProps) => react_jsx_runtime3.JSX.Element;
|
|
234
239
|
//#endregion
|
|
235
240
|
//#region src/components/container/PopoverContainer.d.ts
|
|
236
241
|
interface PopoverContainerProps {
|
|
@@ -252,10 +257,10 @@ declare const PopoverContainer: ({
|
|
|
252
257
|
containerProps,
|
|
253
258
|
onDismiss,
|
|
254
259
|
closeOnFinishMs
|
|
255
|
-
}: PopoverContainerProps) =>
|
|
260
|
+
}: PopoverContainerProps) => react_jsx_runtime3.JSX.Element;
|
|
256
261
|
//#endregion
|
|
257
262
|
//#region src/components/questions/types.d.ts
|
|
258
|
-
type Question = PublicGetSurveyOutput['questions'][number];
|
|
263
|
+
type Question$1 = PublicGetSurveyOutput['questions'][number];
|
|
259
264
|
interface BaseFieldProps {
|
|
260
265
|
id: string;
|
|
261
266
|
title: string | null | undefined;
|
|
@@ -267,7 +272,7 @@ interface BaseFieldProps {
|
|
|
267
272
|
//#endregion
|
|
268
273
|
//#region src/components/questions/factory.d.ts
|
|
269
274
|
interface SurveyQuestionFactoryProps {
|
|
270
|
-
question: Question;
|
|
275
|
+
question: Question$1;
|
|
271
276
|
onChange: (answer: Answer) => void;
|
|
272
277
|
error?: string;
|
|
273
278
|
}
|
|
@@ -275,7 +280,7 @@ declare const SurveyQuestionFactory: ({
|
|
|
275
280
|
question,
|
|
276
281
|
onChange,
|
|
277
282
|
error
|
|
278
|
-
}: SurveyQuestionFactoryProps) =>
|
|
283
|
+
}: SurveyQuestionFactoryProps) => react_jsx_runtime3.JSX.Element;
|
|
279
284
|
//#endregion
|
|
280
285
|
//#region src/components/questions/Rating.d.ts
|
|
281
286
|
interface RatingProps extends BaseFieldProps {
|
|
@@ -308,7 +313,7 @@ declare const Rating: ({
|
|
|
308
313
|
id,
|
|
309
314
|
error,
|
|
310
315
|
required
|
|
311
|
-
}: RatingProps) =>
|
|
316
|
+
}: RatingProps) => react_jsx_runtime3.JSX.Element;
|
|
312
317
|
//#endregion
|
|
313
318
|
//#region src/components/questions/FreeText.d.ts
|
|
314
319
|
interface FreeTextProps extends BaseFieldProps {
|
|
@@ -331,7 +336,7 @@ declare const FreeText: ({
|
|
|
331
336
|
classNames,
|
|
332
337
|
required,
|
|
333
338
|
error
|
|
334
|
-
}: FreeTextProps) =>
|
|
339
|
+
}: FreeTextProps) => react_jsx_runtime3.JSX.Element;
|
|
335
340
|
//#endregion
|
|
336
341
|
//#region src/components/questions/SingleChoice.d.ts
|
|
337
342
|
interface SingleChoiceProps extends BaseFieldProps {
|
|
@@ -339,6 +344,7 @@ interface SingleChoiceProps extends BaseFieldProps {
|
|
|
339
344
|
classNames?: {
|
|
340
345
|
question?: string;
|
|
341
346
|
header?: string;
|
|
347
|
+
label?: string;
|
|
342
348
|
title?: string;
|
|
343
349
|
description?: string;
|
|
344
350
|
group?: string;
|
|
@@ -346,6 +352,7 @@ interface SingleChoiceProps extends BaseFieldProps {
|
|
|
346
352
|
indicator?: string;
|
|
347
353
|
othersChoice?: string;
|
|
348
354
|
othersInput?: string;
|
|
355
|
+
choiceDescription?: string;
|
|
349
356
|
error?: string;
|
|
350
357
|
};
|
|
351
358
|
}
|
|
@@ -358,7 +365,7 @@ declare const SingleChoice: ({
|
|
|
358
365
|
id,
|
|
359
366
|
required,
|
|
360
367
|
error
|
|
361
|
-
}: SingleChoiceProps) =>
|
|
368
|
+
}: SingleChoiceProps) => react_jsx_runtime3.JSX.Element;
|
|
362
369
|
//#endregion
|
|
363
370
|
//#region src/components/questions/MultiChoice.d.ts
|
|
364
371
|
interface MultiChoiceProps extends BaseFieldProps {
|
|
@@ -366,8 +373,10 @@ interface MultiChoiceProps extends BaseFieldProps {
|
|
|
366
373
|
classNames?: {
|
|
367
374
|
question?: string;
|
|
368
375
|
header?: string;
|
|
376
|
+
label?: string;
|
|
369
377
|
title?: string;
|
|
370
378
|
description?: string;
|
|
379
|
+
choiceDescription?: string;
|
|
371
380
|
group?: string;
|
|
372
381
|
checkbox?: string;
|
|
373
382
|
indicator?: string;
|
|
@@ -386,26 +395,26 @@ declare const MultiChoice: ({
|
|
|
386
395
|
id,
|
|
387
396
|
required,
|
|
388
397
|
error
|
|
389
|
-
}: MultiChoiceProps) =>
|
|
398
|
+
}: MultiChoiceProps) => react_jsx_runtime3.JSX.Element;
|
|
390
399
|
//#endregion
|
|
391
400
|
//#region src/components/presentation/Flow.d.ts
|
|
392
401
|
type ChildField = ({
|
|
393
402
|
onChange
|
|
394
403
|
}: {
|
|
395
404
|
onChange: (answer: Answer) => void;
|
|
396
|
-
question: Question;
|
|
405
|
+
question: Question$1;
|
|
397
406
|
error?: string;
|
|
398
407
|
}) => React.ReactNode;
|
|
399
408
|
interface FlowProps {
|
|
400
409
|
onSubmit: (answer: Answer) => void;
|
|
401
410
|
children: ChildField;
|
|
402
|
-
question: Question;
|
|
411
|
+
question: Question$1;
|
|
403
412
|
}
|
|
404
413
|
declare const Flow: ({
|
|
405
414
|
onSubmit,
|
|
406
415
|
children,
|
|
407
416
|
question
|
|
408
|
-
}: FlowProps) =>
|
|
417
|
+
}: FlowProps) => react_jsx_runtime3.JSX.Element;
|
|
409
418
|
//#endregion
|
|
410
419
|
//#region src/components/presentation/Embed.d.ts
|
|
411
420
|
declare const Embed: ({
|
|
@@ -415,13 +424,62 @@ declare const Embed: ({
|
|
|
415
424
|
}: PropsWithChildren<{
|
|
416
425
|
isCompleted: boolean;
|
|
417
426
|
onSubmit: () => void;
|
|
418
|
-
}>) =>
|
|
427
|
+
}>) => react_jsx_runtime3.JSX.Element;
|
|
419
428
|
//#endregion
|
|
420
429
|
//#region src/templates/EmbeddedConfetti.d.ts
|
|
421
|
-
declare const EmbeddedConfetti: (props: ConfettiProps) =>
|
|
430
|
+
declare const EmbeddedConfetti: (props: ConfettiProps) => react_jsx_runtime3.JSX.Element;
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/templates/PopoverConfetti.d.ts
|
|
433
|
+
interface PopoverConfettiProps extends ConfettiProps {
|
|
434
|
+
closeOnFinishMs?: number;
|
|
435
|
+
}
|
|
436
|
+
declare const PopoverConfetti: ({
|
|
437
|
+
closeOnFinishMs,
|
|
438
|
+
...props
|
|
439
|
+
}: PopoverConfettiProps) => react_jsx_runtime3.JSX.Element;
|
|
422
440
|
//#endregion
|
|
423
441
|
//#region src/templates/StepperConfetti.d.ts
|
|
424
|
-
declare const StepperConfetti: (props: ConfettiProps) =>
|
|
442
|
+
declare const StepperConfetti: (props: ConfettiProps) => react_jsx_runtime3.JSX.Element;
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/hooks/useVisibleAfterDelay.d.ts
|
|
445
|
+
interface UseVisibleAfterDelayOptions {
|
|
446
|
+
delay: number;
|
|
447
|
+
}
|
|
448
|
+
declare const useVisibleAfterDelay: ({
|
|
449
|
+
delay
|
|
450
|
+
}: UseVisibleAfterDelayOptions) => {
|
|
451
|
+
isVisible: boolean;
|
|
452
|
+
};
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/hooks/useVisibleAfterPageVisits.d.ts
|
|
455
|
+
interface UseVisibleAfterPageVisitsOptions {
|
|
456
|
+
visits: number;
|
|
457
|
+
respondent?: string;
|
|
458
|
+
}
|
|
459
|
+
declare const useVisibleAfterPageVisits: ({
|
|
460
|
+
visits,
|
|
461
|
+
respondent
|
|
462
|
+
}: UseVisibleAfterPageVisitsOptions) => {
|
|
463
|
+
isVisible: boolean;
|
|
464
|
+
reset: () => void;
|
|
465
|
+
};
|
|
466
|
+
declare const useVisibleAfterSessionPageVisits: ({
|
|
467
|
+
visits,
|
|
468
|
+
respondent
|
|
469
|
+
}: UseVisibleAfterPageVisitsOptions) => {
|
|
470
|
+
isVisible: boolean;
|
|
471
|
+
reset: () => void;
|
|
472
|
+
};
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/hooks/useVisibleAfterScroll.d.ts
|
|
475
|
+
interface UseVisibleAfterScrollOptions {
|
|
476
|
+
threshold: number;
|
|
477
|
+
}
|
|
478
|
+
declare const useVisibleAfterScroll: ({
|
|
479
|
+
threshold
|
|
480
|
+
}: UseVisibleAfterScrollOptions) => {
|
|
481
|
+
isVisible: boolean;
|
|
482
|
+
};
|
|
425
483
|
//#endregion
|
|
426
|
-
export { Confetti, ConfettiContext, ConfettiController, type ConfettiControllerProps, type ConfettiProps, ConfettiProvider, type ConfettiProviderProps, Embed, EmbeddedConfetti, Flow, FreeText, type FreeTextProps, MultiChoice, type MultiChoiceProps, PopoverContainer, type PopoverContainerProps, Rating, type RatingProps, SingleChoice, type SingleChoiceProps, StepperConfetti, SurveyQuestionFactory, type SurveyQuestionFactoryProps, ThankYouMessage, useConfetti };
|
|
484
|
+
export { type Answer, Confetti, ConfettiContext, ConfettiController, type ConfettiControllerProps, type ConfettiProps, ConfettiProvider, type ConfettiProviderProps, type ConfettiState, Embed, EmbeddedConfetti, Flow, FreeText, type FreeTextProps, MultiChoice, type MultiChoiceProps, PopoverConfetti, PopoverContainer, type PopoverContainerProps, type Question, Rating, type RatingProps, SingleChoice, type SingleChoiceProps, StepperConfetti, SurveyQuestionFactory, type SurveyQuestionFactoryProps, ThankYouMessage, useConfetti, useVisibleAfterDelay, useVisibleAfterPageVisits, useVisibleAfterScroll, useVisibleAfterSessionPageVisits };
|
|
427
485
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.js
CHANGED
|
@@ -1,67 +1,16 @@
|
|
|
1
1
|
import { ErrorBoundary } from "react-error-boundary";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useLocalStorage, useSessionStorage, useTimeout } from "usehooks-ts";
|
|
3
4
|
import { z } from "zod/v4/mini";
|
|
4
|
-
import { useLocalStorage, useTimeout } from "usehooks-ts";
|
|
5
5
|
import ky from "ky";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
import { Button, Checkbox, CheckboxGroup, FieldError, Input, Label, Radio, RadioGroup, Text, TextArea, TextField, composeRenderProps } from "react-aria-components";
|
|
9
9
|
|
|
10
|
-
//#region src/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const question = questions[i];
|
|
15
|
-
const answer = question ? answers[question.id] : void 0;
|
|
16
|
-
if (!question || !question.visible) continue;
|
|
17
|
-
const logic = question.routingLogic;
|
|
18
|
-
if (logic.type === "always") if (evaluateTarget({
|
|
19
|
-
position: i,
|
|
20
|
-
value: logic.target
|
|
21
|
-
}) === index) return true;
|
|
22
|
-
else continue;
|
|
23
|
-
if ((answer === void 0 || answer === null) && question.required) continue;
|
|
24
|
-
const cases = logic.cases;
|
|
25
|
-
const match = answer !== null ? cases.find((condition) => evaluateCondition({
|
|
26
|
-
answer,
|
|
27
|
-
condition
|
|
28
|
-
})) : void 0;
|
|
29
|
-
if (!match) if (evaluateTarget({
|
|
30
|
-
position: i,
|
|
31
|
-
value: logic.default
|
|
32
|
-
}) === index) return true;
|
|
33
|
-
else continue;
|
|
34
|
-
if (evaluateTarget({
|
|
35
|
-
position: i,
|
|
36
|
-
value: match.target
|
|
37
|
-
}) === index) return true;
|
|
38
|
-
else continue;
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
};
|
|
42
|
-
const evaluateTarget = ({ position, value }) => {
|
|
43
|
-
switch (value.type) {
|
|
44
|
-
case "next": return position + 1;
|
|
45
|
-
case "jump": return value.target;
|
|
46
|
-
case "end": return null;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const evaluateCondition = ({ answer, condition }) => {
|
|
50
|
-
const value = evaluateValue(condition.value);
|
|
51
|
-
switch (condition.operator) {
|
|
52
|
-
case "equal": return answer === value;
|
|
53
|
-
case "not-equal": return answer !== value;
|
|
54
|
-
case "greater": return typeof answer === "number" && typeof value === "number" && answer > value;
|
|
55
|
-
case "greater-or-equal": return typeof answer === "number" && typeof value === "number" && answer >= value;
|
|
56
|
-
case "less": return typeof answer === "number" && typeof value === "number" && answer < value;
|
|
57
|
-
case "less-or-equal": return typeof answer === "number" && typeof value === "number" && answer <= value;
|
|
58
|
-
case "contains": return Array.isArray(answer) && typeof value === "string" && answer.includes(value);
|
|
59
|
-
case "not-contains": return Array.isArray(answer) && typeof value === "string" && !answer.includes(value);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const evaluateValue = (node) => {
|
|
63
|
-
return node.value;
|
|
64
|
-
};
|
|
10
|
+
//#region src/utils.ts
|
|
11
|
+
function prettifyError(error) {
|
|
12
|
+
return error.issues[0]?.message ?? "Invalid input";
|
|
13
|
+
}
|
|
65
14
|
|
|
66
15
|
//#endregion
|
|
67
16
|
//#region ../validators/src/questions/routing.ts
|
|
@@ -109,12 +58,13 @@ const routingLogicSchema = z.discriminatedUnion("type", [alwaysConditionSchema,
|
|
|
109
58
|
//#endregion
|
|
110
59
|
//#region ../validators/src/questions/properties.ts
|
|
111
60
|
const optionSchema = z.object({
|
|
112
|
-
|
|
113
|
-
|
|
61
|
+
value: z.string().check(z.trim(), z.minLength(1, { error: "Value is required" })),
|
|
62
|
+
description: z.optional(z.string().check(z.trim()))
|
|
114
63
|
});
|
|
115
64
|
const editableSchema = z.nullable(z.object({
|
|
116
|
-
label: z.string().check(z.trim(), z.minLength(1, { error: "
|
|
117
|
-
max: z.number()
|
|
65
|
+
label: z.string().check(z.trim(), z.minLength(1, { error: "Value is required" })),
|
|
66
|
+
max: z.number(),
|
|
67
|
+
description: z.optional(z.string().check(z.trim()))
|
|
118
68
|
}));
|
|
119
69
|
const ratingQuestionSchema = z.object({
|
|
120
70
|
type: z.literal("rating"),
|
|
@@ -201,48 +151,8 @@ const publicPutResponseInputSchema = z.object({
|
|
|
201
151
|
const publicGetRespondentOutputSchema = z.object({ lastRespondedAt: z.nullable(z.union([z.iso.datetime(), z.date()])) });
|
|
202
152
|
|
|
203
153
|
//#endregion
|
|
204
|
-
//#region src/logic/
|
|
205
|
-
const
|
|
206
|
-
rating: ({ scale }, meta) => ratingAnswerSchemaFactory({ scale }, meta),
|
|
207
|
-
"open-ended": ({ max }, meta) => openEndedAnswerSchemaFactory({ maxLength: max }, meta),
|
|
208
|
-
"single-select": ({ editable }, meta) => singleSelectAnswerSchemaFactory({ maxLength: editable?.max }, meta),
|
|
209
|
-
"multi-select": ({ editable, max }, meta) => multiSelectAnswerSchemaFactory({
|
|
210
|
-
maxLength: editable?.max,
|
|
211
|
-
maxSelected: max
|
|
212
|
-
}, meta)
|
|
213
|
-
};
|
|
214
|
-
const getAnswerValidator = (properties, meta) => {
|
|
215
|
-
const type = properties.type;
|
|
216
|
-
switch (type) {
|
|
217
|
-
case "rating": {
|
|
218
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
219
|
-
return validator(properties, meta);
|
|
220
|
-
}
|
|
221
|
-
case "open-ended": {
|
|
222
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
223
|
-
return validator(properties, meta);
|
|
224
|
-
}
|
|
225
|
-
case "single-select": {
|
|
226
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
227
|
-
return validator(properties, meta);
|
|
228
|
-
}
|
|
229
|
-
case "multi-select": {
|
|
230
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
231
|
-
return validator(properties, meta);
|
|
232
|
-
}
|
|
233
|
-
default: throw new Error("Unsupported question type");
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
//#endregion
|
|
238
|
-
//#region src/utils.ts
|
|
239
|
-
function prettifyError(error) {
|
|
240
|
-
return error.issues[0]?.message ?? "Invalid input";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
//#endregion
|
|
244
|
-
//#region src/logic/api.ts
|
|
245
|
-
const API_HOST = "https://confetti.hack2026.gov.sg/api/cfti";
|
|
154
|
+
//#region src/core/logic/api.ts
|
|
155
|
+
const API_HOST = "https://confetti.hack2026.gov.sg/api";
|
|
246
156
|
const client = ky.create({
|
|
247
157
|
prefixUrl: API_HOST,
|
|
248
158
|
retry: {
|
|
@@ -253,7 +163,7 @@ const client = ky.create({
|
|
|
253
163
|
});
|
|
254
164
|
|
|
255
165
|
//#endregion
|
|
256
|
-
//#region src/
|
|
166
|
+
//#region src/core/ConfettiProvider.tsx
|
|
257
167
|
const ConfettiContext = createContext(null);
|
|
258
168
|
const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, metadata, isSurveyVisible: isSurveyVisibleInput, apiBaseUrl }) => {
|
|
259
169
|
const [survey, setSurvey] = useState();
|
|
@@ -270,13 +180,13 @@ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, meta
|
|
|
270
180
|
return client.extend(properties);
|
|
271
181
|
}, [apiBaseUrl, publishableKey]);
|
|
272
182
|
useEffect(() => {
|
|
273
|
-
client$1.get(
|
|
183
|
+
client$1.get(`v1/cfti/${surveyId}`).json().then((data) => {
|
|
274
184
|
const parseResult = publicGetSurveyOutputSchema.safeParse(data);
|
|
275
185
|
if (!parseResult.success) return;
|
|
276
186
|
setSurvey(parseResult.data);
|
|
277
187
|
});
|
|
278
188
|
if (!respondent && !internalRespondentId) return;
|
|
279
|
-
client$1.get(
|
|
189
|
+
client$1.get(`v1/cfti/${surveyId}/respondent/${respondent ?? internalRespondentId}`).json().then((data) => {
|
|
280
190
|
const parseResult = publicGetRespondentOutputSchema.safeParse(data);
|
|
281
191
|
if (!parseResult.success) {
|
|
282
192
|
setLastRespondedAt(null);
|
|
@@ -292,7 +202,7 @@ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, meta
|
|
|
292
202
|
surveyId
|
|
293
203
|
]);
|
|
294
204
|
const submit = useCallback(async ({ answers }) => {
|
|
295
|
-
await client$1.put(
|
|
205
|
+
await client$1.put(`v1/cfti/${surveyId}/response/${responseId}`, { json: {
|
|
296
206
|
respondent: respondent ?? internalRespondentId,
|
|
297
207
|
data: answers,
|
|
298
208
|
metadata
|
|
@@ -332,7 +242,98 @@ const useConfetti = () => {
|
|
|
332
242
|
};
|
|
333
243
|
|
|
334
244
|
//#endregion
|
|
335
|
-
//#region src/
|
|
245
|
+
//#region src/core/logic/route.ts
|
|
246
|
+
const isVisible = ({ index, questions, answers }) => {
|
|
247
|
+
if (index === 0) return true;
|
|
248
|
+
for (let i = 0; i < index; i++) {
|
|
249
|
+
const question = questions[i];
|
|
250
|
+
const answer = question ? answers[question.id] : void 0;
|
|
251
|
+
if (!question || !question.visible) continue;
|
|
252
|
+
const logic = question.routingLogic;
|
|
253
|
+
if (logic.type === "always") if (evaluateTarget({
|
|
254
|
+
position: i,
|
|
255
|
+
value: logic.target
|
|
256
|
+
}) === index) return true;
|
|
257
|
+
else continue;
|
|
258
|
+
if ((answer === void 0 || answer === null) && question.required) continue;
|
|
259
|
+
const cases = logic.cases;
|
|
260
|
+
const match = answer !== null ? cases.find((condition) => evaluateCondition({
|
|
261
|
+
answer,
|
|
262
|
+
condition
|
|
263
|
+
})) : void 0;
|
|
264
|
+
if (!match) if (evaluateTarget({
|
|
265
|
+
position: i,
|
|
266
|
+
value: logic.default
|
|
267
|
+
}) === index) return true;
|
|
268
|
+
else continue;
|
|
269
|
+
if (evaluateTarget({
|
|
270
|
+
position: i,
|
|
271
|
+
value: match.target
|
|
272
|
+
}) === index) return true;
|
|
273
|
+
else continue;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
};
|
|
277
|
+
const evaluateTarget = ({ position, value }) => {
|
|
278
|
+
switch (value.type) {
|
|
279
|
+
case "next": return position + 1;
|
|
280
|
+
case "jump": return value.target;
|
|
281
|
+
case "end": return null;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const evaluateCondition = ({ answer, condition }) => {
|
|
285
|
+
const value = evaluateValue(condition.value);
|
|
286
|
+
switch (condition.operator) {
|
|
287
|
+
case "equal": return answer === value;
|
|
288
|
+
case "not-equal": return answer !== value;
|
|
289
|
+
case "greater": return typeof answer === "number" && typeof value === "number" && answer > value;
|
|
290
|
+
case "greater-or-equal": return typeof answer === "number" && typeof value === "number" && answer >= value;
|
|
291
|
+
case "less": return typeof answer === "number" && typeof value === "number" && answer < value;
|
|
292
|
+
case "less-or-equal": return typeof answer === "number" && typeof value === "number" && answer <= value;
|
|
293
|
+
case "contains": return Array.isArray(answer) && typeof value === "string" && answer.includes(value);
|
|
294
|
+
case "not-contains": return Array.isArray(answer) && typeof value === "string" && !answer.includes(value);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const evaluateValue = (node) => {
|
|
298
|
+
return node.value;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/core/logic/validate.ts
|
|
303
|
+
const VALIDATOR_FACTORY_MAP = {
|
|
304
|
+
rating: ({ scale }, meta) => ratingAnswerSchemaFactory({ scale }, meta),
|
|
305
|
+
"open-ended": ({ max }, meta) => openEndedAnswerSchemaFactory({ maxLength: max }, meta),
|
|
306
|
+
"single-select": ({ editable }, meta) => singleSelectAnswerSchemaFactory({ maxLength: editable?.max }, meta),
|
|
307
|
+
"multi-select": ({ editable, max }, meta) => multiSelectAnswerSchemaFactory({
|
|
308
|
+
maxLength: editable?.max,
|
|
309
|
+
maxSelected: max
|
|
310
|
+
}, meta)
|
|
311
|
+
};
|
|
312
|
+
const getAnswerValidator = (properties, meta) => {
|
|
313
|
+
const type = properties.type;
|
|
314
|
+
switch (type) {
|
|
315
|
+
case "rating": {
|
|
316
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
317
|
+
return validator(properties, meta);
|
|
318
|
+
}
|
|
319
|
+
case "open-ended": {
|
|
320
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
321
|
+
return validator(properties, meta);
|
|
322
|
+
}
|
|
323
|
+
case "single-select": {
|
|
324
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
325
|
+
return validator(properties, meta);
|
|
326
|
+
}
|
|
327
|
+
case "multi-select": {
|
|
328
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
329
|
+
return validator(properties, meta);
|
|
330
|
+
}
|
|
331
|
+
default: throw new Error("Unsupported question type");
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/core/ConfettiController.tsx
|
|
336
337
|
const getNextQuestionState = ({ originalQuestions, answers, errors, pointer = -1 }) => {
|
|
337
338
|
if (!originalQuestions) return {
|
|
338
339
|
questions: [],
|
|
@@ -578,23 +579,29 @@ const FreeText = ({ id, title, description, properties, onChange, classNames, re
|
|
|
578
579
|
|
|
579
580
|
//#endregion
|
|
580
581
|
//#region src/components/questions/MultiChoice.tsx
|
|
581
|
-
const MultiChoiceCheckbox = ({ children, classNames, ...props }) => {
|
|
582
|
+
const MultiChoiceCheckbox = ({ children, classNames, description, ...props }) => {
|
|
582
583
|
return /* @__PURE__ */ jsx(Checkbox, {
|
|
583
584
|
...props,
|
|
584
|
-
children: ({ isIndeterminate }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */
|
|
585
|
-
className: clsx("cf-
|
|
586
|
-
children: /* @__PURE__ */ jsx("
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
585
|
+
children: ({ isIndeterminate }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
586
|
+
className: clsx("cf-choice-label", classNames?.label),
|
|
587
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
588
|
+
className: clsx("cf-checkbox-indicator", classNames?.indicator),
|
|
589
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
590
|
+
viewBox: "0 0 18 18",
|
|
591
|
+
"aria-hidden": "true",
|
|
592
|
+
className: clsx("cf-checkbox-indicator-svg", classNames?.svg),
|
|
593
|
+
children: isIndeterminate ? /* @__PURE__ */ jsx("rect", {
|
|
594
|
+
x: 1,
|
|
595
|
+
y: 7.5,
|
|
596
|
+
width: 16,
|
|
597
|
+
height: 3
|
|
598
|
+
}) : /* @__PURE__ */ jsx("polyline", { points: "2 9 7 14 16 4" })
|
|
599
|
+
}, isIndeterminate ? "indeterminate" : "check")
|
|
600
|
+
}), children]
|
|
601
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
602
|
+
className: clsx("cf-choice-description", classNames?.choiceDescription),
|
|
603
|
+
children: description
|
|
604
|
+
})] })
|
|
598
605
|
});
|
|
599
606
|
};
|
|
600
607
|
const MultiChoice = ({ title, description, properties, classNames, onChange, id, required, error }) => {
|
|
@@ -635,15 +642,16 @@ const MultiChoice = ({ title, description, properties, classNames, onChange, id,
|
|
|
635
642
|
/* @__PURE__ */ jsxs("div", {
|
|
636
643
|
className: clsx("cf-response-choices", classNames?.group),
|
|
637
644
|
children: [properties.options.map((option, index) => /* @__PURE__ */ jsx(MultiChoiceCheckbox, {
|
|
638
|
-
className: clsx("cf-checkbox", classNames?.checkbox),
|
|
645
|
+
className: clsx("cf-choice cf-checkbox", classNames?.checkbox),
|
|
639
646
|
value: option.value,
|
|
640
647
|
classNames: {
|
|
641
648
|
indicator: classNames?.indicator,
|
|
642
649
|
svg: classNames?.svg
|
|
643
650
|
},
|
|
644
|
-
|
|
651
|
+
description: option.description,
|
|
652
|
+
children: option.value
|
|
645
653
|
}, index)), hasEditable && /* @__PURE__ */ jsxs("div", {
|
|
646
|
-
className: clsx("cf-others-choice", classNames?.othersChoice),
|
|
654
|
+
className: clsx("cf-choice cf-others-choice", classNames?.othersChoice),
|
|
647
655
|
children: [/* @__PURE__ */ jsx(MultiChoiceCheckbox, {
|
|
648
656
|
inputRef: othersRef,
|
|
649
657
|
onChange: (isSelected) => {
|
|
@@ -659,6 +667,7 @@ const MultiChoice = ({ title, description, properties, classNames, onChange, id,
|
|
|
659
667
|
indicator: classNames?.indicator,
|
|
660
668
|
svg: classNames?.svg
|
|
661
669
|
},
|
|
670
|
+
description: properties.editable?.description,
|
|
662
671
|
children: properties.editable?.label
|
|
663
672
|
}), /* @__PURE__ */ jsx(Input, {
|
|
664
673
|
value: othersValue,
|
|
@@ -942,11 +951,17 @@ const Rating = ({ properties, title, description, classNames, onChange, id, erro
|
|
|
942
951
|
|
|
943
952
|
//#endregion
|
|
944
953
|
//#region src/components/questions/SingleChoice.tsx
|
|
945
|
-
const SingleChoiceRadio = ({ classNames, className, ...props }) => {
|
|
954
|
+
const SingleChoiceRadio = ({ classNames, className, description, ...props }) => {
|
|
946
955
|
return /* @__PURE__ */ jsx(Radio, {
|
|
947
956
|
...props,
|
|
948
957
|
className: clsx(className, classNames?.radio),
|
|
949
|
-
children: composeRenderProps(props.children, (children) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */
|
|
958
|
+
children: composeRenderProps(props.children, (children) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
959
|
+
className: clsx("cf-choice-label", classNames?.label),
|
|
960
|
+
children: [/* @__PURE__ */ jsx("div", { className: clsx("cf-radio-indicator", classNames?.indicator) }), children]
|
|
961
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
962
|
+
className: clsx("cf-choice-description", classNames?.choiceDescription),
|
|
963
|
+
children: description
|
|
964
|
+
})] }))
|
|
950
965
|
});
|
|
951
966
|
};
|
|
952
967
|
const SingleChoice = ({ title, description, properties, classNames, onChange, id, required, error }) => {
|
|
@@ -992,12 +1007,13 @@ const SingleChoice = ({ title, description, properties, classNames, onChange, id
|
|
|
992
1007
|
children: [properties.options.map((option, index) => /* @__PURE__ */ jsx(SingleChoiceRadio, {
|
|
993
1008
|
classNames: {
|
|
994
1009
|
indicator: classNames?.indicator,
|
|
995
|
-
radio: clsx("cf-radio", classNames?.radio)
|
|
1010
|
+
radio: clsx("cf-choice cf-radio", classNames?.radio)
|
|
996
1011
|
},
|
|
997
1012
|
value: option.value,
|
|
998
|
-
|
|
1013
|
+
description: option.description,
|
|
1014
|
+
children: option.value
|
|
999
1015
|
}, index)), hasEditable && /* @__PURE__ */ jsxs("div", {
|
|
1000
|
-
className: clsx("cf-others-choice", classNames?.othersChoice),
|
|
1016
|
+
className: clsx("cf-choice cf-others-choice", classNames?.othersChoice),
|
|
1001
1017
|
children: [/* @__PURE__ */ jsx(SingleChoiceRadio, {
|
|
1002
1018
|
inputRef: othersRef,
|
|
1003
1019
|
value: RADIO_OTHERS_VALUE,
|
|
@@ -1005,6 +1021,7 @@ const SingleChoice = ({ title, description, properties, classNames, onChange, id
|
|
|
1005
1021
|
indicator: classNames?.indicator,
|
|
1006
1022
|
radio: clsx("cf-radio", classNames?.radio)
|
|
1007
1023
|
},
|
|
1024
|
+
description: properties.editable?.description,
|
|
1008
1025
|
children: properties.editable?.label
|
|
1009
1026
|
}), /* @__PURE__ */ jsx(Input, {
|
|
1010
1027
|
tabIndex: state.selectedValue === RADIO_OTHERS_VALUE ? 0 : -1,
|
|
@@ -1126,6 +1143,40 @@ const EmbeddedConfetti = (props) => {
|
|
|
1126
1143
|
});
|
|
1127
1144
|
};
|
|
1128
1145
|
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/templates/PopoverConfetti.tsx
|
|
1148
|
+
const PopoverConfetti = ({ closeOnFinishMs, ...props }) => {
|
|
1149
|
+
return /* @__PURE__ */ jsx(ConfettiProvider, {
|
|
1150
|
+
...props,
|
|
1151
|
+
children: /* @__PURE__ */ jsx(PopoverContainer, {
|
|
1152
|
+
closeOnFinishMs,
|
|
1153
|
+
children: ({ setIsFinished }) => /* @__PURE__ */ jsx(ConfettiController, {
|
|
1154
|
+
shouldAutoSubmit: true,
|
|
1155
|
+
...props,
|
|
1156
|
+
onFinished: () => {
|
|
1157
|
+
setIsFinished();
|
|
1158
|
+
props.onFinished?.();
|
|
1159
|
+
},
|
|
1160
|
+
children: ({ current, questions, update, isSubmitted }) => {
|
|
1161
|
+
if (isSubmitted) return /* @__PURE__ */ jsx(ThankYouMessage, {});
|
|
1162
|
+
const currentQuestion = questions[current];
|
|
1163
|
+
if (!currentQuestion) return /* @__PURE__ */ jsx(Fragment, {});
|
|
1164
|
+
return /* @__PURE__ */ jsx(Flow, {
|
|
1165
|
+
question: currentQuestion,
|
|
1166
|
+
onSubmit: (answer) => {
|
|
1167
|
+
return update({
|
|
1168
|
+
question: current,
|
|
1169
|
+
answer
|
|
1170
|
+
});
|
|
1171
|
+
},
|
|
1172
|
+
children: (flowProps) => /* @__PURE__ */ jsx(SurveyQuestionFactory, { ...flowProps })
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
})
|
|
1176
|
+
})
|
|
1177
|
+
});
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1129
1180
|
//#endregion
|
|
1130
1181
|
//#region src/templates/StepperConfetti.tsx
|
|
1131
1182
|
const StepperConfetti = (props) => {
|
|
@@ -1151,5 +1202,59 @@ const StepperConfetti = (props) => {
|
|
|
1151
1202
|
};
|
|
1152
1203
|
|
|
1153
1204
|
//#endregion
|
|
1154
|
-
|
|
1205
|
+
//#region src/hooks/useVisibleAfterDelay.ts
|
|
1206
|
+
const useVisibleAfterDelay = ({ delay }) => {
|
|
1207
|
+
const [isVisible$1, setIsVisible] = useState(false);
|
|
1208
|
+
const initialDelay = useMemo(() => delay, []);
|
|
1209
|
+
useEffect(() => {
|
|
1210
|
+
const timeoutId = setTimeout(() => {
|
|
1211
|
+
setIsVisible(true);
|
|
1212
|
+
}, initialDelay);
|
|
1213
|
+
return () => clearTimeout(timeoutId);
|
|
1214
|
+
}, []);
|
|
1215
|
+
return { isVisible: isVisible$1 };
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
//#endregion
|
|
1219
|
+
//#region src/hooks/useVisibleAfterPageVisits.ts
|
|
1220
|
+
const useVisibleAfterPageVisits = ({ visits, respondent }) => {
|
|
1221
|
+
const [visitCount, setVisitCount] = useLocalStorage(`cfti:${useMemo(() => window.location.pathname, [])}:visits:${respondent ?? ""}`, 0);
|
|
1222
|
+
useEffect(() => {
|
|
1223
|
+
setVisitCount((prev) => prev + 1);
|
|
1224
|
+
}, [setVisitCount]);
|
|
1225
|
+
return {
|
|
1226
|
+
isVisible: visitCount >= visits,
|
|
1227
|
+
reset: useCallback(() => setVisitCount(0), [setVisitCount])
|
|
1228
|
+
};
|
|
1229
|
+
};
|
|
1230
|
+
const useVisibleAfterSessionPageVisits = ({ visits, respondent }) => {
|
|
1231
|
+
const [visitCount, setVisitCount] = useSessionStorage(`cfti:${useMemo(() => window.location.pathname, [])}:visits:${respondent ?? ""}`, 0);
|
|
1232
|
+
useEffect(() => {
|
|
1233
|
+
setVisitCount((prev) => prev + 1);
|
|
1234
|
+
}, [setVisitCount]);
|
|
1235
|
+
return {
|
|
1236
|
+
isVisible: visitCount >= visits,
|
|
1237
|
+
reset: useCallback(() => setVisitCount(0), [setVisitCount])
|
|
1238
|
+
};
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/hooks/useVisibleAfterScroll.ts
|
|
1243
|
+
const useVisibleAfterScroll = ({ threshold }) => {
|
|
1244
|
+
const [isVisible$1, setIsVisible] = useState(() => {
|
|
1245
|
+
if (typeof window === "undefined") return false;
|
|
1246
|
+
return window.scrollY >= threshold;
|
|
1247
|
+
});
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
const handleScroll = () => {
|
|
1250
|
+
if (window.scrollY >= threshold) setIsVisible(true);
|
|
1251
|
+
};
|
|
1252
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
1253
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
1254
|
+
}, [threshold]);
|
|
1255
|
+
return { isVisible: isVisible$1 };
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
//#endregion
|
|
1259
|
+
export { Confetti, ConfettiContext, ConfettiController, ConfettiProvider, Embed, EmbeddedConfetti, Flow, FreeText, MultiChoice, PopoverConfetti, PopoverContainer, Rating, SingleChoice, StepperConfetti, SurveyQuestionFactory, ThankYouMessage, useConfetti, useVisibleAfterDelay, useVisibleAfterPageVisits, useVisibleAfterScroll, useVisibleAfterSessionPageVisits };
|
|
1155
1260
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengovsg/confetti",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Widgets for Confetti",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"prettier": "^3.7.4",
|
|
39
39
|
"tsdown": "0.19.0",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"@confetti/validators": "0.0.0",
|
|
42
41
|
"@confetti/tsconfig": "0.0.0",
|
|
43
|
-
"@confetti/
|
|
44
|
-
"@confetti/prettier-config": "0.0.0"
|
|
42
|
+
"@confetti/validators": "0.0.0",
|
|
43
|
+
"@confetti/prettier-config": "0.0.0",
|
|
44
|
+
"@confetti/eslint-config": "0.0.0"
|
|
45
45
|
},
|
|
46
46
|
"prettier": "@confetti/prettier-config",
|
|
47
47
|
"scripts": {
|