@polpo-ai/dashboard 0.1.3 → 0.15.8

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 (184) hide show
  1. package/LICENSE +3 -1
  2. package/README.md +13 -40
  3. package/dist/components.d.ts +53 -0
  4. package/dist/components.d.ts.map +1 -0
  5. package/dist/components.js +81 -0
  6. package/dist/components.js.map +1 -0
  7. package/dist/host.d.ts +24 -0
  8. package/dist/host.d.ts.map +1 -0
  9. package/dist/host.js +18 -0
  10. package/dist/host.js.map +1 -0
  11. package/dist/index.d.ts +13 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +11 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/test/setup.d.ts +2 -0
  16. package/dist/test/setup.d.ts.map +1 -0
  17. package/dist/test/setup.js +2 -0
  18. package/dist/test/setup.js.map +1 -0
  19. package/dist/views/agent-detail.d.ts +4 -0
  20. package/dist/views/agent-detail.d.ts.map +1 -0
  21. package/dist/views/agent-detail.js +31 -0
  22. package/dist/views/agent-detail.js.map +1 -0
  23. package/dist/views/agents.d.ts +2 -0
  24. package/dist/views/agents.d.ts.map +1 -0
  25. package/dist/views/agents.js +81 -0
  26. package/dist/views/agents.js.map +1 -0
  27. package/dist/views/files.d.ts +2 -0
  28. package/dist/views/files.d.ts.map +1 -0
  29. package/dist/views/files.js +30 -0
  30. package/dist/views/files.js.map +1 -0
  31. package/dist/views/memory.d.ts +2 -0
  32. package/dist/views/memory.d.ts.map +1 -0
  33. package/dist/views/memory.js +19 -0
  34. package/dist/views/memory.js.map +1 -0
  35. package/dist/views/playground.d.ts +4 -0
  36. package/dist/views/playground.d.ts.map +1 -0
  37. package/dist/views/playground.js +43 -0
  38. package/dist/views/playground.js.map +1 -0
  39. package/dist/views/session-detail.d.ts +4 -0
  40. package/dist/views/session-detail.d.ts.map +1 -0
  41. package/dist/views/session-detail.js +23 -0
  42. package/dist/views/session-detail.js.map +1 -0
  43. package/dist/views/sessions.d.ts +2 -0
  44. package/dist/views/sessions.d.ts.map +1 -0
  45. package/dist/views/sessions.js +17 -0
  46. package/dist/views/sessions.js.map +1 -0
  47. package/dist/views/skills.d.ts +2 -0
  48. package/dist/views/skills.d.ts.map +1 -0
  49. package/dist/views/skills.js +16 -0
  50. package/dist/views/skills.js.map +1 -0
  51. package/package.json +47 -83
  52. package/src/styles.css +169 -0
  53. package/app/(dashboard)/error.tsx +0 -47
  54. package/app/(dashboard)/layout.tsx +0 -41
  55. package/app/(dashboard)/loading.tsx +0 -30
  56. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +0 -36
  57. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +0 -30
  58. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +0 -51
  59. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +0 -211
  60. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +0 -84
  61. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +0 -525
  62. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +0 -61
  63. package/app/(dashboard)/projects/[id]/agents/page.tsx +0 -39
  64. package/app/(dashboard)/projects/[id]/agents/view.tsx +0 -283
  65. package/app/(dashboard)/projects/[id]/layout.tsx +0 -11
  66. package/app/(dashboard)/projects/[id]/logs/page.tsx +0 -27
  67. package/app/(dashboard)/projects/[id]/logs/view.tsx +0 -184
  68. package/app/(dashboard)/projects/[id]/memory/page.tsx +0 -40
  69. package/app/(dashboard)/projects/[id]/memory/view.tsx +0 -17
  70. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +0 -88
  71. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +0 -569
  72. package/app/(dashboard)/projects/[id]/missions/page.tsx +0 -32
  73. package/app/(dashboard)/projects/[id]/missions/view.tsx +0 -282
  74. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +0 -359
  75. package/app/(dashboard)/projects/[id]/page.tsx +0 -68
  76. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +0 -5
  77. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +0 -433
  78. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +0 -5
  79. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +0 -222
  80. package/app/(dashboard)/projects/[id]/schedules/page.tsx +0 -44
  81. package/app/(dashboard)/projects/[id]/schedules/view.tsx +0 -385
  82. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +0 -55
  83. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +0 -373
  84. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +0 -124
  85. package/app/(dashboard)/projects/[id]/sessions/page.tsx +0 -31
  86. package/app/(dashboard)/projects/[id]/sessions/view.tsx +0 -242
  87. package/app/(dashboard)/projects/[id]/settings/page.tsx +0 -52
  88. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +0 -65
  89. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +0 -227
  90. package/app/(dashboard)/projects/[id]/skills/page.tsx +0 -31
  91. package/app/(dashboard)/projects/[id]/skills/view.tsx +0 -243
  92. package/app/(dashboard)/projects/[id]/storage/page.tsx +0 -5
  93. package/app/(dashboard)/projects/[id]/storage/view.tsx +0 -385
  94. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +0 -336
  95. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +0 -113
  96. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +0 -72
  97. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +0 -90
  98. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +0 -63
  99. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +0 -137
  100. package/app/(dashboard)/projects/[id]/tasks/page.tsx +0 -40
  101. package/app/(dashboard)/projects/[id]/tasks/view.tsx +0 -421
  102. package/app/(dashboard)/projects/[id]/view.tsx +0 -421
  103. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +0 -156
  104. package/app/(dashboard)/projects/projects-list.tsx +0 -126
  105. package/app/(playground)/error.tsx +0 -42
  106. package/app/(playground)/layout.tsx +0 -27
  107. package/app/(playground)/projects/[id]/playground/page.tsx +0 -57
  108. package/app/(playground)/projects/[id]/playground/view.tsx +0 -167
  109. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +0 -47
  110. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +0 -63
  111. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +0 -237
  112. package/app/favicon.ico +0 -0
  113. package/app/globals.css +0 -385
  114. package/app/icon.svg +0 -6
  115. package/app/layout.tsx +0 -39
  116. package/app/page.tsx +0 -11
  117. package/components/ai-elements/code-block.tsx +0 -562
  118. package/components/dashboard/agent-capabilities.tsx +0 -326
  119. package/components/dashboard/agent-identity-header.tsx +0 -50
  120. package/components/dashboard/agent-model-picker.tsx +0 -195
  121. package/components/dashboard/agent-model-selector.tsx +0 -121
  122. package/components/dashboard/agent-picker-card.tsx +0 -46
  123. package/components/dashboard/agent-studio.tsx +0 -242
  124. package/components/dashboard/agents-table.tsx +0 -48
  125. package/components/dashboard/breadcrumb.tsx +0 -70
  126. package/components/dashboard/builder-chat.tsx +0 -418
  127. package/components/dashboard/chat-shell.tsx +0 -40
  128. package/components/dashboard/client-picker.tsx +0 -63
  129. package/components/dashboard/command-snippet.tsx +0 -77
  130. package/components/dashboard/connect-dialog.tsx +0 -680
  131. package/components/dashboard/copy-button.tsx +0 -45
  132. package/components/dashboard/copy-card.tsx +0 -48
  133. package/components/dashboard/file-browser.tsx +0 -161
  134. package/components/dashboard/hint.tsx +0 -22
  135. package/components/dashboard/inference-mode.tsx +0 -15
  136. package/components/dashboard/manual-refresh-button.tsx +0 -52
  137. package/components/dashboard/markdown.tsx +0 -31
  138. package/components/dashboard/mcp-install-panel.tsx +0 -196
  139. package/components/dashboard/mission-graph.tsx +0 -262
  140. package/components/dashboard/nav-tabs.tsx +0 -86
  141. package/components/dashboard/polpo-chat.tsx +0 -468
  142. package/components/dashboard/project-copilot.tsx +0 -253
  143. package/components/dashboard/sdk-snippet-panel.tsx +0 -194
  144. package/components/dashboard/section-header.tsx +0 -37
  145. package/components/dashboard/settings-form.tsx +0 -2477
  146. package/components/dashboard/sidebar.tsx +0 -385
  147. package/components/dashboard/skeletons.tsx +0 -1054
  148. package/components/dashboard/skills-install-wizard.tsx +0 -294
  149. package/components/dashboard/swarm-runs.tsx +0 -316
  150. package/components/dashboard/tab-toggle.tsx +0 -35
  151. package/components/dashboard/task-studio.tsx +0 -224
  152. package/components/dashboard/top-header.tsx +0 -203
  153. package/components/dashboard/webhook-deliveries.tsx +0 -234
  154. package/components/icons/coding-agents.tsx +0 -151
  155. package/components/json-ld.tsx +0 -8
  156. package/components/providers.tsx +0 -25
  157. package/components/ui/badge.tsx +0 -52
  158. package/components/ui/button.tsx +0 -60
  159. package/components/ui/card.tsx +0 -103
  160. package/components/ui/chart.tsx +0 -356
  161. package/components/ui/command.tsx +0 -196
  162. package/components/ui/dialog.tsx +0 -157
  163. package/components/ui/input-group.tsx +0 -158
  164. package/components/ui/input.tsx +0 -20
  165. package/components/ui/multi-select.tsx +0 -161
  166. package/components/ui/popover.tsx +0 -90
  167. package/components/ui/select.tsx +0 -201
  168. package/components/ui/separator.tsx +0 -25
  169. package/components/ui/sheet.tsx +0 -135
  170. package/components/ui/skeleton.tsx +0 -13
  171. package/components/ui/tabs.tsx +0 -56
  172. package/components/ui/textarea.tsx +0 -18
  173. package/components/ui/tooltip.tsx +0 -66
  174. package/hooks/use-desktop-sidebar.tsx +0 -50
  175. package/hooks/use-mobile-sidebar.tsx +0 -37
  176. package/index.ts +0 -17
  177. package/lib/api.ts +0 -194
  178. package/lib/builder-context.ts +0 -60
  179. package/lib/data-client.ts +0 -68
  180. package/lib/get-query-client.ts +0 -25
  181. package/lib/polpo-client.tsx +0 -49
  182. package/lib/tool-catalog.ts +0 -234
  183. package/lib/use-event-catalog.ts +0 -28
  184. package/lib/utils.ts +0 -6
