@papernote/ui 1.10.10 → 1.10.12
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/dist/components/Badge.d.ts +3 -1
- package/dist/components/Badge.d.ts.map +1 -1
- package/dist/components/Button.d.ts +4 -0
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Celebration.d.ts +47 -0
- package/dist/components/Celebration.d.ts.map +1 -0
- package/dist/components/DataTable.d.ts +5 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +60 -4
- package/dist/index.esm.js +1150 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1149 -6
- package/dist/index.js.map +1 -1
- package/dist/styles.css +41 -0
- package/package.json +3 -1
- package/src/components/Badge.stories.tsx +56 -0
- package/src/components/Badge.tsx +5 -0
- package/src/components/Button.stories.tsx +98 -0
- package/src/components/Button.tsx +45 -6
- package/src/components/Celebration.stories.tsx +175 -0
- package/src/components/Celebration.tsx +256 -0
- package/src/components/DataTable.stories.tsx +63 -1
- package/src/components/DataTable.tsx +41 -1
- package/src/components/index.ts +2 -0
- package/tailwind.config.js +12 -0
package/dist/styles.css
CHANGED
|
@@ -2394,6 +2394,25 @@ input:checked + .slider:before{
|
|
|
2394
2394
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
2395
2395
|
}
|
|
2396
2396
|
|
|
2397
|
+
@keyframes rowFlash{
|
|
2398
|
+
|
|
2399
|
+
0%{
|
|
2400
|
+
background-color: rgb(187 247 208);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
25%{
|
|
2404
|
+
background-color: rgb(187 247 208);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
100%{
|
|
2408
|
+
background-color: transparent;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
.animate-row-flash{
|
|
2413
|
+
animation: rowFlash 2s ease-out;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2397
2416
|
@keyframes scaleIn{
|
|
2398
2417
|
|
|
2399
2418
|
0%{
|
|
@@ -2590,6 +2609,28 @@ input:checked + .slider:before{
|
|
|
2590
2609
|
animation: spin 1s linear infinite;
|
|
2591
2610
|
}
|
|
2592
2611
|
|
|
2612
|
+
@keyframes successCheck{
|
|
2613
|
+
|
|
2614
|
+
0%{
|
|
2615
|
+
transform: scale(0);
|
|
2616
|
+
opacity: 0;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
50%{
|
|
2620
|
+
transform: scale(1.2);
|
|
2621
|
+
opacity: 1;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
100%{
|
|
2625
|
+
transform: scale(1);
|
|
2626
|
+
opacity: 1;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
.animate-success-check{
|
|
2631
|
+
animation: successCheck 0.6s ease-out;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2593
2634
|
.cursor-col-resize{
|
|
2594
2635
|
cursor: col-resize;
|
|
2595
2636
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papernote/ui",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A modern React component library with a paper notebook aesthetic - minimal, professional, and expressive",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@testing-library/jest-dom": "^6.9.1",
|
|
55
55
|
"@testing-library/react": "^16.3.0",
|
|
56
56
|
"@testing-library/user-event": "^14.6.1",
|
|
57
|
+
"@types/canvas-confetti": "^1.9.0",
|
|
57
58
|
"@types/jest": "^30.0.0",
|
|
58
59
|
"@types/react": "^19.2.2",
|
|
59
60
|
"@types/react-dom": "^19.2.2",
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
"url": "https://github.com/kwhittenberger/papernote-ui/issues"
|
|
108
109
|
},
|
|
109
110
|
"dependencies": {
|
|
111
|
+
"canvas-confetti": "^1.9.4",
|
|
110
112
|
"react-spreadsheet": "^0.10.1",
|
|
111
113
|
"xlsx": "^0.18.5"
|
|
112
114
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import Badge from './Badge';
|
|
3
4
|
import { Check, X, AlertCircle, Info as InfoIcon } from 'lucide-react';
|
|
4
5
|
|
|
@@ -249,3 +250,58 @@ export const CountBadges: Story = {
|
|
|
249
250
|
</div>
|
|
250
251
|
),
|
|
251
252
|
};
|
|
253
|
+
|
|
254
|
+
export const Animated: Story = {
|
|
255
|
+
args: {
|
|
256
|
+
children: 'Animated Badge',
|
|
257
|
+
variant: 'success',
|
|
258
|
+
animate: true,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const AnimatedBadges: Story = {
|
|
263
|
+
render: () => {
|
|
264
|
+
const [badges, setBadges] = useState<string[]>([]);
|
|
265
|
+
const [counter, setCounter] = useState(0);
|
|
266
|
+
|
|
267
|
+
const addBadge = () => {
|
|
268
|
+
const newBadge = `Badge ${counter + 1}`;
|
|
269
|
+
setBadges([...badges, newBadge]);
|
|
270
|
+
setCounter(counter + 1);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const removeBadge = (index: number) => {
|
|
274
|
+
setBadges(badges.filter((_, i) => i !== index));
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
279
|
+
<button
|
|
280
|
+
onClick={addBadge}
|
|
281
|
+
style={{
|
|
282
|
+
padding: '0.5rem 1rem',
|
|
283
|
+
border: '1px solid #e5e5e5',
|
|
284
|
+
borderRadius: '0.375rem',
|
|
285
|
+
background: 'white',
|
|
286
|
+
cursor: 'pointer',
|
|
287
|
+
width: 'fit-content',
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
Add Badge
|
|
291
|
+
</button>
|
|
292
|
+
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', minHeight: '2rem' }}>
|
|
293
|
+
{badges.map((badge, index) => (
|
|
294
|
+
<Badge
|
|
295
|
+
key={badge}
|
|
296
|
+
variant="info"
|
|
297
|
+
animate
|
|
298
|
+
onRemove={() => removeBadge(index)}
|
|
299
|
+
>
|
|
300
|
+
{badge}
|
|
301
|
+
</Badge>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
};
|
package/src/components/Badge.tsx
CHANGED
|
@@ -16,6 +16,8 @@ export interface BadgeProps {
|
|
|
16
16
|
truncate?: boolean;
|
|
17
17
|
/** Maximum width for the badge (useful with truncate), e.g. '150px' or '10rem' */
|
|
18
18
|
maxWidth?: string;
|
|
19
|
+
/** Apply fade-in animation when badge appears */
|
|
20
|
+
animate?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export default function Badge({
|
|
@@ -29,6 +31,7 @@ export default function Badge({
|
|
|
29
31
|
pill = false,
|
|
30
32
|
truncate = false,
|
|
31
33
|
maxWidth,
|
|
34
|
+
animate = false,
|
|
32
35
|
}: BadgeProps) {
|
|
33
36
|
const variantStyles = {
|
|
34
37
|
success: 'bg-success-50 text-success-700 border-success-200',
|
|
@@ -79,6 +82,7 @@ export default function Badge({
|
|
|
79
82
|
inline-block rounded-full
|
|
80
83
|
${dotVariantStyles[variant]}
|
|
81
84
|
${dotSizeStyles[size]}
|
|
85
|
+
${animate ? 'animate-fade-in' : ''}
|
|
82
86
|
${className}
|
|
83
87
|
`}
|
|
84
88
|
aria-label={`${variant} indicator`}
|
|
@@ -95,6 +99,7 @@ export default function Badge({
|
|
|
95
99
|
${variantStyles[variant]}
|
|
96
100
|
${pill ? pillSizeStyles[size] : sizeStyles[size]}
|
|
97
101
|
${truncate ? 'max-w-full overflow-hidden' : ''}
|
|
102
|
+
${animate ? 'animate-fade-in' : ''}
|
|
98
103
|
${className}
|
|
99
104
|
`}
|
|
100
105
|
style={maxWidth ? { maxWidth } : undefined}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import Button from './Button';
|
|
3
4
|
import { Save, Trash, Plus, Download } from 'lucide-react';
|
|
4
5
|
|
|
@@ -253,3 +254,100 @@ export const AllSizes: Story = {
|
|
|
253
254
|
</div>
|
|
254
255
|
),
|
|
255
256
|
};
|
|
257
|
+
|
|
258
|
+
export const SuccessAnimation: Story = {
|
|
259
|
+
args: {
|
|
260
|
+
variant: 'primary',
|
|
261
|
+
showSuccess: true,
|
|
262
|
+
children: 'Saved!',
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export const SaveWithSuccess: Story = {
|
|
267
|
+
render: () => {
|
|
268
|
+
const [saving, setSaving] = useState(false);
|
|
269
|
+
const [saved, setSaved] = useState(false);
|
|
270
|
+
|
|
271
|
+
const handleSave = async () => {
|
|
272
|
+
setSaving(true);
|
|
273
|
+
setSaved(false);
|
|
274
|
+
|
|
275
|
+
// Simulate API call
|
|
276
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
277
|
+
|
|
278
|
+
setSaving(false);
|
|
279
|
+
setSaved(true);
|
|
280
|
+
|
|
281
|
+
// Reset after animation completes
|
|
282
|
+
setTimeout(() => setSaved(false), 1500);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
|
|
287
|
+
<Button
|
|
288
|
+
variant="primary"
|
|
289
|
+
icon={<Save />}
|
|
290
|
+
loading={saving}
|
|
291
|
+
showSuccess={saved}
|
|
292
|
+
onClick={handleSave}
|
|
293
|
+
>
|
|
294
|
+
Save Changes
|
|
295
|
+
</Button>
|
|
296
|
+
<p style={{ fontSize: '0.875rem', color: '#64748b' }}>
|
|
297
|
+
Click to simulate save with success animation
|
|
298
|
+
</p>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const SuccessAnimationVariants: Story = {
|
|
305
|
+
render: () => {
|
|
306
|
+
const [successStates, setSuccessStates] = useState<Record<string, boolean>>({});
|
|
307
|
+
|
|
308
|
+
const triggerSuccess = (key: string) => {
|
|
309
|
+
setSuccessStates(prev => ({ ...prev, [key]: true }));
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
setSuccessStates(prev => ({ ...prev, [key]: false }));
|
|
312
|
+
}, 1500);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
317
|
+
<p style={{ fontSize: '0.875rem', color: '#64748b', marginBottom: '0.5rem' }}>
|
|
318
|
+
Click any button to see the success animation
|
|
319
|
+
</p>
|
|
320
|
+
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
|
321
|
+
<Button
|
|
322
|
+
variant="primary"
|
|
323
|
+
showSuccess={successStates['primary']}
|
|
324
|
+
onClick={() => triggerSuccess('primary')}
|
|
325
|
+
>
|
|
326
|
+
Primary
|
|
327
|
+
</Button>
|
|
328
|
+
<Button
|
|
329
|
+
variant="secondary"
|
|
330
|
+
showSuccess={successStates['secondary']}
|
|
331
|
+
onClick={() => triggerSuccess('secondary')}
|
|
332
|
+
>
|
|
333
|
+
Secondary
|
|
334
|
+
</Button>
|
|
335
|
+
<Button
|
|
336
|
+
variant="outline"
|
|
337
|
+
showSuccess={successStates['outline']}
|
|
338
|
+
onClick={() => triggerSuccess('outline')}
|
|
339
|
+
>
|
|
340
|
+
Outline
|
|
341
|
+
</Button>
|
|
342
|
+
<Button
|
|
343
|
+
variant="ghost"
|
|
344
|
+
showSuccess={successStates['ghost']}
|
|
345
|
+
onClick={() => triggerSuccess('ghost')}
|
|
346
|
+
>
|
|
347
|
+
Ghost
|
|
348
|
+
</Button>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react';
|
|
2
|
-
import { Loader2 } from 'lucide-react';
|
|
1
|
+
import React, { forwardRef, useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Loader2, Check } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Button component props
|
|
@@ -23,6 +23,10 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
23
23
|
badge?: number | string;
|
|
24
24
|
/** Badge color variant */
|
|
25
25
|
badgeVariant?: 'primary' | 'success' | 'warning' | 'error';
|
|
26
|
+
/** Show success checkmark animation (briefly shows checkmark, then reverts) */
|
|
27
|
+
showSuccess?: boolean;
|
|
28
|
+
/** Duration in ms for success animation (default: 1500) */
|
|
29
|
+
successDuration?: number;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -76,11 +80,40 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|
|
76
80
|
iconOnly = false,
|
|
77
81
|
badge,
|
|
78
82
|
badgeVariant = 'error',
|
|
83
|
+
showSuccess = false,
|
|
84
|
+
successDuration = 1500,
|
|
79
85
|
children,
|
|
80
86
|
disabled,
|
|
81
87
|
className = '',
|
|
82
88
|
...props
|
|
83
89
|
}, ref) => {
|
|
90
|
+
// Track success animation state
|
|
91
|
+
const [isShowingSuccess, setIsShowingSuccess] = useState(false);
|
|
92
|
+
const successTimeoutRef = useRef<number | null>(null);
|
|
93
|
+
|
|
94
|
+
// Handle showSuccess prop changes
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (showSuccess && !isShowingSuccess) {
|
|
97
|
+
setIsShowingSuccess(true);
|
|
98
|
+
|
|
99
|
+
// Clear any existing timeout
|
|
100
|
+
if (successTimeoutRef.current) {
|
|
101
|
+
window.clearTimeout(successTimeoutRef.current);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Set timeout to revert back
|
|
105
|
+
successTimeoutRef.current = window.setTimeout(() => {
|
|
106
|
+
setIsShowingSuccess(false);
|
|
107
|
+
}, successDuration);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
if (successTimeoutRef.current) {
|
|
112
|
+
window.clearTimeout(successTimeoutRef.current);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}, [showSuccess, successDuration, isShowingSuccess]);
|
|
116
|
+
|
|
84
117
|
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-40 disabled:cursor-not-allowed';
|
|
85
118
|
|
|
86
119
|
const variantStyles = {
|
|
@@ -117,12 +150,15 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|
|
117
150
|
lg: 'min-w-[20px] h-5 text-xs px-1.5',
|
|
118
151
|
};
|
|
119
152
|
|
|
153
|
+
// Determine what to show inside button
|
|
154
|
+
const showSuccessState = isShowingSuccess && !loading;
|
|
155
|
+
|
|
120
156
|
const buttonElement = (
|
|
121
157
|
<button
|
|
122
158
|
ref={ref}
|
|
123
159
|
className={`
|
|
124
160
|
${baseStyles}
|
|
125
|
-
${variantStyles[variant]}
|
|
161
|
+
${showSuccessState ? 'bg-success-500 border-success-500 text-white' : variantStyles[variant]}
|
|
126
162
|
${sizeStyles[size]}
|
|
127
163
|
${fullWidth && !iconOnly ? 'w-full' : ''}
|
|
128
164
|
${className}
|
|
@@ -134,11 +170,14 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|
|
134
170
|
{loading && (
|
|
135
171
|
<Loader2 className={`${iconSize[size]} animate-spin`} />
|
|
136
172
|
)}
|
|
137
|
-
{
|
|
173
|
+
{showSuccessState && (
|
|
174
|
+
<Check className={`${iconSize[size]} animate-success-check`} />
|
|
175
|
+
)}
|
|
176
|
+
{!loading && !showSuccessState && icon && iconPosition === 'left' && (
|
|
138
177
|
<span className={iconSize[size]}>{icon}</span>
|
|
139
178
|
)}
|
|
140
|
-
{!iconOnly && children}
|
|
141
|
-
{!loading && icon && iconPosition === 'right' && !iconOnly && (
|
|
179
|
+
{!iconOnly && !showSuccessState && children}
|
|
180
|
+
{!loading && !showSuccessState && icon && iconPosition === 'right' && !iconOnly && (
|
|
142
181
|
<span className={iconSize[size]}>{icon}</span>
|
|
143
182
|
)}
|
|
144
183
|
</button>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Celebration, useCelebration } from './Celebration';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { Stack } from './Stack';
|
|
6
|
+
import { Card, CardHeader, CardTitle, CardContent } from './Card';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof Celebration> = {
|
|
9
|
+
title: 'Feedback/Celebration',
|
|
10
|
+
component: Celebration,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
},
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Celebration>;
|
|
19
|
+
|
|
20
|
+
// Interactive demo with buttons
|
|
21
|
+
function CelebrationDemo() {
|
|
22
|
+
const [trigger, setTrigger] = useState(false);
|
|
23
|
+
const [celebrationType, setCelebrationType] = useState<'confetti' | 'fireworks' | 'stars'>('confetti');
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Card style={{ width: '400px' }}>
|
|
27
|
+
<CardHeader>
|
|
28
|
+
<CardTitle>Celebration Demo</CardTitle>
|
|
29
|
+
</CardHeader>
|
|
30
|
+
<CardContent>
|
|
31
|
+
<Stack gap="md">
|
|
32
|
+
<Stack direction="horizontal" gap="sm">
|
|
33
|
+
<Button
|
|
34
|
+
variant={celebrationType === 'confetti' ? 'primary' : 'secondary'}
|
|
35
|
+
onClick={() => setCelebrationType('confetti')}
|
|
36
|
+
>
|
|
37
|
+
Confetti
|
|
38
|
+
</Button>
|
|
39
|
+
<Button
|
|
40
|
+
variant={celebrationType === 'fireworks' ? 'primary' : 'secondary'}
|
|
41
|
+
onClick={() => setCelebrationType('fireworks')}
|
|
42
|
+
>
|
|
43
|
+
Fireworks
|
|
44
|
+
</Button>
|
|
45
|
+
<Button
|
|
46
|
+
variant={celebrationType === 'stars' ? 'primary' : 'secondary'}
|
|
47
|
+
onClick={() => setCelebrationType('stars')}
|
|
48
|
+
>
|
|
49
|
+
Stars
|
|
50
|
+
</Button>
|
|
51
|
+
</Stack>
|
|
52
|
+
<Button
|
|
53
|
+
variant="primary"
|
|
54
|
+
size="lg"
|
|
55
|
+
onClick={() => setTrigger(true)}
|
|
56
|
+
>
|
|
57
|
+
🎉 Celebrate!
|
|
58
|
+
</Button>
|
|
59
|
+
</Stack>
|
|
60
|
+
</CardContent>
|
|
61
|
+
<Celebration
|
|
62
|
+
trigger={trigger}
|
|
63
|
+
type={celebrationType}
|
|
64
|
+
onComplete={() => setTrigger(false)}
|
|
65
|
+
/>
|
|
66
|
+
</Card>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const Interactive: Story = {
|
|
71
|
+
render: () => <CelebrationDemo />,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Hook demo
|
|
75
|
+
function HookDemo() {
|
|
76
|
+
const { celebrate } = useCelebration();
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Card style={{ width: '400px' }}>
|
|
80
|
+
<CardHeader>
|
|
81
|
+
<CardTitle>Using useCelebration Hook</CardTitle>
|
|
82
|
+
</CardHeader>
|
|
83
|
+
<CardContent>
|
|
84
|
+
<Stack gap="sm">
|
|
85
|
+
<Button onClick={() => celebrate()}>
|
|
86
|
+
Default Confetti
|
|
87
|
+
</Button>
|
|
88
|
+
<Button onClick={() => celebrate({ type: 'fireworks', duration: 3000 })}>
|
|
89
|
+
Fireworks (3s)
|
|
90
|
+
</Button>
|
|
91
|
+
<Button onClick={() => celebrate({ type: 'stars', colors: ['#ffd700', '#ffed4a', '#fff'] })}>
|
|
92
|
+
Golden Stars
|
|
93
|
+
</Button>
|
|
94
|
+
<Button onClick={() => celebrate({ colors: ['#22c55e'], particleCount: 200 })}>
|
|
95
|
+
Green Confetti
|
|
96
|
+
</Button>
|
|
97
|
+
</Stack>
|
|
98
|
+
</CardContent>
|
|
99
|
+
</Card>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const WithHook: Story = {
|
|
104
|
+
render: () => <HookDemo />,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Goal completion scenario
|
|
108
|
+
function GoalCompletionDemo() {
|
|
109
|
+
const [goalCompleted, setGoalCompleted] = useState(false);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Card style={{ width: '400px' }}>
|
|
113
|
+
<CardHeader>
|
|
114
|
+
<CardTitle>Goal: Save $1,000</CardTitle>
|
|
115
|
+
</CardHeader>
|
|
116
|
+
<CardContent>
|
|
117
|
+
<Stack gap="md">
|
|
118
|
+
<div style={{
|
|
119
|
+
width: '100%',
|
|
120
|
+
height: '8px',
|
|
121
|
+
backgroundColor: '#e5e7eb',
|
|
122
|
+
borderRadius: '4px',
|
|
123
|
+
overflow: 'hidden'
|
|
124
|
+
}}>
|
|
125
|
+
<div style={{
|
|
126
|
+
width: goalCompleted ? '100%' : '85%',
|
|
127
|
+
height: '100%',
|
|
128
|
+
backgroundColor: goalCompleted ? '#22c55e' : '#3b82f6',
|
|
129
|
+
transition: 'width 0.5s ease'
|
|
130
|
+
}} />
|
|
131
|
+
</div>
|
|
132
|
+
<Button
|
|
133
|
+
variant="primary"
|
|
134
|
+
onClick={() => setGoalCompleted(true)}
|
|
135
|
+
disabled={goalCompleted}
|
|
136
|
+
>
|
|
137
|
+
{goalCompleted ? '🎉 Goal Reached!' : 'Complete Goal'}
|
|
138
|
+
</Button>
|
|
139
|
+
</Stack>
|
|
140
|
+
</CardContent>
|
|
141
|
+
<Celebration
|
|
142
|
+
trigger={goalCompleted}
|
|
143
|
+
type="fireworks"
|
|
144
|
+
duration={3000}
|
|
145
|
+
colors={['#22c55e', '#10b981', '#34d399']}
|
|
146
|
+
/>
|
|
147
|
+
</Card>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const GoalCompletion: Story = {
|
|
152
|
+
render: () => <GoalCompletionDemo />,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const Confetti: Story = {
|
|
156
|
+
args: {
|
|
157
|
+
trigger: true,
|
|
158
|
+
type: 'confetti',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const Fireworks: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
trigger: true,
|
|
165
|
+
type: 'fireworks',
|
|
166
|
+
duration: 3000,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const Stars: Story = {
|
|
171
|
+
args: {
|
|
172
|
+
trigger: true,
|
|
173
|
+
type: 'stars',
|
|
174
|
+
},
|
|
175
|
+
};
|