@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,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
|
+
}
|