@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,143 @@
1
+ "use client";
2
+ import React, { type ReactNode } from "react";
3
+ import { AlertCircle, FileText, RotateCcw } from "lucide-react";
4
+ import { Button } from "@lindaui/ui/button";
5
+ import { SearchField } from "@lindaui/ui/search-field";
6
+ import { Skeleton } from "@lindaui/ui/skeleton";
7
+
8
+ export interface ListPaneFilter {
9
+ key: string;
10
+ label: string;
11
+ count?: number;
12
+ }
13
+
14
+ export interface ListPaneProps {
15
+ filters?: ListPaneFilter[];
16
+ activeFilter?: string;
17
+ onFilterChange?: (key: string) => void;
18
+ searchValue?: string;
19
+ onSearchChange?: (value: string) => void;
20
+ searchPlaceholder?: string;
21
+ searchLabel?: string;
22
+ loading?: boolean;
23
+ error?: string | null;
24
+ onRetry?: () => void;
25
+ errorTitle?: string;
26
+ retryLabel?: string;
27
+ isEmpty?: boolean;
28
+ emptyTitle?: string;
29
+ emptyDescription?: ReactNode;
30
+ emptyIcon?: ReactNode;
31
+ children?: ReactNode;
32
+ }
33
+
34
+ export function ListPane({
35
+ filters,
36
+ activeFilter,
37
+ onFilterChange,
38
+ searchValue = "",
39
+ onSearchChange,
40
+ searchPlaceholder = "Buscar",
41
+ searchLabel = "Buscar",
42
+ loading = false,
43
+ error = null,
44
+ onRetry,
45
+ errorTitle = "No se pudieron cargar los datos",
46
+ retryLabel = "Reintentar",
47
+ isEmpty,
48
+ emptyTitle = "Sin resultados",
49
+ emptyDescription,
50
+ emptyIcon,
51
+ children,
52
+ }: ListPaneProps) {
53
+ const showEmpty =
54
+ !loading &&
55
+ !error &&
56
+ (isEmpty !== undefined ? isEmpty : React.Children.count(children) === 0);
57
+
58
+ return (
59
+ <div className="flex h-full min-h-0 flex-col bg-background">
60
+ {/* Top controls */}
61
+ <div className="flex-none space-y-3 px-3 pb-3 pt-3 sm:px-4">
62
+ {filters && filters.length > 0 && (
63
+ <div className="flex flex-wrap gap-2 overflow-x-auto">
64
+ {filters.map((f) => (
65
+ <Button
66
+ key={f.key}
67
+ variant={activeFilter === f.key ? "primary" : "ghost"}
68
+ onPress={() => onFilterChange?.(f.key)}
69
+ size="sm"
70
+ >
71
+ {f.label}
72
+ {f.count != null && (
73
+ <span
74
+ aria-hidden
75
+ className="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-muted-foreground/15 px-1 text-[11px] font-semibold tabular-nums text-muted-foreground"
76
+ >
77
+ {f.count}
78
+ </span>
79
+ )}
80
+ </Button>
81
+ ))}
82
+ </div>
83
+ )}
84
+
85
+ <SearchField
86
+ value={searchValue}
87
+ onChange={onSearchChange ?? (() => {})}
88
+ placeholder={searchPlaceholder}
89
+ aria-label={searchLabel}
90
+ />
91
+ </div>
92
+
93
+ {/* Scroll area */}
94
+ <div className="min-h-0 flex-1 overflow-y-auto border-t border-border">
95
+ {loading && (
96
+ <div role="status" aria-label="Cargando">
97
+ {Array.from({ length: 6 }).map((_, i) => (
98
+ <div
99
+ key={i}
100
+ className="flex items-center gap-3 border-b border-border px-3 py-3.5 sm:px-4"
101
+ >
102
+ <Skeleton className="h-[42px] w-[42px] rounded-[11px]" />
103
+ <div className="flex flex-1 flex-col gap-2">
104
+ <Skeleton className="h-3 w-36" />
105
+ <Skeleton className="h-2.5 w-28" />
106
+ <Skeleton className="h-2.5 w-20" />
107
+ </div>
108
+ </div>
109
+ ))}
110
+ </div>
111
+ )}
112
+
113
+ {!loading && error && (
114
+ <div className="m-3 rounded-xl border border-destructive/35 bg-destructive/5 p-6 text-center">
115
+ <AlertCircle className="mx-auto h-6 w-6 text-destructive" />
116
+ <p className="mt-2 text-sm font-semibold">{errorTitle}</p>
117
+ <p className="mt-1 text-xs text-muted-foreground">{error}</p>
118
+ {onRetry && (
119
+ <Button variant="secondary" size="sm" onPress={onRetry} className="mt-3">
120
+ <RotateCcw className="h-3.5 w-3.5" />
121
+ {retryLabel}
122
+ </Button>
123
+ )}
124
+ </div>
125
+ )}
126
+
127
+ {showEmpty && (
128
+ <div className="py-16 text-center">
129
+ <div className="mx-auto grid h-11 w-11 place-items-center rounded-xl bg-secondary">
130
+ {emptyIcon ?? <FileText className="h-5 w-5 text-muted-foreground" />}
131
+ </div>
132
+ <p className="mt-3 text-sm font-semibold">{emptyTitle}</p>
133
+ {emptyDescription && (
134
+ <div className="mt-1 text-xs text-muted-foreground">{emptyDescription}</div>
135
+ )}
136
+ </div>
137
+ )}
138
+
139
+ {!loading && !error && !showEmpty && children}
140
+ </div>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { useForm } from "react-hook-form";
3
+ import {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardContent,
8
+ CardFooter,
9
+ } from "@lindaui/ui/card";
10
+ import { Input } from "@lindaui/ui/input";
11
+ import { Button } from "@lindaui/ui/button";
12
+ import { Field } from "@lindaui/ui/field";
13
+ import { Text } from "@lindaui/ui/text";
14
+
15
+ // Block: login compuesto de primitivos @lindaui/ui (field + input + button). Maneja
16
+ // el form con react-hook-form internamente; el consumidor solo recibe los
17
+ // valores en onSubmit. API propia, no filtra tipos de HeroUI.
18
+ //
19
+ // `bare`: sin Card ni título — campos apilados + botón full-width. Para usar
20
+ // dentro de un shell que ya aporta el heading/contenedor (ej. <AuthLayout/>).
21
+ export interface LoginValues {
22
+ email: string;
23
+ password: string;
24
+ }
25
+
26
+ export interface LoginFormProps {
27
+ onSubmit: (values: LoginValues) => void | Promise<void>;
28
+ title?: string;
29
+ emailLabel?: string;
30
+ passwordLabel?: string;
31
+ submitLabel?: string;
32
+ loading?: boolean;
33
+ error?: string;
34
+ className?: string;
35
+ /** Sin Card ni título (para componer dentro de AuthLayout u otro shell). */
36
+ bare?: boolean;
37
+ }
38
+
39
+ export function LoginForm({
40
+ onSubmit,
41
+ title = "Iniciar sesión",
42
+ emailLabel = "Email",
43
+ passwordLabel = "Contraseña",
44
+ submitLabel = "Ingresar",
45
+ loading = false,
46
+ error,
47
+ className,
48
+ bare = false,
49
+ }: LoginFormProps) {
50
+ const { control, handleSubmit } = useForm<LoginValues>({
51
+ defaultValues: { email: "", password: "" },
52
+ });
53
+
54
+ const fields = (
55
+ <>
56
+ <Field control={control} name="email">
57
+ {({ field, invalid }) => (
58
+ <Input
59
+ label={emailLabel}
60
+ type="email"
61
+ autoComplete="username"
62
+ value={field.value}
63
+ onChange={(value) => field.onChange(value)}
64
+ onBlur={field.onBlur}
65
+ name={field.name}
66
+ isInvalid={invalid}
67
+ />
68
+ )}
69
+ </Field>
70
+ <Field control={control} name="password">
71
+ {({ field, invalid }) => (
72
+ <Input
73
+ label={passwordLabel}
74
+ type="password"
75
+ autoComplete="current-password"
76
+ value={field.value}
77
+ onChange={(value) => field.onChange(value)}
78
+ onBlur={field.onBlur}
79
+ name={field.name}
80
+ isInvalid={invalid}
81
+ />
82
+ )}
83
+ </Field>
84
+ {error && (
85
+ <Text role="alert" className="text-sm text-[var(--danger,#dc2626)]">
86
+ {error}
87
+ </Text>
88
+ )}
89
+ </>
90
+ );
91
+
92
+ if (bare) {
93
+ return (
94
+ <form
95
+ onSubmit={handleSubmit((values) => onSubmit(values))}
96
+ noValidate
97
+ className={`space-y-5 ${className ?? ""}`.trim()}
98
+ >
99
+ <div className="flex flex-col gap-5">{fields}</div>
100
+ <Button
101
+ type="submit"
102
+ variant="primary"
103
+ size="lg"
104
+ isDisabled={loading}
105
+ className="w-full"
106
+ >
107
+ {submitLabel}
108
+ </Button>
109
+ </form>
110
+ );
111
+ }
112
+
113
+ return (
114
+ <Card className={`w-full max-w-sm ${className ?? ""}`.trim()}>
115
+ <CardHeader className="px-4 py-4 sm:px-6 sm:py-5">
116
+ <CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
117
+ </CardHeader>
118
+ <form onSubmit={handleSubmit((values) => onSubmit(values))} noValidate>
119
+ <CardContent className="flex flex-col gap-4 px-4 sm:px-6">{fields}</CardContent>
120
+ <CardFooter className="px-4 pb-4 sm:px-6 sm:pb-5">
121
+ <Button type="submit" variant="primary" isDisabled={loading} className="w-full">
122
+ {submitLabel}
123
+ </Button>
124
+ </CardFooter>
125
+ </form>
126
+ </Card>
127
+ );
128
+ }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { formatShortTime } from "@lindaui/ui/date";
3
+ import { Check, CheckCheck, Clock, AlertCircle } from "lucide-react";
4
+ import { AudioPlayer } from "./audio-player";
5
+ import type { GenericMessage, MessageStatus } from "./chat-types.js";
6
+
7
+ export interface MessageBubbleProps {
8
+ message: GenericMessage;
9
+ onImageClick?: (url: string) => void;
10
+ }
11
+
12
+ const statusIcon: Record<MessageStatus, { icon: typeof Check; label: string }> = {
13
+ sending: { icon: Clock, label: "Enviando" },
14
+ sent: { icon: Check, label: "Enviado" },
15
+ delivered: { icon: CheckCheck, label: "Entregado" },
16
+ read: { icon: CheckCheck, label: "Leído" },
17
+ failed: { icon: AlertCircle, label: "Error al enviar" },
18
+ };
19
+
20
+ export function MessageBubble({ message, onImageClick }: MessageBubbleProps) {
21
+ const isOutgoing = message.side === "outgoing";
22
+ const status = message.status ? statusIcon[message.status] : null;
23
+ const StatusIcon = status?.icon;
24
+
25
+ return (
26
+ <div className={`flex ${isOutgoing ? "justify-end" : "justify-start"}`}>
27
+ <div
28
+ className={`max-w-[75%] rounded-2xl px-3 py-2 text-sm ${
29
+ isOutgoing ? "bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground"
30
+ }`}
31
+ >
32
+ {message.media?.kind === "image" && (
33
+ <button
34
+ type="button"
35
+ onClick={() => onImageClick?.(message.media!.url)}
36
+ className="-mx-1 -mt-1 mb-1 block overflow-hidden rounded-xl"
37
+ >
38
+ <img
39
+ src={message.media.url}
40
+ alt={message.media.alt ?? ""}
41
+ className="max-h-64 w-full object-cover"
42
+ />
43
+ </button>
44
+ )}
45
+ {message.media?.kind === "audio" && (
46
+ <div className="mb-1">
47
+ <AudioPlayer src={message.media.url} durationSeconds={message.media.durationSeconds} />
48
+ </div>
49
+ )}
50
+ {message.media?.kind === "pdf" && (
51
+ <a
52
+ href={message.media.url}
53
+ target="_blank"
54
+ rel="noreferrer"
55
+ className="mb-1 block underline"
56
+ >
57
+ {message.media.name ?? "Documento"}
58
+ </a>
59
+ )}
60
+ {message.text != null && (
61
+ <p className="whitespace-pre-wrap break-words">{message.text}</p>
62
+ )}
63
+ <div className="mt-1 flex items-center justify-end gap-1 text-[10px] opacity-70">
64
+ <span>{formatShortTime(message.sentAt)}</span>
65
+ {isOutgoing && StatusIcon && <StatusIcon className="size-3" aria-label={status!.label} />}
66
+ </div>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import type { KeyboardEvent } from "react";
3
+ import { Send } from "lucide-react";
4
+ import { Textarea } from "@lindaui/ui/textarea";
5
+ import { IconButton } from "@lindaui/ui/icon-button";
6
+
7
+ export interface MessageComposerProps {
8
+ value: string;
9
+ onValueChange: (value: string) => void;
10
+ onSend: () => void;
11
+ placeholder?: string;
12
+ isSending?: boolean;
13
+ isDisabled?: boolean;
14
+ sendLabel?: string;
15
+ }
16
+
17
+ export function MessageComposer({
18
+ value,
19
+ onValueChange,
20
+ onSend,
21
+ placeholder = "Escribí un mensaje…",
22
+ isSending = false,
23
+ isDisabled = false,
24
+ sendLabel = "Enviar",
25
+ }: MessageComposerProps) {
26
+ const canSend = value.trim().length > 0 && !isSending && !isDisabled;
27
+
28
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
29
+ if (e.key === "Enter" && !e.shiftKey) {
30
+ e.preventDefault();
31
+ if (canSend) onSend();
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="flex items-end gap-2 border-t border-border bg-card p-3">
37
+ <div className="flex-1">
38
+ <Textarea
39
+ aria-label={placeholder}
40
+ placeholder={placeholder}
41
+ value={value}
42
+ onChange={onValueChange}
43
+ onKeyDown={handleKeyDown}
44
+ isDisabled={isDisabled}
45
+ />
46
+ </div>
47
+ <IconButton aria-label={sendLabel} onPress={onSend} isDisabled={!canSend}>
48
+ <Send className="size-4" />
49
+ </IconButton>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import {
4
+ PieChart as RPieChart,
5
+ Pie,
6
+ Cell,
7
+ LabelList,
8
+ } from "recharts";
9
+ import {
10
+ ChartContainer,
11
+ ChartTooltip,
12
+ ChartTooltipContent,
13
+ ChartLegend,
14
+ ChartLegendContent,
15
+ type ChartConfig,
16
+ } from "@lindaui/ui/chart";
17
+
18
+ export interface PieChartProps {
19
+ data: Array<Record<string, string | number>>;
20
+ config: ChartConfig;
21
+ categoryKey: string;
22
+ valueKey: string;
23
+ variant?: "pie" | "donut" | "label";
24
+ className?: string;
25
+ }
26
+
27
+ export function PieChart({
28
+ data,
29
+ config,
30
+ categoryKey,
31
+ valueKey,
32
+ variant = "pie",
33
+ className,
34
+ }: PieChartProps) {
35
+ const innerRadius = variant === "donut" ? "55%" : variant === "label" ? "45%" : 0;
36
+
37
+ return (
38
+ <ChartContainer config={config} className={className}>
39
+ <RPieChart>
40
+ <ChartTooltip content={<ChartTooltipContent />} />
41
+ <ChartLegend content={<ChartLegendContent />} />
42
+ <Pie
43
+ data={data}
44
+ dataKey={valueKey}
45
+ nameKey={categoryKey}
46
+ innerRadius={innerRadius}
47
+ outerRadius="80%"
48
+ paddingAngle={2}
49
+ >
50
+ {data.map((entry, index) => {
51
+ const key = String(entry[categoryKey]);
52
+ return (
53
+ <Cell
54
+ key={`cell-${index}`}
55
+ fill={`var(--color-${key})`}
56
+ />
57
+ );
58
+ })}
59
+ {variant === "label" && (
60
+ <LabelList
61
+ dataKey={categoryKey}
62
+ position="outside"
63
+ style={{ fontSize: 12, fill: "var(--foreground)" }}
64
+ />
65
+ )}
66
+ </Pie>
67
+ </RPieChart>
68
+ </ChartContainer>
69
+ );
70
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import {
4
+ RadarChart as RRadarChart,
5
+ Radar,
6
+ PolarGrid,
7
+ PolarAngleAxis,
8
+ PolarRadiusAxis,
9
+ } from "recharts";
10
+ import {
11
+ ChartContainer,
12
+ ChartTooltip,
13
+ ChartTooltipContent,
14
+ ChartLegend,
15
+ ChartLegendContent,
16
+ type ChartConfig,
17
+ } from "@lindaui/ui/chart";
18
+
19
+ export interface RadarChartProps {
20
+ data: Array<Record<string, string | number>>;
21
+ config: ChartConfig;
22
+ categoryKey: string;
23
+ series: string[];
24
+ className?: string;
25
+ }
26
+
27
+ export function RadarChart({
28
+ data,
29
+ config,
30
+ categoryKey,
31
+ series,
32
+ className,
33
+ }: RadarChartProps) {
34
+ return (
35
+ <ChartContainer config={config} className={className}>
36
+ <RRadarChart data={data}>
37
+ <ChartTooltip content={<ChartTooltipContent />} />
38
+ <ChartLegend content={<ChartLegendContent />} />
39
+ <PolarGrid />
40
+ <PolarAngleAxis dataKey={categoryKey} />
41
+ <PolarRadiusAxis />
42
+ {series.map((key) => (
43
+ <Radar
44
+ key={key}
45
+ dataKey={key}
46
+ name={key}
47
+ fill={`var(--color-${key})`}
48
+ stroke={`var(--color-${key})`}
49
+ fillOpacity={0.5}
50
+ />
51
+ ))}
52
+ </RRadarChart>
53
+ </ChartContainer>
54
+ );
55
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import {
4
+ RadialBarChart as RRadialBarChart,
5
+ RadialBar,
6
+ PolarGrid,
7
+ PolarRadiusAxis,
8
+ Label,
9
+ } from "recharts";
10
+ import {
11
+ ChartContainer,
12
+ ChartTooltip,
13
+ ChartTooltipContent,
14
+ type ChartConfig,
15
+ } from "@lindaui/ui/chart";
16
+
17
+ export interface RadialChartProps {
18
+ data: Array<Record<string, string | number>>;
19
+ config: ChartConfig;
20
+ categoryKey: string;
21
+ series: string[];
22
+ variant?: "default" | "stacked" | "label";
23
+ className?: string;
24
+ }
25
+
26
+ export function RadialChart({
27
+ data,
28
+ config,
29
+ categoryKey,
30
+ series,
31
+ variant = "default",
32
+ className,
33
+ }: RadialChartProps) {
34
+ const isStacked = variant === "stacked";
35
+ const showLabel = variant === "label";
36
+
37
+ const total = React.useMemo(() => {
38
+ if (!showLabel) return null;
39
+ return data.reduce((acc, row) => {
40
+ series.forEach((key) => {
41
+ const v = row[key];
42
+ if (typeof v === "number") acc += v;
43
+ });
44
+ return acc;
45
+ }, 0);
46
+ }, [data, series, showLabel]);
47
+
48
+ return (
49
+ <ChartContainer config={config} className={className}>
50
+ <RRadialBarChart
51
+ data={data}
52
+ innerRadius="30%"
53
+ outerRadius="90%"
54
+ startAngle={90}
55
+ endAngle={-270}
56
+ >
57
+ <PolarGrid gridType="circle" radialLines={false} />
58
+ <ChartTooltip content={<ChartTooltipContent />} />
59
+ {showLabel && (
60
+ <PolarRadiusAxis tick={false} axisLine={false}>
61
+ <Label
62
+ content={({ viewBox }) => {
63
+ if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) return null;
64
+ const { cx, cy } = viewBox as { cx: number; cy: number };
65
+ return (
66
+ <text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle">
67
+ <tspan
68
+ x={cx}
69
+ y={cy}
70
+ className="fill-foreground text-2xl font-bold"
71
+ style={{ fontSize: 24, fontWeight: 700 }}
72
+ >
73
+ {total}
74
+ </tspan>
75
+ <tspan
76
+ x={cx}
77
+ y={(cy ?? 0) + 20}
78
+ className="fill-muted-foreground"
79
+ style={{ fontSize: 12 }}
80
+ >
81
+ Total
82
+ </tspan>
83
+ </text>
84
+ );
85
+ }}
86
+ />
87
+ </PolarRadiusAxis>
88
+ )}
89
+ {series.map((key) => (
90
+ <RadialBar
91
+ key={key}
92
+ dataKey={key}
93
+ name={key}
94
+ fill={`var(--color-${key})`}
95
+ stackId={isStacked ? "stack" : undefined}
96
+ background={!isStacked}
97
+ cornerRadius={4}
98
+ />
99
+ ))}
100
+ </RRadialBarChart>
101
+ </ChartContainer>
102
+ );
103
+ }