@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.
Files changed (255) hide show
  1. package/README.md +56 -0
  2. package/dist/ai-cost-card-f403ypsg.js +99 -0
  3. package/dist/ai-performance-card-cggm09nc.js +191 -0
  4. package/dist/analytics/ai-cost-card.d.ts +3 -0
  5. package/dist/analytics/ai-cost-card.d.ts.map +1 -0
  6. package/dist/analytics/ai-performance-card.d.ts +3 -0
  7. package/dist/analytics/ai-performance-card.d.ts.map +1 -0
  8. package/dist/analytics/analytics-card.d.ts +3 -0
  9. package/dist/analytics/analytics-card.d.ts.map +1 -0
  10. package/dist/analytics/analytics-skeleton.d.ts +5 -0
  11. package/dist/analytics/analytics-skeleton.d.ts.map +1 -0
  12. package/dist/analytics/entity-health-card.d.ts +3 -0
  13. package/dist/analytics/entity-health-card.d.ts.map +1 -0
  14. package/dist/analytics/status-distribution-chart.d.ts +3 -0
  15. package/dist/analytics/status-distribution-chart.d.ts.map +1 -0
  16. package/dist/analytics/time-range-picker.d.ts +3 -0
  17. package/dist/analytics/time-range-picker.d.ts.map +1 -0
  18. package/dist/analytics/top-entities-table.d.ts +3 -0
  19. package/dist/analytics/top-entities-table.d.ts.map +1 -0
  20. package/dist/analytics/types.d.ts +104 -0
  21. package/dist/analytics/types.d.ts.map +1 -0
  22. package/dist/analytics-card-bcmijpt6.js +38 -0
  23. package/dist/analytics-skeleton-imve37du.js +164 -0
  24. package/dist/analytics.d.ts +11 -0
  25. package/dist/analytics.d.ts.map +1 -0
  26. package/dist/analytics.js +8 -0
  27. package/dist/app-shell/drawer-focus.d.ts +8 -0
  28. package/dist/app-shell/drawer-focus.d.ts.map +1 -0
  29. package/dist/app-shell/footer.d.ts +39 -0
  30. package/dist/app-shell/footer.d.ts.map +1 -0
  31. package/dist/app-shell/logout-dialog.d.ts +6 -0
  32. package/dist/app-shell/logout-dialog.d.ts.map +1 -0
  33. package/dist/app-shell/navigation.d.ts +15 -0
  34. package/dist/app-shell/navigation.d.ts.map +1 -0
  35. package/dist/app-shell/types.d.ts +55 -0
  36. package/dist/app-shell/types.d.ts.map +1 -0
  37. package/dist/app-shell.d.ts +7 -0
  38. package/dist/app-shell.d.ts.map +1 -0
  39. package/dist/app-shell.js +254 -0
  40. package/dist/channel-settings.d.ts +24 -0
  41. package/dist/channel-settings.d.ts.map +1 -0
  42. package/dist/channel-settings.js +60 -0
  43. package/dist/chat-avatar-jdmeyf2o.js +30 -0
  44. package/dist/chat-layout-fwx3x26g.js +25 -0
  45. package/dist/chat.d.ts +22 -0
  46. package/dist/chat.d.ts.map +1 -0
  47. package/dist/chat.js +66 -0
  48. package/dist/command-palette.d.ts +22 -0
  49. package/dist/command-palette.d.ts.map +1 -0
  50. package/dist/command-palette.js +141 -0
  51. package/dist/compat-chat/chat-avatar.d.ts +16 -0
  52. package/dist/compat-chat/chat-avatar.d.ts.map +1 -0
  53. package/dist/compat-chat/chat-header.d.ts +9 -0
  54. package/dist/compat-chat/chat-header.d.ts.map +1 -0
  55. package/dist/compat-chat/chat-layout.d.ts +10 -0
  56. package/dist/compat-chat/chat-layout.d.ts.map +1 -0
  57. package/dist/compat-chat/conversation-list-item.d.ts +10 -0
  58. package/dist/compat-chat/conversation-list-item.d.ts.map +1 -0
  59. package/dist/compat-chat/conversation-list.d.ts +24 -0
  60. package/dist/compat-chat/conversation-list.d.ts.map +1 -0
  61. package/dist/compat-chat/conversation-thread.d.ts +18 -0
  62. package/dist/compat-chat/conversation-thread.d.ts.map +1 -0
  63. package/dist/compat-chat/date-divider.d.ts +5 -0
  64. package/dist/compat-chat/date-divider.d.ts.map +1 -0
  65. package/dist/compat-chat/empty-chat-view.d.ts +6 -0
  66. package/dist/compat-chat/empty-chat-view.d.ts.map +1 -0
  67. package/dist/compat-chat/image-lightbox.d.ts +7 -0
  68. package/dist/compat-chat/image-lightbox.d.ts.map +1 -0
  69. package/dist/compat-chat/message-bubble.d.ts +6 -0
  70. package/dist/compat-chat/message-bubble.d.ts.map +1 -0
  71. package/dist/compat-chat/message-composer.d.ts +11 -0
  72. package/dist/compat-chat/message-composer.d.ts.map +1 -0
  73. package/dist/compat-chat/time-divider.d.ts +5 -0
  74. package/dist/compat-chat/time-divider.d.ts.map +1 -0
  75. package/dist/compat-chat/time-grouping.d.ts +5 -0
  76. package/dist/compat-chat/time-grouping.d.ts.map +1 -0
  77. package/dist/compat-chat/types.d.ts +35 -0
  78. package/dist/compat-chat/types.d.ts.map +1 -0
  79. package/dist/compat-chat/whatsapp-audio-player.d.ts +7 -0
  80. package/dist/compat-chat/whatsapp-audio-player.d.ts.map +1 -0
  81. package/dist/compat.d.ts +9 -0
  82. package/dist/compat.d.ts.map +1 -0
  83. package/dist/compat.js +4 -0
  84. package/dist/connected-panel-lhwy25yk.js +132 -0
  85. package/dist/connection-panel/connected-panel.d.ts +26 -0
  86. package/dist/connection-panel/connected-panel.d.ts.map +1 -0
  87. package/dist/connection-panel/disconnected-panel.d.ts +12 -0
  88. package/dist/connection-panel/disconnected-panel.d.ts.map +1 -0
  89. package/dist/connection-panel/instructions.d.ts +5 -0
  90. package/dist/connection-panel/instructions.d.ts.map +1 -0
  91. package/dist/connection-panel/qr-panel.d.ts +14 -0
  92. package/dist/connection-panel/qr-panel.d.ts.map +1 -0
  93. package/dist/connection-panel/use-qr-refresh.d.ts +18 -0
  94. package/dist/connection-panel/use-qr-refresh.d.ts.map +1 -0
  95. package/dist/connection-panel.d.ts +39 -0
  96. package/dist/connection-panel.d.ts.map +1 -0
  97. package/dist/connection-panel.js +328 -0
  98. package/dist/conversation-list-ekq2dzca.js +133 -0
  99. package/dist/conversation-list-item-fy6t5af2.js +55 -0
  100. package/dist/conversation-thread-c60431oa.js +205 -0
  101. package/dist/entity-card.d.ts +17 -0
  102. package/dist/entity-card.d.ts.map +1 -0
  103. package/dist/entity-card.js +69 -0
  104. package/dist/entity-health-card-ggnmrtrq.js +192 -0
  105. package/dist/entity-switcher.d.ts +33 -0
  106. package/dist/entity-switcher.d.ts.map +1 -0
  107. package/dist/entity-switcher.js +137 -0
  108. package/dist/filter-bar.d.ts +41 -0
  109. package/dist/filter-bar.d.ts.map +1 -0
  110. package/dist/filter-bar.js +155 -0
  111. package/dist/footer-f7r5e42b.js +192 -0
  112. package/dist/image-lightbox-ci3arb6z.js +62 -0
  113. package/dist/kanban/drop-utils.d.ts +18 -0
  114. package/dist/kanban/drop-utils.d.ts.map +1 -0
  115. package/dist/kanban/navigation-buttons.d.ts +9 -0
  116. package/dist/kanban/navigation-buttons.d.ts.map +1 -0
  117. package/dist/kanban/scroll-indicators.d.ts +6 -0
  118. package/dist/kanban/scroll-indicators.d.ts.map +1 -0
  119. package/dist/kanban/use-keyboard-navigation.d.ts +4 -0
  120. package/dist/kanban/use-keyboard-navigation.d.ts.map +1 -0
  121. package/dist/kanban-board.d.ts +31 -0
  122. package/dist/kanban-board.d.ts.map +1 -0
  123. package/dist/kanban-board.js +128 -0
  124. package/dist/kanban-column.d.ts +13 -0
  125. package/dist/kanban-column.d.ts.map +1 -0
  126. package/dist/kanban-column.js +58 -0
  127. package/dist/legacy-kpi-card.d.ts +16 -0
  128. package/dist/legacy-kpi-card.d.ts.map +1 -0
  129. package/dist/legacy-kpi-card.js +4 -0
  130. package/dist/logout-dialog-bbv59tbi.js +93 -0
  131. package/dist/message-bubble-odjvhroq.js +110 -0
  132. package/dist/message-composer-m2n6soo7.js +59 -0
  133. package/dist/navigation-buttons-e77mshh0.js +65 -0
  134. package/dist/navigation-hotcrqoq.js +115 -0
  135. package/dist/save-bar.d.ts +15 -0
  136. package/dist/save-bar.d.ts.map +1 -0
  137. package/dist/save-bar.js +38 -0
  138. package/dist/scroll-indicators-gvxapogd.js +39 -0
  139. package/dist/settings-layout.d.ts +20 -0
  140. package/dist/settings-layout.d.ts.map +1 -0
  141. package/dist/settings-layout.js +56 -0
  142. package/dist/source-selector.d.ts +18 -0
  143. package/dist/source-selector.d.ts.map +1 -0
  144. package/dist/source-selector.js +59 -0
  145. package/dist/status-distribution-chart-oucedzv7.js +87 -0
  146. package/dist/status-filters.d.ts +12 -0
  147. package/dist/status-filters.d.ts.map +1 -0
  148. package/dist/status-filters.js +31 -0
  149. package/dist/stepper.d.ts +15 -0
  150. package/dist/stepper.d.ts.map +1 -0
  151. package/dist/stepper.js +42 -0
  152. package/dist/time-range-picker-mu38hul4.js +123 -0
  153. package/dist/top-entities-table-o7xzpq96.js +78 -0
  154. package/dist/use-keyboard-navigation-he1xmriz.js +52 -0
  155. package/dist/whatsapp-audio-player-nooty6jb.js +277 -0
  156. package/llms.txt +87 -4
  157. package/package.json +191 -46
  158. package/src/analytics/ai-cost-card.tsx +31 -0
  159. package/src/analytics/ai-performance-card.tsx +48 -0
  160. package/src/analytics/analytics-card.tsx +14 -0
  161. package/src/analytics/analytics-skeleton.tsx +15 -0
  162. package/src/analytics/entity-health-card.tsx +50 -0
  163. package/src/analytics/status-distribution-chart.tsx +20 -0
  164. package/src/analytics/time-range-picker.tsx +43 -0
  165. package/src/analytics/top-entities-table.tsx +24 -0
  166. package/src/analytics/types.ts +43 -0
  167. package/src/analytics.ts +10 -0
  168. package/src/app-shell/drawer-focus.ts +42 -0
  169. package/src/app-shell/footer.tsx +160 -0
  170. package/src/app-shell/logout-dialog.tsx +68 -0
  171. package/src/app-shell/navigation.tsx +147 -0
  172. package/src/app-shell/types.ts +57 -0
  173. package/src/app-shell.tsx +166 -0
  174. package/src/area-chart.tsx +85 -0
  175. package/src/audio-player.tsx +74 -0
  176. package/src/auth-layout.tsx +132 -0
  177. package/src/auth-provider.tsx +29 -0
  178. package/src/bar-chart.tsx +78 -0
  179. package/src/channel-settings.tsx +76 -0
  180. package/src/chart-card.tsx +45 -0
  181. package/src/chat-avatar.tsx +57 -0
  182. package/src/chat-date-divider.tsx +18 -0
  183. package/src/chat-header.tsx +24 -0
  184. package/src/chat-layout.tsx +41 -0
  185. package/src/chat-time-divider.tsx +20 -0
  186. package/src/chat-types.ts +32 -0
  187. package/src/chat.ts +21 -0
  188. package/src/command-palette.tsx +110 -0
  189. package/src/compat-chat/chat-avatar.tsx +23 -0
  190. package/src/compat-chat/chat-header.tsx +12 -0
  191. package/src/compat-chat/chat-layout.tsx +14 -0
  192. package/src/compat-chat/conversation-list-item.tsx +27 -0
  193. package/src/compat-chat/conversation-list.tsx +49 -0
  194. package/src/compat-chat/conversation-thread.tsx +92 -0
  195. package/src/compat-chat/date-divider.tsx +7 -0
  196. package/src/compat-chat/empty-chat-view.tsx +9 -0
  197. package/src/compat-chat/image-lightbox.tsx +22 -0
  198. package/src/compat-chat/message-bubble.tsx +28 -0
  199. package/src/compat-chat/message-composer.tsx +26 -0
  200. package/src/compat-chat/time-divider.tsx +5 -0
  201. package/src/compat-chat/time-grouping.ts +19 -0
  202. package/src/compat-chat/types.ts +31 -0
  203. package/src/compat-chat/whatsapp-audio-player.tsx +100 -0
  204. package/src/compat.ts +8 -0
  205. package/src/confirm-dialog.tsx +85 -0
  206. package/src/connection-panel/connected-panel.tsx +106 -0
  207. package/src/connection-panel/disconnected-panel.tsx +48 -0
  208. package/src/connection-panel/instructions.tsx +9 -0
  209. package/src/connection-panel/qr-panel.tsx +62 -0
  210. package/src/connection-panel/use-qr-refresh.ts +52 -0
  211. package/src/connection-panel.tsx +141 -0
  212. package/src/conversation-list-item.tsx +53 -0
  213. package/src/conversation-list.tsx +67 -0
  214. package/src/conversation-thread.tsx +64 -0
  215. package/src/detail-panel.tsx +123 -0
  216. package/src/document-panel.tsx +109 -0
  217. package/src/document-reader.tsx +29 -0
  218. package/src/empty-chat-view.tsx +25 -0
  219. package/src/entity-card.tsx +74 -0
  220. package/src/entity-health-card.tsx +53 -0
  221. package/src/entity-switcher.tsx +126 -0
  222. package/src/fetching-indicator.tsx +16 -0
  223. package/src/field-array-form.tsx +166 -0
  224. package/src/filter-bar.tsx +203 -0
  225. package/src/image-viewer.tsx +129 -0
  226. package/src/kanban/drop-utils.ts +31 -0
  227. package/src/kanban/navigation-buttons.tsx +80 -0
  228. package/src/kanban/scroll-indicators.tsx +47 -0
  229. package/src/kanban/use-keyboard-navigation.ts +43 -0
  230. package/src/kanban-board.tsx +160 -0
  231. package/src/kanban-column.tsx +62 -0
  232. package/src/legacy-kpi-card.tsx +38 -0
  233. package/src/line-chart.tsx +58 -0
  234. package/src/list-item.tsx +63 -0
  235. package/src/list-pane.tsx +143 -0
  236. package/src/login-form.tsx +128 -0
  237. package/src/message-bubble.tsx +70 -0
  238. package/src/message-composer.tsx +52 -0
  239. package/src/pie-chart.tsx +70 -0
  240. package/src/radar-chart.tsx +55 -0
  241. package/src/radial-chart.tsx +103 -0
  242. package/src/recording-overlay.tsx +159 -0
  243. package/src/save-bar.tsx +35 -0
  244. package/src/settings-layout.tsx +79 -0
  245. package/src/source-selector.tsx +71 -0
  246. package/src/split-workspace.tsx +74 -0
  247. package/src/stat-card.tsx +166 -0
  248. package/src/stats-grid.tsx +23 -0
  249. package/src/status-filters.tsx +28 -0
  250. package/src/stepper.tsx +47 -0
  251. package/src/table-pane.tsx +194 -0
  252. package/src/time-range-picker.tsx +89 -0
  253. package/src/workspace-back-button.tsx +32 -0
  254. package/dist/chat-types.test.d.ts +0 -2
  255. 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
+ }