@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.
Files changed (61) 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/Celebration.d.ts +47 -0
  8. package/dist/components/Celebration.d.ts.map +1 -0
  9. package/dist/components/CollaboratorAvatars.d.ts +33 -0
  10. package/dist/components/CollaboratorAvatars.d.ts.map +1 -0
  11. package/dist/components/InviteCard.d.ts +33 -0
  12. package/dist/components/InviteCard.d.ts.map +1 -0
  13. package/dist/components/MotivationalMessage.d.ts +31 -0
  14. package/dist/components/MotivationalMessage.d.ts.map +1 -0
  15. package/dist/components/PermissionBadge.d.ts +25 -0
  16. package/dist/components/PermissionBadge.d.ts.map +1 -0
  17. package/dist/components/ProgressCelebration.d.ts +30 -0
  18. package/dist/components/ProgressCelebration.d.ts.map +1 -0
  19. package/dist/components/SharedBadge.d.ts +28 -0
  20. package/dist/components/SharedBadge.d.ts.map +1 -0
  21. package/dist/components/StreakBadge.d.ts +27 -0
  22. package/dist/components/StreakBadge.d.ts.map +1 -0
  23. package/dist/components/SuccessCheck.d.ts +27 -0
  24. package/dist/components/SuccessCheck.d.ts.map +1 -0
  25. package/dist/components/index.d.ts +26 -0
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/hooks/useDelighters.d.ts +55 -0
  28. package/dist/hooks/useDelighters.d.ts.map +1 -0
  29. package/dist/index.d.ts +428 -2
  30. package/dist/index.esm.js +2471 -486
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/index.js +2483 -484
  33. package/dist/index.js.map +1 -1
  34. package/dist/styles.css +201 -0
  35. package/package.json +3 -1
  36. package/src/components/AchievementBadge.stories.tsx +290 -0
  37. package/src/components/AchievementBadge.tsx +196 -0
  38. package/src/components/AchievementUnlock.stories.tsx +345 -0
  39. package/src/components/AchievementUnlock.tsx +157 -0
  40. package/src/components/ActivityFeed.stories.tsx +236 -0
  41. package/src/components/ActivityFeed.tsx +160 -0
  42. package/src/components/Celebration.stories.tsx +175 -0
  43. package/src/components/Celebration.tsx +256 -0
  44. package/src/components/CollaboratorAvatars.stories.tsx +215 -0
  45. package/src/components/CollaboratorAvatars.tsx +175 -0
  46. package/src/components/InviteCard.stories.tsx +174 -0
  47. package/src/components/InviteCard.tsx +209 -0
  48. package/src/components/MotivationalMessage.stories.tsx +258 -0
  49. package/src/components/MotivationalMessage.tsx +120 -0
  50. package/src/components/PermissionBadge.stories.tsx +208 -0
  51. package/src/components/PermissionBadge.tsx +204 -0
  52. package/src/components/ProgressCelebration.stories.tsx +321 -0
  53. package/src/components/ProgressCelebration.tsx +143 -0
  54. package/src/components/SharedBadge.stories.tsx +210 -0
  55. package/src/components/SharedBadge.tsx +111 -0
  56. package/src/components/StreakBadge.stories.tsx +222 -0
  57. package/src/components/StreakBadge.tsx +132 -0
  58. package/src/components/SuccessCheck.stories.tsx +233 -0
  59. package/src/components/SuccessCheck.tsx +214 -0
  60. package/src/components/index.ts +40 -0
  61. 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
+ };