@cortexasystem/ui 0.1.0 → 1.0.1

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 (59) hide show
  1. package/dist/index.cjs +1326 -643
  2. package/dist/index.d.cts +171 -13
  3. package/dist/index.d.ts +171 -13
  4. package/dist/index.js +1307 -646
  5. package/package.json +3 -3
  6. package/src/assets/isotipo-cortexa-dark.png +0 -0
  7. package/src/assets/isotipo-cortexa-light.png +0 -0
  8. package/src/components/ai/ai-chat.tsx +597 -0
  9. package/src/components/branding/brand-logo.tsx +77 -0
  10. package/src/components/data-display/icons.tsx +81 -0
  11. package/src/components/data-display/profile-avatar.tsx +154 -0
  12. package/src/components/data-display/typography.tsx +46 -0
  13. package/src/components/feedback/empty-state.tsx +63 -0
  14. package/src/components/feedback/loading-state.tsx +93 -0
  15. package/src/components/feedback/module-skeleton.tsx +76 -0
  16. package/src/components/feedback/notification.tsx +111 -0
  17. package/src/components/feedback/skeleton.tsx +9 -0
  18. package/src/components/feedback/spinner.tsx +18 -0
  19. package/src/components/feedback/status-badge.tsx +44 -0
  20. package/src/components/feedback/sync-status-badge.tsx +54 -0
  21. package/src/components/feedback/sync-status-bar.tsx +92 -0
  22. package/src/components/feedback/toaster.tsx +36 -0
  23. package/src/components/forms/searchable-select.tsx +206 -0
  24. package/src/components/forms/select.tsx +142 -0
  25. package/src/components/layout/app-shell.tsx +44 -0
  26. package/src/components/layout/form-section.tsx +21 -0
  27. package/src/components/layout/page-header.tsx +21 -0
  28. package/src/components/layout/theme-toggle.tsx +33 -0
  29. package/src/components/navigation/breadcrumb.tsx +87 -0
  30. package/src/components/navigation/header-user-menu.tsx +108 -0
  31. package/src/components/navigation/navbar.tsx +30 -0
  32. package/src/components/navigation/page-breadcrumb.tsx +44 -0
  33. package/src/components/navigation/sidebar.tsx +104 -0
  34. package/src/components/navigation/steps.tsx +82 -0
  35. package/src/components/overlays/dialog.tsx +94 -0
  36. package/src/components/overlays/drawer.tsx +85 -0
  37. package/src/components/overlays/dropdown-menu.tsx +179 -0
  38. package/src/components/overlays/sheet.tsx +110 -0
  39. package/src/components/primitives/alert.tsx +43 -0
  40. package/src/components/primitives/avatar.tsx +41 -0
  41. package/src/components/primitives/badge.tsx +26 -0
  42. package/src/components/primitives/button.tsx +49 -0
  43. package/src/components/primitives/card.tsx +97 -0
  44. package/src/components/primitives/checkbox.tsx +52 -0
  45. package/src/components/primitives/input.tsx +23 -0
  46. package/src/components/primitives/label.tsx +18 -0
  47. package/src/components/primitives/radio-group.tsx +57 -0
  48. package/src/components/primitives/separator.tsx +23 -0
  49. package/src/components/primitives/switch.tsx +75 -0
  50. package/src/components/primitives/textarea.tsx +18 -0
  51. package/src/components/tables/data-table.tsx +214 -0
  52. package/src/components/tables/data-table.types.ts +9 -0
  53. package/src/components/tables/table-row-actions.tsx +61 -0
  54. package/src/components/tables/table.tsx +88 -0
  55. package/src/declarations.d.ts +14 -0
  56. package/src/index.ts +50 -0
  57. package/src/lib/cn.ts +6 -0
  58. package/src/providers/theme-provider.tsx +90 -0
  59. package/src/styles.css +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexasystem/ui",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "src/styles.css"
18
+ "src"
19
19
  ],
