@lindaui/blocks 0.4.2 → 0.5.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/README.md +56 -0
- package/dist/ai-cost-card-f403ypsg.js +99 -0
- package/dist/ai-performance-card-cggm09nc.js +191 -0
- package/dist/analytics/ai-cost-card.d.ts +3 -0
- package/dist/analytics/ai-cost-card.d.ts.map +1 -0
- package/dist/analytics/ai-performance-card.d.ts +3 -0
- package/dist/analytics/ai-performance-card.d.ts.map +1 -0
- package/dist/analytics/analytics-card.d.ts +3 -0
- package/dist/analytics/analytics-card.d.ts.map +1 -0
- package/dist/analytics/analytics-skeleton.d.ts +5 -0
- package/dist/analytics/analytics-skeleton.d.ts.map +1 -0
- package/dist/analytics/entity-health-card.d.ts +3 -0
- package/dist/analytics/entity-health-card.d.ts.map +1 -0
- package/dist/analytics/status-distribution-chart.d.ts +3 -0
- package/dist/analytics/status-distribution-chart.d.ts.map +1 -0
- package/dist/analytics/time-range-picker.d.ts +3 -0
- package/dist/analytics/time-range-picker.d.ts.map +1 -0
- package/dist/analytics/top-entities-table.d.ts +3 -0
- package/dist/analytics/top-entities-table.d.ts.map +1 -0
- package/dist/analytics/types.d.ts +104 -0
- package/dist/analytics/types.d.ts.map +1 -0
- package/dist/analytics-card-bcmijpt6.js +38 -0
- package/dist/analytics-skeleton-imve37du.js +164 -0
- package/dist/analytics.d.ts +11 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +8 -0
- package/dist/app-shell/drawer-focus.d.ts +8 -0
- package/dist/app-shell/drawer-focus.d.ts.map +1 -0
- package/dist/app-shell/footer.d.ts +39 -0
- package/dist/app-shell/footer.d.ts.map +1 -0
- package/dist/app-shell/logout-dialog.d.ts +6 -0
- package/dist/app-shell/logout-dialog.d.ts.map +1 -0
- package/dist/app-shell/navigation.d.ts +15 -0
- package/dist/app-shell/navigation.d.ts.map +1 -0
- package/dist/app-shell/types.d.ts +55 -0
- package/dist/app-shell/types.d.ts.map +1 -0
- package/dist/app-shell.d.ts +7 -0
- package/dist/app-shell.d.ts.map +1 -0
- package/dist/app-shell.js +254 -0
- package/dist/channel-settings.d.ts +24 -0
- package/dist/channel-settings.d.ts.map +1 -0
- package/dist/channel-settings.js +60 -0
- package/dist/chat-avatar-jdmeyf2o.js +30 -0
- package/dist/chat-layout-fwx3x26g.js +25 -0
- package/dist/chat.d.ts +22 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +66 -0
- package/dist/command-palette.d.ts +22 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +141 -0
- package/dist/compat-chat/chat-avatar.d.ts +16 -0
- package/dist/compat-chat/chat-avatar.d.ts.map +1 -0
- package/dist/compat-chat/chat-header.d.ts +9 -0
- package/dist/compat-chat/chat-header.d.ts.map +1 -0
- package/dist/compat-chat/chat-layout.d.ts +10 -0
- package/dist/compat-chat/chat-layout.d.ts.map +1 -0
- package/dist/compat-chat/conversation-list-item.d.ts +10 -0
- package/dist/compat-chat/conversation-list-item.d.ts.map +1 -0
- package/dist/compat-chat/conversation-list.d.ts +24 -0
- package/dist/compat-chat/conversation-list.d.ts.map +1 -0
- package/dist/compat-chat/conversation-thread.d.ts +18 -0
- package/dist/compat-chat/conversation-thread.d.ts.map +1 -0
- package/dist/compat-chat/date-divider.d.ts +5 -0
- package/dist/compat-chat/date-divider.d.ts.map +1 -0
- package/dist/compat-chat/empty-chat-view.d.ts +6 -0
- package/dist/compat-chat/empty-chat-view.d.ts.map +1 -0
- package/dist/compat-chat/image-lightbox.d.ts +7 -0
- package/dist/compat-chat/image-lightbox.d.ts.map +1 -0
- package/dist/compat-chat/message-bubble.d.ts +6 -0
- package/dist/compat-chat/message-bubble.d.ts.map +1 -0
- package/dist/compat-chat/message-composer.d.ts +11 -0
- package/dist/compat-chat/message-composer.d.ts.map +1 -0
- package/dist/compat-chat/time-divider.d.ts +5 -0
- package/dist/compat-chat/time-divider.d.ts.map +1 -0
- package/dist/compat-chat/time-grouping.d.ts +5 -0
- package/dist/compat-chat/time-grouping.d.ts.map +1 -0
- package/dist/compat-chat/types.d.ts +35 -0
- package/dist/compat-chat/types.d.ts.map +1 -0
- package/dist/compat-chat/whatsapp-audio-player.d.ts +7 -0
- package/dist/compat-chat/whatsapp-audio-player.d.ts.map +1 -0
- package/dist/compat.d.ts +9 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +4 -0
- package/dist/connected-panel-lhwy25yk.js +132 -0
- package/dist/connection-panel/connected-panel.d.ts +26 -0
- package/dist/connection-panel/connected-panel.d.ts.map +1 -0
- package/dist/connection-panel/disconnected-panel.d.ts +12 -0
- package/dist/connection-panel/disconnected-panel.d.ts.map +1 -0
- package/dist/connection-panel/instructions.d.ts +5 -0
- package/dist/connection-panel/instructions.d.ts.map +1 -0
- package/dist/connection-panel/qr-panel.d.ts +14 -0
- package/dist/connection-panel/qr-panel.d.ts.map +1 -0
- package/dist/connection-panel/use-qr-refresh.d.ts +18 -0
- package/dist/connection-panel/use-qr-refresh.d.ts.map +1 -0
- package/dist/connection-panel.d.ts +39 -0
- package/dist/connection-panel.d.ts.map +1 -0
- package/dist/connection-panel.js +328 -0
- package/dist/conversation-list-ekq2dzca.js +133 -0
- package/dist/conversation-list-item-fy6t5af2.js +55 -0
- package/dist/conversation-thread-c60431oa.js +205 -0
- package/dist/entity-card.d.ts +17 -0
- package/dist/entity-card.d.ts.map +1 -0
- package/dist/entity-card.js +69 -0
- package/dist/entity-health-card-ggnmrtrq.js +192 -0
- package/dist/entity-switcher.d.ts +33 -0
- package/dist/entity-switcher.d.ts.map +1 -0
- package/dist/entity-switcher.js +137 -0
- package/dist/filter-bar.d.ts +41 -0
- package/dist/filter-bar.d.ts.map +1 -0
- package/dist/filter-bar.js +155 -0
- package/dist/footer-f7r5e42b.js +192 -0
- package/dist/image-lightbox-ci3arb6z.js +62 -0
- package/dist/kanban/drop-utils.d.ts +18 -0
- package/dist/kanban/drop-utils.d.ts.map +1 -0
- package/dist/kanban/navigation-buttons.d.ts +9 -0
- package/dist/kanban/navigation-buttons.d.ts.map +1 -0
- package/dist/kanban/scroll-indicators.d.ts +6 -0
- package/dist/kanban/scroll-indicators.d.ts.map +1 -0
- package/dist/kanban/use-keyboard-navigation.d.ts +4 -0
- package/dist/kanban/use-keyboard-navigation.d.ts.map +1 -0
- package/dist/kanban-board.d.ts +31 -0
- package/dist/kanban-board.d.ts.map +1 -0
- package/dist/kanban-board.js +128 -0
- package/dist/kanban-column.d.ts +13 -0
- package/dist/kanban-column.d.ts.map +1 -0
- package/dist/kanban-column.js +58 -0
- package/dist/legacy-kpi-card.d.ts +16 -0
- package/dist/legacy-kpi-card.d.ts.map +1 -0
- package/dist/legacy-kpi-card.js +4 -0
- package/dist/logout-dialog-bbv59tbi.js +93 -0
- package/dist/message-bubble-odjvhroq.js +110 -0
- package/dist/message-composer-m2n6soo7.js +59 -0
- package/dist/navigation-buttons-e77mshh0.js +65 -0
- package/dist/navigation-hotcrqoq.js +115 -0
- package/dist/save-bar.d.ts +15 -0
- package/dist/save-bar.d.ts.map +1 -0
- package/dist/save-bar.js +38 -0
- package/dist/scroll-indicators-gvxapogd.js +39 -0
- package/dist/settings-layout.d.ts +20 -0
- package/dist/settings-layout.d.ts.map +1 -0
- package/dist/settings-layout.js +56 -0
- package/dist/source-selector.d.ts +18 -0
- package/dist/source-selector.d.ts.map +1 -0
- package/dist/source-selector.js +59 -0
- package/dist/status-distribution-chart-oucedzv7.js +87 -0
- package/dist/status-filters.d.ts +12 -0
- package/dist/status-filters.d.ts.map +1 -0
- package/dist/status-filters.js +31 -0
- package/dist/stepper.d.ts +15 -0
- package/dist/stepper.d.ts.map +1 -0
- package/dist/stepper.js +42 -0
- package/dist/time-range-picker-mu38hul4.js +123 -0
- package/dist/top-entities-table-o7xzpq96.js +78 -0
- package/dist/use-keyboard-navigation-he1xmriz.js +52 -0
- package/dist/whatsapp-audio-player-nooty6jb.js +277 -0
- package/llms.txt +87 -4
- package/package.json +191 -46
- package/src/analytics/ai-cost-card.tsx +31 -0
- package/src/analytics/ai-performance-card.tsx +48 -0
- package/src/analytics/analytics-card.tsx +14 -0
- package/src/analytics/analytics-skeleton.tsx +15 -0
- package/src/analytics/entity-health-card.tsx +50 -0
- package/src/analytics/status-distribution-chart.tsx +20 -0
- package/src/analytics/time-range-picker.tsx +43 -0
- package/src/analytics/top-entities-table.tsx +24 -0
- package/src/analytics/types.ts +43 -0
- package/src/analytics.ts +10 -0
- package/src/app-shell/drawer-focus.ts +42 -0
- package/src/app-shell/footer.tsx +160 -0
- package/src/app-shell/logout-dialog.tsx +68 -0
- package/src/app-shell/navigation.tsx +147 -0
- package/src/app-shell/types.ts +57 -0
- package/src/app-shell.tsx +166 -0
- package/src/area-chart.tsx +85 -0
- package/src/audio-player.tsx +74 -0
- package/src/auth-layout.tsx +132 -0
- package/src/auth-provider.tsx +29 -0
- package/src/bar-chart.tsx +78 -0
- package/src/channel-settings.tsx +76 -0
- package/src/chart-card.tsx +45 -0
- package/src/chat-avatar.tsx +57 -0
- package/src/chat-date-divider.tsx +18 -0
- package/src/chat-header.tsx +24 -0
- package/src/chat-layout.tsx +41 -0
- package/src/chat-time-divider.tsx +20 -0
- package/src/chat-types.ts +32 -0
- package/src/chat.ts +21 -0
- package/src/command-palette.tsx +110 -0
- package/src/compat-chat/chat-avatar.tsx +23 -0
- package/src/compat-chat/chat-header.tsx +12 -0
- package/src/compat-chat/chat-layout.tsx +14 -0
- package/src/compat-chat/conversation-list-item.tsx +27 -0
- package/src/compat-chat/conversation-list.tsx +49 -0
- package/src/compat-chat/conversation-thread.tsx +92 -0
- package/src/compat-chat/date-divider.tsx +7 -0
- package/src/compat-chat/empty-chat-view.tsx +9 -0
- package/src/compat-chat/image-lightbox.tsx +22 -0
- package/src/compat-chat/message-bubble.tsx +28 -0
- package/src/compat-chat/message-composer.tsx +26 -0
- package/src/compat-chat/time-divider.tsx +5 -0
- package/src/compat-chat/time-grouping.ts +19 -0
- package/src/compat-chat/types.ts +31 -0
- package/src/compat-chat/whatsapp-audio-player.tsx +100 -0
- package/src/compat.ts +8 -0
- package/src/confirm-dialog.tsx +85 -0
- package/src/connection-panel/connected-panel.tsx +106 -0
- package/src/connection-panel/disconnected-panel.tsx +48 -0
- package/src/connection-panel/instructions.tsx +9 -0
- package/src/connection-panel/qr-panel.tsx +62 -0
- package/src/connection-panel/use-qr-refresh.ts +52 -0
- package/src/connection-panel.tsx +141 -0
- package/src/conversation-list-item.tsx +53 -0
- package/src/conversation-list.tsx +67 -0
- package/src/conversation-thread.tsx +64 -0
- package/src/detail-panel.tsx +123 -0
- package/src/document-panel.tsx +109 -0
- package/src/document-reader.tsx +29 -0
- package/src/empty-chat-view.tsx +25 -0
- package/src/entity-card.tsx +74 -0
- package/src/entity-health-card.tsx +53 -0
- package/src/entity-switcher.tsx +126 -0
- package/src/fetching-indicator.tsx +16 -0
- package/src/field-array-form.tsx +166 -0
- package/src/filter-bar.tsx +203 -0
- package/src/image-viewer.tsx +129 -0
- package/src/kanban/drop-utils.ts +31 -0
- package/src/kanban/navigation-buttons.tsx +80 -0
- package/src/kanban/scroll-indicators.tsx +47 -0
- package/src/kanban/use-keyboard-navigation.ts +43 -0
- package/src/kanban-board.tsx +160 -0
- package/src/kanban-column.tsx +62 -0
- package/src/legacy-kpi-card.tsx +38 -0
- package/src/line-chart.tsx +58 -0
- package/src/list-item.tsx +63 -0
- package/src/list-pane.tsx +143 -0
- package/src/login-form.tsx +128 -0
- package/src/message-bubble.tsx +70 -0
- package/src/message-composer.tsx +52 -0
- package/src/pie-chart.tsx +70 -0
- package/src/radar-chart.tsx +55 -0
- package/src/radial-chart.tsx +103 -0
- package/src/recording-overlay.tsx +159 -0
- package/src/save-bar.tsx +35 -0
- package/src/settings-layout.tsx +79 -0
- package/src/source-selector.tsx +71 -0
- package/src/split-workspace.tsx +74 -0
- package/src/stat-card.tsx +166 -0
- package/src/stats-grid.tsx +23 -0
- package/src/status-filters.tsx +28 -0
- package/src/stepper.tsx +47 -0
- package/src/table-pane.tsx +194 -0
- package/src/time-range-picker.tsx +89 -0
- package/src/workspace-back-button.tsx +32 -0
- package/dist/chat-types.test.d.ts +0 -2
- package/dist/chat-types.test.d.ts.map +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Spinner } from "@lindaui/ui/compound";
|
|
4
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
5
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
6
|
+
import { DisconnectedPanel } from "./connection-panel/disconnected-panel";
|
|
7
|
+
import { QRPanel } from "./connection-panel/qr-panel";
|
|
8
|
+
import { useQRRefresh } from "./connection-panel/use-qr-refresh";
|
|
9
|
+
|
|
10
|
+
export { ConnectedPanel } from "./connection-panel/connected-panel";
|
|
11
|
+
export type { ConnectedPanelProps, ConnectedPanelLabels, ConnectionDetail } from "./connection-panel/connected-panel";
|
|
12
|
+
|
|
13
|
+
export type ConnectionPanelState =
|
|
14
|
+
| { status: "checking" | "disconnected" | "generating" }
|
|
15
|
+
| { status: "awaiting_scan"; qrCode: ReactNode; qrFormat?: "ascii" | "image" }
|
|
16
|
+
| { status: "connected"; content: ReactNode };
|
|
17
|
+
|
|
18
|
+
export interface ConnectionPanelLabels {
|
|
19
|
+
checking: ReactNode;
|
|
20
|
+
generating: ReactNode;
|
|
21
|
+
waiting: ReactNode;
|
|
22
|
+
disconnectedDescription: ReactNode;
|
|
23
|
+
connect: ReactNode;
|
|
24
|
+
connectError: ReactNode;
|
|
25
|
+
refresh: ReactNode;
|
|
26
|
+
expiresSoon: ReactNode;
|
|
27
|
+
expiresIn: (time: string) => ReactNode;
|
|
28
|
+
instructions: readonly ReactNode[];
|
|
29
|
+
disconnectedInstructions: readonly ReactNode[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ConnectionPanelProps {
|
|
33
|
+
state: ConnectionPanelState;
|
|
34
|
+
labels: ConnectionPanelLabels;
|
|
35
|
+
error?: ReactNode;
|
|
36
|
+
isActive?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
qrExpirySeconds?: number;
|
|
39
|
+
onConnect: () => Promise<void>;
|
|
40
|
+
onRefreshQR: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Controlled connection presentation. Authentication and automatic connection policy stay in the app. */
|
|
44
|
+
export function ConnectionPanel({
|
|
45
|
+
state,
|
|
46
|
+
labels,
|
|
47
|
+
error,
|
|
48
|
+
isActive = true,
|
|
49
|
+
className = "",
|
|
50
|
+
qrExpirySeconds = 60,
|
|
51
|
+
onConnect,
|
|
52
|
+
onRefreshQR,
|
|
53
|
+
}: ConnectionPanelProps) {
|
|
54
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
55
|
+
const pending = useRef(false);
|
|
56
|
+
const hasQR = state.status === "awaiting_scan";
|
|
57
|
+
const qr = useQRRefresh({
|
|
58
|
+
enabled: hasQR && isActive,
|
|
59
|
+
expirySeconds: Number.isFinite(qrExpirySeconds) ? Math.max(1, Math.floor(qrExpirySeconds)) : 60,
|
|
60
|
+
error,
|
|
61
|
+
errorLabel: labels.connectError,
|
|
62
|
+
onRefresh: onRefreshQR,
|
|
63
|
+
});
|
|
64
|
+
const actionError = error || qr.actionError;
|
|
65
|
+
|
|
66
|
+
async function connect() {
|
|
67
|
+
if (pending.current) return;
|
|
68
|
+
pending.current = true;
|
|
69
|
+
setIsConnecting(true);
|
|
70
|
+
qr.setActionError(null);
|
|
71
|
+
try {
|
|
72
|
+
await onConnect();
|
|
73
|
+
} catch {
|
|
74
|
+
qr.setActionError(labels.connectError);
|
|
75
|
+
} finally {
|
|
76
|
+
pending.current = false;
|
|
77
|
+
setIsConnecting(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (state.status === "checking") {
|
|
82
|
+
return (
|
|
83
|
+
<div className={`flex items-center justify-center gap-3 py-10 text-muted text-sm ${className}`} role="status">
|
|
84
|
+
<Spinner size="sm" />
|
|
85
|
+
{labels.checking}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={`${state.status === "connected" ? "space-y-4" : "mx-auto w-full max-w-sm space-y-6 py-6 text-center"} ${className}`}>
|
|
92
|
+
<AnimatePresence mode="wait">
|
|
93
|
+
{state.status === "connected" ? (
|
|
94
|
+
<motion.div
|
|
95
|
+
animate={{ opacity: 1, y: 0 }}
|
|
96
|
+
className="space-y-3"
|
|
97
|
+
exit={{ opacity: 0, y: -10 }}
|
|
98
|
+
initial={{ opacity: 0, y: 10 }}
|
|
99
|
+
key="connected"
|
|
100
|
+
>
|
|
101
|
+
{state.content}
|
|
102
|
+
</motion.div>
|
|
103
|
+
) : state.status === "awaiting_scan" ? (
|
|
104
|
+
<motion.div
|
|
105
|
+
animate={{ opacity: 1, y: 0 }}
|
|
106
|
+
className="space-y-4"
|
|
107
|
+
exit={{ opacity: 0, y: -10 }}
|
|
108
|
+
initial={{ opacity: 0, y: 10 }}
|
|
109
|
+
key="qr"
|
|
110
|
+
>
|
|
111
|
+
<QRPanel
|
|
112
|
+
error={actionError}
|
|
113
|
+
isRefreshing={qr.isRefreshing}
|
|
114
|
+
labels={labels}
|
|
115
|
+
onRefresh={qr.refresh}
|
|
116
|
+
qrCode={state.qrCode}
|
|
117
|
+
qrFormat={state.qrFormat}
|
|
118
|
+
timeRemaining={qr.timeRemaining}
|
|
119
|
+
/>
|
|
120
|
+
</motion.div>
|
|
121
|
+
) : (
|
|
122
|
+
<motion.div
|
|
123
|
+
animate={{ opacity: 1, y: 0 }}
|
|
124
|
+
className="space-y-4"
|
|
125
|
+
exit={{ opacity: 0, y: -10 }}
|
|
126
|
+
initial={{ opacity: 0, y: 10 }}
|
|
127
|
+
key="initial"
|
|
128
|
+
>
|
|
129
|
+
<DisconnectedPanel
|
|
130
|
+
error={actionError}
|
|
131
|
+
generating={state.status === "generating"}
|
|
132
|
+
labels={labels}
|
|
133
|
+
onConnect={connect}
|
|
134
|
+
pending={isConnecting}
|
|
135
|
+
/>
|
|
136
|
+
</motion.div>
|
|
137
|
+
)}
|
|
138
|
+
</AnimatePresence>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { formatRelativeTime } from "@lindaui/ui/date";
|
|
4
|
+
import { ChatAvatar } from "./chat-avatar";
|
|
5
|
+
import type { ConversationSummary } from "./chat-types.js";
|
|
6
|
+
|
|
7
|
+
export interface ConversationListItemProps {
|
|
8
|
+
conversation: ConversationSummary;
|
|
9
|
+
selected?: boolean;
|
|
10
|
+
onSelect?: () => void;
|
|
11
|
+
trailing?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ConversationListItem({
|
|
15
|
+
conversation,
|
|
16
|
+
selected = false,
|
|
17
|
+
onSelect,
|
|
18
|
+
trailing,
|
|
19
|
+
}: ConversationListItemProps) {
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
aria-pressed={selected}
|
|
24
|
+
onClick={onSelect}
|
|
25
|
+
className={`flex w-full items-center gap-3 border-b border-border px-3 py-3 text-left font-[inherit] text-[inherit] transition-colors ${
|
|
26
|
+
selected ? "bg-primary/5" : "bg-card hover:bg-accent/50"
|
|
27
|
+
}`}
|
|
28
|
+
>
|
|
29
|
+
<ChatAvatar name={conversation.title} src={conversation.avatarUrl} />
|
|
30
|
+
<div className="min-w-0 flex-1">
|
|
31
|
+
<div className="flex items-center justify-between gap-2">
|
|
32
|
+
<span className="truncate text-sm font-semibold">{conversation.title}</span>
|
|
33
|
+
{conversation.lastMessageAt != null && (
|
|
34
|
+
<span className="shrink-0 text-[11px] text-muted-foreground">
|
|
35
|
+
{formatRelativeTime(conversation.lastMessageAt)}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex items-center justify-between gap-2">
|
|
40
|
+
{conversation.subtitle != null && (
|
|
41
|
+
<span className="truncate text-xs text-muted-foreground">{conversation.subtitle}</span>
|
|
42
|
+
)}
|
|
43
|
+
{!!conversation.unreadCount && (
|
|
44
|
+
<span className="grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[10px] font-semibold text-primary-foreground">
|
|
45
|
+
{conversation.unreadCount}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
{trailing}
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { SearchField } from "@lindaui/ui/search-field";
|
|
4
|
+
import { ScrollShadow } from "@lindaui/ui/scroll-shadow";
|
|
5
|
+
import { Spinner } from "@lindaui/ui/spinner";
|
|
6
|
+
import { ConversationListItem } from "./conversation-list-item";
|
|
7
|
+
import type { ConversationSummary } from "./chat-types.js";
|
|
8
|
+
|
|
9
|
+
export interface ConversationListProps {
|
|
10
|
+
conversations: ConversationSummary[];
|
|
11
|
+
selectedId?: string;
|
|
12
|
+
onSelect?: (id: string) => void;
|
|
13
|
+
searchValue?: string;
|
|
14
|
+
onSearchChange?: (value: string) => void;
|
|
15
|
+
searchLabel?: string;
|
|
16
|
+
searchPlaceholder?: string;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
emptyContent?: ReactNode;
|
|
19
|
+
filters?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ConversationList({
|
|
23
|
+
conversations,
|
|
24
|
+
selectedId,
|
|
25
|
+
onSelect,
|
|
26
|
+
searchValue,
|
|
27
|
+
onSearchChange,
|
|
28
|
+
searchLabel = "Buscar",
|
|
29
|
+
searchPlaceholder = "Buscar conversación…",
|
|
30
|
+
loading = false,
|
|
31
|
+
emptyContent,
|
|
32
|
+
filters,
|
|
33
|
+
}: ConversationListProps) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex h-full flex-col">
|
|
36
|
+
<div className="border-b border-border p-3">
|
|
37
|
+
<SearchField
|
|
38
|
+
aria-label={searchLabel}
|
|
39
|
+
placeholder={searchPlaceholder}
|
|
40
|
+
value={searchValue}
|
|
41
|
+
onChange={onSearchChange}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
{filters != null && <div className="border-b border-border px-3 py-2">{filters}</div>}
|
|
45
|
+
<ScrollShadow className="flex-1">
|
|
46
|
+
{loading ? (
|
|
47
|
+
<div className="flex items-center justify-center p-8">
|
|
48
|
+
<Spinner aria-label="Cargando" />
|
|
49
|
+
</div>
|
|
50
|
+
) : conversations.length === 0 ? (
|
|
51
|
+
<div className="p-8 text-center text-sm text-muted-foreground">
|
|
52
|
+
{emptyContent ?? "Sin conversaciones"}
|
|
53
|
+
</div>
|
|
54
|
+
) : (
|
|
55
|
+
conversations.map((c) => (
|
|
56
|
+
<ConversationListItem
|
|
57
|
+
key={c.id}
|
|
58
|
+
conversation={c}
|
|
59
|
+
selected={c.id === selectedId}
|
|
60
|
+
onSelect={() => onSelect?.(c.id)}
|
|
61
|
+
/>
|
|
62
|
+
))
|
|
63
|
+
)}
|
|
64
|
+
</ScrollShadow>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { ScrollShadow } from "@lindaui/ui/scroll-shadow";
|
|
4
|
+
import { Spinner } from "@lindaui/ui/spinner";
|
|
5
|
+
import { groupByDay } from "@lindaui/ui/time-grouping";
|
|
6
|
+
import { ChatDateDivider } from "./chat-date-divider";
|
|
7
|
+
import { MessageBubble } from "./message-bubble";
|
|
8
|
+
import { EmptyChatView } from "./empty-chat-view";
|
|
9
|
+
import type { GenericMessage } from "./chat-types.js";
|
|
10
|
+
|
|
11
|
+
export interface ConversationThreadProps {
|
|
12
|
+
messages: GenericMessage[];
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
onImageClick?: (url: string) => void;
|
|
15
|
+
emptyTitle?: string;
|
|
16
|
+
emptyDescription?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Timeline scrolleable con auto-scroll al último mensaje + agrupado por día. */
|
|
20
|
+
export function ConversationThread({
|
|
21
|
+
messages,
|
|
22
|
+
loading = false,
|
|
23
|
+
onImageClick,
|
|
24
|
+
emptyTitle = "Sin mensajes",
|
|
25
|
+
emptyDescription,
|
|
26
|
+
}: ConversationThreadProps) {
|
|
27
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
bottomRef.current?.scrollIntoView({ block: "end" });
|
|
31
|
+
}, [messages.length]);
|
|
32
|
+
|
|
33
|
+
if (loading) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex h-full items-center justify-center">
|
|
36
|
+
<Spinner aria-label="Cargando mensajes" />
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (messages.length === 0) {
|
|
42
|
+
return <EmptyChatView title={emptyTitle} description={emptyDescription} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const groups = groupByDay(messages, (m) => m.sentAt);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<ScrollShadow className="h-full">
|
|
49
|
+
<div className="flex flex-col gap-1 p-4">
|
|
50
|
+
{groups.map((group) => (
|
|
51
|
+
<div key={group.key}>
|
|
52
|
+
<ChatDateDivider date={group.items[0]!.sentAt} />
|
|
53
|
+
<div className="flex flex-col gap-1.5">
|
|
54
|
+
{group.items.map((message) => (
|
|
55
|
+
<MessageBubble key={message.id} message={message} onImageClick={onImageClick} />
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
))}
|
|
60
|
+
<div ref={bottomRef} />
|
|
61
|
+
</div>
|
|
62
|
+
</ScrollShadow>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { FileText } from "lucide-react";
|
|
4
|
+
import { Button } from "@lindaui/ui/button";
|
|
5
|
+
import { Card } from "@lindaui/ui/card";
|
|
6
|
+
import { Divider } from "@lindaui/ui/divider";
|
|
7
|
+
|
|
8
|
+
export interface DetailPanelMetaRow {
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
value: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DetailPanelAction {
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
onPress: () => void;
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DetailPanelProps {
|
|
21
|
+
selected?: boolean;
|
|
22
|
+
emptyTitle?: string;
|
|
23
|
+
emptyDescription?: ReactNode;
|
|
24
|
+
emptyIcon?: ReactNode;
|
|
25
|
+
title?: string;
|
|
26
|
+
subtitle?: ReactNode;
|
|
27
|
+
badges?: ReactNode;
|
|
28
|
+
media?: ReactNode;
|
|
29
|
+
meta?: DetailPanelMetaRow[];
|
|
30
|
+
action?: DetailPanelAction;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function DetailPanel({
|
|
34
|
+
selected = false,
|
|
35
|
+
emptyTitle = "Seleccioná un elemento",
|
|
36
|
+
emptyDescription,
|
|
37
|
+
emptyIcon,
|
|
38
|
+
title,
|
|
39
|
+
subtitle,
|
|
40
|
+
badges,
|
|
41
|
+
media,
|
|
42
|
+
meta,
|
|
43
|
+
action,
|
|
44
|
+
}: DetailPanelProps) {
|
|
45
|
+
if (!selected || title === undefined) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="grid h-full place-items-center p-6 sm:p-10">
|
|
48
|
+
<div className="max-w-[280px] text-center">
|
|
49
|
+
<div className="mx-auto grid h-14 w-14 place-items-center rounded-2xl bg-secondary">
|
|
50
|
+
{emptyIcon ?? <FileText className="h-7 w-7 text-muted-foreground" strokeWidth={1.6} />}
|
|
51
|
+
</div>
|
|
52
|
+
<h3 className="mt-4 text-[15px] font-semibold">{emptyTitle}</h3>
|
|
53
|
+
{emptyDescription && (
|
|
54
|
+
<p className="mt-1.5 text-sm text-muted-foreground">{emptyDescription}</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="h-full overflow-y-auto px-4 py-5 sm:px-8 sm:py-7">
|
|
63
|
+
<div className="mx-auto max-w-2xl">
|
|
64
|
+
<div>
|
|
65
|
+
{badges && (
|
|
66
|
+
<div className="flex flex-wrap items-center gap-2.5">{badges}</div>
|
|
67
|
+
)}
|
|
68
|
+
<h1 className="mt-3 text-xl font-semibold tracking-tight sm:text-2xl">{title}</h1>
|
|
69
|
+
{subtitle && (
|
|
70
|
+
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{(media || (meta && meta.length > 0)) && (
|
|
75
|
+
// Mobile-first: stack (media arriba, meta abajo). Desde `sm` y solo
|
|
76
|
+
// cuando hay media, split en dos columnas (media fija 200px + meta
|
|
77
|
+
// 1fr). Sin media la Card de metadata ocupa todo el ancho siempre
|
|
78
|
+
// (si no, queda atrapada en la columna 1fr con un gutter vacío).
|
|
79
|
+
<div
|
|
80
|
+
className={
|
|
81
|
+
media
|
|
82
|
+
? "mt-6 flex flex-col gap-4 sm:grid sm:grid-cols-[200px_1fr] sm:gap-6"
|
|
83
|
+
: "mt-6"
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
{media && <div>{media}</div>}
|
|
87
|
+
{meta && meta.length > 0 && (
|
|
88
|
+
<Card>
|
|
89
|
+
<div className="px-4 py-1">
|
|
90
|
+
{meta.map((row, i) => (
|
|
91
|
+
<div key={i}>
|
|
92
|
+
{i > 0 && <Divider />}
|
|
93
|
+
<div className="flex items-start gap-2.5 py-2.5">
|
|
94
|
+
{row.icon && (
|
|
95
|
+
<div className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground">
|
|
96
|
+
{row.icon}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
<div className="flex flex-1 justify-between gap-3">
|
|
100
|
+
<span className="text-sm text-muted-foreground">{row.label}</span>
|
|
101
|
+
<span className="text-right text-sm font-medium">{row.value}</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</Card>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{action && (
|
|
113
|
+
<div className="mt-6">
|
|
114
|
+
<Button size="lg" onPress={action.onPress} className="w-full sm:w-auto">
|
|
115
|
+
{action.icon}
|
|
116
|
+
{action.label}
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
// @lindaui/ui primitives
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
import { Divider } from "@lindaui/ui/divider";
|
|
5
|
+
import { Skeleton } from "@lindaui/ui/skeleton";
|
|
6
|
+
|
|
7
|
+
export interface DocumentPanelProps {
|
|
8
|
+
title: string;
|
|
9
|
+
subtitle?: ReactNode;
|
|
10
|
+
meta?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* `block` (default) stacks subtitle and meta on their own lines. `inline`
|
|
13
|
+
* flows them on one wrapping row (subtitle · meta) so a date sits next to the
|
|
14
|
+
* subtitle instead of forcing a new line.
|
|
15
|
+
*/
|
|
16
|
+
metaPlacement?: "block" | "inline";
|
|
17
|
+
statusBadge?: ReactNode;
|
|
18
|
+
state: "empty" | "loading" | "ready";
|
|
19
|
+
emptyContent?: ReactNode;
|
|
20
|
+
loadingLabel?: string;
|
|
21
|
+
notices?: ReactNode;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
footer?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DocumentPanel({
|
|
27
|
+
title,
|
|
28
|
+
subtitle,
|
|
29
|
+
meta,
|
|
30
|
+
metaPlacement = "block",
|
|
31
|
+
statusBadge,
|
|
32
|
+
state,
|
|
33
|
+
emptyContent,
|
|
34
|
+
loadingLabel = "Cargando…",
|
|
35
|
+
notices,
|
|
36
|
+
children,
|
|
37
|
+
footer,
|
|
38
|
+
}: DocumentPanelProps) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex flex-col h-full">
|
|
41
|
+
{/* Header */}
|
|
42
|
+
<div className="flex flex-wrap items-start justify-between gap-3 px-4 pb-3 pt-4 flex-none sm:px-5">
|
|
43
|
+
<div className="min-w-0">
|
|
44
|
+
<h2 className="truncate text-lg font-bold leading-tight tracking-tight sm:text-xl">
|
|
45
|
+
{title}
|
|
46
|
+
</h2>
|
|
47
|
+
{metaPlacement === "inline"
|
|
48
|
+
? (subtitle || meta) && (
|
|
49
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 text-sm text-muted-foreground">
|
|
50
|
+
{subtitle && <span>{subtitle}</span>}
|
|
51
|
+
{meta && (
|
|
52
|
+
<span className="text-xs tabular-nums">
|
|
53
|
+
{subtitle ? "· " : ""}
|
|
54
|
+
{meta}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
: (
|
|
60
|
+
<>
|
|
61
|
+
{subtitle && (
|
|
62
|
+
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
63
|
+
{subtitle}
|
|
64
|
+
</p>
|
|
65
|
+
)}
|
|
66
|
+
{meta && (
|
|
67
|
+
<p className="mt-0.5 text-xs text-muted-foreground tabular-nums">
|
|
68
|
+
{meta}
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
{statusBadge && <div className="flex-none">{statusBadge}</div>}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<Divider />
|
|
78
|
+
|
|
79
|
+
{/* Body */}
|
|
80
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 sm:px-5">
|
|
81
|
+
{state === "empty" && emptyContent}
|
|
82
|
+
|
|
83
|
+
{state === "loading" && (
|
|
84
|
+
<div role="status" className="flex flex-col gap-3">
|
|
85
|
+
<span className="text-sm text-muted-foreground">{loadingLabel}</span>
|
|
86
|
+
<Skeleton className="h-3 w-full" />
|
|
87
|
+
<Skeleton className="h-3 w-full" />
|
|
88
|
+
<Skeleton className="h-3 w-3/4" />
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{state === "ready" && (
|
|
93
|
+
<>
|
|
94
|
+
{notices}
|
|
95
|
+
{children}
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Footer */}
|
|
101
|
+
{footer && (
|
|
102
|
+
<>
|
|
103
|
+
<Divider />
|
|
104
|
+
<div className="flex-none flex flex-wrap gap-2 p-4">{footer}</div>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface DocumentReaderProps {
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function DocumentReader({ content }: DocumentReaderProps) {
|
|
8
|
+
const blocks = content.trim().split(/\n\s*\n/);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col gap-4">
|
|
12
|
+
{blocks.map((block, i) => {
|
|
13
|
+
const lines = block.split("\n");
|
|
14
|
+
const heading = lines[0];
|
|
15
|
+
const body = lines.slice(1).join(" ");
|
|
16
|
+
return (
|
|
17
|
+
<div key={i}>
|
|
18
|
+
<div className="text-xs font-semibold tracking-widest uppercase text-muted-foreground">
|
|
19
|
+
{heading}
|
|
20
|
+
</div>
|
|
21
|
+
{body && (
|
|
22
|
+
<p className="mt-1.5 text-[13px] leading-relaxed text-pretty sm:text-sm">{body}</p>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
})}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { MessageCircle } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export interface EmptyChatViewProps {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EmptyChatView({
|
|
12
|
+
title = "Elegí una conversación",
|
|
13
|
+
description,
|
|
14
|
+
icon,
|
|
15
|
+
}: EmptyChatViewProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex h-full flex-col items-center justify-center gap-3 p-8 text-center text-muted-foreground">
|
|
18
|
+
<div className="grid size-14 place-items-center rounded-full bg-secondary">
|
|
19
|
+
{icon ?? <MessageCircle className="size-6" aria-hidden="true" />}
|
|
20
|
+
</div>
|
|
21
|
+
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
22
|
+
{description != null && <p className="max-w-xs text-xs">{description}</p>}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { UniqueIdentifier } from "@dnd-kit/core";
|
|
4
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
5
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
6
|
+
import { Card } from "@lindaui/ui/compound";
|
|
7
|
+
import { cn } from "@lindaui/ui/cn";
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
export interface EntityCardProps<Entity extends object> {
|
|
11
|
+
id: UniqueIdentifier;
|
|
12
|
+
entity: Entity;
|
|
13
|
+
isDragging?: boolean;
|
|
14
|
+
onActivate?: (entity: Entity) => void;
|
|
15
|
+
cardClassName?: string;
|
|
16
|
+
badges?: ReactNode;
|
|
17
|
+
time?: ReactNode;
|
|
18
|
+
avatar?: ReactNode;
|
|
19
|
+
title: ReactNode;
|
|
20
|
+
subtitle?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Sortable presentation only: metadata, urgency and navigation belong to the caller. */
|
|
24
|
+
export function EntityCard<Entity extends object>({
|
|
25
|
+
id,
|
|
26
|
+
entity,
|
|
27
|
+
isDragging = false,
|
|
28
|
+
onActivate,
|
|
29
|
+
cardClassName,
|
|
30
|
+
badges,
|
|
31
|
+
time,
|
|
32
|
+
avatar,
|
|
33
|
+
title,
|
|
34
|
+
subtitle,
|
|
35
|
+
}: EntityCardProps<Entity>) {
|
|
36
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging: isSortableDragging } =
|
|
37
|
+
useSortable({ data: entity, id });
|
|
38
|
+
const isCurrentlyDragging = isDragging || isSortableDragging;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={setNodeRef}
|
|
43
|
+
style={{ transform: CSS.Transform.toString(transform), transition }}
|
|
44
|
+
{...attributes}
|
|
45
|
+
{...listeners}
|
|
46
|
+
className={cn("touch-none", isCurrentlyDragging && "scale-105 opacity-50")}
|
|
47
|
+
>
|
|
48
|
+
<Card
|
|
49
|
+
className={cn(
|
|
50
|
+
"cursor-grab border-none bg-surface transition-all hover:bg-surface-secondary active:cursor-grabbing",
|
|
51
|
+
"h-full min-w-full shadow-sm hover:shadow-md",
|
|
52
|
+
cardClassName,
|
|
53
|
+
)}
|
|
54
|
+
onClick={() => {
|
|
55
|
+
if (!isCurrentlyDragging) onActivate?.(entity);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<Card.Content className="flex min-h-[96px] min-w-full flex-col justify-between p-3">
|
|
59
|
+
<div className="mb-2 flex items-start justify-between">
|
|
60
|
+
<div className="flex flex-1 flex-wrap items-center gap-1 overflow-hidden">{badges}</div>
|
|
61
|
+
<div className="flex items-center gap-1 text-[10px] text-muted">{time}</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="mb-2 flex items-center gap-3">
|
|
64
|
+
{avatar}
|
|
65
|
+
<div className="min-w-0 flex-1">
|
|
66
|
+
<p className="truncate font-semibold text-sm">{title}</p>
|
|
67
|
+
<p className="truncate text-[10px] text-muted">{subtitle}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</Card.Content>
|
|
71
|
+
</Card>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|