@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.
- 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/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 +24 -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 +382 -2
- package/dist/index.esm.js +1385 -486
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1395 -484
- package/dist/index.js.map +1 -1
- package/dist/styles.css +201 -0
- package/package.json +1 -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 +3 -3
- 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 +38 -0
- package/src/hooks/useDelighters.ts +133 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { SuccessCheck } from './SuccessCheck';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { Stack } from './Stack';
|
|
6
|
+
import { Text } from './Text';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof SuccessCheck> = {
|
|
9
|
+
title: 'Feedback/SuccessCheck',
|
|
10
|
+
component: SuccessCheck,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'An animated checkmark for completed actions. Features SVG stroke animation with configurable size, color, and delay. Respects prefers-reduced-motion for accessibility.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
argTypes: {
|
|
21
|
+
size: {
|
|
22
|
+
control: 'select',
|
|
23
|
+
options: ['sm', 'md', 'lg'],
|
|
24
|
+
description: 'Size of the checkmark',
|
|
25
|
+
},
|
|
26
|
+
color: {
|
|
27
|
+
control: 'color',
|
|
28
|
+
description: 'Color of the checkmark',
|
|
29
|
+
},
|
|
30
|
+
delay: {
|
|
31
|
+
control: { type: 'number', min: 0, max: 2000, step: 100 },
|
|
32
|
+
description: 'Delay in ms before animation starts',
|
|
33
|
+
},
|
|
34
|
+
enabled: {
|
|
35
|
+
control: 'boolean',
|
|
36
|
+
description: 'Whether animations are enabled',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof SuccessCheck>;
|
|
43
|
+
|
|
44
|
+
// Basic example
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
size: 'md',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// All sizes
|
|
52
|
+
export const Sizes: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<Stack direction="horizontal" gap="lg" align="center">
|
|
55
|
+
<Stack align="center" gap="sm">
|
|
56
|
+
<SuccessCheck size="sm" />
|
|
57
|
+
<Text size="sm" className="text-ink-500">Small</Text>
|
|
58
|
+
</Stack>
|
|
59
|
+
<Stack align="center" gap="sm">
|
|
60
|
+
<SuccessCheck size="md" />
|
|
61
|
+
<Text size="sm" className="text-ink-500">Medium</Text>
|
|
62
|
+
</Stack>
|
|
63
|
+
<Stack align="center" gap="sm">
|
|
64
|
+
<SuccessCheck size="lg" />
|
|
65
|
+
<Text size="sm" className="text-ink-500">Large</Text>
|
|
66
|
+
</Stack>
|
|
67
|
+
</Stack>
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Custom colors
|
|
72
|
+
export const CustomColors: Story = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<Stack direction="horizontal" gap="lg" align="center">
|
|
75
|
+
<Stack align="center" gap="sm">
|
|
76
|
+
<SuccessCheck color="#10b981" />
|
|
77
|
+
<Text size="sm" className="text-ink-500">Success</Text>
|
|
78
|
+
</Stack>
|
|
79
|
+
<Stack align="center" gap="sm">
|
|
80
|
+
<SuccessCheck color="#3b82f6" />
|
|
81
|
+
<Text size="sm" className="text-ink-500">Blue</Text>
|
|
82
|
+
</Stack>
|
|
83
|
+
<Stack align="center" gap="sm">
|
|
84
|
+
<SuccessCheck color="#8b5cf6" />
|
|
85
|
+
<Text size="sm" className="text-ink-500">Purple</Text>
|
|
86
|
+
</Stack>
|
|
87
|
+
<Stack align="center" gap="sm">
|
|
88
|
+
<SuccessCheck color="#f59e0b" />
|
|
89
|
+
<Text size="sm" className="text-ink-500">Amber</Text>
|
|
90
|
+
</Stack>
|
|
91
|
+
</Stack>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// With delay
|
|
96
|
+
export const WithDelay: Story = {
|
|
97
|
+
render: () => (
|
|
98
|
+
<Stack direction="horizontal" gap="lg" align="center">
|
|
99
|
+
<Stack align="center" gap="sm">
|
|
100
|
+
<SuccessCheck delay={0} />
|
|
101
|
+
<Text size="sm" className="text-ink-500">No delay</Text>
|
|
102
|
+
</Stack>
|
|
103
|
+
<Stack align="center" gap="sm">
|
|
104
|
+
<SuccessCheck delay={500} />
|
|
105
|
+
<Text size="sm" className="text-ink-500">500ms</Text>
|
|
106
|
+
</Stack>
|
|
107
|
+
<Stack align="center" gap="sm">
|
|
108
|
+
<SuccessCheck delay={1000} />
|
|
109
|
+
<Text size="sm" className="text-ink-500">1000ms</Text>
|
|
110
|
+
</Stack>
|
|
111
|
+
</Stack>
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Interactive demo with trigger
|
|
116
|
+
export const Interactive: Story = {
|
|
117
|
+
render: function InteractiveDemo() {
|
|
118
|
+
const [key, setKey] = useState(0);
|
|
119
|
+
const [completed, setCompleted] = useState(false);
|
|
120
|
+
|
|
121
|
+
const triggerAnimation = () => {
|
|
122
|
+
setCompleted(false);
|
|
123
|
+
setKey(prev => prev + 1);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Stack align="center" gap="lg">
|
|
128
|
+
<div className="w-24 h-24 flex items-center justify-center bg-paper-100 rounded-xl">
|
|
129
|
+
<SuccessCheck
|
|
130
|
+
key={key}
|
|
131
|
+
size="lg"
|
|
132
|
+
onComplete={() => setCompleted(true)}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<Stack align="center" gap="sm">
|
|
136
|
+
<Button onClick={triggerAnimation} variant="primary">
|
|
137
|
+
Replay Animation
|
|
138
|
+
</Button>
|
|
139
|
+
<Text size="sm" className="text-ink-500">
|
|
140
|
+
{completed ? 'Animation complete!' : 'Animating...'}
|
|
141
|
+
</Text>
|
|
142
|
+
</Stack>
|
|
143
|
+
</Stack>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Static (no animation)
|
|
149
|
+
export const Static: Story = {
|
|
150
|
+
args: {
|
|
151
|
+
enabled: false,
|
|
152
|
+
size: 'lg',
|
|
153
|
+
},
|
|
154
|
+
parameters: {
|
|
155
|
+
docs: {
|
|
156
|
+
description: {
|
|
157
|
+
story: 'When `enabled` is false, the checkmark appears immediately without animation. This also happens automatically when the user prefers reduced motion.',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// In context - Task completion
|
|
164
|
+
export const TaskCompletion: Story = {
|
|
165
|
+
render: function TaskCompletionDemo() {
|
|
166
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="p-6 bg-white rounded-xl shadow-card border border-paper-200 w-80">
|
|
170
|
+
<Stack gap="md">
|
|
171
|
+
<Stack direction="horizontal" justify="between" align="center">
|
|
172
|
+
<Text weight="semibold">Complete your profile</Text>
|
|
173
|
+
{isComplete && <SuccessCheck size="sm" />}
|
|
174
|
+
</Stack>
|
|
175
|
+
|
|
176
|
+
<Stack gap="xs">
|
|
177
|
+
<Stack direction="horizontal" gap="sm" align="center">
|
|
178
|
+
<SuccessCheck size="sm" />
|
|
179
|
+
<Text size="sm" className="text-ink-600">Add profile photo</Text>
|
|
180
|
+
</Stack>
|
|
181
|
+
<Stack direction="horizontal" gap="sm" align="center">
|
|
182
|
+
<SuccessCheck size="sm" />
|
|
183
|
+
<Text size="sm" className="text-ink-600">Verify email address</Text>
|
|
184
|
+
</Stack>
|
|
185
|
+
<Stack direction="horizontal" gap="sm" align="center">
|
|
186
|
+
{isComplete ? (
|
|
187
|
+
<SuccessCheck size="sm" />
|
|
188
|
+
) : (
|
|
189
|
+
<div className="w-6 h-6 rounded-full border-2 border-paper-300" />
|
|
190
|
+
)}
|
|
191
|
+
<Text size="sm" className={isComplete ? 'text-ink-600' : 'text-ink-400'}>
|
|
192
|
+
Set up payment method
|
|
193
|
+
</Text>
|
|
194
|
+
</Stack>
|
|
195
|
+
</Stack>
|
|
196
|
+
|
|
197
|
+
{!isComplete && (
|
|
198
|
+
<Button
|
|
199
|
+
onClick={() => setIsComplete(true)}
|
|
200
|
+
variant="primary"
|
|
201
|
+
size="sm"
|
|
202
|
+
className="w-full"
|
|
203
|
+
>
|
|
204
|
+
Mark Complete
|
|
205
|
+
</Button>
|
|
206
|
+
)}
|
|
207
|
+
</Stack>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Staggered animation
|
|
214
|
+
export const Staggered: Story = {
|
|
215
|
+
render: function StaggeredDemo() {
|
|
216
|
+
const [key, setKey] = useState(0);
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<Stack align="center" gap="lg">
|
|
220
|
+
<Stack direction="horizontal" gap="md" align="center">
|
|
221
|
+
<SuccessCheck key={`${key}-1`} delay={0} />
|
|
222
|
+
<SuccessCheck key={`${key}-2`} delay={200} />
|
|
223
|
+
<SuccessCheck key={`${key}-3`} delay={400} />
|
|
224
|
+
<SuccessCheck key={`${key}-4`} delay={600} />
|
|
225
|
+
<SuccessCheck key={`${key}-5`} delay={800} />
|
|
226
|
+
</Stack>
|
|
227
|
+
<Button onClick={() => setKey(k => k + 1)} variant="secondary">
|
|
228
|
+
Replay Staggered
|
|
229
|
+
</Button>
|
|
230
|
+
</Stack>
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SuccessCheckProps {
|
|
4
|
+
/** Size of the checkmark */
|
|
5
|
+
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
/** Color of the checkmark (defaults to success green) */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Delay in ms before animation starts */
|
|
9
|
+
delay?: number;
|
|
10
|
+
/** Callback when animation completes */
|
|
11
|
+
onComplete?: () => void;
|
|
12
|
+
/** Additional CSS classes */
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Whether the animation is enabled (respects prefers-reduced-motion) */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sizeStyles = {
|
|
19
|
+
sm: { size: 24, strokeWidth: 3 },
|
|
20
|
+
md: { size: 32, strokeWidth: 3 },
|
|
21
|
+
lg: { size: 48, strokeWidth: 4 },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SuccessCheck - An animated checkmark for completed actions.
|
|
26
|
+
*
|
|
27
|
+
* Features:
|
|
28
|
+
* - SVG checkmark with stroke animation
|
|
29
|
+
* - Configurable size and color
|
|
30
|
+
* - Optional delay before animation
|
|
31
|
+
* - Respects prefers-reduced-motion
|
|
32
|
+
* - Callback on animation complete
|
|
33
|
+
*/
|
|
34
|
+
export function SuccessCheck({
|
|
35
|
+
size = 'md',
|
|
36
|
+
color,
|
|
37
|
+
delay = 0,
|
|
38
|
+
onComplete,
|
|
39
|
+
className = '',
|
|
40
|
+
enabled = true,
|
|
41
|
+
}: SuccessCheckProps) {
|
|
42
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
43
|
+
const [showCheck, setShowCheck] = useState(false);
|
|
44
|
+
const timeoutRef = useRef<number | null>(null);
|
|
45
|
+
const animationTimeoutRef = useRef<number | null>(null);
|
|
46
|
+
|
|
47
|
+
const { size: dimensions, strokeWidth } = sizeStyles[size];
|
|
48
|
+
const checkColor = color || '#10b981'; // success-500
|
|
49
|
+
|
|
50
|
+
// Check for reduced motion preference
|
|
51
|
+
const prefersReducedMotion =
|
|
52
|
+
typeof window !== 'undefined' &&
|
|
53
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled) {
|
|
57
|
+
setShowCheck(true);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Start animation after delay
|
|
62
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
63
|
+
setShowCheck(true);
|
|
64
|
+
setIsAnimating(true);
|
|
65
|
+
|
|
66
|
+
// Animation duration is 0.6s for the draw + 0.3s for the scale
|
|
67
|
+
const animationDuration = prefersReducedMotion ? 0 : 900;
|
|
68
|
+
|
|
69
|
+
animationTimeoutRef.current = window.setTimeout(() => {
|
|
70
|
+
setIsAnimating(false);
|
|
71
|
+
onComplete?.();
|
|
72
|
+
}, animationDuration);
|
|
73
|
+
}, delay);
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
if (timeoutRef.current) {
|
|
77
|
+
clearTimeout(timeoutRef.current);
|
|
78
|
+
}
|
|
79
|
+
if (animationTimeoutRef.current) {
|
|
80
|
+
clearTimeout(animationTimeoutRef.current);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}, [delay, enabled, onComplete, prefersReducedMotion]);
|
|
84
|
+
|
|
85
|
+
if (!showCheck) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={`inline-flex items-center justify-center ${className}`}
|
|
89
|
+
style={{ width: dimensions, height: dimensions }}
|
|
90
|
+
role="status"
|
|
91
|
+
aria-label="Loading"
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// For reduced motion, show static checkmark
|
|
97
|
+
if (prefersReducedMotion || !enabled) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
className={`inline-flex items-center justify-center ${className}`}
|
|
101
|
+
style={{ width: dimensions, height: dimensions }}
|
|
102
|
+
role="status"
|
|
103
|
+
aria-label="Success"
|
|
104
|
+
>
|
|
105
|
+
<svg
|
|
106
|
+
width={dimensions}
|
|
107
|
+
height={dimensions}
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="none"
|
|
110
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
111
|
+
>
|
|
112
|
+
<circle
|
|
113
|
+
cx="12"
|
|
114
|
+
cy="12"
|
|
115
|
+
r="10"
|
|
116
|
+
stroke={checkColor}
|
|
117
|
+
strokeWidth={strokeWidth * 0.8}
|
|
118
|
+
fill="none"
|
|
119
|
+
opacity={0.2}
|
|
120
|
+
/>
|
|
121
|
+
<path
|
|
122
|
+
d="M7 12.5L10.5 16L17 9"
|
|
123
|
+
stroke={checkColor}
|
|
124
|
+
strokeWidth={strokeWidth}
|
|
125
|
+
strokeLinecap="round"
|
|
126
|
+
strokeLinejoin="round"
|
|
127
|
+
/>
|
|
128
|
+
</svg>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Animated version
|
|
134
|
+
// The checkmark path length for the animation
|
|
135
|
+
const pathLength = 20; // Approximate length of the check path
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
className={`inline-flex items-center justify-center ${className}`}
|
|
140
|
+
style={{ width: dimensions, height: dimensions }}
|
|
141
|
+
role="status"
|
|
142
|
+
aria-label="Success"
|
|
143
|
+
>
|
|
144
|
+
<svg
|
|
145
|
+
width={dimensions}
|
|
146
|
+
height={dimensions}
|
|
147
|
+
viewBox="0 0 24 24"
|
|
148
|
+
fill="none"
|
|
149
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
150
|
+
className={isAnimating ? 'animate-success-check' : ''}
|
|
151
|
+
>
|
|
152
|
+
{/* Background circle */}
|
|
153
|
+
<circle
|
|
154
|
+
cx="12"
|
|
155
|
+
cy="12"
|
|
156
|
+
r="10"
|
|
157
|
+
stroke={checkColor}
|
|
158
|
+
strokeWidth={strokeWidth * 0.8}
|
|
159
|
+
fill="none"
|
|
160
|
+
opacity={0.2}
|
|
161
|
+
className="origin-center"
|
|
162
|
+
style={{
|
|
163
|
+
animation: isAnimating ? 'successCircle 0.4s ease-out forwards' : 'none',
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
{/* Checkmark path with stroke animation */}
|
|
168
|
+
<path
|
|
169
|
+
d="M7 12.5L10.5 16L17 9"
|
|
170
|
+
stroke={checkColor}
|
|
171
|
+
strokeWidth={strokeWidth}
|
|
172
|
+
strokeLinecap="round"
|
|
173
|
+
strokeLinejoin="round"
|
|
174
|
+
style={{
|
|
175
|
+
strokeDasharray: pathLength,
|
|
176
|
+
strokeDashoffset: isAnimating ? 0 : pathLength,
|
|
177
|
+
animation: isAnimating
|
|
178
|
+
? `successDraw 0.4s 0.2s ease-out forwards`
|
|
179
|
+
: 'none',
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
</svg>
|
|
183
|
+
|
|
184
|
+
{/* Inject keyframes for the animations */}
|
|
185
|
+
<style>{`
|
|
186
|
+
@keyframes successDraw {
|
|
187
|
+
0% {
|
|
188
|
+
stroke-dashoffset: ${pathLength};
|
|
189
|
+
}
|
|
190
|
+
100% {
|
|
191
|
+
stroke-dashoffset: 0;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@keyframes successCircle {
|
|
196
|
+
0% {
|
|
197
|
+
transform: scale(0);
|
|
198
|
+
opacity: 0;
|
|
199
|
+
}
|
|
200
|
+
50% {
|
|
201
|
+
transform: scale(1.1);
|
|
202
|
+
opacity: 0.3;
|
|
203
|
+
}
|
|
204
|
+
100% {
|
|
205
|
+
transform: scale(1);
|
|
206
|
+
opacity: 0.2;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
`}</style>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default SuccessCheck;
|
package/src/components/index.ts
CHANGED
|
@@ -69,6 +69,44 @@ export { Celebration, useCelebration } from './Celebration';
|
|
|
69
69
|
export type { CelebrationProps } from './Celebration';
|
|
70
70
|
export type { CardProps } from './Card';
|
|
71
71
|
|
|
72
|
+
// Delighter Components (PAP-12)
|
|
73
|
+
export { SuccessCheck } from './SuccessCheck';
|
|
74
|
+
export type { SuccessCheckProps } from './SuccessCheck';
|
|
75
|
+
|
|
76
|
+
export { MotivationalMessage } from './MotivationalMessage';
|
|
77
|
+
export type { MotivationalMessageProps } from './MotivationalMessage';
|
|
78
|
+
|
|
79
|
+
export { StreakBadge } from './StreakBadge';
|
|
80
|
+
export type { StreakBadgeProps } from './StreakBadge';
|
|
81
|
+
|
|
82
|
+
export { AchievementBadge } from './AchievementBadge';
|
|
83
|
+
export type { AchievementBadgeProps, AchievementBadgeData } from './AchievementBadge';
|
|
84
|
+
|
|
85
|
+
export { ProgressCelebration } from './ProgressCelebration';
|
|
86
|
+
export type { ProgressCelebrationProps } from './ProgressCelebration';
|
|
87
|
+
|
|
88
|
+
export { AchievementUnlock } from './AchievementUnlock';
|
|
89
|
+
export type { AchievementUnlockProps } from './AchievementUnlock';
|
|
90
|
+
|
|
91
|
+
export { useDelighters } from '../hooks/useDelighters';
|
|
92
|
+
export type { UseDelightersReturn, CelebrationType, CelebrationOptions } from '../hooks/useDelighters';
|
|
93
|
+
|
|
94
|
+
// Collaboration Components (PAP-11)
|
|
95
|
+
export { CollaboratorAvatars } from './CollaboratorAvatars';
|
|
96
|
+
export type { CollaboratorAvatarsProps, Collaborator } from './CollaboratorAvatars';
|
|
97
|
+
|
|
98
|
+
export { PermissionBadge } from './PermissionBadge';
|
|
99
|
+
export type { PermissionBadgeProps, PermissionLevel } from './PermissionBadge';
|
|
100
|
+
|
|
101
|
+
export { SharedBadge } from './SharedBadge';
|
|
102
|
+
export type { SharedBadgeProps } from './SharedBadge';
|
|
103
|
+
|
|
104
|
+
export { ActivityFeed } from './ActivityFeed';
|
|
105
|
+
export type { ActivityFeedProps, ActivityItem } from './ActivityFeed';
|
|
106
|
+
|
|
107
|
+
export { InviteCard } from './InviteCard';
|
|
108
|
+
export type { InviteCardProps, PendingInvite } from './InviteCard';
|
|
109
|
+
|
|
72
110
|
export { default as Stack } from './Stack';
|
|
73
111
|
export type { StackProps } from './Stack';
|
|
74
112
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useCelebration } from '../components/Celebration';
|
|
3
|
+
import type { AchievementBadgeData } from '../components/AchievementBadge';
|
|
4
|
+
|
|
5
|
+
export type CelebrationType = 'confetti' | 'fireworks' | 'stars';
|
|
6
|
+
|
|
7
|
+
export interface CelebrationOptions {
|
|
8
|
+
/** Duration of the celebration in ms */
|
|
9
|
+
duration?: number;
|
|
10
|
+
/** Number of particles */
|
|
11
|
+
particleCount?: number;
|
|
12
|
+
/** Custom colors for the celebration */
|
|
13
|
+
colors?: string[];
|
|
14
|
+
/** Origin point (x, y in 0-1 range) */
|
|
15
|
+
origin?: { x: number; y: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseDelightersReturn {
|
|
19
|
+
/** Trigger a celebration animation */
|
|
20
|
+
celebrate: (type: CelebrationType, options?: CelebrationOptions) => void;
|
|
21
|
+
/** Show an achievement unlock (returns a function to trigger the achievement modal) */
|
|
22
|
+
showAchievement: (badge: AchievementBadgeData) => void;
|
|
23
|
+
/** Whether delighters are globally enabled */
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
/** Set the global enabled state */
|
|
26
|
+
setEnabled: (enabled: boolean) => void;
|
|
27
|
+
/** The currently pending achievement (for rendering AchievementUnlock) */
|
|
28
|
+
pendingAchievement: AchievementBadgeData | null;
|
|
29
|
+
/** Clear the pending achievement */
|
|
30
|
+
clearAchievement: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STORAGE_KEY = 'notebook-ui-delighters-enabled';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* useDelighters - Global control hook for celebration effects.
|
|
37
|
+
*
|
|
38
|
+
* Features:
|
|
39
|
+
* - Wraps useCelebration for consistent celebration API
|
|
40
|
+
* - Global enabled/disabled state with localStorage persistence
|
|
41
|
+
* - Respects prefers-reduced-motion automatically
|
|
42
|
+
* - Provides showAchievement for achievement unlock modals
|
|
43
|
+
*
|
|
44
|
+
* Usage:
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const { celebrate, showAchievement, enabled, setEnabled } = useDelighters();
|
|
47
|
+
*
|
|
48
|
+
* // Trigger a celebration
|
|
49
|
+
* celebrate('confetti', { particleCount: 100 });
|
|
50
|
+
*
|
|
51
|
+
* // Show an achievement
|
|
52
|
+
* showAchievement({ icon: <Trophy />, name: 'Budget Master', description: '...' });
|
|
53
|
+
*
|
|
54
|
+
* // Toggle celebrations globally
|
|
55
|
+
* setEnabled(false);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function useDelighters(): UseDelightersReturn {
|
|
59
|
+
const { celebrate: baseCelebrate } = useCelebration();
|
|
60
|
+
const [pendingAchievement, setPendingAchievement] = useState<AchievementBadgeData | null>(null);
|
|
61
|
+
|
|
62
|
+
// Initialize enabled state from localStorage
|
|
63
|
+
const [enabled, setEnabledState] = useState<boolean>(() => {
|
|
64
|
+
if (typeof window === 'undefined') return true;
|
|
65
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
66
|
+
return stored === null ? true : stored === 'true';
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Check for reduced motion preference
|
|
70
|
+
const prefersReducedMotion =
|
|
71
|
+
typeof window !== 'undefined' &&
|
|
72
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
73
|
+
|
|
74
|
+
// Persist enabled state to localStorage
|
|
75
|
+
const setEnabled = useCallback((newEnabled: boolean) => {
|
|
76
|
+
setEnabledState(newEnabled);
|
|
77
|
+
if (typeof window !== 'undefined') {
|
|
78
|
+
localStorage.setItem(STORAGE_KEY, String(newEnabled));
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// Sync with system preference changes
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (typeof window === 'undefined') return;
|
|
85
|
+
|
|
86
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
87
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
88
|
+
if (e.matches) {
|
|
89
|
+
// System now prefers reduced motion, but don't change user's preference
|
|
90
|
+
// Just let the celebrate function respect it
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
95
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const celebrate = useCallback((type: CelebrationType, options?: CelebrationOptions) => {
|
|
99
|
+
// Skip if disabled or prefers reduced motion
|
|
100
|
+
if (!enabled || prefersReducedMotion) return;
|
|
101
|
+
|
|
102
|
+
baseCelebrate({
|
|
103
|
+
type,
|
|
104
|
+
duration: options?.duration,
|
|
105
|
+
particleCount: options?.particleCount,
|
|
106
|
+
colors: options?.colors,
|
|
107
|
+
origin: options?.origin,
|
|
108
|
+
});
|
|
109
|
+
}, [enabled, prefersReducedMotion, baseCelebrate]);
|
|
110
|
+
|
|
111
|
+
const showAchievement = useCallback((badge: AchievementBadgeData) => {
|
|
112
|
+
// Always show achievement (unless disabled), even with reduced motion
|
|
113
|
+
// The modal itself will handle reduced motion for its animations
|
|
114
|
+
if (!enabled) return;
|
|
115
|
+
|
|
116
|
+
setPendingAchievement(badge);
|
|
117
|
+
}, [enabled]);
|
|
118
|
+
|
|
119
|
+
const clearAchievement = useCallback(() => {
|
|
120
|
+
setPendingAchievement(null);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
celebrate,
|
|
125
|
+
showAchievement,
|
|
126
|
+
enabled,
|
|
127
|
+
setEnabled,
|
|
128
|
+
pendingAchievement,
|
|
129
|
+
clearAchievement,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default useDelighters;
|