@papernote/ui 1.10.12 → 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.
Files changed (58) hide show
  1. package/dist/components/AchievementBadge.d.ts +37 -0
  2. package/dist/components/AchievementBadge.d.ts.map +1 -0
  3. package/dist/components/AchievementUnlock.d.ts +31 -0
  4. package/dist/components/AchievementUnlock.d.ts.map +1 -0
  5. package/dist/components/ActivityFeed.d.ts +42 -0
  6. package/dist/components/ActivityFeed.d.ts.map +1 -0
  7. package/dist/components/CollaboratorAvatars.d.ts +33 -0
  8. package/dist/components/CollaboratorAvatars.d.ts.map +1 -0
  9. package/dist/components/InviteCard.d.ts +33 -0
  10. package/dist/components/InviteCard.d.ts.map +1 -0
  11. package/dist/components/MotivationalMessage.d.ts +31 -0
  12. package/dist/components/MotivationalMessage.d.ts.map +1 -0
  13. package/dist/components/PermissionBadge.d.ts +25 -0
  14. package/dist/components/PermissionBadge.d.ts.map +1 -0
  15. package/dist/components/ProgressCelebration.d.ts +30 -0
  16. package/dist/components/ProgressCelebration.d.ts.map +1 -0
  17. package/dist/components/SharedBadge.d.ts +28 -0
  18. package/dist/components/SharedBadge.d.ts.map +1 -0
  19. package/dist/components/StreakBadge.d.ts +27 -0
  20. package/dist/components/StreakBadge.d.ts.map +1 -0
  21. package/dist/components/SuccessCheck.d.ts +27 -0
  22. package/dist/components/SuccessCheck.d.ts.map +1 -0
  23. package/dist/components/index.d.ts +24 -0
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/hooks/useDelighters.d.ts +55 -0
  26. package/dist/hooks/useDelighters.d.ts.map +1 -0
  27. package/dist/index.d.ts +382 -2
  28. package/dist/index.esm.js +1385 -486
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/index.js +1395 -484
  31. package/dist/index.js.map +1 -1
  32. package/dist/styles.css +201 -0
  33. package/package.json +1 -1
  34. package/src/components/AchievementBadge.stories.tsx +290 -0
  35. package/src/components/AchievementBadge.tsx +196 -0
  36. package/src/components/AchievementUnlock.stories.tsx +345 -0
  37. package/src/components/AchievementUnlock.tsx +157 -0
  38. package/src/components/ActivityFeed.stories.tsx +236 -0
  39. package/src/components/ActivityFeed.tsx +160 -0
  40. package/src/components/Celebration.stories.tsx +3 -3
  41. package/src/components/CollaboratorAvatars.stories.tsx +215 -0
  42. package/src/components/CollaboratorAvatars.tsx +175 -0
  43. package/src/components/InviteCard.stories.tsx +174 -0
  44. package/src/components/InviteCard.tsx +209 -0
  45. package/src/components/MotivationalMessage.stories.tsx +258 -0
  46. package/src/components/MotivationalMessage.tsx +120 -0
  47. package/src/components/PermissionBadge.stories.tsx +208 -0
  48. package/src/components/PermissionBadge.tsx +204 -0
  49. package/src/components/ProgressCelebration.stories.tsx +321 -0
  50. package/src/components/ProgressCelebration.tsx +143 -0
  51. package/src/components/SharedBadge.stories.tsx +210 -0
  52. package/src/components/SharedBadge.tsx +111 -0
  53. package/src/components/StreakBadge.stories.tsx +222 -0
  54. package/src/components/StreakBadge.tsx +132 -0
  55. package/src/components/SuccessCheck.stories.tsx +233 -0
  56. package/src/components/SuccessCheck.tsx +214 -0
  57. package/src/components/index.ts +38 -0
  58. package/src/hooks/useDelighters.ts +133 -0