20
20
  "publishConfig": {
21
21
  "access": "public"
@@ -42,7 +42,7 @@
42
42
  "sonner": "^2.0.7",
43
43
  "tailwind-merge": "^3.5.0",
44
44
  "vaul": "^1.1.2",
45
- "@cortexasystem/tokens": "0.1.0"
45
+ "@cortexasystem/tokens": "0.1.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^19.2.14",
@@ -0,0 +1,597 @@
1
+ import * as React from "react";
2
+ import { Bot, Clock3, MessageSquareText, PanelRightOpen, Send, Sparkles, type LucideIcon } from "lucide-react";
3
+
4
+ import { cn } from "../../lib/cn";
5
+ import { Sheet, SheetContent, SheetTrigger } from "../overlays/sheet";
6
+ import { Badge } from "../primitives/badge";
7
+ import { Button, type ButtonProps } from "../primitives/button";
8
+ import { Textarea } from "../primitives/textarea";
9
+
10
+ type AiChatLayoutVariant = "screen" | "sidebar";
11
+ type AiChatRole = "assistant" | "user" | "system";
12
+ type AiChatMessageStatus = "complete" | "streaming" | "error";
13
+
14
+ interface AiChatLayoutProps extends React.HTMLAttributes<HTMLElement> {
15
+ variant?: AiChatLayoutVariant;
16
+ }
17
+
18
+ function AiChatLayout({ className, variant = "screen", ...props }: AiChatLayoutProps) {
19
+ return (
20
+ <section
21
+ className={cn(
22
+ "overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-sm",
23
+ variant === "screen"
24
+ ? "grid min-h-[720px] w-full lg:grid-cols-[320px_minmax(0,1fr)]"
25
+ : "flex h-full min-h-0 w-full flex-col",
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ const AiChatSidebar = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
34
+ ({ className, ...props }, ref) => (
35
+ <aside
36
+ ref={ref}
37
+ className={cn(
38
+ "flex min-h-0 flex-col border-b bg-muted/20 lg:border-b-0 lg:border-r",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ )
44
+ );
45
+
46
+ AiChatSidebar.displayName = "AiChatSidebar";
47
+
48
+ interface AiChatSidebarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
49
+ title: string;
50
+ description?: string;
51
+ action?: React.ReactNode;
52
+ }
53
+
54
+ function AiChatSidebarHeader({ className, title, description, action, ...props }: AiChatSidebarHeaderProps) {
55
+ return (
56
+ <div className={cn("border-b px-4 py-4", className)} {...props}>
57
+ <div className="flex items-start justify-between gap-3">
58
+ <div className="grid gap-1">
59
+ <p className="text-sm font-semibold text-foreground">{title}</p>
60
+ {description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
61
+ </div>
62
+ {action ? <div className="shrink-0">{action}</div> : null}
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ const AiChatThreadList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
69
+ ({ className, ...props }, ref) => (
70
+ <div ref={ref} className={cn("flex-1 space-y-2 overflow-y-auto p-3", className)} {...props} />
71
+ )
72
+ );
73
+
74
+ AiChatThreadList.displayName = "AiChatThreadList";
75
+
76
+ interface AiChatThreadButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
77
+ title: string;
78
+ preview?: string;
79
+ timestamp?: string;
80
+ active?: boolean;
81
+ unreadCount?: number;
82
+ icon?: LucideIcon;
83
+ }
84
+
85
+ const AiChatThreadButton = React.forwardRef<HTMLButtonElement, AiChatThreadButtonProps>(
86
+ (
87
+ {
88
+ className,
89
+ title,
90
+ preview,
91
+ timestamp,
92
+ active = false,
93
+ unreadCount,
94
+ icon: Icon = MessageSquareText,
95
+ type = "button",
96
+ ...props
97
+ },
98
+ ref
99
+ ) => (
100
+ <button
101
+ ref={ref}
102
+ type={type}
103
+ className={cn(
104
+ "flex w-full items-start gap-3 rounded-xl border px-3 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
105
+ active
106
+ ? "border-[var(--color-accent-blue)]/40 bg-[var(--color-accent-blue)]/8"
107
+ : "border-transparent bg-transparent hover:border-border hover:bg-accent/40",
108
+ className
109
+ )}
110
+ {...props}
111
+ >
112
+ <span
113
+ className={cn(
114
+ "mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border",
115
+ active
116
+ ? "border-[var(--color-accent-blue)]/30 bg-[var(--color-accent-blue)]/12 text-[var(--color-accent-blue)]"
117
+ : "border-border bg-background text-muted-foreground"
118
+ )}
119
+ >
120
+ <Icon className="h-4 w-4" />
121
+ </span>
122
+
123
+ <span className="grid min-w-0 flex-1 gap-1">
124
+ <span className="flex items-start justify-between gap-3">
125
+ <span className="truncate text-sm font-medium text-foreground">{title}</span>
126
+ {timestamp ? (
127
+ <span className="inline-flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground">
128
+ <Clock3 className="h-3 w-3" />
129
+ {timestamp}
130
+ </span>
131
+ ) : null}
132
+ </span>
133
+ {preview ? <span className="line-clamp-2 text-xs text-muted-foreground">{preview}</span> : null}
134
+ </span>
135
+
136
+ {typeof unreadCount === "number" && unreadCount > 0 ? (
137
+ <Badge className="shrink-0 rounded-full px-2 py-0.5 text-[11px]">{unreadCount > 9 ? "9+" : unreadCount}</Badge>
138
+ ) : null}
139
+ </button>
140
+ )
141
+ );
142
+
143
+ AiChatThreadButton.displayName = "AiChatThreadButton";
144
+
145
+ const AiChatPanel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
146
+ ({ className, ...props }, ref) => (
147
+ <div ref={ref} className={cn("flex min-h-0 flex-1 flex-col bg-background/40", className)} {...props} />
148
+ )
149
+ );
150
+
151
+ AiChatPanel.displayName = "AiChatPanel";
152
+
153
+ interface AiChatHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
154
+ title: string;
155
+ description?: string;
156
+ meta?: React.ReactNode;
157
+ actions?: React.ReactNode;
158
+ }
159
+
160
+ function AiChatHeader({ className, title, description, meta, actions, ...props }: AiChatHeaderProps) {
161
+ return (
162
+ <header className={cn("border-b bg-card/95 px-5 py-4 backdrop-blur", className)} {...props}>
163
+ <div className="flex flex-wrap items-start justify-between gap-4">
164
+ <div className="grid gap-1">
165
+ <div className="flex flex-wrap items-center gap-2">
166
+ <p className="text-base font-semibold text-foreground">{title}</p>
167
+ {meta ? <div className="flex items-center gap-2 text-xs text-muted-foreground">{meta}</div> : null}
168
+ </div>
169
+ {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
170
+ </div>
171
+ {actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
172
+ </div>
173
+ </header>
174
+ );
175
+ }
176
+
177
+ const AiChatMessageList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
178
+ ({ className, ...props }, ref) => (
179
+ <div
180
+ ref={ref}
181
+ role="log"
182
+ aria-live="polite"
183
+ className={cn("flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-5", className)}
184
+ {...props}
185
+ />
186
+ )
187
+ );
188
+
189
+ AiChatMessageList.displayName = "AiChatMessageList";
190
+
191
+ interface AiChatMessageSource {
192
+ label: string;
193
+ href?: string;
194
+ }
195
+
196
+ interface AiChatMessageProps extends React.HTMLAttributes<HTMLDivElement> {
197
+ role?: AiChatRole;
198
+ author?: string;
199
+ timestamp?: string;
200
+ status?: AiChatMessageStatus;
201
+ avatar?: React.ReactNode;
202
+ actions?: React.ReactNode;
203
+ sources?: AiChatMessageSource[];
204
+ }
205
+
206
+ function getDefaultAvatar(role: AiChatRole) {
207
+ if (role === "assistant") {
208
+ return (
209
+ <span className="flex h-9 w-9 items-center justify-center rounded-full bg-[var(--color-brand)] text-white shadow-sm">
210
+ <Bot className="h-4 w-4" />
211
+ </span>
212
+ );
213
+ }
214
+
215
+ if (role === "system") {
216
+ return (
217
+ <span className="flex h-9 w-9 items-center justify-center rounded-full bg-secondary text-secondary-foreground shadow-sm">
218
+ <Sparkles className="h-4 w-4" />
219
+ </span>
220
+ );
221
+ }
222
+
223
+ return (
224
+ <span className="flex h-9 w-9 items-center justify-center rounded-full bg-foreground text-background shadow-sm">
225
+ <span className="text-xs font-semibold">TU</span>
226
+ </span>
227
+ );
228
+ }
229
+
230
+ function AiChatMessage({
231
+ className,
232
+ role = "assistant",
233
+ author,
234
+ timestamp,
235
+ status = "complete",
236
+ avatar,
237
+ actions,
238
+ sources,
239
+ children,
240
+ ...props
241
+ }: AiChatMessageProps) {
242
+ const isUser = role === "user";
243
+
244
+ return (
245
+ <article
246
+ className={cn("flex gap-3", isUser ? "justify-end" : "justify-start", className)}
247
+ {...props}
248
+ >
249
+ {!isUser ? <div className="shrink-0">{avatar ?? getDefaultAvatar(role)}</div> : null}
250
+
251
+ <div className={cn("grid max-w-[90%] gap-2 md:max-w-[74%]", isUser && "justify-items-end")}>
252
+ <div className={cn("flex items-center gap-2", isUser && "justify-end")}>
253
+ <span className="text-xs font-medium text-foreground">{author ?? (isUser ? "Tu" : "Cortexa AI")}</span>
254
+ {timestamp ? <span className="text-xs text-muted-foreground">{timestamp}</span> : null}
255
+ {status !== "complete" ? (
256
+ <Badge variant={status === "error" ? "destructive" : "secondary"}>
257
+ {status === "streaming" ? "Generando" : "Error"}
258
+ </Badge>
259
+ ) : null}
260
+ </div>
261
+
262
+ <div
263
+ className={cn(
264
+ "rounded-2xl border px-4 py-3 text-sm leading-6 shadow-sm",
265
+ isUser
266
+ ? "border-primary bg-primary text-primary-foreground"
267
+ : role === "system"
268
+ ? "border-secondary bg-secondary text-secondary-foreground"
269
+ : "bg-card text-foreground"
270
+ )}
271
+ >
272
+ <div className="whitespace-pre-wrap">{children}</div>
273
+ </div>
274
+
275
+ {sources?.length ? (
276
+ <div className={cn("flex flex-wrap gap-2", isUser && "justify-end")}>
277
+ {sources.map((source) =>
278
+ source.href ? (
279
+ <a
280
+ key={`${source.label}-${source.href}`}
281
+ href={source.href}
282
+ className="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
283
+ >
284
+ {source.label}
285
+ <PanelRightOpen className="h-3 w-3" />
286
+ </a>
287
+ ) : (
288
+ <span
289
+ key={source.label}
290
+ className="inline-flex items-center rounded-full border px-2.5 py-1 text-xs text-muted-foreground"
291
+ >
292
+ {source.label}
293
+ </span>
294
+ )
295
+ )}
296
+ </div>
297
+ ) : null}
298
+
299
+ {actions ? <div className={cn("flex flex-wrap gap-2", isUser && "justify-end")}>{actions}</div> : null}
300
+ </div>
301
+
302
+ {isUser ? <div className="shrink-0">{avatar ?? getDefaultAvatar(role)}</div> : null}
303
+ </article>
304
+ );
305
+ }
306
+
307
+ type AiChatAssistantMessageProps = Omit<AiChatMessageProps, "role">;
308
+
309
+ function AiChatAssistantMessage(props: AiChatAssistantMessageProps) {
310
+ return <AiChatMessage role="assistant" {...props} />;
311
+ }
312
+
313
+ type AiChatUserMessageProps = Omit<AiChatMessageProps, "role">;
314
+
315
+ function AiChatUserMessage(props: AiChatUserMessageProps) {
316
+ return <AiChatMessage role="user" {...props} />;
317
+ }
318
+
319
+ interface AiChatSummaryActionsProps extends React.HTMLAttributes<HTMLDivElement> {
320
+ title?: string;
321
+ description?: string;
322
+ actionsClassName?: string;
323
+ }
324
+
325
+ function AiChatSummaryActions({
326
+ className,
327
+ title = "Quick actions",
328
+ description,
329
+ actionsClassName,
330
+ children,
331
+ ...props
332
+ }: AiChatSummaryActionsProps) {
333
+ return (
334
+ <section className={cn("grid gap-3 rounded-2xl border bg-card p-4 shadow-sm", className)} {...props}>
335
+ <div className="grid gap-1">
336
+ <p className="text-sm font-semibold text-foreground">{title}</p>
337
+ {description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
338
+ </div>
339
+ <div className={cn("grid gap-3 md:grid-cols-3", actionsClassName)}>{children}</div>
340
+ </section>
341
+ );
342
+ }
343
+
344
+ interface AiChatSummaryActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
345
+ title: string;
346
+ description?: string;
347
+ icon?: LucideIcon;
348
+ active?: boolean;
349
+ badge?: string;
350
+ }
351
+
352
+ const AiChatSummaryAction = React.forwardRef<HTMLButtonElement, AiChatSummaryActionProps>(
353
+ (
354
+ {
355
+ className,
356
+ title,
357
+ description,
358
+ icon: Icon = Sparkles,
359
+ active = false,
360
+ badge,
361
+ type = "button",
362
+ ...props
363
+ },
364
+ ref
365
+ ) => (
366
+ <button
367
+ ref={ref}
368
+ type={type}
369
+ className={cn(
370
+ "flex w-full items-start gap-3 rounded-xl border px-4 py-4 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
371
+ active
372
+ ? "border-[var(--color-accent-blue)]/40 bg-[var(--color-accent-blue)]/10"
373
+ : "hover:border-border hover:bg-accent/40",
374
+ className
375
+ )}
376
+ {...props}
377
+ >
378
+ <span className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-brand)]/10 text-[var(--color-brand)]">
379
+ <Icon className="h-5 w-5" />
380
+ </span>
381
+ <span className="grid flex-1 gap-1">
382
+ <span className="flex flex-wrap items-center gap-2">
383
+ <span className="text-sm font-medium text-foreground">{title}</span>
384
+ {badge ? <Badge variant="secondary">{badge}</Badge> : null}
385
+ </span>
386
+ {description ? <span className="text-xs leading-5 text-muted-foreground">{description}</span> : null}
387
+ </span>
388
+ </button>
389
+ )
390
+ );
391
+
392
+ AiChatSummaryAction.displayName = "AiChatSummaryAction";
393
+
394
+ interface AiChatComposerProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
395
+ textareaProps?: React.ComponentPropsWithoutRef<typeof Textarea>;
396
+ onSubmit?: React.FormEventHandler<HTMLFormElement>;
397
+ submitLabel?: string;
398
+ submitIcon?: React.ReactNode;
399
+ leadingActions?: React.ReactNode;
400
+ trailingActions?: React.ReactNode;
401
+ helperText?: React.ReactNode;
402
+ disabled?: boolean;
403
+ }
404
+
405
+ function AiChatComposer({
406
+ className,
407
+ textareaProps,
408
+ onSubmit,
409
+ submitLabel = "Send",
410
+ submitIcon = <Send className="h-4 w-4" />,
411
+ leadingActions,
412
+ trailingActions,
413
+ helperText,
414
+ disabled = false,
415
+ ...props
416
+ }: AiChatComposerProps) {
417
+ const { className: textareaClassName, placeholder, disabled: textareaDisabled, ...restTextareaProps } = textareaProps ?? {};
418
+ const isDisabled = disabled || Boolean(textareaDisabled);
419
+
420
+ return (
421
+ <form className={cn("border-t bg-card px-4 py-4", className)} onSubmit={onSubmit} {...props}>
422
+ <div className="rounded-2xl border bg-background p-3 shadow-sm">
423
+ <Textarea
424
+ className={cn(
425
+ "min-h-24 resize-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0",
426
+ textareaClassName
427
+ )}
428
+ placeholder={placeholder ?? "Describe what you need from the assistant..."}
429
+ disabled={isDisabled}
430
+ {...restTextareaProps}
431
+ />
432
+ <div className="mt-3 flex flex-wrap items-center justify-between gap-3">
433
+ <div className="flex flex-wrap items-center gap-2">{leadingActions}</div>
434
+ <div className="flex flex-wrap items-center gap-2">
435
+ {trailingActions}
436
+ <Button type="submit" disabled={isDisabled}>
437
+ {submitIcon}
438
+ {submitLabel}
439
+ </Button>
440
+ </div>
441
+ </div>
442
+ </div>
443
+ {helperText ? <p className="mt-2 text-xs text-muted-foreground">{helperText}</p> : null}
444
+ </form>
445
+ );
446
+ }
447
+
448
+ interface AiChatEmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
449
+ title: string;
450
+ description: string;
451
+ prompts?: string[];
452
+ onPromptSelect?: (prompt: string) => void;
453
+ }
454
+
455
+ function AiChatEmptyState({
456
+ className,
457
+ title,
458
+ description,
459
+ prompts,
460
+ onPromptSelect,
461
+ ...props
462
+ }: AiChatEmptyStateProps) {
463
+ return (
464
+ <div
465
+ className={cn(
466
+ "flex flex-1 flex-col items-center justify-center gap-5 rounded-2xl border border-dashed bg-card/70 px-6 py-10 text-center",
467
+ className
468
+ )}
469
+ {...props}
470
+ >
471
+ <span className="flex h-14 w-14 items-center justify-center rounded-2xl bg-[var(--color-brand)]/10 text-[var(--color-brand)]">
472
+ <Sparkles className="h-6 w-6" />
473
+ </span>
474
+ <div className="grid max-w-xl gap-2">
475
+ <p className="text-lg font-semibold text-foreground">{title}</p>
476
+ <p className="text-sm leading-6 text-muted-foreground">{description}</p>
477
+ </div>
478
+
479
+ {prompts?.length ? (
480
+ <div className="flex max-w-2xl flex-wrap items-center justify-center gap-2">
481
+ {prompts.map((prompt) => (
482
+ <Button key={prompt} type="button" variant="outline" size="sm" onClick={() => onPromptSelect?.(prompt)}>
483
+ {prompt}
484
+ </Button>
485
+ ))}
486
+ </div>
487
+ ) : null}
488
+ </div>
489
+ );
490
+ }
491
+
492
+ interface AiChatFloatingButtonProps extends ButtonProps {
493
+ label?: string;
494
+ unreadCount?: number;
495
+ icon?: LucideIcon;
496
+ }
497
+
498
+ function AiChatFloatingButton({
499
+ className,
500
+ label = "Asistente IA",
501
+ unreadCount,
502
+ icon: Icon = Sparkles,
503
+ variant = "default",
504
+ size = "lg",
505
+ children,
506
+ ...props
507
+ }: AiChatFloatingButtonProps) {
508
+ return (
509
+ <Button
510
+ variant={variant}
511
+ size={size}
512
+ className={cn("fixed bottom-6 right-6 h-14 rounded-full px-5 shadow-lg", className)}
513
+ {...props}
514
+ >
515
+ <Icon className="h-4 w-4" />
516
+ <span>{children ?? label}</span>
517
+ {typeof unreadCount === "number" && unreadCount > 0 ? (
518
+ <span className="rounded-full bg-white/15 px-2 py-0.5 text-xs font-semibold text-inherit">
519
+ {unreadCount > 9 ? "9+" : unreadCount}
520
+ </span>
521
+ ) : null}
522
+ </Button>
523
+ );
524
+ }
525
+
526
+ type AiChatFloatingSidebarSide = "left" | "right";
527
+
528
+ interface AiChatFloatingSidebarProps {
529
+ children: React.ReactNode;
530
+ trigger?: React.ReactNode;
531
+ side?: AiChatFloatingSidebarSide;
532
+ open?: boolean;
533
+ defaultOpen?: boolean;
534
+ onOpenChange?: (open: boolean) => void;
535
+ contentClassName?: string;
536
+ triggerAsChild?: boolean;
537
+ }
538
+
539
+ function AiChatFloatingSidebar({
540
+ children,
541
+ trigger,
542
+ side = "right",
543
+ open,
544
+ defaultOpen,
545
+ onOpenChange,
546
+ contentClassName,
547
+ triggerAsChild = true
548
+ }: AiChatFloatingSidebarProps) {
549
+ const resolvedTrigger = trigger ?? <AiChatFloatingButton />;
550
+
551
+ return (
552
+ <Sheet open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
553
+ <SheetTrigger asChild={triggerAsChild}>{resolvedTrigger}</SheetTrigger>
554
+ <SheetContent side={side} className={cn("w-full p-0 sm:max-w-[460px]", contentClassName)}>
555
+ <AiChatLayout variant="sidebar" className="h-full rounded-none border-0 shadow-none">
556
+ {children}
557
+ </AiChatLayout>
558
+ </SheetContent>
559
+ </Sheet>
560
+ );
561
+ }
562
+
563
+ export {
564
+ AiChatAssistantMessage,
565
+ AiChatComposer,
566
+ AiChatEmptyState,
567
+ AiChatFloatingButton,
568
+ AiChatFloatingSidebar,
569
+ AiChatHeader,
570
+ AiChatLayout,
571
+ AiChatMessage,
572
+ AiChatMessageList,
573
+ AiChatPanel,
574
+ AiChatSidebar,
575
+ AiChatSidebarHeader,
576
+ AiChatSummaryAction,
577
+ AiChatSummaryActions,
578
+ AiChatThreadButton,
579
+ AiChatThreadList,
580
+ AiChatUserMessage
581
+ };
582
+ export type {
583
+ AiChatAssistantMessageProps,
584
+ AiChatFloatingButtonProps,
585
+ AiChatFloatingSidebarProps,
586
+ AiChatFloatingSidebarSide,
587
+ AiChatLayoutProps,
588
+ AiChatLayoutVariant,
589
+ AiChatMessageProps,
590
+ AiChatMessageSource,
591
+ AiChatMessageStatus,
592
+ AiChatRole,
593
+ AiChatSummaryActionProps,
594
+ AiChatSummaryActionsProps,
595
+ AiChatThreadButtonProps,
596
+ AiChatUserMessageProps
597
+ };
@@ -0,0 +1,77 @@
1
+ import { cn } from "../../lib/cn";
2
+ import { useTheme } from "../../providers/theme-provider";
3
+ import LogoDark from "../../assets/isotipo-cortexa-dark.png";
4
+ import LogoLight from "../../assets/isotipo-cortexa-light.png";
5
+
6
+ interface BrandLogoProps {
7
+ className?: string;
8
+ size?: "sm" | "md" | "lg";
9
+ href?: string;
10
+ }
11
+
12
+ const sizeMap = {
13
+ sm: { icon: "h-6 w-6", text: "text-sm", gap: "gap-2" },
14
+ md: { icon: "h-7 w-7", text: "text-lg", gap: "gap-3" },
15
+ lg: { icon: "h-9 w-9", text: "text-xl", gap: "gap-3" }
16
+ } as const;
17
+
18
+ export function BrandLogo({ className, size = "md", href }: BrandLogoProps) {
19
+ const { resolvedTheme } = useTheme();
20
+ const isDark = resolvedTheme === "dark";
21
+ const s = sizeMap[size];
22
+
23
+ const content = (
24
+ <>
25
+ <div
26
+ className={cn(
27
+ "flex shrink-0 items-center justify-center rounded-md",
28
+ s.icon,
29
+ isDark ? "bg-[var(--color-brand)] p-1" : "bg-transparent"
30
+ )}
31
+ >
32
+ <img
33
+ src={isDark ? LogoDark : LogoLight}
34
+ alt="Cortexa"
35
+ className="max-h-full max-w-full object-contain"
36
+ />
37
+ </div>
38
+
39
+ <div className="flex items-baseline">
40
+ <span
41
+ className={cn(
42
+ "font-bold leading-none",
43
+ s.text,
44
+ isDark ? "text-white" : "text-[var(--color-brand)]"
45
+ )}
46
+ >
47
+ Cortexa
48
+ </span>
49
+ <span
50
+ className={cn(
51
+ "ml-1 font-normal leading-none",
52
+ s.text,
53
+ isDark ? "text-white" : "text-[var(--color-accent-blue)]"
54
+ )}
55
+ >
56
+ Fiscal
57
+ </span>
58
+ </div>
59
+ </>
60
+ );
61
+
62
+ const wrapperClass = cn("flex items-center", s.gap, className);
63
+
64
+ if (href) {
65
+ return (
66
+ <a href={href} className={wrapperClass} aria-label="Cortexa Fiscal — Inicio">
67
+ {content}
68
+ </a>
69
+ );
70
+ }
71
+
72
+ return (
73
+ <div className={wrapperClass} aria-label="Cortexa Fiscal">
74
+ {content}
75
+ </div>
76
+ );
77
+ }