@opengovsg/confetti 0.0.0 → 0.0.2

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 ADDED
@@ -0,0 +1,94 @@
1
+ # @opengovsg/confetti
2
+
3
+ Embeddable React survey widget for collecting user feedback.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@opengovsg/confetti.svg)](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
+ ```
@@ -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
- &:hover {
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: center;
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: var(--confetti-input-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: 4px;
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 react_jsx_runtime1 from "react/jsx-runtime";
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">;
@@ -126,17 +131,22 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
126
131
  }, z.core.$strip>], "type">;
127
132
  }, z.core.$strip>], "type">;
128
133
  }, z.core.$strip>>;
134
+ confirmation: z.ZodMiniObject<{
135
+ title: z.ZodMiniString<string>;
136
+ description: z.ZodMiniString<string>;
137
+ buttonText: z.ZodMiniString<string>;
138
+ }, z.core.$strip>;
129
139
  }, z.core.$strip>;
130
140
  type PublicGetSurveyOutput = z.infer<typeof publicGetSurveyOutputSchema>;
131
141
  //#endregion
132
- //#region src/types/index.d.ts
133
- type Question$1 = {
142
+ //#region src/core/types.d.ts
143
+ type Question = {
134
144
  visible: boolean;
135
145
  error?: string;
136
146
  } & PublicGetSurveyOutput['questions'][number];
137
147
  type Answer = string | number | string[] | undefined;
138
148
  interface ConfettiState {
139
- questions: Question$1[];
149
+ questions: Question[];
140
150
  current: number;
141
151
  update: ({
142
152
  question,
@@ -151,10 +161,10 @@ interface ConfettiState {
151
161
  submit: () => void;
152
162
  isCompleted: boolean;
153
163
  isSubmitted: boolean;
154
- errors: Record<Question$1['id'], string>;
164
+ errors: Record<Question['id'], string>;
155
165
  }
156
166
  //#endregion
157
- //#region src/components/ConfettiController.d.ts
167
+ //#region src/core/ConfettiController.d.ts
158
168
  interface ConfettiControllerProps {
159
169
  children: (props: ConfettiState) => React.ReactElement;
160
170
  /**
@@ -170,16 +180,16 @@ declare const ConfettiController: ({
170
180
  shouldAutoSubmit,
171
181
  onFinished,
172
182
  children
173
- }: ConfettiControllerProps) => react_jsx_runtime1.JSX.Element;
183
+ }: ConfettiControllerProps) => react_jsx_runtime3.JSX.Element;
174
184
  //#endregion
175
- //#region src/components/ConfettiProvider.d.ts
185
+ //#region src/core/ConfettiProvider.d.ts
176
186
  interface ConfettiContextValue {
177
187
  surveyId: string;
178
188
  metadata: Record<string, string>;
179
189
  submit: ({
180
190
  answers
181
191
  }: {
182
- answers: Record<Question$1['id'], Answer>;
192
+ answers: Record<Question['id'], Answer>;
183
193
  }) => Promise<void>;
184
194
  dismiss: () => Promise<void> | void;
185
195
  survey: PublicGetSurveyOutput | undefined;
@@ -190,7 +200,7 @@ interface ConfettiProviderProps {
190
200
  surveyId: string;
191
201
  publishableKey: string;
192
202
  respondent?: string;
193
- metadata: Record<string, string>;
203
+ metadata?: Record<string, string>;
194
204
  apiBaseUrl?: string;
195
205
  isSurveyVisible?: boolean | ((_: {
196
206
  lastRespondedAt: Date | null;
@@ -204,16 +214,17 @@ declare const ConfettiProvider: ({
204
214
  metadata,
205
215
  isSurveyVisible: isSurveyVisibleInput,
206
216
  apiBaseUrl
207
- }: ConfettiProviderProps) => react_jsx_runtime1.JSX.Element | null;
217
+ }: ConfettiProviderProps) => react_jsx_runtime3.JSX.Element | null;
208
218
  declare const useConfetti: () => ConfettiContextValue;
209
219
  //#endregion
210
220
  //#region src/components/Confetti.d.ts
211
221
  interface ConfettiProps extends Omit<ConfettiProviderProps, 'children'>, Pick<ConfettiControllerProps, 'onFinished' | 'shouldAutoSubmit'> {}
212
222
  declare const Confetti: ({
213
223
  children,
224
+ onFinished,
214
225
  shouldAutoSubmit,
215
226
  ...props
216
- }: ConfettiProps & Pick<ConfettiControllerProps, "children" | "shouldAutoSubmit">) => react_jsx_runtime1.JSX.Element;
227
+ }: ConfettiProps & Pick<ConfettiControllerProps, "children" | "shouldAutoSubmit">) => react_jsx_runtime3.JSX.Element;
217
228
  //#endregion
218
229
  //#region src/components/ThankYouMessage.d.ts
219
230
  interface ThankYouMessageProps {
@@ -230,11 +241,11 @@ declare const ThankYouMessage: ({
230
241
  title,
231
242
  description,
232
243
  classNames
233
- }: ThankYouMessageProps) => react_jsx_runtime1.JSX.Element;
244
+ }: ThankYouMessageProps) => react_jsx_runtime3.JSX.Element | null;
234
245
  //#endregion
235
246
  //#region src/components/container/PopoverContainer.d.ts
236
247
  interface PopoverContainerProps {
237
- closeOnFinishMs?: number;
248
+ closeOnFinishMs?: number | null;
238
249
  classNames?: {
239
250
  dialog?: string;
240
251
  closeButton?: string;
@@ -252,10 +263,10 @@ declare const PopoverContainer: ({
252
263
  containerProps,
253
264
  onDismiss,
254
265
  closeOnFinishMs
255
- }: PopoverContainerProps) => react_jsx_runtime1.JSX.Element;
266
+ }: PopoverContainerProps) => react_jsx_runtime3.JSX.Element;
256
267
  //#endregion
257
268
  //#region src/components/questions/types.d.ts
258
- type Question = PublicGetSurveyOutput['questions'][number];
269
+ type Question$1 = PublicGetSurveyOutput['questions'][number];
259
270
  interface BaseFieldProps {
260
271
  id: string;
261
272
  title: string | null | undefined;
@@ -267,7 +278,7 @@ interface BaseFieldProps {
267
278
  //#endregion
268
279
  //#region src/components/questions/factory.d.ts
269
280
  interface SurveyQuestionFactoryProps {
270
- question: Question;
281
+ question: Question$1;
271
282
  onChange: (answer: Answer) => void;
272
283
  error?: string;
273
284
  }
@@ -275,7 +286,7 @@ declare const SurveyQuestionFactory: ({
275
286
  question,
276
287
  onChange,
277
288
  error
278
- }: SurveyQuestionFactoryProps) => react_jsx_runtime1.JSX.Element;
289
+ }: SurveyQuestionFactoryProps) => react_jsx_runtime3.JSX.Element;
279
290
  //#endregion
280
291
  //#region src/components/questions/Rating.d.ts
281
292
  interface RatingProps extends BaseFieldProps {
@@ -308,7 +319,7 @@ declare const Rating: ({
308
319
  id,
309
320
  error,
310
321
  required
311
- }: RatingProps) => react_jsx_runtime1.JSX.Element;
322
+ }: RatingProps) => react_jsx_runtime3.JSX.Element;
312
323
  //#endregion
313
324
  //#region src/components/questions/FreeText.d.ts
314
325
  interface FreeTextProps extends BaseFieldProps {
@@ -331,7 +342,7 @@ declare const FreeText: ({
331
342
  classNames,
332
343
  required,
333
344
  error
334
- }: FreeTextProps) => react_jsx_runtime1.JSX.Element;
345
+ }: FreeTextProps) => react_jsx_runtime3.JSX.Element;
335
346
  //#endregion
336
347
  //#region src/components/questions/SingleChoice.d.ts
337
348
  interface SingleChoiceProps extends BaseFieldProps {
@@ -339,6 +350,7 @@ interface SingleChoiceProps extends BaseFieldProps {
339
350
  classNames?: {
340
351
  question?: string;
341
352
  header?: string;
353
+ label?: string;
342
354
  title?: string;
343
355
  description?: string;
344
356
  group?: string;
@@ -346,6 +358,7 @@ interface SingleChoiceProps extends BaseFieldProps {
346
358
  indicator?: string;
347
359
  othersChoice?: string;
348
360
  othersInput?: string;
361
+ choiceDescription?: string;
349
362
  error?: string;
350
363
  };
351
364
  }
@@ -358,7 +371,7 @@ declare const SingleChoice: ({
358
371
  id,
359
372
  required,
360
373
  error
361
- }: SingleChoiceProps) => react_jsx_runtime1.JSX.Element;
374
+ }: SingleChoiceProps) => react_jsx_runtime3.JSX.Element;
362
375
  //#endregion
363
376
  //#region src/components/questions/MultiChoice.d.ts
364
377
  interface MultiChoiceProps extends BaseFieldProps {
@@ -366,8 +379,10 @@ interface MultiChoiceProps extends BaseFieldProps {
366
379
  classNames?: {
367
380
  question?: string;
368
381
  header?: string;
382
+ label?: string;
369
383
  title?: string;
370
384
  description?: string;
385
+ choiceDescription?: string;
371
386
  group?: string;
372
387
  checkbox?: string;
373
388
  indicator?: string;
@@ -386,26 +401,26 @@ declare const MultiChoice: ({
386
401
  id,
387
402
  required,
388
403
  error
389
- }: MultiChoiceProps) => react_jsx_runtime1.JSX.Element;
404
+ }: MultiChoiceProps) => react_jsx_runtime3.JSX.Element;
390
405
  //#endregion
391
406
  //#region src/components/presentation/Flow.d.ts
392
407
  type ChildField = ({
393
408
  onChange
394
409
  }: {
395
410
  onChange: (answer: Answer) => void;
396
- question: Question;
411
+ question: Question$1;
397
412
  error?: string;
398
413
  }) => React.ReactNode;
399
414
  interface FlowProps {
400
415
  onSubmit: (answer: Answer) => void;
401
416
  children: ChildField;
402
- question: Question;
417
+ question: Question$1;
403
418
  }
404
419
  declare const Flow: ({
405
420
  onSubmit,
406
421
  children,
407
422
  question
408
- }: FlowProps) => react_jsx_runtime1.JSX.Element;
423
+ }: FlowProps) => react_jsx_runtime3.JSX.Element;
409
424
  //#endregion
410
425
  //#region src/components/presentation/Embed.d.ts
411
426
  declare const Embed: ({
@@ -415,13 +430,68 @@ declare const Embed: ({
415
430
  }: PropsWithChildren<{
416
431
  isCompleted: boolean;
417
432
  onSubmit: () => void;
418
- }>) => react_jsx_runtime1.JSX.Element;
433
+ }>) => react_jsx_runtime3.JSX.Element;
419
434
  //#endregion
420
435
  //#region src/templates/EmbeddedConfetti.d.ts
421
- declare const EmbeddedConfetti: (props: ConfettiProps) => react_jsx_runtime1.JSX.Element;
436
+ declare const EmbeddedConfetti: (props: ConfettiProps) => react_jsx_runtime3.JSX.Element;
437
+ //#endregion
438
+ //#region src/templates/PopoverConfetti.d.ts
439
+ interface PopoverConfettiProps extends ConfettiProps {
440
+ /**
441
+ * The popover will automatically close this many milliseconds
442
+ * after the survey is finished.
443
+ * Set to `null` to disable automatic closing.
444
+ * @default 3000
445
+ */
446
+ closeOnFinishMs?: number | null;
447
+ }
448
+ declare const PopoverConfetti: ({
449
+ closeOnFinishMs,
450
+ ...props
451
+ }: PopoverConfettiProps) => react_jsx_runtime3.JSX.Element;
422
452
  //#endregion
423
453
  //#region src/templates/StepperConfetti.d.ts
424
- declare const StepperConfetti: (props: ConfettiProps) => react_jsx_runtime1.JSX.Element;
454
+ declare const StepperConfetti: (props: ConfettiProps) => react_jsx_runtime3.JSX.Element;
455
+ //#endregion
456
+ //#region src/hooks/useVisibleAfterDelay.d.ts
457
+ interface UseVisibleAfterDelayOptions {
458
+ delay: number;
459
+ }
460
+ declare const useVisibleAfterDelay: ({
461
+ delay
462
+ }: UseVisibleAfterDelayOptions) => {
463
+ isVisible: boolean;
464
+ };
465
+ //#endregion
466
+ //#region src/hooks/useVisibleAfterPageVisits.d.ts
467
+ interface UseVisibleAfterPageVisitsOptions {
468
+ visits: number;
469
+ respondent?: string;
470
+ }
471
+ declare const useVisibleAfterPageVisits: ({
472
+ visits,
473
+ respondent
474
+ }: UseVisibleAfterPageVisitsOptions) => {
475
+ isVisible: boolean;
476
+ reset: () => void;
477
+ };
478
+ declare const useVisibleAfterSessionPageVisits: ({
479
+ visits,
480
+ respondent
481
+ }: UseVisibleAfterPageVisitsOptions) => {
482
+ isVisible: boolean;
483
+ reset: () => void;
484
+ };
485
+ //#endregion
486
+ //#region src/hooks/useVisibleAfterScroll.d.ts
487
+ interface UseVisibleAfterScrollOptions {
488
+ threshold: number;
489
+ }
490
+ declare const useVisibleAfterScroll: ({
491
+ threshold
492
+ }: UseVisibleAfterScrollOptions) => {
493
+ isVisible: boolean;
494
+ };
425
495
  //#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 };
496
+ 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
497
  //# 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/logic/route.ts
11
- const isVisible = ({ index, questions, answers }) => {
12
- if (index === 0) return true;
13
- for (let i = 0; i < index; i++) {
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
- label: z.string().check(z.trim(), z.minLength(1, { error: "Label is required" })),
113
- value: z.string().check(z.trim(), z.minLength(1, { error: "Value is required" }))
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: "Label is required" })),
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"),
@@ -173,6 +123,11 @@ const multiSelectAnswerSchemaFactory = ({ maxSelected, maxLength }, { required }
173
123
 
174
124
  //#endregion
175
125
  //#region ../validators/src/survey.ts
126
+ const surveyConfirmationSchema = z.object({
127
+ title: z.string().check(z.trim()),
128
+ description: z.string().check(z.trim()),
129
+ buttonText: z.string().check(z.trim())
130
+ });
176
131
  const publicGetSurveyOutputSchema = z.object({
177
132
  title: z.string(),
178
133
  description: z.nullable(z.string()),
@@ -184,7 +139,8 @@ const publicGetSurveyOutputSchema = z.object({
184
139
  required: z.boolean(),
185
140
  properties: questionPropertiesSchema,
186
141
  routingLogic: routingLogicSchema
187
- }))
142
+ })),
143
+ confirmation: surveyConfirmationSchema
188
144
  });
189
145
 
190
146
  //#endregion
@@ -201,48 +157,8 @@ const publicPutResponseInputSchema = z.object({
201
157
  const publicGetRespondentOutputSchema = z.object({ lastRespondedAt: z.nullable(z.union([z.iso.datetime(), z.date()])) });
202
158
 
203
159
  //#endregion
204
- //#region src/logic/validate.ts
205
- const VALIDATOR_FACTORY_MAP = {
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";
160
+ //#region src/core/logic/api.ts
161
+ const API_HOST = "https://confetti.hack2026.gov.sg/api";
246
162
  const client = ky.create({
247
163
  prefixUrl: API_HOST,
248
164
  retry: {
@@ -253,9 +169,9 @@ const client = ky.create({
253
169
  });
254
170
 
255
171
  //#endregion
256
- //#region src/components/ConfettiProvider.tsx
172
+ //#region src/core/ConfettiProvider.tsx
257
173
  const ConfettiContext = createContext(null);
258
- const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, metadata, isSurveyVisible: isSurveyVisibleInput, apiBaseUrl }) => {
174
+ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, metadata = {}, isSurveyVisible: isSurveyVisibleInput, apiBaseUrl }) => {
259
175
  const [survey, setSurvey] = useState();
260
176
  const [lastRespondedAt, setLastRespondedAt] = useState();
261
177
  const responseId = useMemo(() => crypto.randomUUID(), []);
@@ -270,13 +186,13 @@ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, meta
270
186
  return client.extend(properties);
271
187
  }, [apiBaseUrl, publishableKey]);
272
188
  useEffect(() => {
273
- client$1.get(`${surveyId}`).json().then((data) => {
189
+ client$1.get(`v1/cfti/${surveyId}`).json().then((data) => {
274
190
  const parseResult = publicGetSurveyOutputSchema.safeParse(data);
275
191
  if (!parseResult.success) return;
276
192
  setSurvey(parseResult.data);
277
193
  });
278
194
  if (!respondent && !internalRespondentId) return;
279
- client$1.get(`${surveyId}/respondent/${respondent ?? internalRespondentId}`).json().then((data) => {
195
+ client$1.get(`v1/cfti/${surveyId}/respondent/${respondent ?? internalRespondentId}`).json().then((data) => {
280
196
  const parseResult = publicGetRespondentOutputSchema.safeParse(data);
281
197
  if (!parseResult.success) {
282
198
  setLastRespondedAt(null);
@@ -292,7 +208,7 @@ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, meta
292
208
  surveyId
293
209
  ]);
294
210
  const submit = useCallback(async ({ answers }) => {
295
- await client$1.put(`${surveyId}/response/${responseId}`, { json: {
211
+ await client$1.put(`v1/cfti/${surveyId}/response/${responseId}`, { json: {
296
212
  respondent: respondent ?? internalRespondentId,
297
213
  data: answers,
298
214
  metadata
@@ -332,7 +248,98 @@ const useConfetti = () => {
332
248
  };
333
249
 
334
250
  //#endregion
335
- //#region src/components/ConfettiController.tsx
251
+ //#region src/core/logic/route.ts
252
+ const isVisible = ({ index, questions, answers }) => {
253
+ if (index === 0) return true;
254
+ for (let i = 0; i < index; i++) {
255
+ const question = questions[i];
256
+ const answer = question ? answers[question.id] : void 0;
257
+ if (!question || !question.visible) continue;
258
+ const logic = question.routingLogic;
259
+ if (logic.type === "always") if (evaluateTarget({
260
+ position: i,
261
+ value: logic.target
262
+ }) === index) return true;
263
+ else continue;
264
+ if ((answer === void 0 || answer === null) && question.required) continue;
265
+ const cases = logic.cases;
266
+ const match = answer !== null ? cases.find((condition) => evaluateCondition({
267
+ answer,
268
+ condition
269
+ })) : void 0;
270
+ if (!match) if (evaluateTarget({
271
+ position: i,
272
+ value: logic.default
273
+ }) === index) return true;
274
+ else continue;
275
+ if (evaluateTarget({
276
+ position: i,
277
+ value: match.target
278
+ }) === index) return true;
279
+ else continue;
280
+ }
281
+ return false;
282
+ };
283
+ const evaluateTarget = ({ position, value }) => {
284
+ switch (value.type) {
285
+ case "next": return position + 1;
286
+ case "jump": return value.target;
287
+ case "end": return null;
288
+ }
289
+ };
290
+ const evaluateCondition = ({ answer, condition }) => {
291
+ const value = evaluateValue(condition.value);
292
+ switch (condition.operator) {
293
+ case "equal": return answer === value;
294
+ case "not-equal": return answer !== value;
295
+ case "greater": return typeof answer === "number" && typeof value === "number" && answer > value;
296
+ case "greater-or-equal": return typeof answer === "number" && typeof value === "number" && answer >= value;
297
+ case "less": return typeof answer === "number" && typeof value === "number" && answer < value;
298
+ case "less-or-equal": return typeof answer === "number" && typeof value === "number" && answer <= value;
299
+ case "contains": return Array.isArray(answer) && typeof value === "string" && answer.includes(value);
300
+ case "not-contains": return Array.isArray(answer) && typeof value === "string" && !answer.includes(value);
301
+ }
302
+ };
303
+ const evaluateValue = (node) => {
304
+ return node.value;
305
+ };
306
+
307
+ //#endregion
308
+ //#region src/core/logic/validate.ts
309
+ const VALIDATOR_FACTORY_MAP = {
310
+ rating: ({ scale }, meta) => ratingAnswerSchemaFactory({ scale }, meta),
311
+ "open-ended": ({ max }, meta) => openEndedAnswerSchemaFactory({ maxLength: max }, meta),
312
+ "single-select": ({ editable }, meta) => singleSelectAnswerSchemaFactory({ maxLength: editable?.max }, meta),
313
+ "multi-select": ({ editable, max }, meta) => multiSelectAnswerSchemaFactory({
314
+ maxLength: editable?.max,
315
+ maxSelected: max
316
+ }, meta)
317
+ };
318
+ const getAnswerValidator = (properties, meta) => {
319
+ const type = properties.type;
320
+ switch (type) {
321
+ case "rating": {
322
+ const validator = VALIDATOR_FACTORY_MAP[type];
323
+ return validator(properties, meta);
324
+ }
325
+ case "open-ended": {
326
+ const validator = VALIDATOR_FACTORY_MAP[type];
327
+ return validator(properties, meta);
328
+ }
329
+ case "single-select": {
330
+ const validator = VALIDATOR_FACTORY_MAP[type];
331
+ return validator(properties, meta);
332
+ }
333
+ case "multi-select": {
334
+ const validator = VALIDATOR_FACTORY_MAP[type];
335
+ return validator(properties, meta);
336
+ }
337
+ default: throw new Error("Unsupported question type");
338
+ }
339
+ };
340
+
341
+ //#endregion
342
+ //#region src/core/ConfettiController.tsx
336
343
  const getNextQuestionState = ({ originalQuestions, answers, errors, pointer = -1 }) => {
337
344
  if (!originalQuestions) return {
338
345
  questions: [],
@@ -465,13 +472,14 @@ const ConfettiController = ({ current: currentProp, shouldAutoSubmit, onFinished
465
472
 
466
473
  //#endregion
467
474
  //#region src/components/Confetti.tsx
468
- const Confetti = ({ children, shouldAutoSubmit, ...props }) => {
475
+ const Confetti = ({ children, onFinished, shouldAutoSubmit, ...props }) => {
469
476
  return /* @__PURE__ */ jsx(ErrorBoundary, {
470
477
  fallback: /* @__PURE__ */ jsx(Fragment, {}),
471
478
  children: /* @__PURE__ */ jsx(ConfettiProvider, {
472
479
  ...props,
473
480
  children: /* @__PURE__ */ jsx(ConfettiController, {
474
481
  shouldAutoSubmit,
482
+ onFinished,
475
483
  children
476
484
  })
477
485
  })
@@ -481,29 +489,36 @@ const Confetti = ({ children, shouldAutoSubmit, ...props }) => {
481
489
  //#endregion
482
490
  //#region src/components/ThankYouMessage.tsx
483
491
  const ThankYouMessage = ({ title, description, classNames }) => {
484
- return /* @__PURE__ */ jsx("div", {
492
+ const { survey, dismiss } = useConfetti();
493
+ if (!survey) return null;
494
+ const descriptionText = description || survey.confirmation.description;
495
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
485
496
  className: clsx("cf-question", classNames?.question),
486
497
  children: /* @__PURE__ */ jsxs("div", {
487
498
  className: clsx("cf-header", classNames?.header),
488
499
  children: [/* @__PURE__ */ jsx(Label, {
489
500
  className: clsx("cf-title", classNames?.title),
490
- children: title ?? "Feedback submitted!"
491
- }), /* @__PURE__ */ jsx(Text, {
501
+ children: title || survey.confirmation.title || "Feedback submitted!"
502
+ }), descriptionText && /* @__PURE__ */ jsx(Text, {
492
503
  slot: "description",
493
504
  className: clsx("cf-description", classNames?.description),
494
- children: description ?? "Thank you for your feedback!"
505
+ children: descriptionText
495
506
  })]
496
507
  })
497
- });
508
+ }), /* @__PURE__ */ jsx(Button, {
509
+ className: "cf-submit-button",
510
+ onPress: dismiss,
511
+ children: survey.confirmation.buttonText || "Close"
512
+ })] });
498
513
  };
499
514
 
500
515
  //#endregion
501
516
  //#region src/components/container/PopoverContainer.tsx
502
517
  const PopoverContainer = ({ children, classNames, containerProps, onDismiss, closeOnFinishMs = 3e3 }) => {
503
- const { surveyId, dismiss } = useConfetti();
518
+ const { surveyId, survey, dismiss } = useConfetti();
504
519
  const [isFinished, setIsFinished] = useState(false);
505
520
  useTimeout(() => {
506
- dismiss();
521
+ if (survey) dismiss();
507
522
  }, isFinished ? closeOnFinishMs : null);
508
523
  return /* @__PURE__ */ jsxs("div", {
509
524
  className: clsx("confetti-dialog", classNames?.dialog),
@@ -578,23 +593,29 @@ const FreeText = ({ id, title, description, properties, onChange, classNames, re
578
593
 
579
594
  //#endregion
580
595
  //#region src/components/questions/MultiChoice.tsx
581
- const MultiChoiceCheckbox = ({ children, classNames, ...props }) => {
596
+ const MultiChoiceCheckbox = ({ children, classNames, description, ...props }) => {
582
597
  return /* @__PURE__ */ jsx(Checkbox, {
583
598
  ...props,
584
- children: ({ isIndeterminate }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
585
- className: clsx("cf-checkbox-indicator", classNames?.indicator),
586
- children: /* @__PURE__ */ jsx("svg", {
587
- viewBox: "0 0 18 18",
588
- "aria-hidden": "true",
589
- className: clsx("cf-checkbox-indicator-svg", classNames?.svg),
590
- children: isIndeterminate ? /* @__PURE__ */ jsx("rect", {
591
- x: 1,
592
- y: 7.5,
593
- width: 16,
594
- height: 3
595
- }) : /* @__PURE__ */ jsx("polyline", { points: "2 9 7 14 16 4" })
596
- }, isIndeterminate ? "indeterminate" : "check")
597
- }), children] })
599
+ children: ({ isIndeterminate }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
600
+ className: clsx("cf-choice-label", classNames?.label),
601
+ children: [/* @__PURE__ */ jsx("div", {
602
+ className: clsx("cf-checkbox-indicator", classNames?.indicator),
603
+ children: /* @__PURE__ */ jsx("svg", {
604
+ viewBox: "0 0 18 18",
605
+ "aria-hidden": "true",
606
+ className: clsx("cf-checkbox-indicator-svg", classNames?.svg),
607
+ children: isIndeterminate ? /* @__PURE__ */ jsx("rect", {
608
+ x: 1,
609
+ y: 7.5,
610
+ width: 16,
611
+ height: 3
612
+ }) : /* @__PURE__ */ jsx("polyline", { points: "2 9 7 14 16 4" })
613
+ }, isIndeterminate ? "indeterminate" : "check")
614
+ }), children]
615
+ }), description && /* @__PURE__ */ jsx("span", {
616
+ className: clsx("cf-choice-description", classNames?.choiceDescription),
617
+ children: description
618
+ })] })
598
619
  });
599
620
  };
600
621
  const MultiChoice = ({ title, description, properties, classNames, onChange, id, required, error }) => {
@@ -635,15 +656,16 @@ const MultiChoice = ({ title, description, properties, classNames, onChange, id,
635
656
  /* @__PURE__ */ jsxs("div", {
636
657
  className: clsx("cf-response-choices", classNames?.group),
637
658
  children: [properties.options.map((option, index) => /* @__PURE__ */ jsx(MultiChoiceCheckbox, {
638
- className: clsx("cf-checkbox", classNames?.checkbox),
659
+ className: clsx("cf-choice cf-checkbox", classNames?.checkbox),
639
660
  value: option.value,
640
661
  classNames: {
641
662
  indicator: classNames?.indicator,
642
663
  svg: classNames?.svg
643
664
  },
644
- children: option.label
665
+ description: option.description,
666
+ children: option.value
645
667
  }, index)), hasEditable && /* @__PURE__ */ jsxs("div", {
646
- className: clsx("cf-others-choice", classNames?.othersChoice),
668
+ className: clsx("cf-choice cf-others-choice", classNames?.othersChoice),
647
669
  children: [/* @__PURE__ */ jsx(MultiChoiceCheckbox, {
648
670
  inputRef: othersRef,
649
671
  onChange: (isSelected) => {
@@ -659,6 +681,7 @@ const MultiChoice = ({ title, description, properties, classNames, onChange, id,
659
681
  indicator: classNames?.indicator,
660
682
  svg: classNames?.svg
661
683
  },
684
+ description: properties.editable?.description,
662
685
  children: properties.editable?.label
663
686
  }), /* @__PURE__ */ jsx(Input, {
664
687
  value: othersValue,
@@ -942,11 +965,17 @@ const Rating = ({ properties, title, description, classNames, onChange, id, erro
942
965
 
943
966
  //#endregion
944
967
  //#region src/components/questions/SingleChoice.tsx
945
- const SingleChoiceRadio = ({ classNames, className, ...props }) => {
968
+ const SingleChoiceRadio = ({ classNames, className, description, ...props }) => {
946
969
  return /* @__PURE__ */ jsx(Radio, {
947
970
  ...props,
948
971
  className: clsx(className, classNames?.radio),
949
- children: composeRenderProps(props.children, (children) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { className: clsx("cf-radio-indicator", classNames?.indicator) }), children] }))
972
+ children: composeRenderProps(props.children, (children) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
973
+ className: clsx("cf-choice-label", classNames?.label),
974
+ children: [/* @__PURE__ */ jsx("div", { className: clsx("cf-radio-indicator", classNames?.indicator) }), children]
975
+ }), description && /* @__PURE__ */ jsx("span", {
976
+ className: clsx("cf-choice-description", classNames?.choiceDescription),
977
+ children: description
978
+ })] }))
950
979
  });
951
980
  };
952
981
  const SingleChoice = ({ title, description, properties, classNames, onChange, id, required, error }) => {
@@ -992,12 +1021,13 @@ const SingleChoice = ({ title, description, properties, classNames, onChange, id
992
1021
  children: [properties.options.map((option, index) => /* @__PURE__ */ jsx(SingleChoiceRadio, {
993
1022
  classNames: {
994
1023
  indicator: classNames?.indicator,
995
- radio: clsx("cf-radio", classNames?.radio)
1024
+ radio: clsx("cf-choice cf-radio", classNames?.radio)
996
1025
  },
997
1026
  value: option.value,
998
- children: option.label
1027
+ description: option.description,
1028
+ children: option.value
999
1029
  }, index)), hasEditable && /* @__PURE__ */ jsxs("div", {
1000
- className: clsx("cf-others-choice", classNames?.othersChoice),
1030
+ className: clsx("cf-choice cf-others-choice", classNames?.othersChoice),
1001
1031
  children: [/* @__PURE__ */ jsx(SingleChoiceRadio, {
1002
1032
  inputRef: othersRef,
1003
1033
  value: RADIO_OTHERS_VALUE,
@@ -1005,6 +1035,7 @@ const SingleChoice = ({ title, description, properties, classNames, onChange, id
1005
1035
  indicator: classNames?.indicator,
1006
1036
  radio: clsx("cf-radio", classNames?.radio)
1007
1037
  },
1038
+ description: properties.editable?.description,
1008
1039
  children: properties.editable?.label
1009
1040
  }), /* @__PURE__ */ jsx(Input, {
1010
1041
  tabIndex: state.selectedValue === RADIO_OTHERS_VALUE ? 0 : -1,
@@ -1126,6 +1157,40 @@ const EmbeddedConfetti = (props) => {
1126
1157
  });
1127
1158
  };
1128
1159
 
1160
+ //#endregion
1161
+ //#region src/templates/PopoverConfetti.tsx
1162
+ const PopoverConfetti = ({ closeOnFinishMs, ...props }) => {
1163
+ return /* @__PURE__ */ jsx(ConfettiProvider, {
1164
+ ...props,
1165
+ children: /* @__PURE__ */ jsx(PopoverContainer, {
1166
+ closeOnFinishMs,
1167
+ children: ({ setIsFinished }) => /* @__PURE__ */ jsx(ConfettiController, {
1168
+ shouldAutoSubmit: true,
1169
+ ...props,
1170
+ onFinished: () => {
1171
+ setIsFinished();
1172
+ props.onFinished?.();
1173
+ },
1174
+ children: ({ current, questions, update, isSubmitted }) => {
1175
+ if (isSubmitted) return /* @__PURE__ */ jsx(ThankYouMessage, {});
1176
+ const currentQuestion = questions[current];
1177
+ if (!currentQuestion) return /* @__PURE__ */ jsx(Fragment, {});
1178
+ return /* @__PURE__ */ jsx(Flow, {
1179
+ question: currentQuestion,
1180
+ onSubmit: (answer) => {
1181
+ return update({
1182
+ question: current,
1183
+ answer
1184
+ });
1185
+ },
1186
+ children: (flowProps) => /* @__PURE__ */ jsx(SurveyQuestionFactory, { ...flowProps })
1187
+ });
1188
+ }
1189
+ })
1190
+ })
1191
+ });
1192
+ };
1193
+
1129
1194
  //#endregion
1130
1195
  //#region src/templates/StepperConfetti.tsx
1131
1196
  const StepperConfetti = (props) => {
@@ -1151,5 +1216,59 @@ const StepperConfetti = (props) => {
1151
1216
  };
1152
1217
 
1153
1218
  //#endregion
1154
- export { Confetti, ConfettiContext, ConfettiController, ConfettiProvider, Embed, EmbeddedConfetti, Flow, FreeText, MultiChoice, PopoverContainer, Rating, SingleChoice, StepperConfetti, SurveyQuestionFactory, ThankYouMessage, useConfetti };
1219
+ //#region src/hooks/useVisibleAfterDelay.ts
1220
+ const useVisibleAfterDelay = ({ delay }) => {
1221
+ const [isVisible$1, setIsVisible] = useState(false);
1222
+ const initialDelay = useMemo(() => delay, []);
1223
+ useEffect(() => {
1224
+ const timeoutId = setTimeout(() => {
1225
+ setIsVisible(true);
1226
+ }, initialDelay);
1227
+ return () => clearTimeout(timeoutId);
1228
+ }, []);
1229
+ return { isVisible: isVisible$1 };
1230
+ };
1231
+
1232
+ //#endregion
1233
+ //#region src/hooks/useVisibleAfterPageVisits.ts
1234
+ const useVisibleAfterPageVisits = ({ visits, respondent }) => {
1235
+ const [visitCount, setVisitCount] = useLocalStorage(`cfti:${useMemo(() => window.location.pathname, [])}:visits:${respondent ?? ""}`, 0);
1236
+ useEffect(() => {
1237
+ setVisitCount((prev) => prev + 1);
1238
+ }, [setVisitCount]);
1239
+ return {
1240
+ isVisible: visitCount >= visits,
1241
+ reset: useCallback(() => setVisitCount(0), [setVisitCount])
1242
+ };
1243
+ };
1244
+ const useVisibleAfterSessionPageVisits = ({ visits, respondent }) => {
1245
+ const [visitCount, setVisitCount] = useSessionStorage(`cfti:${useMemo(() => window.location.pathname, [])}:visits:${respondent ?? ""}`, 0);
1246
+ useEffect(() => {
1247
+ setVisitCount((prev) => prev + 1);
1248
+ }, [setVisitCount]);
1249
+ return {
1250
+ isVisible: visitCount >= visits,
1251
+ reset: useCallback(() => setVisitCount(0), [setVisitCount])
1252
+ };
1253
+ };
1254
+
1255
+ //#endregion
1256
+ //#region src/hooks/useVisibleAfterScroll.ts
1257
+ const useVisibleAfterScroll = ({ threshold }) => {
1258
+ const [isVisible$1, setIsVisible] = useState(() => {
1259
+ if (typeof window === "undefined") return false;
1260
+ return window.scrollY >= threshold;
1261
+ });
1262
+ useEffect(() => {
1263
+ const handleScroll = () => {
1264
+ if (window.scrollY >= threshold) setIsVisible(true);
1265
+ };
1266
+ window.addEventListener("scroll", handleScroll, { passive: true });
1267
+ return () => window.removeEventListener("scroll", handleScroll);
1268
+ }, [threshold]);
1269
+ return { isVisible: isVisible$1 };
1270
+ };
1271
+
1272
+ //#endregion
1273
+ export { Confetti, ConfettiContext, ConfettiController, ConfettiProvider, Embed, EmbeddedConfetti, Flow, FreeText, MultiChoice, PopoverConfetti, PopoverContainer, Rating, SingleChoice, StepperConfetti, SurveyQuestionFactory, ThankYouMessage, useConfetti, useVisibleAfterDelay, useVisibleAfterPageVisits, useVisibleAfterScroll, useVisibleAfterSessionPageVisits };
1155
1274
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/confetti",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "sideEffects": false,
5
5
  "description": "Widgets for Confetti",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -38,8 +38,8 @@
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",
42
+ "@confetti/validators": "0.0.0",
43
43
  "@confetti/eslint-config": "0.0.0",
44
44
  "@confetti/prettier-config": "0.0.0"
45
45
  },