@@ -0,0 +1,345 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { AchievementUnlock } from './AchievementUnlock';
4
+ import { Button } from './Button';
5
+ import { Stack } from './Stack';
6
+ import { Text } from './Text';
7
+ import {
8
+ Trophy,
9
+ Star,
10
+ Target,
11
+ Wallet,
12
+ PiggyBank,
13
+ TrendingUp,
14
+ Calendar,
15
+ Award,
16
+ Zap,
17
+ Crown,
18
+ } from 'lucide-react';
19
+
20
+ const meta: Meta<typeof AchievementUnlock> = {
21
+ title: 'Feedback/AchievementUnlock',
22
+ component: AchievementUnlock,
23
+ parameters: {
24
+ layout: 'centered',
25
+ docs: {
26
+ description: {
27
+ component: 'Modal for newly unlocked achievements. Combines Modal + Celebration + AchievementBadge with configurable celebration types and optional auto-close.',
28
+ },
29
+ },
30
+ },
31
+ tags: ['autodocs'],
32
+ argTypes: {
33
+ celebrationType: {
34
+ control: 'select',
35
+ options: ['confetti', 'glow', 'bounce'],
36
+ description: 'Type of celebration animation',
37
+ },
38
+ autoClose: {
39
+ control: 'boolean',
40
+ description: 'Whether to auto-close after a delay',
41
+ },
42
+ autoCloseDelay: {
43
+ control: { type: 'number', min: 1000, max: 10000, step: 500 },
44
+ description: 'Delay before auto-close in ms',
45
+ },
46
+ enabled: {
47
+ control: 'boolean',
48
+ description: 'Whether celebrations are enabled',
49
+ },
50
+ },
51
+ };
52
+
53
+ export default meta;
54
+ type Story = StoryObj<typeof AchievementUnlock>;
55
+
56
+ // Basic example with trigger button
57
+ export const Default: Story = {
58
+ render: function DefaultDemo() {
59
+ const [isOpen, setIsOpen] = useState(false);
60
+
61
+ return (
62
+ <Stack align="center" gap="md">
63
+ <Button onClick={() => setIsOpen(true)}>
64
+ Unlock Achievement
65
+ </Button>
66
+ <AchievementUnlock
67
+ isOpen={isOpen}
68
+ onClose={() => setIsOpen(false)}
69
+ badge={{
70
+ icon: <Trophy className="w-full h-full" />,
71
+ name: 'Budget Master',
72
+ description: 'You stayed under budget for 3 consecutive months. Keep up the great work!',
73
+ }}
74
+ celebrationType="confetti"
75
+ />
76
+ </Stack>
77
+ );
78
+ },
79
+ };
80
+
81
+ // Different celebration types
82
+ export const CelebrationTypes: Story = {
83
+ render: function CelebrationTypesDemo() {
84
+ const [openType, setOpenType] = useState<'confetti' | 'glow' | 'bounce' | null>(null);
85
+
86
+ const badge = {
87
+ icon: <Star className="w-full h-full" />,
88
+ name: 'First Steps',
89
+ description: 'You connected your first bank account and started your financial journey!',
90
+ };
91
+
92
+ return (
93
+ <Stack direction="horizontal" gap="md">
94
+ <Button onClick={() => setOpenType('confetti')}>Confetti</Button>
95
+ <Button onClick={() => setOpenType('glow')}>Glow</Button>
96
+ <Button onClick={() => setOpenType('bounce')}>Bounce</Button>
97
+
98
+ {openType && (
99
+ <AchievementUnlock
100
+ isOpen={true}
101
+ onClose={() => setOpenType(null)}
102
+ badge={badge}
103
+ celebrationType={openType}
104
+ />
105
+ )}
106
+ </Stack>
107
+ );
108
+ },
109
+ };
110
+
111
+ // Auto-close
112
+ export const AutoClose: Story = {
113
+ render: function AutoCloseDemo() {
114
+ const [isOpen, setIsOpen] = useState(false);
115
+
116
+ return (
117
+ <Stack align="center" gap="md">
118
+ <Button onClick={() => setIsOpen(true)}>
119
+ Unlock (Auto-closes in 3s)
120
+ </Button>
121
+ <AchievementUnlock
122
+ isOpen={isOpen}
123
+ onClose={() => setIsOpen(false)}
124
+ badge={{
125
+ icon: <Zap className="w-full h-full" />,
126
+ name: 'Quick Start',
127
+ description: 'You completed the onboarding in record time!',
128
+ }}
129
+ autoClose
130
+ autoCloseDelay={3000}
131
+ />
132
+ </Stack>
133
+ );
134
+ },
135
+ };
136
+
137
+ // Various achievements showcase
138
+ export const AchievementShowcase: Story = {
139
+ render: function AchievementShowcaseDemo() {
140
+ const [currentAchievement, setCurrentAchievement] = useState<number | null>(null);
141
+
142
+ const achievements = [
143
+ {
144
+ icon: <Star className="w-full h-full" />,
145
+ name: 'First Steps',
146
+ description: 'Connected your first bank account',
147
+ },
148
+ {
149
+ icon: <Wallet className="w-full h-full" />,
150
+ name: 'Budget Creator',
151
+ description: 'Created your first monthly budget',
152
+ },
153
+ {
154
+ icon: <Trophy className="w-full h-full" />,
155
+ name: 'Budget Master',
156
+ description: 'Stayed under budget for 3 consecutive months',
157
+ },
158
+ {
159
+ icon: <PiggyBank className="w-full h-full" />,
160
+ name: 'Super Saver',
161
+ description: 'Saved $1,000 in a single month',
162
+ },
163
+ {
164
+ icon: <TrendingUp className="w-full h-full" />,
165
+ name: 'Investor',
166
+ description: 'Started tracking your investment portfolio',
167
+ },
168
+ {
169
+ icon: <Calendar className="w-full h-full" />,
170
+ name: '30-Day Streak',
171
+ description: 'Tracked your finances for 30 days straight',
172
+ },
173
+ {
174
+ icon: <Award className="w-full h-full" />,
175
+ name: 'Tax Pro',
176
+ description: 'Categorized all expenses for tax season',
177
+ },
178
+ {
179
+ icon: <Crown className="w-full h-full" />,
180
+ name: 'Finance King',
181
+ description: 'Reached $100,000 in total savings',
182
+ },
183
+ ];
184
+
185
+ return (
186
+ <Stack gap="lg">
187
+ <Text weight="semibold">Click an achievement to see the unlock modal:</Text>
188
+ <div className="grid grid-cols-4 gap-4">
189
+ {achievements.map((achievement, index) => (
190
+ <Button
191
+ key={index}
192
+ variant="ghost"
193
+ onClick={() => setCurrentAchievement(index)}
194
+ className="flex flex-col items-center gap-2 p-4 h-auto"
195
+ >
196
+ <div className="w-10 h-10 text-accent-500">
197
+ {achievement.icon}
198
+ </div>
199
+ <Text size="xs" className="text-center">{achievement.name}</Text>
200
+ </Button>
201
+ ))}
202
+ </div>
203
+
204
+ {currentAchievement !== null && (
205
+ <AchievementUnlock
206
+ isOpen={true}
207
+ onClose={() => setCurrentAchievement(null)}
208
+ badge={achievements[currentAchievement]}
209
+ celebrationType="confetti"
210
+ />
211
+ )}
212
+ </Stack>
213
+ );
214
+ },
215
+ };
216
+
217
+ // Disabled celebrations (accessibility)
218
+ export const DisabledCelebrations: Story = {
219
+ render: function DisabledDemo() {
220
+ const [isOpen, setIsOpen] = useState(false);
221
+
222
+ return (
223
+ <Stack align="center" gap="md">
224
+ <Button onClick={() => setIsOpen(true)}>
225
+ Unlock (No Animations)
226
+ </Button>
227
+ <Text size="sm" className="text-ink-500">
228
+ When enabled=false, no confetti or animations appear.
229
+ </Text>
230
+ <AchievementUnlock
231
+ isOpen={isOpen}
232
+ onClose={() => setIsOpen(false)}
233
+ badge={{
234
+ icon: <Target className="w-full h-full" />,
235
+ name: 'Goal Setter',
236
+ description: 'Set and tracked 5 financial goals',
237
+ }}
238
+ enabled={false}
239
+ />
240
+ </Stack>
241
+ );
242
+ },
243
+ };
244
+
245
+ // Programmatic unlock simulation
246
+ export const ProgrammaticUnlock: Story = {
247
+ render: function ProgrammaticDemo() {
248
+ const [progress, setProgress] = useState(0);
249
+ const [unlockedAchievement, setUnlockedAchievement] = useState<{
250
+ icon: React.ReactNode;
251
+ name: string;
252
+ description: string;
253
+ } | null>(null);
254
+
255
+ const handleIncrement = () => {
256
+ const newProgress = Math.min(100, progress + 20);
257
+ setProgress(newProgress);
258
+
259
+ // Simulate unlocking an achievement at 100%
260
+ if (newProgress >= 100 && progress < 100) {
261
+ setUnlockedAchievement({
262
+ icon: <Trophy className="w-full h-full" />,
263
+ name: 'Goal Achieved!',
264
+ description: 'You reached 100% of your savings goal. Amazing work!',
265
+ });
266
+ }
267
+ };
268
+
269
+ return (
270
+ <Stack align="center" gap="lg" className="w-64">
271
+ <Text weight="semibold">Savings Goal Progress</Text>
272
+ <div className="w-full bg-paper-200 rounded-full h-4 overflow-hidden">
273
+ <div
274
+ className="bg-success-500 h-full transition-all duration-300"
275
+ style={{ width: `${progress}%` }}
276
+ />
277
+ </div>
278
+ <Text size="sm" className="text-ink-500">{progress}% complete</Text>
279
+
280
+ <Stack direction="horizontal" gap="sm">
281
+ <Button onClick={handleIncrement} disabled={progress >= 100}>
282
+ Add 20%
283
+ </Button>
284
+ <Button variant="ghost" onClick={() => setProgress(0)}>
285
+ Reset
286
+ </Button>
287
+ </Stack>
288
+
289
+ {unlockedAchievement && (
290
+ <AchievementUnlock
291
+ isOpen={true}
292
+ onClose={() => setUnlockedAchievement(null)}
293
+ badge={unlockedAchievement}
294
+ celebrationType="confetti"
295
+ />
296
+ )}
297
+ </Stack>
298
+ );
299
+ },
300
+ };
301
+
302
+ // Multiple sequential achievements
303
+ export const SequentialAchievements: Story = {
304
+ render: function SequentialDemo() {
305
+ const [queue, setQueue] = useState<Array<{
306
+ icon: React.ReactNode;
307
+ name: string;
308
+ description: string;
309
+ }>>([]);
310
+
311
+ const achievements = [
312
+ { icon: <Star className="w-full h-full" />, name: 'First Steps', description: 'Welcome to your financial journey!' },
313
+ { icon: <Wallet className="w-full h-full" />, name: 'Budget Creator', description: 'You created your first budget!' },
314
+ { icon: <Trophy className="w-full h-full" />, name: 'Champion', description: 'You completed the challenge!' },
315
+ ];
316
+
317
+ const triggerMultiple = () => {
318
+ setQueue([...achievements]);
319
+ };
320
+
321
+ const handleClose = () => {
322
+ setQueue((q) => q.slice(1));
323
+ };
324
+
325
+ return (
326
+ <Stack align="center" gap="md">
327
+ <Button onClick={triggerMultiple}>
328
+ Unlock 3 Achievements
329
+ </Button>
330
+ <Text size="sm" className="text-ink-500">
331
+ {queue.length > 0 ? `${queue.length} achievements remaining` : 'Click to start'}
332
+ </Text>
333
+
334
+ {queue.length > 0 && (
335
+ <AchievementUnlock
336
+ isOpen={true}
337
+ onClose={handleClose}
338
+ badge={queue[0]}
339
+ celebrationType="confetti"
340
+ />
341
+ )}
342
+ </Stack>
343
+ );
344
+ },
345
+ };
@@ -0,0 +1,157 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import Modal from './Modal';
3
+ import { useCelebration } from './Celebration';
4
+ import { AchievementBadge, AchievementBadgeData } from './AchievementBadge';
5
+ import Button from './Button';
6
+ import Stack from './Stack';
7
+ import Text from './Text';
8
+
9
+ export interface AchievementUnlockProps {
10
+ /** Whether the modal is open */
11
+ isOpen: boolean;
12
+ /** Callback when the modal is closed */
13
+ onClose: () => void;
14
+ /** The badge data for the achievement */
15
+ badge: AchievementBadgeData;
16
+ /** Type of celebration animation */
17
+ celebrationType?: 'confetti' | 'glow' | 'bounce';
18
+ /** Whether to auto-close after a delay */
19
+ autoClose?: boolean;
20
+ /** Delay before auto-close in ms (default: 5000) */
21
+ autoCloseDelay?: number;
22
+ /** Whether celebrations are enabled */
23
+ enabled?: boolean;
24
+ }
25
+
26
+ /**
27
+ * AchievementUnlock - Modal/toast for newly unlocked achievements.
28
+ *
29
+ * Features:
30
+ * - Composes Modal + Celebration + AchievementBadge
31
+ * - Center the badge with scale-in animation
32
+ * - Triggers celebration effect on open
33
+ * - "Awesome!" dismiss button
34
+ * - Optional auto-close after delay
35
+ * - Mobile: uses BottomSheet via Modal's adaptive behavior
36
+ */
37
+ export function AchievementUnlock({
38
+ isOpen,
39
+ onClose,
40
+ badge,
41
+ celebrationType = 'confetti',
42
+ autoClose = false,
43
+ autoCloseDelay = 5000,
44
+ enabled = true,
45
+ }: AchievementUnlockProps) {
46
+ const { celebrate } = useCelebration();
47
+ const autoCloseTimeoutRef = useRef<number | null>(null);
48
+ const hasTriggeredRef = useRef(false);
49
+
50
+ // Check for reduced motion preference
51
+ const prefersReducedMotion =
52
+ typeof window !== 'undefined' &&
53
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
54
+
55
+ // Trigger celebration when modal opens
56
+ useEffect(() => {
57
+ if (isOpen && enabled && !prefersReducedMotion && !hasTriggeredRef.current) {
58
+ hasTriggeredRef.current = true;
59
+
60
+ if (celebrationType === 'confetti') {
61
+ // Delay celebration slightly for visual effect after modal animation
62
+ setTimeout(() => {
63
+ celebrate({
64
+ type: 'confetti',
65
+ particleCount: 100,
66
+ duration: 2500,
67
+ colors: ['#22c55e', '#3b82f6', '#a855f7', '#f59e0b', '#ec4899'],
68
+ });
69
+ }, 300);
70
+ }
71
+ }
72
+
73
+ if (!isOpen) {
74
+ hasTriggeredRef.current = false;
75
+ }
76
+ }, [isOpen, enabled, prefersReducedMotion, celebrationType, celebrate]);
77
+
78
+ // Auto-close functionality
79
+ useEffect(() => {
80
+ if (isOpen && autoClose) {
81
+ autoCloseTimeoutRef.current = window.setTimeout(() => {
82
+ onClose();
83
+ }, autoCloseDelay);
84
+ }
85
+
86
+ return () => {
87
+ if (autoCloseTimeoutRef.current) {
88
+ clearTimeout(autoCloseTimeoutRef.current);
89
+ }
90
+ };
91
+ }, [isOpen, autoClose, autoCloseDelay, onClose]);
92
+
93
+ // Mark badge as earned with current date
94
+ const earnedBadge = {
95
+ ...badge,
96
+ earnedAt: badge.earnedAt || new Date(),
97
+ };
98
+
99
+ return (
100
+ <Modal
101
+ isOpen={isOpen}
102
+ onClose={onClose}
103
+ title="Achievement Unlocked!"
104
+ size="sm"
105
+ animation="scale"
106
+ showCloseButton={false}
107
+ mobileMode="auto"
108
+ mobileHeight="md"
109
+ >
110
+ <Stack align="center" gap="lg" className="py-6">
111
+ {/* Achievement badge with glow/bounce effect */}
112
+ <div
113
+ className={`
114
+ ${celebrationType === 'glow' && enabled && !prefersReducedMotion ? 'animate-pulse' : ''}
115
+ ${celebrationType === 'bounce' && enabled && !prefersReducedMotion ? 'animate-bounce-subtle' : ''}
116
+ `}
117
+ >
118
+ <AchievementBadge
119
+ badge={earnedBadge}
120
+ variant="earned"
121
+ size="lg"
122
+ showTooltip={false}
123
+ />
124
+ </div>
125
+
126
+ {/* Achievement details */}
127
+ <Stack align="center" gap="sm">
128
+ <Text size="xl" weight="bold" className="text-ink-800">
129
+ {badge.name}
130
+ </Text>
131
+ <Text size="sm" className="text-ink-500 text-center max-w-64">
132
+ {badge.description}
133
+ </Text>
134
+ </Stack>
135
+
136
+ {/* Dismiss button */}
137
+ <Button
138
+ onClick={onClose}
139
+ variant="primary"
140
+ size="lg"
141
+ className="min-w-32"
142
+ >
143
+ Awesome!
144
+ </Button>
145
+
146
+ {/* Auto-close indicator */}
147
+ {autoClose && (
148
+ <Text size="xs" className="text-ink-400">
149
+ Closing in {Math.ceil(autoCloseDelay / 1000)} seconds...
150
+ </Text>
151
+ )}
152
+ </Stack>
153
+ </Modal>
154
+ );
155
+ }
156
+
157
+ export default AchievementUnlock;