@papernote/ui 1.10.11 → 1.10.13
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/AchievementBadge.d.ts +37 -0
- package/dist/components/AchievementBadge.d.ts.map +1 -0
- package/dist/components/AchievementUnlock.d.ts +31 -0
- package/dist/components/AchievementUnlock.d.ts.map +1 -0
- package/dist/components/ActivityFeed.d.ts +42 -0
- package/dist/components/ActivityFeed.d.ts.map +1 -0
- package/dist/components/Celebration.d.ts +47 -0
- package/dist/components/Celebration.d.ts.map +1 -0
- package/dist/components/CollaboratorAvatars.d.ts +33 -0
- package/dist/components/CollaboratorAvatars.d.ts.map +1 -0
- package/dist/components/InviteCard.d.ts +33 -0
- package/dist/components/InviteCard.d.ts.map +1 -0
- package/dist/components/MotivationalMessage.d.ts +31 -0
- package/dist/components/MotivationalMessage.d.ts.map +1 -0
- package/dist/components/PermissionBadge.d.ts +25 -0
- package/dist/components/PermissionBadge.d.ts.map +1 -0
- package/dist/components/ProgressCelebration.d.ts +30 -0
- package/dist/components/ProgressCelebration.d.ts.map +1 -0
- package/dist/components/SharedBadge.d.ts +28 -0
- package/dist/components/SharedBadge.d.ts.map +1 -0
- package/dist/components/StreakBadge.d.ts +27 -0
- package/dist/components/StreakBadge.d.ts.map +1 -0
- package/dist/components/SuccessCheck.d.ts +27 -0
- package/dist/components/SuccessCheck.d.ts.map +1 -0
- package/dist/components/index.d.ts +26 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/hooks/useDelighters.d.ts +55 -0
- package/dist/hooks/useDelighters.d.ts.map +1 -0
- package/dist/index.d.ts +428 -2
- package/dist/index.esm.js +2471 -486
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2483 -484
- package/dist/index.js.map +1 -1
- package/dist/styles.css +201 -0
- package/package.json +3 -1
- package/src/components/AchievementBadge.stories.tsx +290 -0
- package/src/components/AchievementBadge.tsx +196 -0
- package/src/components/AchievementUnlock.stories.tsx +345 -0
- package/src/components/AchievementUnlock.tsx +157 -0
- package/src/components/ActivityFeed.stories.tsx +236 -0
- package/src/components/ActivityFeed.tsx +160 -0
- package/src/components/Celebration.stories.tsx +175 -0
- package/src/components/Celebration.tsx +256 -0
- package/src/components/CollaboratorAvatars.stories.tsx +215 -0
- package/src/components/CollaboratorAvatars.tsx +175 -0
- package/src/components/InviteCard.stories.tsx +174 -0
- package/src/components/InviteCard.tsx +209 -0
- package/src/components/MotivationalMessage.stories.tsx +258 -0
- package/src/components/MotivationalMessage.tsx +120 -0
- package/src/components/PermissionBadge.stories.tsx +208 -0
- package/src/components/PermissionBadge.tsx +204 -0
- package/src/components/ProgressCelebration.stories.tsx +321 -0
- package/src/components/ProgressCelebration.tsx +143 -0
- package/src/components/SharedBadge.stories.tsx +210 -0
- package/src/components/SharedBadge.tsx +111 -0
- package/src/components/StreakBadge.stories.tsx +222 -0
- package/src/components/StreakBadge.tsx +132 -0
- package/src/components/SuccessCheck.stories.tsx +233 -0
- package/src/components/SuccessCheck.tsx +214 -0
- package/src/components/index.ts +40 -0
- package/src/hooks/useDelighters.ts +133 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ActivityFeed, ActivityItem } from './ActivityFeed';
|
|
4
|
+
import Stack from './Stack';
|
|
5
|
+
import Text from './Text';
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from './Card';
|
|
7
|
+
import { FileEdit, Upload, MessageSquare, UserPlus, Trash2, Eye } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof ActivityFeed> = {
|
|
10
|
+
title: 'Collaboration/ActivityFeed',
|
|
11
|
+
component: ActivityFeed,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component: 'Timeline of collaborative activity. Shows user avatars with actions and relative timestamps. Supports load more functionality.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
tags: ['autodocs'],
|
|
21
|
+
argTypes: {
|
|
22
|
+
maxItems: {
|
|
23
|
+
control: { type: 'number', min: 1, max: 20 },
|
|
24
|
+
description: 'Maximum items to display',
|
|
25
|
+
},
|
|
26
|
+
showTimestamps: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
description: 'Whether to show timestamps',
|
|
29
|
+
},
|
|
30
|
+
loading: {
|
|
31
|
+
control: 'boolean',
|
|
32
|
+
description: 'Whether more items are loading',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof ActivityFeed>;
|
|
39
|
+
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const sampleActivities: ActivityItem[] = [
|
|
42
|
+
{
|
|
43
|
+
id: '1',
|
|
44
|
+
user: { name: 'Alice Johnson', avatar: 'https://i.pravatar.cc/100?u=alice' },
|
|
45
|
+
action: 'edited',
|
|
46
|
+
target: 'Q4 Budget.xlsx',
|
|
47
|
+
timestamp: new Date(now.getTime() - 5 * 60000), // 5 mins ago
|
|
48
|
+
icon: <FileEdit className="w-4 h-4" />,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: '2',
|
|
52
|
+
user: { name: 'Bob Smith' },
|
|
53
|
+
action: 'added a comment on',
|
|
54
|
+
target: 'Expense Report',
|
|
55
|
+
timestamp: new Date(now.getTime() - 30 * 60000), // 30 mins ago
|
|
56
|
+
icon: <MessageSquare className="w-4 h-4" />,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: '3',
|
|
60
|
+
user: { name: 'Carol Williams', avatar: 'https://i.pravatar.cc/100?u=carol' },
|
|
61
|
+
action: 'uploaded',
|
|
62
|
+
target: 'Invoice-2024-001.pdf',
|
|
63
|
+
timestamp: new Date(now.getTime() - 2 * 3600000), // 2 hours ago
|
|
64
|
+
icon: <Upload className="w-4 h-4" />,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: '4',
|
|
68
|
+
user: { name: 'David Brown' },
|
|
69
|
+
action: 'invited',
|
|
70
|
+
target: 'Eve Davis',
|
|
71
|
+
timestamp: new Date(now.getTime() - 24 * 3600000), // 1 day ago
|
|
72
|
+
icon: <UserPlus className="w-4 h-4" />,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: '5',
|
|
76
|
+
user: { name: 'Eve Davis', avatar: 'https://i.pravatar.cc/100?u=eve' },
|
|
77
|
+
action: 'viewed',
|
|
78
|
+
target: 'Annual Report',
|
|
79
|
+
timestamp: new Date(now.getTime() - 3 * 24 * 3600000), // 3 days ago
|
|
80
|
+
icon: <Eye className="w-4 h-4" />,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Basic example
|
|
85
|
+
export const Default: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
activities: sampleActivities,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// With max items
|
|
92
|
+
export const WithMaxItems: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
activities: sampleActivities,
|
|
95
|
+
maxItems: 3,
|
|
96
|
+
onLoadMore: () => alert('Load more clicked'),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Without timestamps
|
|
101
|
+
export const WithoutTimestamps: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
activities: sampleActivities,
|
|
104
|
+
showTimestamps: false,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Empty state
|
|
109
|
+
export const Empty: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
activities: [],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Loading more
|
|
116
|
+
export const LoadingMore: Story = {
|
|
117
|
+
args: {
|
|
118
|
+
activities: sampleActivities.slice(0, 3),
|
|
119
|
+
loading: true,
|
|
120
|
+
onLoadMore: () => {},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Interactive with load more
|
|
125
|
+
export const Interactive: Story = {
|
|
126
|
+
render: function InteractiveDemo() {
|
|
127
|
+
const [items, setItems] = useState(sampleActivities.slice(0, 3));
|
|
128
|
+
const [loading, setLoading] = useState(false);
|
|
129
|
+
|
|
130
|
+
const loadMore = () => {
|
|
131
|
+
setLoading(true);
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
setItems(sampleActivities);
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}, 1000);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="w-96">
|
|
140
|
+
<ActivityFeed
|
|
141
|
+
activities={items}
|
|
142
|
+
onLoadMore={items.length < sampleActivities.length ? loadMore : undefined}
|
|
143
|
+
loading={loading}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// In context - Sidebar panel
|
|
151
|
+
export const SidebarPanel: Story = {
|
|
152
|
+
render: () => (
|
|
153
|
+
<Card className="w-80">
|
|
154
|
+
<CardHeader>
|
|
155
|
+
<CardTitle>Recent Activity</CardTitle>
|
|
156
|
+
</CardHeader>
|
|
157
|
+
<CardContent>
|
|
158
|
+
<ActivityFeed
|
|
159
|
+
activities={sampleActivities.slice(0, 4)}
|
|
160
|
+
maxItems={4}
|
|
161
|
+
/>
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Without icons
|
|
168
|
+
export const WithoutIcons: Story = {
|
|
169
|
+
args: {
|
|
170
|
+
activities: sampleActivities.map(({ icon, ...rest }) => rest),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Various action types
|
|
175
|
+
export const ActionTypes: Story = {
|
|
176
|
+
render: () => {
|
|
177
|
+
const actions: ActivityItem[] = [
|
|
178
|
+
{
|
|
179
|
+
id: '1',
|
|
180
|
+
user: { name: 'User' },
|
|
181
|
+
action: 'created',
|
|
182
|
+
target: 'New Document',
|
|
183
|
+
timestamp: new Date(),
|
|
184
|
+
icon: <FileEdit className="w-4 h-4" />,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: '2',
|
|
188
|
+
user: { name: 'User' },
|
|
189
|
+
action: 'shared',
|
|
190
|
+
target: 'Budget.xlsx',
|
|
191
|
+
timestamp: new Date(),
|
|
192
|
+
icon: <UserPlus className="w-4 h-4" />,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: '3',
|
|
196
|
+
user: { name: 'User' },
|
|
197
|
+
action: 'commented on',
|
|
198
|
+
target: 'Report',
|
|
199
|
+
timestamp: new Date(),
|
|
200
|
+
icon: <MessageSquare className="w-4 h-4" />,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: '4',
|
|
204
|
+
user: { name: 'User' },
|
|
205
|
+
action: 'deleted',
|
|
206
|
+
target: 'Old File.pdf',
|
|
207
|
+
timestamp: new Date(),
|
|
208
|
+
icon: <Trash2 className="w-4 h-4" />,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className="w-96">
|
|
214
|
+
<ActivityFeed activities={actions} showTimestamps={false} />
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Full page activity log
|
|
221
|
+
export const FullPageLog: Story = {
|
|
222
|
+
render: () => (
|
|
223
|
+
<div className="bg-white p-6 rounded-xl shadow-card border border-paper-200 w-[500px]">
|
|
224
|
+
<Stack gap="md">
|
|
225
|
+
<Stack direction="horizontal" justify="between" align="center">
|
|
226
|
+
<Text size="lg" weight="semibold">Activity Log</Text>
|
|
227
|
+
<Text size="sm" className="text-ink-400">Last 7 days</Text>
|
|
228
|
+
</Stack>
|
|
229
|
+
<ActivityFeed
|
|
230
|
+
activities={sampleActivities}
|
|
231
|
+
onLoadMore={() => alert('Load more')}
|
|
232
|
+
/>
|
|
233
|
+
</Stack>
|
|
234
|
+
</div>
|
|
235
|
+
),
|
|
236
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Activity } from 'lucide-react';
|
|
2
|
+
import { CollaboratorAvatars, Collaborator } from './CollaboratorAvatars';
|
|
3
|
+
import Button from './Button';
|
|
4
|
+
|
|
5
|
+
export interface ActivityItem {
|
|
6
|
+
/** Unique identifier for the activity */
|
|
7
|
+
id: string;
|
|
8
|
+
/** User who performed the action */
|
|
9
|
+
user: Collaborator;
|
|
10
|
+
/** Description of the action */
|
|
11
|
+
action: string;
|
|
12
|
+
/** Optional target of the action (e.g., document name) */
|
|
13
|
+
target?: string;
|
|
14
|
+
/** When the activity occurred */
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
/** Optional custom icon */
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ActivityFeedProps {
|
|
21
|
+
/** Array of activities to display */
|
|
22
|
+
activities: ActivityItem[];
|
|
23
|
+
/** Maximum number of items to display */
|
|
24
|
+
maxItems?: number;
|
|
25
|
+
/** Whether to show timestamps */
|
|
26
|
+
showTimestamps?: boolean;
|
|
27
|
+
/** Callback to load more activities */
|
|
28
|
+
onLoadMore?: () => void;
|
|
29
|
+
/** Whether more items are being loaded */
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
/** Additional CSS classes */
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Format relative time
|
|
36
|
+
function formatRelativeTime(date: Date): string {
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const diffMs = now.getTime() - date.getTime();
|
|
39
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
40
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
41
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
42
|
+
|
|
43
|
+
if (diffMins < 1) return 'Just now';
|
|
44
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
45
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
46
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
47
|
+
|
|
48
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ActivityFeed - Timeline of collaborative activity.
|
|
53
|
+
*
|
|
54
|
+
* Features:
|
|
55
|
+
* - Displays activities in a vertical timeline
|
|
56
|
+
* - Shows user avatars with actions
|
|
57
|
+
* - Relative timestamps
|
|
58
|
+
* - Load more functionality
|
|
59
|
+
* - Custom icons per activity
|
|
60
|
+
*/
|
|
61
|
+
export function ActivityFeed({
|
|
62
|
+
activities,
|
|
63
|
+
maxItems,
|
|
64
|
+
showTimestamps = true,
|
|
65
|
+
onLoadMore,
|
|
66
|
+
loading = false,
|
|
67
|
+
className = '',
|
|
68
|
+
}: ActivityFeedProps) {
|
|
69
|
+
const displayedActivities = maxItems ? activities.slice(0, maxItems) : activities;
|
|
70
|
+
const hasMore = maxItems ? activities.length > maxItems : false;
|
|
71
|
+
|
|
72
|
+
if (activities.length === 0) {
|
|
73
|
+
return (
|
|
74
|
+
<div className={`text-center py-8 ${className}`}>
|
|
75
|
+
<Activity className="w-8 h-8 text-ink-300 mx-auto mb-2" />
|
|
76
|
+
<p className="text-ink-500 text-sm">No activity yet</p>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={`${className}`} role="feed" aria-label="Activity feed">
|
|
83
|
+
<div className="space-y-4">
|
|
84
|
+
{displayedActivities.map((activity, index) => (
|
|
85
|
+
<div
|
|
86
|
+
key={activity.id}
|
|
87
|
+
className="flex gap-3"
|
|
88
|
+
role="article"
|
|
89
|
+
aria-label={`${activity.user.name} ${activity.action}${activity.target ? ` ${activity.target}` : ''}`}
|
|
90
|
+
>
|
|
91
|
+
{/* Timeline connector */}
|
|
92
|
+
<div className="flex flex-col items-center">
|
|
93
|
+
<CollaboratorAvatars
|
|
94
|
+
collaborators={[activity.user]}
|
|
95
|
+
max={1}
|
|
96
|
+
size="sm"
|
|
97
|
+
/>
|
|
98
|
+
{index < displayedActivities.length - 1 && (
|
|
99
|
+
<div className="w-0.5 flex-1 bg-paper-200 mt-2" />
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Content */}
|
|
104
|
+
<div className="flex-1 min-w-0 pb-4">
|
|
105
|
+
<div className="flex items-start justify-between gap-2">
|
|
106
|
+
<div className="flex-1 min-w-0">
|
|
107
|
+
<p className="text-sm text-ink-700">
|
|
108
|
+
<span className="font-medium text-ink-800">
|
|
109
|
+
{activity.user.name}
|
|
110
|
+
</span>
|
|
111
|
+
{' '}
|
|
112
|
+
{activity.action}
|
|
113
|
+
{activity.target && (
|
|
114
|
+
<>
|
|
115
|
+
{' '}
|
|
116
|
+
<span className="font-medium text-ink-800">
|
|
117
|
+
{activity.target}
|
|
118
|
+
</span>
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Icon or timestamp */}
|
|
125
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
126
|
+
{activity.icon && (
|
|
127
|
+
<span className="text-ink-400">
|
|
128
|
+
{activity.icon}
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
{showTimestamps && (
|
|
132
|
+
<span className="text-xs text-ink-400 whitespace-nowrap">
|
|
133
|
+
{formatRelativeTime(activity.timestamp)}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Load more */}
|
|
144
|
+
{(hasMore || onLoadMore) && (
|
|
145
|
+
<div className="mt-4 text-center">
|
|
146
|
+
<Button
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="sm"
|
|
149
|
+
onClick={onLoadMore}
|
|
150
|
+
loading={loading}
|
|
151
|
+
>
|
|
152
|
+
{loading ? 'Loading...' : 'Load more'}
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default ActivityFeed;
|
|
@@ -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
|
+
};
|