@tuturuuu/ui 0.4.1 → 0.6.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +126 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  22. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  23. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  24. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  25. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  29. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  30. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
  34. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
  35. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  36. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  37. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  38. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  39. package/src/components/ui/finance/wallets/form.tsx +41 -197
  40. package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
  41. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  42. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  43. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  48. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
  49. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
  50. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  51. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  52. package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
  53. package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
  54. package/src/components/ui/storefront/accent-button.tsx +33 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  56. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  58. package/src/components/ui/storefront/image-panel.tsx +40 -0
  59. package/src/components/ui/storefront/index.ts +12 -0
  60. package/src/components/ui/storefront/listing-card.tsx +129 -0
  61. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  62. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  63. package/src/components/ui/storefront/types.ts +99 -0
  64. package/src/components/ui/storefront/utils.ts +90 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  67. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  68. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  69. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  70. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  71. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  72. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  73. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  74. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  75. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
  76. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  77. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  79. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  80. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  81. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  83. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  100. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
  101. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  102. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  103. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  104. package/src/hooks/use-task-actions.ts +45 -0
  105. package/src/hooks/useBoardRealtime.ts +54 -1
  106. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  107. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { ClipboardCheck, Plus } from '@tuturuuu/icons';
5
+ import {
6
+ createWalletCheckpoint,
7
+ deleteWalletCheckpoint,
8
+ listWalletCheckpoints,
9
+ updateWalletCheckpoint,
10
+ type WalletCheckpoint,
11
+ type WalletCheckpointInterval,
12
+ } from '@tuturuuu/internal-api/finance';
13
+ import { Button } from '@tuturuuu/ui/button';
14
+ import { Card } from '@tuturuuu/ui/card';
15
+ import { Separator } from '@tuturuuu/ui/separator';
16
+ import { Skeleton } from '@tuturuuu/ui/skeleton';
17
+ import { toast } from '@tuturuuu/ui/sonner';
18
+ import { useLocale, useTranslations } from 'next-intl';
19
+ import { useState } from 'react';
20
+ import { invalidateWalletMutationQueries } from '../query-invalidation';
21
+ import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
22
+ import { WalletCheckpointDeleteDialog } from './wallet-checkpoint-delete-dialog';
23
+ import { WalletCheckpointDialog } from './wallet-checkpoint-dialog';
24
+ import {
25
+ LatestCheckpoint,
26
+ WalletCheckpointIntervals,
27
+ WalletCheckpointTimeline,
28
+ } from './wallet-checkpoint-sections';
29
+
30
+ function checkpointKey(wsId: string, walletId: string) {
31
+ return ['wallet-checkpoints', wsId, walletId] as const;
32
+ }
33
+
34
+ export function WalletCheckpointPanel({
35
+ canCreateTransactions,
36
+ canUpdateWallets,
37
+ currency,
38
+ walletId,
39
+ walletName,
40
+ wsId,
41
+ }: {
42
+ canCreateTransactions: boolean;
43
+ canUpdateWallets: boolean;
44
+ currency: string;
45
+ walletId: string;
46
+ walletName: string;
47
+ wsId: string;
48
+ }) {
49
+ const t = useTranslations('wallet-checkpoints');
50
+ const locale = useLocale();
51
+ const queryClient = useQueryClient();
52
+ const [createOpen, setCreateOpen] = useState(false);
53
+ const [editing, setEditing] = useState<WalletCheckpoint | null>(null);
54
+ const [deleting, setDeleting] = useState<WalletCheckpoint | null>(null);
55
+ const [adjusting, setAdjusting] = useState<WalletCheckpointInterval | null>(
56
+ null
57
+ );
58
+ const query = useQuery({
59
+ queryKey: checkpointKey(wsId, walletId),
60
+ queryFn: () => listWalletCheckpoints(wsId, walletId, { limit: 50 }),
61
+ });
62
+
63
+ const refresh = () => {
64
+ queryClient.invalidateQueries({ queryKey: checkpointKey(wsId, walletId) });
65
+ invalidateWalletMutationQueries(queryClient, wsId);
66
+ };
67
+ const createMutation = useMutation({
68
+ mutationFn: (payload: Parameters<typeof createWalletCheckpoint>[2]) =>
69
+ createWalletCheckpoint(wsId, walletId, payload),
70
+ onSuccess: () => {
71
+ toast.success(t('checkpoint_saved'));
72
+ setCreateOpen(false);
73
+ refresh();
74
+ },
75
+ onError: (error) =>
76
+ toast.error(error instanceof Error ? error.message : t('save_error')),
77
+ });
78
+ const updateMutation = useMutation({
79
+ mutationFn: (payload: Parameters<typeof updateWalletCheckpoint>[3]) =>
80
+ editing
81
+ ? updateWalletCheckpoint(wsId, walletId, editing.id, payload)
82
+ : Promise.reject(new Error(t('checkpoint_not_found'))),
83
+ onSuccess: () => {
84
+ toast.success(t('checkpoint_saved'));
85
+ setEditing(null);
86
+ refresh();
87
+ },
88
+ onError: (error) =>
89
+ toast.error(error instanceof Error ? error.message : t('save_error')),
90
+ });
91
+ const deleteMutation = useMutation({
92
+ mutationFn: () =>
93
+ deleting
94
+ ? deleteWalletCheckpoint(wsId, walletId, deleting.id)
95
+ : Promise.reject(new Error(t('checkpoint_not_found'))),
96
+ onSuccess: () => {
97
+ toast.success(t('checkpoint_deleted'));
98
+ setDeleting(null);
99
+ refresh();
100
+ },
101
+ onError: (error) =>
102
+ toast.error(error instanceof Error ? error.message : t('delete_error')),
103
+ });
104
+ const formatDate = (value: string) =>
105
+ new Intl.DateTimeFormat(locale, {
106
+ dateStyle: 'medium',
107
+ timeStyle: 'short',
108
+ }).format(new Date(value));
109
+
110
+ return (
111
+ <Card className="grid gap-4 p-4">
112
+ <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
113
+ <div className="space-y-1">
114
+ <div className="flex items-center gap-2 font-semibold text-lg">
115
+ <ClipboardCheck className="h-5 w-5" />
116
+ {t('title')}
117
+ </div>
118
+ <p className="text-muted-foreground text-sm">{t('description')}</p>
119
+ </div>
120
+ {canUpdateWallets && (
121
+ <Button size="sm" onClick={() => setCreateOpen(true)}>
122
+ <Plus className="mr-2 h-4 w-4" />
123
+ {t('record_checkpoint')}
124
+ </Button>
125
+ )}
126
+ </div>
127
+ <Separator />
128
+ {query.isLoading ? (
129
+ <div className="space-y-2">
130
+ <Skeleton className="h-16 w-full" />
131
+ <Skeleton className="h-24 w-full" />
132
+ </div>
133
+ ) : query.data?.latest ? (
134
+ <LatestCheckpoint
135
+ checkpoint={query.data.latest}
136
+ formatDate={formatDate}
137
+ currency={currency}
138
+ />
139
+ ) : (
140
+ <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
141
+ {t('empty_state')}
142
+ </div>
143
+ )}
144
+ <WalletCheckpointIntervals
145
+ canCreateTransactions={canCreateTransactions}
146
+ currency={currency}
147
+ formatDate={formatDate}
148
+ intervals={query.data?.intervals ?? []}
149
+ onReconcile={setAdjusting}
150
+ />
151
+ <WalletCheckpointTimeline
152
+ canUpdateWallets={canUpdateWallets}
153
+ checkpoints={query.data?.data ?? []}
154
+ formatDate={formatDate}
155
+ onDelete={setDeleting}
156
+ onEdit={setEditing}
157
+ />
158
+ <WalletCheckpointDialog
159
+ currency={currency}
160
+ isPending={createMutation.isPending}
161
+ mode="create"
162
+ open={createOpen}
163
+ onOpenChange={setCreateOpen}
164
+ onSubmit={(payload) => createMutation.mutate(payload)}
165
+ />
166
+ <WalletCheckpointDialog
167
+ checkpoint={editing}
168
+ currency={currency}
169
+ isPending={updateMutation.isPending}
170
+ mode="edit"
171
+ open={!!editing}
172
+ onOpenChange={(open) => !open && setEditing(null)}
173
+ onSubmit={(payload) => updateMutation.mutate(payload)}
174
+ />
175
+ <WalletCheckpointDeleteDialog
176
+ open={!!deleting}
177
+ onOpenChange={(open) => !open && setDeleting(null)}
178
+ isPending={deleteMutation.isPending}
179
+ onConfirm={() => deleteMutation.mutate()}
180
+ />
181
+ {adjusting && (
182
+ <WalletCheckpointAdjustmentDialog
183
+ checkedAt={adjusting.end_checked_at}
184
+ checkpointId={adjusting.end_checkpoint_id}
185
+ currency={currency}
186
+ onCreated={refresh}
187
+ onOpenChange={(open) => !open && setAdjusting(null)}
188
+ open={!!adjusting}
189
+ variance={adjusting.interval_variance}
190
+ walletId={walletId}
191
+ walletName={walletName}
192
+ wsId={wsId}
193
+ />
194
+ )}
195
+ </Card>
196
+ );
197
+ }
@@ -0,0 +1,201 @@
1
+ 'use client';
2
+
3
+ import { CheckCircle2, Pencil, Trash2, TriangleAlert } from '@tuturuuu/icons';
4
+ import type {
5
+ WalletCheckpoint,
6
+ WalletCheckpointInterval,
7
+ } from '@tuturuuu/internal-api/finance';
8
+ import { Badge } from '@tuturuuu/ui/badge';
9
+ import { Button } from '@tuturuuu/ui/button';
10
+ import { useTranslations } from 'next-intl';
11
+ import { WalletCheckpointAmount } from './wallet-checkpoint-amount';
12
+
13
+ export function LatestCheckpoint({
14
+ checkpoint,
15
+ currency,
16
+ formatDate,
17
+ }: {
18
+ checkpoint: WalletCheckpoint;
19
+ currency: string;
20
+ formatDate: (value: string) => string;
21
+ }) {
22
+ const t = useTranslations('wallet-checkpoints');
23
+ return (
24
+ <div className="grid gap-2 rounded-md border p-3 text-sm md:grid-cols-3">
25
+ <Metric label={t('latest_checkpoint')}>
26
+ {formatDate(checkpoint.checked_at)}
27
+ </Metric>
28
+ <Metric label={t('actual_balance')}>
29
+ <WalletCheckpointAmount
30
+ amount={checkpoint.actual_balance}
31
+ currency={currency}
32
+ />
33
+ </Metric>
34
+ <Metric label={t('current_variance')}>
35
+ <WalletCheckpointAmount
36
+ amount={checkpoint.current_variance}
37
+ currency={currency}
38
+ signDisplay="always"
39
+ />
40
+ </Metric>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export function WalletCheckpointIntervals({
46
+ canCreateTransactions,
47
+ currency,
48
+ formatDate,
49
+ intervals,
50
+ onReconcile,
51
+ }: {
52
+ canCreateTransactions: boolean;
53
+ currency: string;
54
+ formatDate: (value: string) => string;
55
+ intervals: WalletCheckpointInterval[];
56
+ onReconcile: (interval: WalletCheckpointInterval) => void;
57
+ }) {
58
+ const t = useTranslations('wallet-checkpoints');
59
+ if (intervals.length === 0) return null;
60
+
61
+ return (
62
+ <div className="space-y-2">
63
+ <div className="font-medium text-sm">{t('intervals')}</div>
64
+ {intervals.map((interval) => (
65
+ <div
66
+ key={interval.end_checkpoint_id}
67
+ className={
68
+ interval.is_clean
69
+ ? 'rounded-md border border-dynamic-green/40 p-3'
70
+ : 'rounded-md border border-dynamic-yellow/50 p-3'
71
+ }
72
+ >
73
+ <div className="flex flex-wrap items-center justify-between gap-2">
74
+ <div className="flex items-center gap-2 text-sm">
75
+ {interval.is_clean ? (
76
+ <CheckCircle2 className="h-4 w-4 text-dynamic-green" />
77
+ ) : (
78
+ <TriangleAlert className="h-4 w-4 text-dynamic-yellow" />
79
+ )}
80
+ <span>
81
+ {formatDate(interval.start_checked_at)} -{' '}
82
+ {formatDate(interval.end_checked_at)}
83
+ </span>
84
+ </div>
85
+ <Badge variant={interval.is_clean ? 'secondary' : 'outline'}>
86
+ {interval.is_clean ? t('clean') : t('unresolved')}
87
+ </Badge>
88
+ </div>
89
+ <div className="mt-3 grid gap-2 text-sm md:grid-cols-3">
90
+ <Metric label={t('actual_delta')}>
91
+ <WalletCheckpointAmount
92
+ amount={interval.actual_delta}
93
+ currency={currency}
94
+ />
95
+ </Metric>
96
+ <Metric label={t('ledger_delta')}>
97
+ <WalletCheckpointAmount
98
+ amount={interval.ledger_delta}
99
+ currency={currency}
100
+ />
101
+ </Metric>
102
+ <Metric label={t('variance')}>
103
+ <WalletCheckpointAmount
104
+ amount={interval.interval_variance}
105
+ currency={currency}
106
+ signDisplay="always"
107
+ />
108
+ </Metric>
109
+ </div>
110
+ {!interval.is_clean && canCreateTransactions && (
111
+ <Button
112
+ className="mt-3"
113
+ size="sm"
114
+ variant="outline"
115
+ onClick={() => onReconcile(interval)}
116
+ >
117
+ {t('reconcile')}
118
+ </Button>
119
+ )}
120
+ </div>
121
+ ))}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export function WalletCheckpointTimeline({
127
+ canUpdateWallets,
128
+ checkpoints,
129
+ formatDate,
130
+ onDelete,
131
+ onEdit,
132
+ }: {
133
+ canUpdateWallets: boolean;
134
+ checkpoints: WalletCheckpoint[];
135
+ formatDate: (value: string) => string;
136
+ onDelete: (checkpoint: WalletCheckpoint) => void;
137
+ onEdit: (checkpoint: WalletCheckpoint) => void;
138
+ }) {
139
+ const t = useTranslations('wallet-checkpoints');
140
+ if (checkpoints.length === 0) return null;
141
+
142
+ return (
143
+ <div className="space-y-2">
144
+ <div className="font-medium text-sm">{t('timeline')}</div>
145
+ {checkpoints.map((checkpoint) => (
146
+ <div
147
+ key={checkpoint.id}
148
+ className="flex flex-col gap-2 rounded-md border p-3 md:flex-row md:items-center md:justify-between"
149
+ >
150
+ <div>
151
+ <div className="font-medium text-sm">
152
+ {formatDate(checkpoint.checked_at)}
153
+ </div>
154
+ <div className="text-muted-foreground text-sm">
155
+ <WalletCheckpointAmount
156
+ amount={checkpoint.actual_balance}
157
+ currency={checkpoint.currency}
158
+ />{' '}
159
+ {checkpoint.note ? `- ${checkpoint.note}` : null}
160
+ </div>
161
+ </div>
162
+ {canUpdateWallets && (
163
+ <div className="flex items-center gap-2">
164
+ <Button
165
+ size="icon"
166
+ variant="ghost"
167
+ onClick={() => onEdit(checkpoint)}
168
+ aria-label={t('edit_checkpoint')}
169
+ >
170
+ <Pencil className="h-4 w-4" />
171
+ </Button>
172
+ <Button
173
+ size="icon"
174
+ variant="ghost"
175
+ onClick={() => onDelete(checkpoint)}
176
+ aria-label={t('delete_checkpoint')}
177
+ >
178
+ <Trash2 className="h-4 w-4" />
179
+ </Button>
180
+ </div>
181
+ )}
182
+ </div>
183
+ ))}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ function Metric({
189
+ children,
190
+ label,
191
+ }: {
192
+ children: React.ReactNode;
193
+ label: string;
194
+ }) {
195
+ return (
196
+ <div>
197
+ <div className="text-muted-foreground text-xs">{label}</div>
198
+ <div className="font-medium">{children}</div>
199
+ </div>
200
+ );
201
+ }