@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,31 @@
1
+ import type { UniqueIdentifier } from "@dnd-kit/core";
2
+
3
+ export interface KanbanDropInput<Entity, ColumnId extends UniqueIdentifier> {
4
+ activeId: UniqueIdentifier;
5
+ overId: UniqueIdentifier | null;
6
+ entities: readonly Entity[];
7
+ columns: readonly { id: ColumnId }[];
8
+ getEntityId: (entity: Entity) => UniqueIdentifier;
9
+ getColumnId: (entity: Entity) => ColumnId;
10
+ }
11
+
12
+ /** Resolve column and card targets without mutating the controlled collection. */
13
+ export function resolveKanbanDrop<Entity, ColumnId extends UniqueIdentifier>({
14
+ activeId,
15
+ overId,
16
+ entities,
17
+ columns,
18
+ getEntityId,
19
+ getColumnId,
20
+ }: KanbanDropInput<Entity, ColumnId>) {
21
+ if (overId === null) return null;
22
+ const entity = entities.find((item) => getEntityId(item) === activeId);
23
+ if (entity === undefined) return null;
24
+
25
+ const targetEntity = entities.find((item) => getEntityId(item) === overId);
26
+ const targetColumn = columns.find((column) => column.id === overId);
27
+ const columnId = targetEntity !== undefined ? getColumnId(targetEntity) : targetColumn?.id;
28
+ if (columnId === undefined || columnId === getColumnId(entity)) return null;
29
+ if (!columns.some((column) => column.id === columnId)) return null;
30
+ return { entity, entityId: activeId, columnId };
31
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { Button } from "@lindaui/ui/compound";
4
+ import { cn } from "@lindaui/ui/cn";
5
+ import { ChevronLeft, ChevronRight } from "lucide-react";
6
+ import { useEffect, useState, type RefObject } from "react";
7
+
8
+ export interface NavigationButtonsProps {
9
+ scrollRef: RefObject<HTMLElement | null>;
10
+ columnWidth?: number;
11
+ leftLabel?: string;
12
+ rightLabel?: string;
13
+ }
14
+
15
+ export function NavigationButtons({
16
+ scrollRef,
17
+ columnWidth = 320,
18
+ leftLabel = "Scroll left",
19
+ rightLabel = "Scroll right",
20
+ }: NavigationButtonsProps) {
21
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
22
+ const [canScrollRight, setCanScrollRight] = useState(false);
23
+
24
+ useEffect(() => {
25
+ const element = scrollRef.current;
26
+ if (!element) return;
27
+ const checkScroll = () => {
28
+ const { scrollLeft, scrollWidth, clientWidth } = element;
29
+ setCanScrollLeft(scrollLeft > 0);
30
+ setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1);
31
+ };
32
+ checkScroll();
33
+ element.addEventListener("scroll", checkScroll);
34
+ window.addEventListener("resize", checkScroll);
35
+ return () => {
36
+ element.removeEventListener("scroll", checkScroll);
37
+ window.removeEventListener("resize", checkScroll);
38
+ };
39
+ }, [scrollRef]);
40
+
41
+ const scroll = (direction: "left" | "right") => {
42
+ const element = scrollRef.current;
43
+ if (!element) return;
44
+ element.scrollTo({
45
+ behavior: "smooth",
46
+ left: element.scrollLeft + (direction === "left" ? -columnWidth : columnWidth),
47
+ });
48
+ };
49
+
50
+ return (
51
+ <>
52
+ <Button
53
+ aria-label={leftLabel}
54
+ className={cn(
55
+ "absolute top-1/2 left-4 z-20 -translate-y-1/2 rounded-full shadow-xl transition-opacity",
56
+ canScrollLeft ? "opacity-100" : "pointer-events-none opacity-0",
57
+ )}
58
+ isIconOnly
59
+ onPress={() => scroll("left")}
60
+ size="md"
61
+ variant="primary"
62
+ >
63
+ <ChevronLeft className="h-6 w-6" />
64
+ </Button>
65
+ <Button
66
+ aria-label={rightLabel}
67
+ className={cn(
68
+ "absolute top-1/2 right-4 z-20 -translate-y-1/2 rounded-full shadow-xl transition-opacity",
69
+ canScrollRight ? "opacity-100" : "pointer-events-none opacity-0",
70
+ )}
71
+ isIconOnly
72
+ onPress={() => scroll("right")}
73
+ size="md"
74
+ variant="primary"
75
+ >
76
+ <ChevronRight className="h-6 w-6" />
77
+ </Button>
78
+ </>
79
+ );
80
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type RefObject } from "react";
4
+ import { cn } from "@lindaui/ui/cn";
5
+
6
+ export interface ScrollIndicatorsProps {
7
+ scrollRef: RefObject<HTMLElement | null>;
8
+ }
9
+
10
+ export function ScrollIndicators({ scrollRef }: ScrollIndicatorsProps) {
11
+ const [showLeft, setShowLeft] = useState(false);
12
+ const [showRight, setShowRight] = useState(false);
13
+
14
+ useEffect(() => {
15
+ const element = scrollRef.current;
16
+ if (!element) return;
17
+ const checkScroll = () => {
18
+ const { scrollLeft, scrollWidth, clientWidth } = element;
19
+ setShowLeft(scrollLeft > 10);
20
+ setShowRight(scrollLeft < scrollWidth - clientWidth - 10);
21
+ };
22
+ checkScroll();
23
+ element.addEventListener("scroll", checkScroll);
24
+ window.addEventListener("resize", checkScroll);
25
+ return () => {
26
+ element.removeEventListener("scroll", checkScroll);
27
+ window.removeEventListener("resize", checkScroll);
28
+ };
29
+ }, [scrollRef]);
30
+
31
+ return (
32
+ <>
33
+ <div
34
+ className={cn(
35
+ "pointer-events-none absolute top-0 left-0 z-10 h-full w-12 bg-gradient-to-r from-background to-transparent transition-opacity duration-300",
36
+ showLeft ? "opacity-100" : "opacity-0",
37
+ )}
38
+ />
39
+ <div
40
+ className={cn(
41
+ "pointer-events-none absolute top-0 right-0 z-10 h-full w-12 bg-gradient-to-l from-background to-transparent transition-opacity duration-300",
42
+ showRight ? "opacity-100" : "opacity-0",
43
+ )}
44
+ />
45
+ </>
46
+ );
47
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { useEffect, type RefObject } from "react";
4
+
5
+ /** Preserve horizontal board navigation, without intercepting text editing. */
6
+ export function useKeyboardNavigation<T extends HTMLElement>(
7
+ scrollRef: RefObject<T | null>,
8
+ columnWidth = 320,
9
+ ) {
10
+ useEffect(() => {
11
+ const element = scrollRef.current;
12
+ if (!element) return;
13
+
14
+ const handleKeyDown = (event: KeyboardEvent) => {
15
+ const target = event.target as HTMLElement;
16
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
17
+ return;
18
+ }
19
+
20
+ switch (event.key) {
21
+ case "ArrowLeft":
22
+ event.preventDefault();
23
+ element.scrollBy({ behavior: "smooth", left: -columnWidth });
24
+ break;
25
+ case "ArrowRight":
26
+ event.preventDefault();
27
+ element.scrollBy({ behavior: "smooth", left: columnWidth });
28
+ break;
29
+ case "Home":
30
+ event.preventDefault();
31
+ element.scrollTo({ behavior: "smooth", left: 0 });
32
+ break;
33
+ case "End":
34
+ event.preventDefault();
35
+ element.scrollTo({ behavior: "smooth", left: element.scrollWidth });
36
+ break;
37
+ }
38
+ };
39
+
40
+ window.addEventListener("keydown", handleKeyDown);
41
+ return () => window.removeEventListener("keydown", handleKeyDown);
42
+ }, [scrollRef, columnWidth]);
43
+ }
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import {
4
+ closestCorners,
5
+ DndContext,
6
+ DragOverlay,
7
+ PointerSensor,
8
+ TouchSensor,
9
+ useSensor,
10
+ useSensors,
11
+ type DragEndEvent,
12
+ type DragStartEvent,
13
+ type UniqueIdentifier,
14
+ } from "@dnd-kit/core";
15
+ import { Skeleton } from "@lindaui/ui/compound";
16
+ import { Fragment, useRef, useState, type ReactNode } from "react";
17
+ import { KanbanColumn } from "./kanban-column";
18
+ import { resolveKanbanDrop } from "./kanban/drop-utils";
19
+ import { NavigationButtons } from "./kanban/navigation-buttons";
20
+ import { ScrollIndicators } from "./kanban/scroll-indicators";
21
+ import { useKeyboardNavigation } from "./kanban/use-keyboard-navigation";
22
+
23
+ export { resolveKanbanDrop } from "./kanban/drop-utils";
24
+ export { NavigationButtons, type NavigationButtonsProps } from "./kanban/navigation-buttons";
25
+ export { ScrollIndicators, type ScrollIndicatorsProps } from "./kanban/scroll-indicators";
26
+ export { useKeyboardNavigation } from "./kanban/use-keyboard-navigation";
27
+
28
+ export interface KanbanColumnDefinition<ColumnId extends UniqueIdentifier = string> {
29
+ id: ColumnId;
30
+ title: ReactNode;
31
+ colorClassName?: string;
32
+ }
33
+
34
+ export interface KanbanCardState {
35
+ isDragging: boolean;
36
+ }
37
+
38
+ export interface KanbanBoardProps<Entity, ColumnId extends UniqueIdentifier = string> {
39
+ /** Entity and column IDs must be stable and unique across the whole board. */
40
+ entities: readonly Entity[];
41
+ columns: readonly KanbanColumnDefinition<ColumnId>[];
42
+ getEntityId: (entity: Entity) => UniqueIdentifier;
43
+ getColumnId: (entity: Entity) => ColumnId;
44
+ onColumnChange: (entityId: UniqueIdentifier, columnId: ColumnId, entity: Entity) => void;
45
+ renderCard: (entity: Entity, state: KanbanCardState) => ReactNode;
46
+ renderColumn?: (column: KanbanColumnDefinition<ColumnId>, entities: readonly Entity[]) => ReactNode;
47
+ isLoading?: boolean;
48
+ emptyContent?: ReactNode;
49
+ ariaLabel?: string;
50
+ scrollLeftLabel?: string;
51
+ scrollRightLabel?: string;
52
+ }
53
+
54
+ export function KanbanBoard<Entity, ColumnId extends UniqueIdentifier = string>({
55
+ entities,
56
+ columns,
57
+ getEntityId,
58
+ getColumnId,
59
+ onColumnChange,
60
+ renderCard,
61
+ renderColumn,
62
+ isLoading = false,
63
+ emptyContent,
64
+ ariaLabel = "Kanban board",
65
+ scrollLeftLabel,
66
+ scrollRightLabel,
67
+ }: KanbanBoardProps<Entity, ColumnId>) {
68
+ const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
69
+ const scrollRef = useRef<HTMLElement>(null);
70
+ useKeyboardNavigation(scrollRef);
71
+ const sensors = useSensors(
72
+ useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
73
+ useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }),
74
+ );
75
+
76
+ const handleDragStart = (event: DragStartEvent) => setActiveId(event.active.id);
77
+ const handleDragEnd = (event: DragEndEvent) => {
78
+ setActiveId(null);
79
+ const drop = resolveKanbanDrop({
80
+ activeId: event.active.id,
81
+ overId: event.over?.id ?? null,
82
+ entities,
83
+ columns,
84
+ getEntityId,
85
+ getColumnId,
86
+ });
87
+ if (drop) onColumnChange(drop.entityId, drop.columnId, drop.entity);
88
+ };
89
+ const activeEntity = activeId !== null
90
+ ? entities.find((entity) => getEntityId(entity) === activeId)
91
+ : null;
92
+
93
+ if (isLoading) {
94
+ return (
95
+ <div className="flex h-full gap-4 overflow-x-auto p-4 lg:p-6">
96
+ {columns.map((column) => (
97
+ <div className="flex w-80 flex-shrink-0 flex-col gap-3" key={column.id}>
98
+ <Skeleton className="h-8 w-24 rounded-lg" />
99
+ <div className="min-h-[500px] space-y-3 rounded-xl bg-surface-secondary/50 p-2">
100
+ {Array.from({ length: 3 }).map((_, index) => (
101
+ <Skeleton className="h-32 w-full rounded-xl" key={index} />
102
+ ))}
103
+ </div>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <DndContext
112
+ collisionDetection={closestCorners}
113
+ onDragEnd={handleDragEnd}
114
+ onDragStart={handleDragStart}
115
+ sensors={sensors}
116
+ >
117
+ <div className="relative h-full w-full overflow-hidden">
118
+ <ScrollIndicators scrollRef={scrollRef} />
119
+ <NavigationButtons
120
+ scrollRef={scrollRef}
121
+ leftLabel={scrollLeftLabel}
122
+ rightLabel={scrollRightLabel}
123
+ />
124
+ <section
125
+ aria-label={ariaLabel}
126
+ className="flex h-full gap-4 overflow-x-auto overflow-y-visible p-4 lg:p-6"
127
+ ref={scrollRef}
128
+ >
129
+ {columns.map((column) => {
130
+ const items = entities.filter((entity) => getColumnId(entity) === column.id);
131
+ return (
132
+ <Fragment key={column.id}>
133
+ {renderColumn ? (
134
+ renderColumn(column, items)
135
+ ) : (
136
+ <KanbanColumn
137
+ id={column.id}
138
+ title={column.title}
139
+ entities={items}
140
+ getEntityId={getEntityId}
141
+ renderCard={(entity) => renderCard(entity, { isDragging: false })}
142
+ colorClassName={column.colorClassName}
143
+ emptyContent={emptyContent}
144
+ />
145
+ )}
146
+ </Fragment>
147
+ );
148
+ })}
149
+ </section>
150
+ </div>
151
+ <DragOverlay>
152
+ {activeEntity !== null && activeEntity !== undefined ? (
153
+ <div className="rotate-3 opacity-80">
154
+ {renderCard(activeEntity, { isDragging: true })}
155
+ </div>
156
+ ) : null}
157
+ </DragOverlay>
158
+ </DndContext>
159
+ );
160
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useDroppable, type UniqueIdentifier } from "@dnd-kit/core";
4
+ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
5
+ import { cn } from "@lindaui/ui/cn";
6
+ import { Fragment, type ReactNode } from "react";
7
+
8
+ export interface KanbanColumnProps<Entity> {
9
+ id: UniqueIdentifier;
10
+ title: ReactNode;
11
+ entities: readonly Entity[];
12
+ getEntityId: (entity: Entity) => UniqueIdentifier;
13
+ renderCard: (entity: Entity) => ReactNode;
14
+ colorClassName?: string;
15
+ emptyContent?: ReactNode;
16
+ }
17
+
18
+ export function KanbanColumn<Entity>({
19
+ id,
20
+ title,
21
+ entities,
22
+ getEntityId,
23
+ renderCard,
24
+ colorClassName = "bg-accent/60",
25
+ emptyContent = "No items",
26
+ }: KanbanColumnProps<Entity>) {
27
+ const { setNodeRef, isOver } = useDroppable({ id });
28
+
29
+ return (
30
+ <div className="flex h-full w-80 flex-shrink-0 flex-col">
31
+ <div className="flex h-full flex-col overflow-hidden rounded-2xl bg-surface/50">
32
+ <div className={cn("h-2.5", colorClassName)} />
33
+ <div className="flex items-center justify-between px-4 py-3">
34
+ <h3 className="font-semibold text-foreground">{title}</h3>
35
+ <span className="rounded-full bg-surface-secondary px-2 py-1 font-mono text-muted text-xs">
36
+ {entities.length}
37
+ </span>
38
+ </div>
39
+ <div
40
+ className={cn(
41
+ "min-h-[500px] flex-1 overflow-y-auto p-2 transition-colors",
42
+ isOver ? "bg-accent/10" : "",
43
+ )}
44
+ ref={setNodeRef}
45
+ >
46
+ <SortableContext items={entities.map(getEntityId)} strategy={verticalListSortingStrategy}>
47
+ <div className="space-y-3">
48
+ {entities.map((entity) => (
49
+ <Fragment key={getEntityId(entity)}>{renderCard(entity)}</Fragment>
50
+ ))}
51
+ </div>
52
+ </SortableContext>
53
+ {entities.length === 0 && (
54
+ <div className="flex h-32 items-center justify-center text-muted text-sm">
55
+ {emptyContent}
56
+ </div>
57
+ )}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { Card, Skeleton } from "@lindaui/ui/compound";
4
+ import { cn } from "@lindaui/ui/cn";
5
+ import type { LucideIcon } from "lucide-react";
6
+
7
+ export type KPICardColor = "default" | "success" | "primary" | "warning";
8
+ export interface KPICardProps {
9
+ color?: KPICardColor;
10
+ icon: LucideIcon;
11
+ isLoading?: boolean;
12
+ title: string;
13
+ trend?: { value: number; isPositive: boolean };
14
+ value: string | number;
15
+ }
16
+ const COLOR_CONFIG: Record<KPICardColor, { icon: string; value: string }> = {
17
+ default: { icon: "bg-surface-secondary text-muted", value: "text-foreground" },
18
+ primary: { icon: "bg-accent/10 text-accent", value: "text-accent" },
19
+ success: { icon: "bg-success/10 text-success", value: "text-success" },
20
+ warning: { icon: "bg-warning/10 text-warning", value: "text-warning" },
21
+ };
22
+ /** Exact legacy KPI anatomy; StatCard and its featured variant remain unchanged. */
23
+ export function KPICard({ title, value, icon: Icon, trend, color = "default", isLoading = false }: KPICardProps) {
24
+ const colorConfig = COLOR_CONFIG[color];
25
+ if (isLoading) return <Card className="border-none bg-surface"><Card.Content className="p-4">
26
+ <div className="flex items-start justify-between"><Skeleton className="h-10 w-10 rounded-xl" /><Skeleton className="h-4 w-16 rounded" /></div>
27
+ <Skeleton className="mt-4 h-8 w-24 rounded" /><Skeleton className="mt-2 h-4 w-20 rounded" />
28
+ </Card.Content></Card>;
29
+ return <Card className="border-none bg-surface transition-colors hover:bg-surface-secondary"><Card.Content className="p-4">
30
+ <div className="flex items-start justify-between">
31
+ <div className={cn("rounded-xl p-2.5", colorConfig.icon)}><Icon size={20} /></div>
32
+ {!!trend && <span className={cn("rounded-full px-2 py-1 font-medium text-xs", trend.isPositive ? "bg-success/10 text-success" : "bg-danger/10 text-danger")}>
33
+ {trend.isPositive ? "+" : "-"}{Math.abs(trend.value)}%
34
+ </span>}
35
+ </div>
36
+ <div className="mt-4"><p className={cn("font-bold text-2xl", colorConfig.value)}>{value}</p><p className="mt-1 text-muted text-sm">{title}</p></div>
37
+ </Card.Content></Card>;
38
+ }
@@ -0,0 +1,58 @@
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
+ LineChart as RLineChart,
12
+ Line,
13
+ CartesianGrid,
14
+ XAxis,
15
+ YAxis,
16
+ } from "recharts";
17
+
18
+ export interface LineChartProps {
19
+ data: Array<Record<string, string | number>>;
20
+ config: ChartConfig;
21
+ categoryKey: string;
22
+ series: string[];
23
+ variant?: "default" | "dots";
24
+ className?: string;
25
+ }
26
+
27
+ export function LineChart({
28
+ data,
29
+ config,
30
+ categoryKey,
31
+ series,
32
+ variant = "default",
33
+ className,
34
+ }: LineChartProps) {
35
+ const showDots = variant === "dots";
36
+
37
+ return (
38
+ <ChartContainer config={config} className={className}>
39
+ <RLineChart data={data}>
40
+ <CartesianGrid vertical={false} />
41
+ <XAxis dataKey={categoryKey} tickLine={false} axisLine={false} />
42
+ <YAxis tickLine={false} axisLine={false} />
43
+ <ChartTooltip content={<ChartTooltipContent />} />
44
+ {series.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
45
+ {series.map((key) => (
46
+ <Line
47
+ key={key}
48
+ dataKey={key}
49
+ stroke={`var(--color-${key})`}
50
+ strokeWidth={2}
51
+ dot={showDots}
52
+ type="monotone"
53
+ />
54
+ ))}
55
+ </RLineChart>
56
+ </ChartContainer>
57
+ );
58
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import { ChevronRight } from "lucide-react";
4
+ import { Badge } from "@lindaui/ui/badge";
5
+
6
+ export interface ListItemProps {
7
+ title: string;
8
+ subtitle?: ReactNode;
9
+ meta?: ReactNode;
10
+ leading?: ReactNode;
11
+ badge?: { label: string; variant?: "default" | "success" | "warning" | "danger" | "info" };
12
+ selected?: boolean;
13
+ onSelect?: () => void;
14
+ showChevron?: boolean;
15
+ }
16
+
17
+ export function ListItem({
18
+ title,
19
+ subtitle,
20
+ meta,
21
+ leading,
22
+ badge,
23
+ selected = false,
24
+ onSelect,
25
+ showChevron = false,
26
+ }: ListItemProps) {
27
+ return (
28
+ <button
29
+ type="button"
30
+ aria-pressed={selected}
31
+ onClick={onSelect}
32
+ className={[
33
+ "flex w-full items-center gap-3 border-b border-border px-3 py-3.5 text-left transition-colors sm:px-4",
34
+ "min-h-[72px] cursor-pointer font-[inherit] text-[inherit] active:bg-accent",
35
+ selected
36
+ ? "bg-primary/5 lg:border-l-2 lg:border-l-primary"
37
+ : "bg-card hover:bg-accent/50",
38
+ ].join(" ")}
39
+ >
40
+ {leading != null && (
41
+ <div className="flex-none">{leading}</div>
42
+ )}
43
+ <div className="min-w-0 flex-1">
44
+ <div className="truncate text-sm font-semibold">{title}</div>
45
+ {subtitle != null && (
46
+ <div className="mt-0.5 truncate text-xs text-muted-foreground">{subtitle}</div>
47
+ )}
48
+ {meta != null && (
49
+ <div className="mt-0.5 text-xs text-muted-foreground tabular-nums">{meta}</div>
50
+ )}
51
+ </div>
52
+ {badge != null && (
53
+ <Badge variant={badge.variant ?? "default"}>{badge.label}</Badge>
54
+ )}
55
+ {showChevron && (
56
+ <ChevronRight
57
+ aria-hidden
58
+ className="-mr-1 size-4 flex-none text-muted-foreground/50 lg:hidden"
59
+ />
60
+ )}
61
+ </button>
62
+ );
63
+ }