@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,166 @@
1
+ "use client";
2
+
3
+ import { Button } from "@lindaui/ui/compound";
4
+ import { cn } from "@lindaui/ui/cn";
5
+ import { Menu, PanelLeft, PanelLeftClose, X } from "lucide-react";
6
+ import { useId, useState } from "react";
7
+ import { useDrawerFocus } from "./app-shell/drawer-focus";
8
+ import { MobileBottomNavigation, ShellLink, SidebarNavLink } from "./app-shell/navigation";
9
+ import type { AppShellLabels, AppShellProps } from "./app-shell/types";
10
+
11
+ export type {
12
+ AppShellBrand, AppShellImageProps, AppShellLabels, AppShellLinkProps, AppShellNavItem, AppShellProps,
13
+ } from "./app-shell/types";
14
+ export { AppShellFooter } from "./app-shell/footer";
15
+ export type { AppShellFooterLabels, AppShellFooterProps, AppShellUser } from "./app-shell/footer";
16
+
17
+ const defaultLabels: AppShellLabels = {
18
+ collapseSidebar: "Collapse sidebar",
19
+ expandSidebar: "Expand sidebar",
20
+ openMenu: "Open menu",
21
+ closeMenu: "Close menu",
22
+ mainNavigation: "Main navigation",
23
+ bottomNavigation: "Bottom navigation",
24
+ };
25
+
26
+ /** The original collapsible desktop sidebar, mobile drawer and floating bottom navigation. */
27
+ export function AppShell({
28
+ children, navItems, sidebarTopSlot, sidebarFooterSlot, contentTopSlot, headerRightSlot,
29
+ brand, isVisiting = false, defaultCollapsed = false, labels: labelOverrides,
30
+ renderLink, renderImage,
31
+ }: AppShellProps) {
32
+ const [sidebarOpen, setSidebarOpen] = useState(false);
33
+ const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
34
+ const drawerFocus = useDrawerFocus(sidebarOpen, () => setSidebarOpen(false));
35
+ const drawerId = useId();
36
+ const labels = { ...defaultLabels, ...labelOverrides };
37
+ // Preserve the existing slot API: the top slot follows desktop collapse state in both sidebars.
38
+ const topSlot = typeof sidebarTopSlot === "function" ? sidebarTopSlot(isCollapsed) : sidebarTopSlot;
39
+ const footer = (collapsed: boolean) => typeof sidebarFooterSlot === "function"
40
+ ? sidebarFooterSlot(collapsed) : sidebarFooterSlot;
41
+ const logo = (size: number, className: string) => {
42
+ if (!brand.logoSrc) return null;
43
+ const props = { alt: brand.label, className, height: size, src: brand.logoSrc, width: size };
44
+ return renderImage ? renderImage(props) : <img {...props} />;
45
+ };
46
+ const mobileBrand = (
47
+ <ShellLink className="flex items-center gap-2" href={brand.href} renderLink={renderLink}>
48
+ {logo(28, "h-7 w-7 rounded-lg")}
49
+ <span className="font-extrabold text-sm uppercase">{brand.label}</span>
50
+ </ShellLink>
51
+ );
52
+
53
+ return (
54
+ <div className="flex min-h-screen bg-background">
55
+ <aside className={cn(
56
+ "fixed z-40 hidden h-full flex-col lg:flex",
57
+ "border-border border-r",
58
+ "transition-all duration-300 ease-in-out",
59
+ isVisiting ? "bg-warning-soft dark:bg-warning/20" : "bg-background",
60
+ isCollapsed ? "w-16" : "w-48"
61
+ )}>
62
+ <div className={cn(
63
+ "flex h-14 items-center border-border border-b",
64
+ isCollapsed ? "flex-col justify-center gap-0 px-2" : "justify-between px-4"
65
+ )}>
66
+ <ShellLink
67
+ aria-label={brand.label}
68
+ className={cn("flex items-center gap-2", isCollapsed ? "mb-0" : "")}
69
+ href={brand.href}
70
+ renderLink={renderLink}
71
+ >
72
+ {logo(32, cn("rounded-lg", isCollapsed ? "h-7 w-7" : "h-8 w-8") ?? "")}
73
+ {isCollapsed ? null : <span className="font-extrabold text-sm uppercase">{brand.label}</span>}
74
+ </ShellLink>
75
+ {isCollapsed ? null : (
76
+ <Button
77
+ aria-label={labels.collapseSidebar}
78
+ className="text-muted hover:text-foreground"
79
+ isIconOnly onPress={() => setIsCollapsed(true)} size="sm" variant="ghost"
80
+ ><PanelLeftClose size={18} /></Button>
81
+ )}
82
+ </div>
83
+ {isCollapsed ? (
84
+ <div className="flex justify-center border-border border-b py-2">
85
+ <Button
86
+ aria-label={labels.expandSidebar}
87
+ className="text-muted hover:text-foreground"
88
+ isIconOnly onPress={() => setIsCollapsed(false)} size="sm" variant="ghost"
89
+ ><PanelLeft size={18} /></Button>
90
+ </div>
91
+ ) : null}
92
+ {topSlot ?? null}
93
+ <nav aria-label={labels.mainNavigation} className="flex-1 space-y-1 px-2 py-4">
94
+ {navItems.map((item) => (
95
+ <SidebarNavLink collapsed={isCollapsed} item={item} key={item.key} renderLink={renderLink} />
96
+ ))}
97
+ </nav>
98
+ {footer(isCollapsed)}
99
+ </aside>
100
+
101
+ <header className="!bg-transparent fixed top-0 right-0 left-0 z-50 border-border border-b backdrop-blur backdrop-saturate-200 lg:hidden dark:border-white/10">
102
+ <div className="flex h-14 items-center justify-between px-4">
103
+ <Button
104
+ ref={drawerFocus.triggerRef}
105
+ aria-label={sidebarOpen ? labels.closeMenu : labels.openMenu}
106
+ aria-controls={drawerId} aria-expanded={sidebarOpen}
107
+ isIconOnly onPress={() => setSidebarOpen(!sidebarOpen)} size="sm" variant="ghost"
108
+ >{sidebarOpen ? <X size={20} /> : <Menu size={20} />}</Button>
109
+ {mobileBrand}
110
+ {headerRightSlot}
111
+ </div>
112
+ </header>
113
+
114
+ {sidebarOpen ? (
115
+ <div
116
+ aria-label={labels.closeMenu}
117
+ className="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm lg:hidden"
118
+ onClick={() => setSidebarOpen(false)}
119
+ onKeyDown={(event) => {
120
+ if (["Enter", " ", "Escape"].includes(event.key)) {
121
+ event.preventDefault();
122
+ setSidebarOpen(false);
123
+ }
124
+ }}
125
+ role="button" tabIndex={0}
126
+ />
127
+ ) : null}
128
+
129
+ <aside
130
+ ref={drawerFocus.drawerRef}
131
+ id={drawerId}
132
+ inert={!sidebarOpen}
133
+ onKeyDown={drawerFocus.onKeyDown}
134
+ className={cn(
135
+ "fixed inset-y-0 left-0 z-[60] flex w-64 flex-col border-border border-r lg:hidden",
136
+ "transform transition-transform duration-300 ease-in-out",
137
+ isVisiting ? "bg-warning-soft dark:bg-warning/20" : "bg-background",
138
+ sidebarOpen ? "translate-x-0" : "-translate-x-full"
139
+ )}
140
+ >
141
+ <div className="flex h-14 items-center justify-between border-border border-b px-4">
142
+ {mobileBrand}
143
+ <Button aria-label={labels.closeMenu} isIconOnly onPress={() => setSidebarOpen(false)} size="sm" variant="ghost">
144
+ <X size={20} />
145
+ </Button>
146
+ </div>
147
+ {topSlot ?? null}
148
+ <nav aria-label={labels.mainNavigation} className="flex-1 space-y-1 p-3">
149
+ {navItems.map((item) => (
150
+ <SidebarNavLink item={item} key={item.key} onClick={() => setSidebarOpen(false)} renderLink={renderLink} />
151
+ ))}
152
+ </nav>
153
+ {footer(false)}
154
+ </aside>
155
+
156
+ <main className={cn(
157
+ "flex h-screen min-w-0 flex-1 flex-col overflow-x-hidden pt-14 transition-all duration-300 lg:pt-0",
158
+ isCollapsed ? "lg:ml-16" : "lg:ml-48"
159
+ )}>
160
+ {contentTopSlot}
161
+ <div className="min-h-0 flex-1 overflow-auto pb-20 lg:pb-0">{children}</div>
162
+ </main>
163
+ <MobileBottomNavigation navItems={navItems} isVisiting={isVisiting} label={labels.bottomNavigation} renderLink={renderLink} />
164
+ </div>
165
+ );
166
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import {
3
+ ChartContainer,
4
+ ChartTooltip,
5
+ ChartTooltipContent,
6
+ ChartLegend,
7
+ ChartLegendContent,
8
+ type ChartConfig,
9
+ } from "@lindaui/ui/chart";
10
+ import {
11
+ AreaChart as RAreaChart,
12
+ Area,
13
+ CartesianGrid,
14
+ XAxis,
15
+ YAxis,
16
+ } from "recharts";
17
+
18
+ export interface AreaChartProps {
19
+ data: Array<Record<string, string | number>>;
20
+ config: ChartConfig;
21
+ categoryKey: string;
22
+ series: string[];
23
+ variant?: "default" | "stacked" | "gradient";
24
+ className?: string;
25
+ }
26
+
27
+ export function AreaChart({
28
+ data,
29
+ config,
30
+ categoryKey,
31
+ series,
32
+ variant = "default",
33
+ className,
34
+ }: AreaChartProps) {
35
+ const isStacked = variant === "stacked";
36
+ const isGradient = variant === "gradient";
37
+
38
+ return (
39
+ <ChartContainer config={config} className={className}>
40
+ <RAreaChart data={data}>
41
+ {isGradient && (
42
+ <defs>
43
+ {series.map((key) => (
44
+ <linearGradient
45
+ key={key}
46
+ id={`gradient-${key}`}
47
+ x1="0"
48
+ y1="0"
49
+ x2="0"
50
+ y2="1"
51
+ >
52
+ <stop
53
+ offset="5%"
54
+ stopColor={`var(--color-${key})`}
55
+ stopOpacity={0.8}
56
+ />
57
+ <stop
58
+ offset="95%"
59
+ stopColor={`var(--color-${key})`}
60
+ stopOpacity={0.1}
61
+ />
62
+ </linearGradient>
63
+ ))}
64
+ </defs>
65
+ )}
66
+ <CartesianGrid vertical={false} />
67
+ <XAxis dataKey={categoryKey} tickLine={false} axisLine={false} />
68
+ <YAxis tickLine={false} axisLine={false} />
69
+ <ChartTooltip content={<ChartTooltipContent />} />
70
+ {series.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
71
+ {series.map((key) => (
72
+ <Area
73
+ key={key}
74
+ dataKey={key}
75
+ fill={
76
+ isGradient ? `url(#gradient-${key})` : `var(--color-${key})`
77
+ }
78
+ stroke={`var(--color-${key})`}
79
+ stackId={isStacked ? "a" : undefined}
80
+ />
81
+ ))}
82
+ </RAreaChart>
83
+ </ChartContainer>
84
+ );
85
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import { useRef, useState } from "react";
3
+ import { Play, Pause } from "lucide-react";
4
+ import { IconButton } from "@lindaui/ui/icon-button";
5
+
6
+ export interface AudioPlayerProps {
7
+ src: string;
8
+ durationSeconds?: number;
9
+ playLabel?: string;
10
+ pauseLabel?: string;
11
+ }
12
+
13
+ function formatDuration(seconds: number): string {
14
+ const m = Math.floor(seconds / 60);
15
+ const s = Math.floor(seconds % 60);
16
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
17
+ }
18
+
19
+ // Alturas deterministas (mismo seed que recording-overlay) — barra estática,
20
+ // no una waveform real del audio.
21
+ const BAR_HEIGHTS = Array.from({ length: 24 }, (_, i) => {
22
+ const seed = Math.abs(Math.sin(i * 12.9898) * 43758.5453) % 1;
23
+ return Math.max(20, Math.round((0.25 + seed * 0.75) * 100));
24
+ });
25
+
26
+ /** Reproductor de nota de voz genérico: play/pause + waveform estática + duración. */
27
+ export function AudioPlayer({
28
+ src,
29
+ durationSeconds,
30
+ playLabel = "Reproducir",
31
+ pauseLabel = "Pausar",
32
+ }: AudioPlayerProps) {
33
+ const audioRef = useRef<HTMLAudioElement>(null);
34
+ const [isPlaying, setIsPlaying] = useState(false);
35
+ const [currentTime, setCurrentTime] = useState(0);
36
+
37
+ const toggle = () => {
38
+ const audio = audioRef.current;
39
+ if (!audio) return;
40
+ if (isPlaying) audio.pause();
41
+ else void audio.play();
42
+ };
43
+
44
+ return (
45
+ <div className="flex items-center gap-2.5 rounded-full bg-secondary px-2 py-1.5">
46
+ <audio
47
+ ref={audioRef}
48
+ src={src}
49
+ onPlay={() => setIsPlaying(true)}
50
+ onPause={() => setIsPlaying(false)}
51
+ onEnded={() => setIsPlaying(false)}
52
+ onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
53
+ >
54
+ <track kind="captions" />
55
+ </audio>
56
+ <IconButton
57
+ aria-label={isPlaying ? pauseLabel : playLabel}
58
+ onPress={toggle}
59
+ variant="ghost"
60
+ size="sm"
61
+ >
62
+ {isPlaying ? <Pause className="size-4" /> : <Play className="size-4" />}
63
+ </IconButton>
64
+ <div className="flex h-6 flex-1 items-end gap-[2px]" aria-hidden="true">
65
+ {BAR_HEIGHTS.map((h, i) => (
66
+ <span key={i} className="w-[2.5px] rounded-full bg-primary/60" style={{ height: `${h}%` }} />
67
+ ))}
68
+ </div>
69
+ <span className="font-mono text-[11px] tabular-nums text-muted-foreground">
70
+ {formatDuration(durationSeconds ?? currentTime)}
71
+ </span>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+
4
+ // Block: shell de autenticación split-screen. Panel de marca (solo desktop,
5
+ // fondo oscuro estilo `viewer`) + panel de contenido (logo compacto en mobile,
6
+ // título, descripción, slot para el form y footer). Mobile-first: stackea, el
7
+ // panel de marca se oculta y aparece el logo compacto. API por slots, genérica
8
+ // (sin acoplar dominio); el `children` recibe el form (ej. <LoginForm/>).
9
+
10
+ export interface AuthFeature {
11
+ icon?: ReactNode;
12
+ label: string;
13
+ }
14
+
15
+ export interface AuthLayoutProps {
16
+ /** Marca (logo) del panel de marca (desktop). */
17
+ logo?: ReactNode;
18
+ /** Logo compacto del panel de contenido (mobile). Si no se pasa, usa `logo`. */
19
+ compactLogo?: ReactNode;
20
+ /** Contenido del panel de marca (solo desktop). Si no hay headline ni
21
+ * features, el panel de marca se omite y queda solo el form centrado. */
22
+ brandHeadline?: ReactNode;
23
+ brandDescription?: ReactNode;
24
+ brandFeatures?: AuthFeature[];
25
+ brandFooter?: ReactNode;
26
+ /** Panel de contenido. */
27
+ title: ReactNode;
28
+ description?: ReactNode;
29
+ /** Nota al pie del panel de contenido (ej. "sesión protegida"). */
30
+ footer?: ReactNode;
31
+ /** El formulario u otro contenido de auth. */
32
+ children: ReactNode;
33
+ }
34
+
35
+ export function AuthLayout({
36
+ logo,
37
+ compactLogo,
38
+ brandHeadline,
39
+ brandDescription,
40
+ brandFeatures,
41
+ brandFooter,
42
+ title,
43
+ description,
44
+ footer,
45
+ children,
46
+ }: AuthLayoutProps) {
47
+ const hasBrand =
48
+ brandHeadline != null ||
49
+ brandDescription != null ||
50
+ (brandFeatures != null && brandFeatures.length > 0) ||
51
+ brandFooter != null;
52
+
53
+ return (
54
+ <div
55
+ className={
56
+ hasBrand
57
+ ? "flex min-h-dvh flex-col lg:grid lg:grid-cols-[1.05fr_0.95fr]"
58
+ : "flex min-h-dvh flex-col"
59
+ }
60
+ >
61
+ {/* Panel de marca: solo desktop */}
62
+ {hasBrand && (
63
+ <div
64
+ className="relative hidden flex-col justify-between bg-viewer-bg p-12 text-viewer-foreground lg:flex"
65
+ style={{
66
+ backgroundImage:
67
+ "repeating-linear-gradient(135deg, color-mix(in oklch, var(--viewer-foreground) 3%, transparent) 0 2px, transparent 2px 13px)",
68
+ }}
69
+ >
70
+ {logo && <div className="flex items-center gap-2.5">{logo}</div>}
71
+
72
+ <div className="max-w-md">
73
+ {brandHeadline && (
74
+ <h1 className="text-3xl font-semibold leading-tight tracking-tight">
75
+ {brandHeadline}
76
+ </h1>
77
+ )}
78
+ {brandDescription && (
79
+ <p className="mt-4 text-sm leading-relaxed opacity-70">{brandDescription}</p>
80
+ )}
81
+ {brandFeatures && brandFeatures.length > 0 && (
82
+ <div className="mt-7 flex gap-6">
83
+ {brandFeatures.map((f, i) => (
84
+ <div key={i} className="flex items-center gap-2 text-sm opacity-80">
85
+ {f.icon}
86
+ {f.label}
87
+ </div>
88
+ ))}
89
+ </div>
90
+ )}
91
+ </div>
92
+
93
+ {brandFooter ? (
94
+ <p className="text-xs opacity-40">{brandFooter}</p>
95
+ ) : (
96
+ <span aria-hidden />
97
+ )}
98
+ </div>
99
+ )}
100
+
101
+ {/* Panel de contenido */}
102
+ <div className="flex flex-1 flex-col justify-between p-6 pt-[max(env(safe-area-inset-top),2rem)] lg:justify-center lg:p-10 lg:pb-10">
103
+ <div className="mx-auto w-full max-w-[360px]">
104
+ {/* Logo compacto del panel de contenido. Con panel de marca: solo en
105
+ mobile (desktop ya lo tiene en la marca). Sin marca: siempre. */}
106
+ {(compactLogo ?? logo) && hasBrand && (
107
+ <div className="mb-10 flex items-center gap-2.5 lg:hidden">
108
+ {compactLogo ?? logo}
109
+ </div>
110
+ )}
111
+ {(compactLogo ?? logo) && !hasBrand && (
112
+ <div className="mb-10 flex items-center gap-2.5">{compactLogo ?? logo}</div>
113
+ )}
114
+
115
+ <h2 className="mb-1.5 text-[28px] font-semibold tracking-tight lg:text-2xl">
116
+ {title}
117
+ </h2>
118
+ {description && (
119
+ <p className="mb-7 text-sm text-muted-foreground">{description}</p>
120
+ )}
121
+ {children}
122
+ </div>
123
+
124
+ {footer && (
125
+ <div className="mx-auto mt-10 flex w-full max-w-[360px] items-center justify-center gap-1.5 text-xs text-muted-foreground lg:mt-6">
126
+ {footer}
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { createContext, useContext, type ReactNode } from "react";
3
+
4
+ export interface AuthUser {
5
+ name?: string;
6
+ [key: string]: unknown;
7
+ }
8
+
9
+ interface AuthContextValue {
10
+ user: AuthUser | null;
11
+ }
12
+
13
+ const AuthContext = createContext<AuthContextValue>({ user: null });
14
+
15
+ export function AuthProvider({
16
+ user,
17
+ children,
18
+ }: {
19
+ user: AuthUser | null;
20
+ children: ReactNode;
21
+ }) {
22
+ return (
23
+ <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
24
+ );
25
+ }
26
+
27
+ export function useAuth(): { user: AuthUser | null } {
28
+ return useContext(AuthContext);
29
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import {
3
+ ChartContainer,
4
+ ChartTooltip,
5
+ ChartTooltipContent,
6
+ ChartLegend,
7
+ ChartLegendContent,
8
+ type ChartConfig,
9
+ } from "@lindaui/ui/chart";
10
+ import {
11
+ BarChart as RBarChart,
12
+ Bar,
13
+ CartesianGrid,
14
+ XAxis,
15
+ YAxis,
16
+ } from "recharts";
17
+
18
+ export interface BarChartProps {
19
+ data: Array<Record<string, string | number>>;
20
+ config: ChartConfig;
21
+ categoryKey: string;
22
+ series: string[];
23
+ variant?: "default" | "stacked" | "grouped" | "horizontal";
24
+ className?: string;
25
+ }
26
+
27
+ export function BarChart({
28
+ data,
29
+ config,
30
+ categoryKey,
31
+ series,
32
+ variant = "default",
33
+ className,
34
+ }: BarChartProps) {
35
+ const isHorizontal = variant === "horizontal";
36
+ const isStacked = variant === "stacked";
37
+
38
+ return (
39
+ <ChartContainer config={config} className={className}>
40
+ <RBarChart
41
+ data={data}
42
+ layout={isHorizontal ? "vertical" : "horizontal"}
43
+ >
44
+ <CartesianGrid
45
+ vertical={isHorizontal}
46
+ horizontal={!isHorizontal}
47
+ />
48
+ {isHorizontal ? (
49
+ <>
50
+ <XAxis type="number" tickLine={false} axisLine={false} />
51
+ <YAxis
52
+ dataKey={categoryKey}
53
+ type="category"
54
+ tickLine={false}
55
+ axisLine={false}
56
+ />
57
+ </>
58
+ ) : (
59
+ <>
60
+ <XAxis dataKey={categoryKey} tickLine={false} axisLine={false} />
61
+ <YAxis tickLine={false} axisLine={false} />
62
+ </>
63
+ )}
64
+ <ChartTooltip content={<ChartTooltipContent />} />
65
+ {series.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
66
+ {series.map((key) => (
67
+ <Bar
68
+ key={key}
69
+ dataKey={key}
70
+ fill={`var(--color-${key})`}
71
+ stackId={isStacked ? "a" : undefined}
72
+ radius={4}
73
+ />
74
+ ))}
75
+ </RBarChart>
76
+ </ChartContainer>
77
+ );
78
+ }
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { Tabs } from "@lindaui/ui/compound";
4
+ import { useState, type ReactNode } from "react";
5
+
6
+ export interface SettingsChannel {
7
+ id: string;
8
+ label: ReactNode;
9
+ status: ReactNode;
10
+ icon?: ReactNode;
11
+ isDisabled?: boolean;
12
+ panel: ReactNode | ((isSelected: boolean) => ReactNode);
13
+ /** Optional mounting order, independent of the tab order. */
14
+ panelOrder?: number;
15
+ }
16
+
17
+ export interface ChannelSettingsProps {
18
+ channels: readonly SettingsChannel[];
19
+ navigationLabel: string;
20
+ selected?: string;
21
+ defaultSelected?: string;
22
+ onSelect?: (id: string) => void;
23
+ className?: string;
24
+ /** Two channels retain the original two-column layout; other counts adapt automatically. */
25
+ listClassName?: string;
26
+ }
27
+
28
+ /** Every panel stays mounted so switching channels preserves form and challenge state. */
29
+ export function ChannelSettings({
30
+ channels,
31
+ navigationLabel,
32
+ selected,
33
+ defaultSelected,
34
+ onSelect,
35
+ className = "w-full gap-5",
36
+ listClassName,
37
+ }: ChannelSettingsProps) {
38
+ const [localSelected, setLocalSelected] = useState(defaultSelected ?? channels[0]?.id);
39
+ const current = selected ?? localSelected;
40
+
41
+ return (
42
+ <Tabs
43
+ className={className}
44
+ onSelectionChange={(key) => {
45
+ const id = String(key);
46
+ if (selected === undefined) setLocalSelected(id);
47
+ onSelect?.(id);
48
+ }}
49
+ selectedKey={current}
50
+ variant="secondary"
51
+ >
52
+ <Tabs.ListContainer className="w-full">
53
+ <Tabs.List
54
+ aria-label={navigationLabel}
55
+ className={listClassName ?? (channels.length === 2 ? "grid w-full grid-cols-2" : "grid w-full auto-cols-fr grid-flow-col")}
56
+ >
57
+ {channels.map(({ id, label, status, icon, isDisabled }) => (
58
+ <Tabs.Tab className="h-auto min-w-0 justify-start gap-3 px-3 py-4" id={id} isDisabled={isDisabled} key={id}>
59
+ {icon}
60
+ <span className="min-w-0 text-left">
61
+ <span className="block font-semibold text-sm">{label}</span>
62
+ <span className="block whitespace-normal font-normal text-muted text-xs">{status}</span>
63
+ </span>
64
+ <Tabs.Indicator />
65
+ </Tabs.Tab>
66
+ ))}
67
+ </Tabs.List>
68
+ </Tabs.ListContainer>
69
+ {[...channels].sort((a, b) => (a.panelOrder ?? 0) - (b.panelOrder ?? 0)).map(({ id, panel }) => (
70
+ <Tabs.Panel className={current === id ? "" : "hidden"} id={id} key={id} shouldForceMount>
71
+ {typeof panel === "function" ? panel(current === id) : panel}
72
+ </Tabs.Panel>
73
+ ))}
74
+ </Tabs>
75
+ );
76
+ }
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ Card,
4
+ CardHeader,
5
+ CardTitle,
6
+ CardDescription,
7
+ CardContent,
8
+ CardFooter,
9
+ } from "@lindaui/ui/card";
10
+
11
+ export interface ChartCardProps {
12
+ title: ReactNode;
13
+ description?: ReactNode;
14
+ action?: ReactNode;
15
+ footer?: ReactNode;
16
+ children: ReactNode;
17
+ className?: string;
18
+ }
19
+
20
+ export function ChartCard({
21
+ title,
22
+ description,
23
+ action,
24
+ footer,
25
+ children,
26
+ className = "",
27
+ }: ChartCardProps) {
28
+ return (
29
+ <Card className={className}>
30
+ <CardHeader className="pb-2">
31
+ <div className="flex items-start justify-between gap-4">
32
+ <div className="min-w-0 flex-1">
33
+ <CardTitle>{title}</CardTitle>
34
+ {description && (
35
+ <CardDescription className="mt-1">{description}</CardDescription>
36
+ )}
37
+ </div>
38
+ {action && <div className="shrink-0">{action}</div>}
39
+ </div>
40
+ </CardHeader>
41
+ <CardContent className="pt-0">{children}</CardContent>
42
+ {footer && <CardFooter>{footer}</CardFooter>}
43
+ </Card>
44
+ );
45
+ }