@tuturuuu/ui 0.30.2 → 0.32.0
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/CHANGELOG.md +49 -0
- package/biome.json +1 -1
- package/package.json +95 -49
- package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
- package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
- package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
- package/src/components/ui/chat/ai-message-markdown.tsx +17 -2
- package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
- package/src/components/ui/custom/nav-link.tsx +7 -7
- package/src/components/ui/custom/nav.tsx +15 -63
- package/src/components/ui/custom/notification-card.tsx +361 -0
- package/src/components/ui/custom/notification-list.tsx +178 -0
- package/src/components/ui/custom/notification-popover-client.tsx +12 -780
- package/src/components/ui/custom/notification-runtime.tsx +23 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
- package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
- package/src/components/ui/custom/satellite-brand.tsx +122 -0
- package/src/components/ui/custom/satellite-content.tsx +148 -0
- package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
- package/src/components/ui/custom/satellite-language-item.tsx +26 -0
- package/src/components/ui/custom/satellite-navigation.tsx +86 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
- package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
- package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
- package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
- package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
- package/src/components/ui/custom/satellite-shell.tsx +50 -0
- package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
- package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
- package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
- package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
- package/src/components/ui/custom/sidebar-context.tsx +5 -14
- package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
- package/src/components/ui/custom/structure.tsx +14 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
- package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
- package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
- package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
- package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
- package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
- package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
- package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
- package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
- package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
- package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
- package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
- package/src/components/ui/pwa-status.test.tsx +53 -0
- package/src/components/ui/pwa-status.tsx +113 -0
- package/src/components/ui/report-problem-dialog-content.tsx +350 -0
- package/src/components/ui/report-problem-dialog.tsx +6 -975
- package/src/components/ui/report-problem-model.ts +133 -0
- package/src/components/ui/report-problem-mutation.ts +122 -0
- package/src/components/ui/save-notification.test.tsx +36 -0
- package/src/components/ui/save-notification.tsx +17 -0
- package/src/components/ui/use-report-problem.ts +488 -0
- package/src/declarations.d.ts +2 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
- package/src/hooks/calendar-date-utils.ts +11 -0
- package/src/hooks/calendar-event-lookup.test.ts +77 -0
- package/src/hooks/calendar-event-lookup.ts +47 -0
- package/src/hooks/calendar-range-cache.ts +52 -0
- package/src/hooks/pwa-install-prompt.ts +44 -0
- package/src/hooks/use-calendar-sync.tsx +23 -100
- package/src/hooks/use-calendar.tsx +21 -77
- package/src/hooks/use-notifications.ts +9 -24
- package/src/hooks/use-open-initial-calendar-event.ts +27 -0
- package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
- package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
- package/src/hooks/use-workspace-presence.ts +0 -18
- package/src/lib/tasks-app-url.test.ts +25 -0
- package/src/lib/tasks-app-url.ts +8 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { Archive, ChevronRight, Inbox, Loader2 } from '@tuturuuu/icons';
|
|
5
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
6
|
+
import {
|
|
7
|
+
dedupeNotifications,
|
|
8
|
+
useInfiniteNotifications,
|
|
9
|
+
useMarkAllAsRead,
|
|
10
|
+
useNotificationSubscription,
|
|
11
|
+
useUnreadCount,
|
|
12
|
+
useUpdateNotification,
|
|
13
|
+
} from '@tuturuuu/ui/hooks/use-notifications';
|
|
14
|
+
import { Popover, PopoverContent } from '@tuturuuu/ui/popover';
|
|
15
|
+
import { toast } from '@tuturuuu/ui/sonner';
|
|
16
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
17
|
+
import dayjs from 'dayjs';
|
|
18
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
19
|
+
import { useState } from 'react';
|
|
20
|
+
import { NotificationList } from './notification-list';
|
|
21
|
+
import { NotificationPopoverTriggerButton } from './notification-popover-trigger';
|
|
22
|
+
import { useNotificationRuntime } from './notification-runtime';
|
|
23
|
+
|
|
24
|
+
dayjs.extend(relativeTime);
|
|
25
|
+
type TabType = 'inbox' | 'archive';
|
|
26
|
+
|
|
27
|
+
export interface NotificationPopoverClientProps {
|
|
28
|
+
userId?: string;
|
|
29
|
+
noNotificationsText: string;
|
|
30
|
+
notificationsText: string;
|
|
31
|
+
viewAllText: string;
|
|
32
|
+
markAsReadText: string;
|
|
33
|
+
markAsUnreadText: string;
|
|
34
|
+
inboxText?: string;
|
|
35
|
+
archiveText?: string;
|
|
36
|
+
archiveAllText?: string;
|
|
37
|
+
emptyArchiveText?: string;
|
|
38
|
+
loadingMoreText?: string;
|
|
39
|
+
retryText?: string;
|
|
40
|
+
acceptText?: string;
|
|
41
|
+
declineText?: string;
|
|
42
|
+
acceptedText?: string;
|
|
43
|
+
declinedText?: string;
|
|
44
|
+
/** Base URL for external redirect (e.g. 'https://tuturuuu.com'). When set, "View All" links to {webAppUrl}/{wsId}/notifications. */
|
|
45
|
+
webAppUrl?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Workspace identifier validation regex
|
|
49
|
+
const UUID_REGEX =
|
|
50
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
51
|
+
const SPECIAL_WORKSPACE_SLUGS = new Set(['personal', 'internal']);
|
|
52
|
+
|
|
53
|
+
function isValidWorkspaceIdentifier(value: unknown): value is string {
|
|
54
|
+
return (
|
|
55
|
+
typeof value === 'string' &&
|
|
56
|
+
(UUID_REGEX.test(value) || SPECIAL_WORKSPACE_SLUGS.has(value))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isValidWorkspaceFilterId(value: unknown): value is string {
|
|
61
|
+
return typeof value === 'string' && UUID_REGEX.test(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function SatelliteNotificationPopover({
|
|
65
|
+
userId,
|
|
66
|
+
noNotificationsText,
|
|
67
|
+
notificationsText,
|
|
68
|
+
viewAllText,
|
|
69
|
+
markAsReadText,
|
|
70
|
+
markAsUnreadText,
|
|
71
|
+
inboxText = 'Inbox',
|
|
72
|
+
archiveText = 'Archive',
|
|
73
|
+
archiveAllText = 'Archive all',
|
|
74
|
+
emptyArchiveText = 'No archived notifications yet.',
|
|
75
|
+
loadingMoreText = 'Loading more...',
|
|
76
|
+
retryText = 'Retry',
|
|
77
|
+
acceptText = 'Accept',
|
|
78
|
+
declineText = 'Decline',
|
|
79
|
+
acceptedText = 'Joined',
|
|
80
|
+
declinedText = 'Declined',
|
|
81
|
+
webAppUrl,
|
|
82
|
+
}: NotificationPopoverClientProps) {
|
|
83
|
+
const [open, setOpen] = useState(false);
|
|
84
|
+
const [activeTab, setActiveTab] = useState<TabType>('inbox');
|
|
85
|
+
const { params, Link, realtime } = useNotificationRuntime();
|
|
86
|
+
const queryClient = useQueryClient();
|
|
87
|
+
|
|
88
|
+
const workspaceIdParam =
|
|
89
|
+
typeof params.wsId === 'string' ? params.wsId : undefined;
|
|
90
|
+
const wsIdForFiltering = isValidWorkspaceFilterId(workspaceIdParam)
|
|
91
|
+
? workspaceIdParam
|
|
92
|
+
: undefined;
|
|
93
|
+
const wsIdForTaskContext = isValidWorkspaceIdentifier(workspaceIdParam)
|
|
94
|
+
? workspaceIdParam
|
|
95
|
+
: undefined;
|
|
96
|
+
|
|
97
|
+
// Accurate unread count from dedicated endpoint
|
|
98
|
+
const { data: unreadCount = 0 } = useUnreadCount(wsIdForFiltering, {
|
|
99
|
+
cacheScope: userId,
|
|
100
|
+
enabled: Boolean(userId),
|
|
101
|
+
refetchInterval: realtime ? undefined : 30_000,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Infinite scroll for inbox (unread) and archive (read)
|
|
105
|
+
const inboxQuery = useInfiniteNotifications({
|
|
106
|
+
cacheScope: userId,
|
|
107
|
+
wsId: wsIdForFiltering,
|
|
108
|
+
unreadOnly: true,
|
|
109
|
+
pageSize: 15,
|
|
110
|
+
refetchInterval: realtime ? undefined : 30_000,
|
|
111
|
+
enabled: Boolean(userId) && open && activeTab === 'inbox',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const archiveQuery = useInfiniteNotifications({
|
|
115
|
+
cacheScope: userId,
|
|
116
|
+
wsId: wsIdForFiltering,
|
|
117
|
+
readOnly: true,
|
|
118
|
+
pageSize: 15,
|
|
119
|
+
refetchInterval: realtime ? undefined : 30_000,
|
|
120
|
+
enabled: Boolean(userId) && open && activeTab === 'archive',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const markAllAsRead = useMarkAllAsRead();
|
|
124
|
+
const updateNotification = useUpdateNotification();
|
|
125
|
+
|
|
126
|
+
// Subscribe to realtime updates
|
|
127
|
+
useNotificationSubscription(
|
|
128
|
+
wsIdForFiltering || '',
|
|
129
|
+
realtime ? userId || '' : ''
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const activeQuery = activeTab === 'inbox' ? inboxQuery : archiveQuery;
|
|
133
|
+
const allNotifications = dedupeNotifications(
|
|
134
|
+
activeQuery.data?.pages.flatMap((p) => p.notifications) ?? []
|
|
135
|
+
);
|
|
136
|
+
const hasNotifications = allNotifications.length > 0;
|
|
137
|
+
|
|
138
|
+
// When webAppUrl is set, link to the external web app's notifications page
|
|
139
|
+
const baseUrl = webAppUrl ?? '';
|
|
140
|
+
const notificationsPageUrl = params.wsId
|
|
141
|
+
? `${baseUrl}/${params.wsId}/notifications`
|
|
142
|
+
: `${baseUrl}/notifications`;
|
|
143
|
+
|
|
144
|
+
const handleMarkAsRead = async (id: string, isUnread: boolean) => {
|
|
145
|
+
try {
|
|
146
|
+
await updateNotification.mutateAsync({ id, read: isUnread });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Failed to update notification:', error);
|
|
149
|
+
toast.error('Failed to update notification');
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleArchiveAll = async () => {
|
|
154
|
+
try {
|
|
155
|
+
await markAllAsRead.mutateAsync(wsIdForFiltering);
|
|
156
|
+
toast.success('All notifications archived');
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Failed to archive all:', error);
|
|
159
|
+
toast.error('Failed to archive notifications');
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (!userId) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
169
|
+
<NotificationPopoverTriggerButton
|
|
170
|
+
notificationsText={notificationsText}
|
|
171
|
+
unreadCount={unreadCount}
|
|
172
|
+
/>
|
|
173
|
+
<PopoverContent
|
|
174
|
+
className="w-100 max-w-[calc(100vw-1rem)] overflow-hidden rounded-xl border p-0 shadow-xl"
|
|
175
|
+
align="end"
|
|
176
|
+
>
|
|
177
|
+
{/* Header */}
|
|
178
|
+
<div className="flex items-center justify-between border-b bg-background px-4 py-3">
|
|
179
|
+
<h3 className="font-semibold text-base">{notificationsText}</h3>
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
{unreadCount > 0 && (
|
|
182
|
+
<Button
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="sm"
|
|
185
|
+
className="h-7 gap-1 text-foreground/60 text-xs hover:text-foreground"
|
|
186
|
+
onClick={handleArchiveAll}
|
|
187
|
+
disabled={markAllAsRead.isPending}
|
|
188
|
+
>
|
|
189
|
+
{markAllAsRead.isPending ? (
|
|
190
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
191
|
+
) : (
|
|
192
|
+
<Archive className="h-3 w-3" />
|
|
193
|
+
)}
|
|
194
|
+
{archiveAllText}
|
|
195
|
+
</Button>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Tabs */}
|
|
201
|
+
<div className="flex border-b bg-background">
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => setActiveTab('inbox')}
|
|
205
|
+
className={cn(
|
|
206
|
+
'flex flex-1 items-center justify-center gap-1.5 px-4 py-2.5 font-medium text-sm transition-colors',
|
|
207
|
+
activeTab === 'inbox'
|
|
208
|
+
? 'border-primary border-b-2 text-primary'
|
|
209
|
+
: 'text-foreground/50 hover:text-foreground/80'
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
<Inbox className="h-3.5 w-3.5" />
|
|
213
|
+
{inboxText}
|
|
214
|
+
{unreadCount > 0 && (
|
|
215
|
+
<span className="rounded-full bg-dynamic-red/10 px-1.5 py-0.5 font-semibold text-[10px] text-dynamic-red leading-none">
|
|
216
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
</button>
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={() => setActiveTab('archive')}
|
|
223
|
+
className={cn(
|
|
224
|
+
'flex flex-1 items-center justify-center gap-1.5 px-4 py-2.5 font-medium text-sm transition-colors',
|
|
225
|
+
activeTab === 'archive'
|
|
226
|
+
? 'border-primary border-b-2 text-primary'
|
|
227
|
+
: 'text-foreground/50 hover:text-foreground/80'
|
|
228
|
+
)}
|
|
229
|
+
>
|
|
230
|
+
<Archive className="h-3.5 w-3.5" />
|
|
231
|
+
{archiveText}
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Notification List with Infinite Scroll — key forces remount on tab switch to reset scroll */}
|
|
236
|
+
<NotificationList
|
|
237
|
+
key={activeTab}
|
|
238
|
+
query={activeQuery}
|
|
239
|
+
notifications={allNotifications}
|
|
240
|
+
hasNotifications={hasNotifications}
|
|
241
|
+
activeTab={activeTab}
|
|
242
|
+
wsId={wsIdForTaskContext}
|
|
243
|
+
noNotificationsText={noNotificationsText}
|
|
244
|
+
emptyArchiveText={emptyArchiveText}
|
|
245
|
+
loadingMoreText={loadingMoreText}
|
|
246
|
+
retryText={retryText}
|
|
247
|
+
acceptText={acceptText}
|
|
248
|
+
declineText={declineText}
|
|
249
|
+
acceptedText={acceptedText}
|
|
250
|
+
declinedText={declinedText}
|
|
251
|
+
markAsReadText={markAsReadText}
|
|
252
|
+
markAsUnreadText={markAsUnreadText}
|
|
253
|
+
onMarkAsRead={handleMarkAsRead}
|
|
254
|
+
queryClient={queryClient}
|
|
255
|
+
onActionComplete={() => setOpen(false)}
|
|
256
|
+
/>
|
|
257
|
+
|
|
258
|
+
{/* Footer with View All */}
|
|
259
|
+
<div className="border-t bg-background">
|
|
260
|
+
<Link
|
|
261
|
+
href={notificationsPageUrl}
|
|
262
|
+
onClick={() => setOpen(false)}
|
|
263
|
+
className="flex items-center justify-center gap-1.5 px-4 py-2.5 font-medium text-sm transition-colors hover:rounded-b-xl hover:bg-foreground/5"
|
|
264
|
+
>
|
|
265
|
+
{viewAllText}
|
|
266
|
+
<ChevronRight className="h-4 w-4" />
|
|
267
|
+
</Link>
|
|
268
|
+
</div>
|
|
269
|
+
</PopoverContent>
|
|
270
|
+
</Popover>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { MessageSquareText, Play, RotateCcw } from '@tuturuuu/icons';
|
|
3
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
4
|
+
export function SatelliteOnboardingSettingsPanel({
|
|
5
|
+
t,
|
|
6
|
+
pending,
|
|
7
|
+
replayApp,
|
|
8
|
+
restartJourney,
|
|
9
|
+
shareFeedback,
|
|
10
|
+
}: {
|
|
11
|
+
t: (
|
|
12
|
+
key:
|
|
13
|
+
| 'feedback'
|
|
14
|
+
| 'feedback_description'
|
|
15
|
+
| 'replay_app'
|
|
16
|
+
| 'replay_app_description'
|
|
17
|
+
| 'restart_journey'
|
|
18
|
+
| 'restart_journey_description'
|
|
19
|
+
) => string;
|
|
20
|
+
pending: 'replay' | 'restart' | null;
|
|
21
|
+
replayApp: () => void;
|
|
22
|
+
restartJourney: () => void;
|
|
23
|
+
shareFeedback: () => void;
|
|
24
|
+
}) {
|
|
25
|
+
const actions = [
|
|
26
|
+
{
|
|
27
|
+
description: t('replay_app_description'),
|
|
28
|
+
icon: Play,
|
|
29
|
+
label: t('replay_app'),
|
|
30
|
+
onClick: replayApp,
|
|
31
|
+
pending: pending === 'replay',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: t('restart_journey_description'),
|
|
35
|
+
icon: RotateCcw,
|
|
36
|
+
label: t('restart_journey'),
|
|
37
|
+
onClick: restartJourney,
|
|
38
|
+
pending: pending === 'restart',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description: t('feedback_description'),
|
|
42
|
+
icon: MessageSquareText,
|
|
43
|
+
label: t('feedback'),
|
|
44
|
+
onClick: shareFeedback,
|
|
45
|
+
pending: false,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="space-y-3">
|
|
51
|
+
{actions.map((action) => (
|
|
52
|
+
<div
|
|
53
|
+
key={action.label}
|
|
54
|
+
className="flex flex-col gap-4 rounded-xl border p-4 sm:flex-row sm:items-center sm:justify-between"
|
|
55
|
+
>
|
|
56
|
+
<div className="flex gap-3">
|
|
57
|
+
<div className="grid size-9 shrink-0 place-items-center rounded-lg border bg-muted/40">
|
|
58
|
+
<action.icon className="size-4" />
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<p className="font-medium text-sm">{action.label}</p>
|
|
62
|
+
<p className="mt-1 max-w-xl text-muted-foreground text-sm">
|
|
63
|
+
{action.description}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<Button
|
|
68
|
+
type="button"
|
|
69
|
+
variant="outline"
|
|
70
|
+
disabled={action.pending}
|
|
71
|
+
onClick={action.onClick}
|
|
72
|
+
>
|
|
73
|
+
<action.icon className="size-4" />
|
|
74
|
+
{action.label}
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|