@@ -1,262 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo } from "react";
4
- import {
5
- ReactFlow,
6
- Background,
7
- BackgroundVariant,
8
- Controls,
9
- MarkerType,
10
- type Node,
11
- type Edge,
12
- type NodeProps,
13
- Position,
14
- useNodesState,
15
- useEdgesState,
16
- Handle,
17
- } from "@xyflow/react";
18
- import "@xyflow/react/dist/style.css";
19
- import type { Task } from "@polpo-ai/core";
20
- import Link from "next/link";
21
-
22
- // ── Status config ──
23
-
24
- const STATUS_CONFIG: Record<string, { bg: string; border: string; dot: string; text: string }> = {
25
- done: { bg: "bg-emerald-500/5", border: "border-emerald-500/40", dot: "bg-emerald-500", text: "text-emerald-500" },
26
- completed: { bg: "bg-emerald-500/5", border: "border-emerald-500/40", dot: "bg-emerald-500", text: "text-emerald-500" },
27
- in_progress: { bg: "bg-blue-500/5", border: "border-blue-500/40", dot: "bg-blue-500", text: "text-blue-500" },
28
- review: { bg: "bg-amber-500/5", border: "border-amber-500/40", dot: "bg-amber-500", text: "text-amber-500" },
29
- pending: { bg: "bg-card/90", border: "border-border", dot: "bg-muted-foreground/30", text: "text-muted-foreground" },
30
- assigned: { bg: "bg-card/90", border: "border-border", dot: "bg-muted-foreground/50", text: "text-muted-foreground" },
31
- draft: { bg: "bg-card/90", border: "border-border/60", dot: "bg-muted-foreground/20", text: "text-muted-foreground/60" },
32
- failed: { bg: "bg-red-500/5", border: "border-red-500/40", dot: "bg-red-500", text: "text-red-500" },
33
- awaiting_approval: { bg: "bg-amber-500/5", border: "border-amber-500/40", dot: "bg-amber-500", text: "text-amber-500" },
34
- };
35
-
36
- const DEFAULT_CFG = { bg: "bg-card/90", border: "border-border", dot: "bg-muted-foreground/30", text: "text-muted-foreground" };
37
-
38
- // ── Task Node ──
39
-
40
- interface TaskNodeData {
41
- task: Task;
42
- index: number;
43
- projectId: string;
44
- readonly?: boolean;
45
- [key: string]: unknown;
46
- }
47
-
48
- function TaskNode({ data }: NodeProps<Node<TaskNodeData>>) {
49
- const { task, index, projectId, readonly: isReadonly } = data;
50
- const cfg = STATUS_CONFIG[task.status] ?? DEFAULT_CFG;
51
- const duration = task.result?.duration != null ? `${Math.round(task.result.duration / 1000)}s` : null;
52
-
53
- return (
54
- <>
55
- <Handle type="target" position={Position.Top} className="!bg-muted-foreground/40 !border-none !w-2 !h-2" />
56
- <div className={`rounded-xl border-2 ${cfg.border} ${cfg.bg} px-4 py-3 min-w-[220px] max-w-[280px] shadow-sm backdrop-blur-sm`}>
57
- {/* Header */}
58
- <div className="flex items-center gap-2 mb-1.5">
59
- <span className="text-[10px] font-mono text-muted-foreground/60">#{index + 1}</span>
60
- <span className={`h-2.5 w-2.5 rounded-full ${cfg.dot} ${task.status === "in_progress" ? "animate-pulse" : ""}`} />
61
- <span className={`text-[10px] font-medium ${cfg.text}`}>{task.status.replace(/_/g, " ")}</span>
62
- {task.phase && task.status === "in_progress" && (
63
- <span className="text-[9px] text-muted-foreground/50">({task.phase})</span>
64
- )}
65
- {duration && (
66
- <span className="text-[9px] text-muted-foreground ml-auto font-mono">{duration}</span>
67
- )}
68
- </div>
69
-
70
- {/* Title */}
71
- {isReadonly ? (
72
- <span className="text-xs font-semibold leading-tight line-clamp-2 block">{task.title}</span>
73
- ) : (
74
- <Link
75
- href={`/projects/${projectId}/tasks/${task.id}`}
76
- className="text-xs font-semibold leading-tight line-clamp-2 hover:underline underline-offset-2 block"
77
- onClick={(e) => e.stopPropagation()}
78
- >
79
- {task.title}
80
- </Link>
81
- )}
82
-
83
- {/* Footer */}
84
- <div className="mt-2 flex items-center gap-2">
85
- <span className="rounded bg-secondary/80 px-1.5 py-0.5 text-[9px] font-mono text-muted-foreground">
86
- {task.assignTo}
87
- </span>
88
- {task.dependsOn && task.dependsOn.length > 0 && (
89
- <span className="text-[9px] text-muted-foreground/50">
90
- {task.dependsOn.length} dep{task.dependsOn.length > 1 ? "s" : ""}
91
- </span>
92
- )}
93
- {task.result?.assessment?.globalScore != null && (
94
- <span className={`text-[9px] font-medium ml-auto ${task.result.assessment.globalScore >= 0.7 ? "text-emerald-500" : "text-red-400"}`}>
95
- {Math.round(task.result.assessment.globalScore * 100)}%
96
- </span>
97
- )}
98
- </div>
99
- </div>
100
- <Handle type="source" position={Position.Bottom} className="!bg-muted-foreground/40 !border-none !w-2 !h-2" />
101
- </>
102
- );
103
- }
104
-
105
- const nodeTypes = { task: TaskNode };
106
-
107
- // ── Layout ──
108
-
109
- const NODE_WIDTH = 240;
110
- const NODE_HEIGHT = 120;
111
- const H_GAP = 60;
112
- const V_GAP = 80;
113
-
114
- function layoutNodes(tasks: Task[], projectId: string): { nodes: Node<TaskNodeData>[]; edges: Edge[] } {
115
- const taskMap = new Map(tasks.map(t => [t.id, t]));
116
-
117
- // Compute depth per task
118
- const depths = new Map<string, number>();
119
-
120
- function getDepth(taskId: string, visited = new Set<string>()): number {
121
- if (depths.has(taskId)) return depths.get(taskId)!;
122
- if (visited.has(taskId)) return 0;
123
- visited.add(taskId);
124
-
125
- const task = taskMap.get(taskId);
126
- if (!task?.dependsOn?.length) {
127
- depths.set(taskId, 0);
128
- return 0;
129
- }
130
-
131
- const maxParent = Math.max(...task.dependsOn.map(d => getDepth(d, visited)));
132
- const depth = maxParent + 1;
133
- depths.set(taskId, depth);
134
- return depth;
135
- }
136
-
137
- for (const task of tasks) getDepth(task.id);
138
-
139
- // Group by depth
140
- const layers = new Map<number, Task[]>();
141
- for (const task of tasks) {
142
- const d = depths.get(task.id) ?? 0;
143
- if (!layers.has(d)) layers.set(d, []);
144
- layers.get(d)!.push(task);
145
- }
146
-
147
- const maxDepth = Math.max(...layers.keys(), 0);
148
- const nodes: Node<TaskNodeData>[] = [];
149
-
150
- // Track task index globally
151
- let globalIndex = 0;
152
-
153
- for (let depth = 0; depth <= maxDepth; depth++) {
154
- const layerTasks = layers.get(depth) ?? [];
155
- const totalWidth = layerTasks.length * NODE_WIDTH + (layerTasks.length - 1) * H_GAP;
156
- const startX = -totalWidth / 2;
157
-
158
- layerTasks.forEach((task, i) => {
159
- nodes.push({
160
- id: task.id,
161
- type: "task",
162
- position: {
163
- x: startX + i * (NODE_WIDTH + H_GAP),
164
- y: depth * (NODE_HEIGHT + V_GAP),
165
- },
166
- data: { task, index: globalIndex++, projectId },
167
- });
168
- });
169
- }
170
-
171
- // Edges with arrows
172
- const edges: Edge[] = [];
173
- for (const task of tasks) {
174
- if (!task.dependsOn) continue;
175
- for (const depId of task.dependsOn) {
176
- const sourceTask = taskMap.get(depId);
177
- if (!sourceTask) continue;
178
-
179
- const isDone = sourceTask.status === "done";
180
- const isFailed = sourceTask.status === "failed";
181
- const isActive = sourceTask.status === "in_progress";
182
-
183
- const edgeColor = "#888";
184
-
185
- edges.push({
186
- id: `${depId}->${task.id}`,
187
- source: depId,
188
- target: task.id,
189
- type: "smoothstep",
190
- animated: isActive,
191
- markerEnd: {
192
- type: MarkerType.ArrowClosed,
193
- width: 14,
194
- height: 14,
195
- color: edgeColor,
196
- },
197
- style: {
198
- stroke: edgeColor,
199
- strokeWidth: 1.5,
200
- strokeDasharray: isActive ? undefined : "6 4",
201
- },
202
- });
203
- }
204
- }
205
-
206
- return { nodes, edges };
207
- }
208
-
209
- // ── Component ──
210
-
211
- interface MissionGraphProps {
212
- tasks: Task[];
213
- projectId: string;
214
- readonly?: boolean;
215
- }
216
-
217
- export function MissionGraph({ tasks, projectId, readonly = false }: MissionGraphProps) {
218
- const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
219
- const layout = layoutNodes(tasks, projectId);
220
- if (readonly) {
221
- for (const node of layout.nodes) {
222
- node.data.readonly = true;
223
- }
224
- }
225
- return layout;
226
- }, [tasks, projectId, readonly]);
227
-
228
- const [nodes, , onNodesChange] = useNodesState(initialNodes);
229
- const [edges, , onEdgesChange] = useEdgesState(initialEdges);
230
-
231
- return (
232
- <div className="h-[600px] w-full rounded-xl border border-border overflow-hidden">
233
- <ReactFlow
234
- nodes={nodes}
235
- edges={edges}
236
- onNodesChange={onNodesChange}
237
- onEdgesChange={onEdgesChange}
238
- nodeTypes={nodeTypes}
239
- fitView
240
- fitViewOptions={{ padding: 0.3 }}
241
- minZoom={0.2}
242
- maxZoom={2}
243
- proOptions={{ hideAttribution: true }}
244
- nodesDraggable
245
- nodesConnectable={false}
246
- defaultEdgeOptions={{ type: "smoothstep" }}
247
- style={{ background: "hsl(var(--card))" }}
248
- >
249
- <Background
250
- variant={BackgroundVariant.Dots}
251
- gap={20}
252
- size={1.5}
253
- color="#555"
254
- />
255
- <Controls
256
- showInteractive={false}
257
- className="!bg-card !border-border !shadow-none [&>button]:!bg-card [&>button]:!border-border [&>button]:!text-muted-foreground [&>button:hover]:!bg-secondary"
258
- />
259
- </ReactFlow>
260
- </div>
261
- );
262
- }
@@ -1,86 +0,0 @@
1
- "use client";
2
-
3
- import { usePathname } from "next/navigation";
4
- import Link from "next/link";
5
- import type { LucideIcon } from "lucide-react";
6
-
7
- interface Tab {
8
- label: string;
9
- href: string;
10
- icon?: LucideIcon;
11
- }
12
-
13
- export function NavTabs({
14
- basePath,
15
- tabs,
16
- className,
17
- orientation = "horizontal",
18
- }: {
19
- basePath: string;
20
- tabs: Tab[];
21
- className?: string;
22
- orientation?: "horizontal" | "vertical";
23
- }) {
24
- const pathname = usePathname();
25
- const vertical = orientation === "vertical";
26
-
27
- return (
28
- <div
29
- data-testid="nav-tabs"
30
- className={
31
- vertical
32
- ? `flex flex-col gap-0.5 ${className ?? ""}`
33
- : `flex gap-1 border-b border-border overflow-x-auto scrollbar-none ${className ?? ""}`
34
- }
35
- >
36
- {tabs.map((tab) => {
37
- const href = `${basePath}${tab.href}`;
38
- const active = tab.href === ""
39
- ? pathname === basePath
40
- : pathname.startsWith(href);
41
-
42
- if (vertical) {
43
- const Icon = tab.icon;
44
- return (
45
- <Link
46
- key={tab.label}
47
- href={href}
48
- data-testid={`tab-${tab.label.toLowerCase().replace(/\s+/g, "-")}`}
49
- className={`relative flex items-center gap-2.5 border-l-2 px-3 py-1.5 text-sm transition-colors ${
50
- active
51
- ? "border-foreground bg-foreground/5 font-medium text-foreground"
52
- : "border-transparent text-muted-foreground hover:border-border hover:text-foreground"
53
- }`}
54
- >
55
- {Icon && (
56
- <Icon
57
- className={`h-4 w-4 shrink-0 ${active ? "text-foreground" : "text-muted-foreground/60"}`}
58
- strokeWidth={1.5}
59
- />
60
- )}
61
- {tab.label}
62
- </Link>
63
- );
64
- }
65
-
66
- return (
67
- <Link
68
- key={tab.label}
69
- href={href}
70
- data-testid={`tab-${tab.label.toLowerCase().replace(/\s+/g, "-")}`}
71
- className={`relative px-3 py-2 text-sm transition-colors whitespace-nowrap ${
72
- active
73
- ? "text-foreground font-medium"
74
- : "text-muted-foreground hover:text-foreground"
75
- }`}
76
- >
77
- {tab.label}
78
- {active && (
79
- <span className="absolute bottom-0 left-0 right-0 h-[2px] bg-foreground" />
80
- )}
81
- </Link>
82
- );
83
- })}
84
- </div>
85
- );
86
- }