@lindaui/blocks 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/ai-cost-card-f403ypsg.js +99 -0
- package/dist/ai-performance-card-cggm09nc.js +191 -0
- package/dist/analytics/ai-cost-card.d.ts +3 -0
- package/dist/analytics/ai-cost-card.d.ts.map +1 -0
- package/dist/analytics/ai-performance-card.d.ts +3 -0
- package/dist/analytics/ai-performance-card.d.ts.map +1 -0
- package/dist/analytics/analytics-card.d.ts +3 -0
- package/dist/analytics/analytics-card.d.ts.map +1 -0
- package/dist/analytics/analytics-skeleton.d.ts +5 -0
- package/dist/analytics/analytics-skeleton.d.ts.map +1 -0
- package/dist/analytics/entity-health-card.d.ts +3 -0
- package/dist/analytics/entity-health-card.d.ts.map +1 -0
- package/dist/analytics/status-distribution-chart.d.ts +3 -0
- package/dist/analytics/status-distribution-chart.d.ts.map +1 -0
- package/dist/analytics/time-range-picker.d.ts +3 -0
- package/dist/analytics/time-range-picker.d.ts.map +1 -0
- package/dist/analytics/top-entities-table.d.ts +3 -0
- package/dist/analytics/top-entities-table.d.ts.map +1 -0
- package/dist/analytics/types.d.ts +104 -0
- package/dist/analytics/types.d.ts.map +1 -0
- package/dist/analytics-card-bcmijpt6.js +38 -0
- package/dist/analytics-skeleton-imve37du.js +164 -0
- package/dist/analytics.d.ts +11 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +8 -0
- package/dist/app-shell/drawer-focus.d.ts +8 -0
- package/dist/app-shell/drawer-focus.d.ts.map +1 -0
- package/dist/app-shell/footer.d.ts +39 -0
- package/dist/app-shell/footer.d.ts.map +1 -0
- package/dist/app-shell/logout-dialog.d.ts +6 -0
- package/dist/app-shell/logout-dialog.d.ts.map +1 -0
- package/dist/app-shell/navigation.d.ts +15 -0
- package/dist/app-shell/navigation.d.ts.map +1 -0
- package/dist/app-shell/types.d.ts +55 -0
- package/dist/app-shell/types.d.ts.map +1 -0
- package/dist/app-shell.d.ts +7 -0
- package/dist/app-shell.d.ts.map +1 -0
- package/dist/app-shell.js +254 -0
- package/dist/channel-settings.d.ts +24 -0
- package/dist/channel-settings.d.ts.map +1 -0
- package/dist/channel-settings.js +60 -0
- package/dist/chat-avatar-jdmeyf2o.js +30 -0
- package/dist/chat-layout-fwx3x26g.js +25 -0
- package/dist/chat.d.ts +22 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +66 -0
- package/dist/command-palette.d.ts +22 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +141 -0
- package/dist/compat-chat/chat-avatar.d.ts +16 -0
- package/dist/compat-chat/chat-avatar.d.ts.map +1 -0
- package/dist/compat-chat/chat-header.d.ts +9 -0
- package/dist/compat-chat/chat-header.d.ts.map +1 -0
- package/dist/compat-chat/chat-layout.d.ts +10 -0
- package/dist/compat-chat/chat-layout.d.ts.map +1 -0
- package/dist/compat-chat/conversation-list-item.d.ts +10 -0
- package/dist/compat-chat/conversation-list-item.d.ts.map +1 -0
- package/dist/compat-chat/conversation-list.d.ts +24 -0
- package/dist/compat-chat/conversation-list.d.ts.map +1 -0
- package/dist/compat-chat/conversation-thread.d.ts +18 -0
- package/dist/compat-chat/conversation-thread.d.ts.map +1 -0
- package/dist/compat-chat/date-divider.d.ts +5 -0
- package/dist/compat-chat/date-divider.d.ts.map +1 -0
- package/dist/compat-chat/empty-chat-view.d.ts +6 -0
- package/dist/compat-chat/empty-chat-view.d.ts.map +1 -0
- package/dist/compat-chat/image-lightbox.d.ts +7 -0
- package/dist/compat-chat/image-lightbox.d.ts.map +1 -0
- package/dist/compat-chat/message-bubble.d.ts +6 -0
- package/dist/compat-chat/message-bubble.d.ts.map +1 -0
- package/dist/compat-chat/message-composer.d.ts +11 -0
- package/dist/compat-chat/message-composer.d.ts.map +1 -0
- package/dist/compat-chat/time-divider.d.ts +5 -0
- package/dist/compat-chat/time-divider.d.ts.map +1 -0
- package/dist/compat-chat/time-grouping.d.ts +5 -0
- package/dist/compat-chat/time-grouping.d.ts.map +1 -0
- package/dist/compat-chat/types.d.ts +35 -0
- package/dist/compat-chat/types.d.ts.map +1 -0
- package/dist/compat-chat/whatsapp-audio-player.d.ts +7 -0
- package/dist/compat-chat/whatsapp-audio-player.d.ts.map +1 -0
- package/dist/compat.d.ts +9 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +4 -0
- package/dist/connected-panel-lhwy25yk.js +132 -0
- package/dist/connection-panel/connected-panel.d.ts +26 -0
- package/dist/connection-panel/connected-panel.d.ts.map +1 -0
- package/dist/connection-panel/disconnected-panel.d.ts +12 -0
- package/dist/connection-panel/disconnected-panel.d.ts.map +1 -0
- package/dist/connection-panel/instructions.d.ts +5 -0
- package/dist/connection-panel/instructions.d.ts.map +1 -0
- package/dist/connection-panel/qr-panel.d.ts +14 -0
- package/dist/connection-panel/qr-panel.d.ts.map +1 -0
- package/dist/connection-panel/use-qr-refresh.d.ts +18 -0
- package/dist/connection-panel/use-qr-refresh.d.ts.map +1 -0
- package/dist/connection-panel.d.ts +39 -0
- package/dist/connection-panel.d.ts.map +1 -0
- package/dist/connection-panel.js +328 -0
- package/dist/conversation-list-ekq2dzca.js +133 -0
- package/dist/conversation-list-item-fy6t5af2.js +55 -0
- package/dist/conversation-thread-c60431oa.js +205 -0
- package/dist/entity-card.d.ts +17 -0
- package/dist/entity-card.d.ts.map +1 -0
- package/dist/entity-card.js +69 -0
- package/dist/entity-health-card-ggnmrtrq.js +192 -0
- package/dist/entity-switcher.d.ts +33 -0
- package/dist/entity-switcher.d.ts.map +1 -0
- package/dist/entity-switcher.js +137 -0
- package/dist/filter-bar.d.ts +41 -0
- package/dist/filter-bar.d.ts.map +1 -0
- package/dist/filter-bar.js +155 -0
- package/dist/footer-f7r5e42b.js +192 -0
- package/dist/image-lightbox-ci3arb6z.js +62 -0
- package/dist/kanban/drop-utils.d.ts +18 -0
- package/dist/kanban/drop-utils.d.ts.map +1 -0
- package/dist/kanban/navigation-buttons.d.ts +9 -0
- package/dist/kanban/navigation-buttons.d.ts.map +1 -0
- package/dist/kanban/scroll-indicators.d.ts +6 -0
- package/dist/kanban/scroll-indicators.d.ts.map +1 -0
- package/dist/kanban/use-keyboard-navigation.d.ts +4 -0
- package/dist/kanban/use-keyboard-navigation.d.ts.map +1 -0
- package/dist/kanban-board.d.ts +31 -0
- package/dist/kanban-board.d.ts.map +1 -0
- package/dist/kanban-board.js +128 -0
- package/dist/kanban-column.d.ts +13 -0
- package/dist/kanban-column.d.ts.map +1 -0
- package/dist/kanban-column.js +58 -0
- package/dist/legacy-kpi-card.d.ts +16 -0
- package/dist/legacy-kpi-card.d.ts.map +1 -0
- package/dist/legacy-kpi-card.js +4 -0
- package/dist/logout-dialog-bbv59tbi.js +93 -0
- package/dist/message-bubble-odjvhroq.js +110 -0
- package/dist/message-composer-m2n6soo7.js +59 -0
- package/dist/navigation-buttons-e77mshh0.js +65 -0
- package/dist/navigation-hotcrqoq.js +115 -0
- package/dist/save-bar.d.ts +15 -0
- package/dist/save-bar.d.ts.map +1 -0
- package/dist/save-bar.js +38 -0
- package/dist/scroll-indicators-gvxapogd.js +39 -0
- package/dist/settings-layout.d.ts +20 -0
- package/dist/settings-layout.d.ts.map +1 -0
- package/dist/settings-layout.js +56 -0
- package/dist/source-selector.d.ts +18 -0
- package/dist/source-selector.d.ts.map +1 -0
- package/dist/source-selector.js +59 -0
- package/dist/status-distribution-chart-oucedzv7.js +87 -0
- package/dist/status-filters.d.ts +12 -0
- package/dist/status-filters.d.ts.map +1 -0
- package/dist/status-filters.js +31 -0
- package/dist/stepper.d.ts +15 -0
- package/dist/stepper.d.ts.map +1 -0
- package/dist/stepper.js +42 -0
- package/dist/time-range-picker-mu38hul4.js +123 -0
- package/dist/top-entities-table-o7xzpq96.js +78 -0
- package/dist/use-keyboard-navigation-he1xmriz.js +52 -0
- package/dist/whatsapp-audio-player-nooty6jb.js +277 -0
- package/llms.txt +87 -4
- package/package.json +191 -46
- package/src/analytics/ai-cost-card.tsx +31 -0
- package/src/analytics/ai-performance-card.tsx +48 -0
- package/src/analytics/analytics-card.tsx +14 -0
- package/src/analytics/analytics-skeleton.tsx +15 -0
- package/src/analytics/entity-health-card.tsx +50 -0
- package/src/analytics/status-distribution-chart.tsx +20 -0
- package/src/analytics/time-range-picker.tsx +43 -0
- package/src/analytics/top-entities-table.tsx +24 -0
- package/src/analytics/types.ts +43 -0
- package/src/analytics.ts +10 -0
- package/src/app-shell/drawer-focus.ts +42 -0
- package/src/app-shell/footer.tsx +160 -0
- package/src/app-shell/logout-dialog.tsx +68 -0
- package/src/app-shell/navigation.tsx +147 -0
- package/src/app-shell/types.ts +57 -0
- package/src/app-shell.tsx +166 -0
- package/src/area-chart.tsx +85 -0
- package/src/audio-player.tsx +74 -0
- package/src/auth-layout.tsx +132 -0
- package/src/auth-provider.tsx +29 -0
- package/src/bar-chart.tsx +78 -0
- package/src/channel-settings.tsx +76 -0
- package/src/chart-card.tsx +45 -0
- package/src/chat-avatar.tsx +57 -0
- package/src/chat-date-divider.tsx +18 -0
- package/src/chat-header.tsx +24 -0
- package/src/chat-layout.tsx +41 -0
- package/src/chat-time-divider.tsx +20 -0
- package/src/chat-types.ts +32 -0
- package/src/chat.ts +21 -0
- package/src/command-palette.tsx +110 -0
- package/src/compat-chat/chat-avatar.tsx +23 -0
- package/src/compat-chat/chat-header.tsx +12 -0
- package/src/compat-chat/chat-layout.tsx +14 -0
- package/src/compat-chat/conversation-list-item.tsx +27 -0
- package/src/compat-chat/conversation-list.tsx +49 -0
- package/src/compat-chat/conversation-thread.tsx +92 -0
- package/src/compat-chat/date-divider.tsx +7 -0
- package/src/compat-chat/empty-chat-view.tsx +9 -0
- package/src/compat-chat/image-lightbox.tsx +22 -0
- package/src/compat-chat/message-bubble.tsx +28 -0
- package/src/compat-chat/message-composer.tsx +26 -0
- package/src/compat-chat/time-divider.tsx +5 -0
- package/src/compat-chat/time-grouping.ts +19 -0
- package/src/compat-chat/types.ts +31 -0
- package/src/compat-chat/whatsapp-audio-player.tsx +100 -0
- package/src/compat.ts +8 -0
- package/src/confirm-dialog.tsx +85 -0
- package/src/connection-panel/connected-panel.tsx +106 -0
- package/src/connection-panel/disconnected-panel.tsx +48 -0
- package/src/connection-panel/instructions.tsx +9 -0
- package/src/connection-panel/qr-panel.tsx +62 -0
- package/src/connection-panel/use-qr-refresh.ts +52 -0
- package/src/connection-panel.tsx +141 -0
- package/src/conversation-list-item.tsx +53 -0
- package/src/conversation-list.tsx +67 -0
- package/src/conversation-thread.tsx +64 -0
- package/src/detail-panel.tsx +123 -0
- package/src/document-panel.tsx +109 -0
- package/src/document-reader.tsx +29 -0
- package/src/empty-chat-view.tsx +25 -0
- package/src/entity-card.tsx +74 -0
- package/src/entity-health-card.tsx +53 -0
- package/src/entity-switcher.tsx +126 -0
- package/src/fetching-indicator.tsx +16 -0
- package/src/field-array-form.tsx +166 -0
- package/src/filter-bar.tsx +203 -0
- package/src/image-viewer.tsx +129 -0
- package/src/kanban/drop-utils.ts +31 -0
- package/src/kanban/navigation-buttons.tsx +80 -0
- package/src/kanban/scroll-indicators.tsx +47 -0
- package/src/kanban/use-keyboard-navigation.ts +43 -0
- package/src/kanban-board.tsx +160 -0
- package/src/kanban-column.tsx +62 -0
- package/src/legacy-kpi-card.tsx +38 -0
- package/src/line-chart.tsx +58 -0
- package/src/list-item.tsx +63 -0
- package/src/list-pane.tsx +143 -0
- package/src/login-form.tsx +128 -0
- package/src/message-bubble.tsx +70 -0
- package/src/message-composer.tsx +52 -0
- package/src/pie-chart.tsx +70 -0
- package/src/radar-chart.tsx +55 -0
- package/src/radial-chart.tsx +103 -0
- package/src/recording-overlay.tsx +159 -0
- package/src/save-bar.tsx +35 -0
- package/src/settings-layout.tsx +79 -0
- package/src/source-selector.tsx +71 -0
- package/src/split-workspace.tsx +74 -0
- package/src/stat-card.tsx +166 -0
- package/src/stats-grid.tsx +23 -0
- package/src/status-filters.tsx +28 -0
- package/src/stepper.tsx +47 -0
- package/src/table-pane.tsx +194 -0
- package/src/time-range-picker.tsx +89 -0
- package/src/workspace-back-button.tsx +32 -0
- package/dist/chat-types.test.d.ts +0 -2
- package/dist/chat-types.test.d.ts.map +0 -1
|
@@ -0,0 +1,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
|
+
}
|