forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtabs.tsx
More file actions
159 lines (137 loc) · 4.84 KB
/
Copy pathtabs.tsx
File metadata and controls
159 lines (137 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"use client";
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { useRouter, useSearchParams } from 'next/navigation';
interface TabProps {
value: string;
children: React.ReactNode;
}
interface TabItem {
value: string;
icon?: React.ReactNode;
}
interface TabsProps {
items: (TabItem | string)[];
children: React.ReactNode;
defaultValue?: string;
groupId: string;
persist?: boolean;
}
// Global state to sync tabs with the same groupId
const tabGroups: Record<string, Set<(value: string) => void>> = {};
const getStorageKey = (groupId: string) => `copilotkit-tabs-${groupId}`;
export function Tabs({ items, children, defaultValue, groupId, persist, ...props }: TabsProps) {
const router = useRouter();
const searchParams = useSearchParams();
const normalizedItems = items.map(item =>
typeof item === 'string' ? { value: item } : item
);
// Initialize value from URL or default
const [value, setValue] = React.useState(() => {
// First try URL
const urlValue = searchParams.get(groupId);
if (urlValue && normalizedItems.some(item => item.value === urlValue)) {
return urlValue;
}
// Then try localStorage if persist is enabled
if (persist && typeof window !== 'undefined') {
try {
const stored = localStorage.getItem(getStorageKey(groupId));
if (stored && normalizedItems.some(item => item.value === stored)) {
return stored;
}
} catch (e) {
console.warn('Failed to read from localStorage:', e);
}
}
return defaultValue || normalizedItems[0].value;
});
// Subscribe to group updates
React.useEffect(() => {
if (!groupId) return;
// Create a Set for this group if it doesn't exist
if (!tabGroups[groupId]) {
tabGroups[groupId] = new Set();
}
// Create a setter function that updates this instance
const setter = (newValue: string) => {
setValue(newValue);
};
// Add this instance's setter to the group
tabGroups[groupId].add(setter);
return () => {
// Cleanup: remove this instance's setter from the group
tabGroups[groupId]?.delete(setter);
if (tabGroups[groupId]?.size === 0) {
delete tabGroups[groupId];
}
};
}, [groupId]);
const handleValueChange = (newValue: string) => {
// Update URL
const newParams = new URLSearchParams(searchParams.toString());
newParams.set(groupId, newValue);
router.replace(`?${newParams.toString()}`, { scroll: false });
// Update state
setValue(newValue);
// Update all other tabs in the same group
if (groupId && tabGroups[groupId]) {
tabGroups[groupId].forEach(setter => setter(newValue));
}
// Persist if enabled
if (persist && typeof window !== 'undefined') {
try {
localStorage.setItem(getStorageKey(groupId), newValue);
} catch (e) {
console.warn('Failed to write to localStorage:', e);
}
}
};
return (
<TabsPrimitive.Root
className="border rounded-md"
value={value}
onValueChange={handleValueChange}
{...props}
>
<ScrollArea className="w-full rounded-md rounded-b-none relative bg-secondary dark:bg-secondary/40 border-b">
<TabsPrimitive.List className="px-4 py-3 flex" role="tablist">
{normalizedItems.map((item) => (
<TabsPrimitive.Trigger
key={item.value}
value={item.value}
className="relative px-3 mr-2 py-1 text-sm font-medium rounded-md whitespace-nowrap flex gap-2 items-center border text-primary
hover:bg-indigo-200/80 dark:hover:bg-indigo-900/80
bg-indigo-200/30 dark:bg-indigo-800/20
border-black/20 dark:border-gray-500/50
data-[state=active]:bg-indigo-200/80 dark:data-[state=active]:bg-indigo-800/50
data-[state=active]:border-indigo-400 dark:data-[state=active]:border-indigo-400"
role="tab"
aria-selected={value === item.value}
>
{item.icon && (
<span className="w-4 h-4 flex items-center justify-center">
{item.icon}
</span>
)}
{item.value}
</TabsPrimitive.Trigger>
))}
<ScrollBar orientation="horizontal" className=""/>
</TabsPrimitive.List>
</ScrollArea>
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return null;
return React.cloneElement(child as React.ReactElement<TabProps>);
})}
</TabsPrimitive.Root>
);
}
export function Tab({ value, children }: TabProps) {
return (
<TabsPrimitive.Content value={value} className="px-4" role="tabpanel">
{children}
</TabsPrimitive.Content>
);
}