@polpo-ai/dashboard 0.1.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/LICENSE +13 -0
- package/README.md +44 -0
- package/app/(dashboard)/error.tsx +47 -0
- package/app/(dashboard)/layout.tsx +41 -0
- package/app/(dashboard)/loading.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
- package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
- package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
- package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
- package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
- package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
- package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
- package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
- package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
- package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
- package/app/(dashboard)/projects/[id]/page.tsx +68 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
- package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
- package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
- package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
- package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
- package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
- package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
- package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
- package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
- package/app/(dashboard)/projects/projects-list.tsx +126 -0
- package/app/(playground)/error.tsx +42 -0
- package/app/(playground)/layout.tsx +27 -0
- package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
- package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
- package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
- package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
- package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +385 -0
- package/app/icon.svg +6 -0
- package/app/layout.tsx +39 -0
- package/app/page.tsx +11 -0
- package/components/ai-elements/code-block.tsx +562 -0
- package/components/dashboard/agent-capabilities.tsx +326 -0
- package/components/dashboard/agent-identity-header.tsx +50 -0
- package/components/dashboard/agent-model-picker.tsx +195 -0
- package/components/dashboard/agent-model-selector.tsx +121 -0
- package/components/dashboard/agent-picker-card.tsx +46 -0
- package/components/dashboard/agent-studio.tsx +242 -0
- package/components/dashboard/agents-table.tsx +48 -0
- package/components/dashboard/breadcrumb.tsx +70 -0
- package/components/dashboard/builder-chat.tsx +418 -0
- package/components/dashboard/chat-shell.tsx +40 -0
- package/components/dashboard/client-picker.tsx +63 -0
- package/components/dashboard/command-snippet.tsx +77 -0
- package/components/dashboard/connect-dialog.tsx +680 -0
- package/components/dashboard/copy-button.tsx +45 -0
- package/components/dashboard/copy-card.tsx +48 -0
- package/components/dashboard/file-browser.tsx +161 -0
- package/components/dashboard/hint.tsx +22 -0
- package/components/dashboard/inference-mode.tsx +15 -0
- package/components/dashboard/manual-refresh-button.tsx +52 -0
- package/components/dashboard/markdown.tsx +31 -0
- package/components/dashboard/mcp-install-panel.tsx +196 -0
- package/components/dashboard/mission-graph.tsx +262 -0
- package/components/dashboard/nav-tabs.tsx +86 -0
- package/components/dashboard/polpo-chat.tsx +468 -0
- package/components/dashboard/project-copilot.tsx +253 -0
- package/components/dashboard/sdk-snippet-panel.tsx +194 -0
- package/components/dashboard/section-header.tsx +37 -0
- package/components/dashboard/settings-form.tsx +2477 -0
- package/components/dashboard/sidebar.tsx +385 -0
- package/components/dashboard/skeletons.tsx +1054 -0
- package/components/dashboard/skills-install-wizard.tsx +273 -0
- package/components/dashboard/swarm-runs.tsx +316 -0
- package/components/dashboard/tab-toggle.tsx +35 -0
- package/components/dashboard/task-studio.tsx +224 -0
- package/components/dashboard/top-header.tsx +203 -0
- package/components/dashboard/webhook-deliveries.tsx +234 -0
- package/components/icons/coding-agents.tsx +151 -0
- package/components/json-ld.tsx +8 -0
- package/components/providers.tsx +25 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +356 -0
- package/components/ui/command.tsx +196 -0
- package/components/ui/dialog.tsx +157 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/multi-select.tsx +161 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +135 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/tabs.tsx +56 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/hooks/use-desktop-sidebar.tsx +50 -0
- package/hooks/use-mobile-sidebar.tsx +37 -0
- package/index.ts +14 -0
- package/lib/api.ts +194 -0
- package/lib/builder-context.ts +60 -0
- package/lib/data-client.ts +68 -0
- package/lib/get-query-client.ts +25 -0
- package/lib/polpo-client.tsx +49 -0
- package/lib/tool-catalog.ts +234 -0
- package/lib/use-event-catalog.ts +28 -0
- package/lib/utils.ts +6 -0
- package/package.json +99 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import {
|
|
7
|
+
Boxes, Settings, BarChart3,
|
|
8
|
+
Home, ListChecks, Target, Users,
|
|
9
|
+
BookMarked, Brain, Menu, KeyRound,
|
|
10
|
+
MessageCircle, Library, Waypoints, CreditCard,
|
|
11
|
+
PanelLeft, PanelRight, MessageSquare, ExternalLink, Clock,
|
|
12
|
+
LineChart, ArrowLeft,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import {
|
|
15
|
+
Sheet, SheetContent, SheetTitle,
|
|
16
|
+
} from "#/components/ui/sheet";
|
|
17
|
+
import {
|
|
18
|
+
Tooltip, TooltipTrigger, TooltipContent,
|
|
19
|
+
} from "#/components/ui/tooltip";
|
|
20
|
+
import { useMobileSidebar } from "#/hooks/use-mobile-sidebar";
|
|
21
|
+
import { useDesktopSidebar } from "#/hooks/use-desktop-sidebar";
|
|
22
|
+
import { useQuery } from "@tanstack/react-query";
|
|
23
|
+
import { fetchControlPlane } from "#/lib/data-client";
|
|
24
|
+
import { ProjectSwitcher } from "#/components/dashboard/top-header";
|
|
25
|
+
|
|
26
|
+
const globalNav = [
|
|
27
|
+
{ label: "Projects", href: "/projects", icon: Boxes },
|
|
28
|
+
{ label: "AI Gateway", href: "/llm-gateway", icon: Waypoints },
|
|
29
|
+
{ label: "Usage", href: "/usage", icon: BarChart3 },
|
|
30
|
+
{ label: "Billing", href: "/billing", icon: CreditCard },
|
|
31
|
+
{ label: "API Keys", href: "/keys", icon: KeyRound },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
type NavItem = {
|
|
35
|
+
label: string;
|
|
36
|
+
href: string;
|
|
37
|
+
icon: typeof Boxes;
|
|
38
|
+
/** Open in a new browser tab — used for the standalone Playground surface. */
|
|
39
|
+
external?: boolean;
|
|
40
|
+
};
|
|
41
|
+
type NavGroup = { separator?: boolean; heading?: string; items: NavItem[] };
|
|
42
|
+
|
|
43
|
+
const projectNav: NavGroup[] = [
|
|
44
|
+
{
|
|
45
|
+
heading: "Control",
|
|
46
|
+
items: [
|
|
47
|
+
{ label: "Dashboard", href: "", icon: Home },
|
|
48
|
+
{ label: "Spending", href: "/spending", icon: LineChart },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
separator: true,
|
|
53
|
+
heading: "Manage",
|
|
54
|
+
items: [
|
|
55
|
+
{ label: "Agents", href: "/agents", icon: Users },
|
|
56
|
+
{ label: "Skills Library", href: "/skills", icon: BookMarked },
|
|
57
|
+
{ label: "Shared Memory", href: "/memory", icon: Brain },
|
|
58
|
+
{ label: "Files", href: "/storage", icon: Library },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
separator: true,
|
|
63
|
+
heading: "Activity",
|
|
64
|
+
items: [
|
|
65
|
+
{ label: "Sessions", href: "/sessions", icon: MessageCircle },
|
|
66
|
+
{ label: "Tasks", href: "/tasks", icon: ListChecks },
|
|
67
|
+
{ label: "Missions", href: "/missions", icon: Target },
|
|
68
|
+
{ label: "Schedules", href: "/schedules", icon: Clock },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
separator: true,
|
|
73
|
+
items: [
|
|
74
|
+
{ label: "Playground", href: "/playground", icon: MessageSquare, external: true },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const PROJECT_ID_RE = /^\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/;
|
|
80
|
+
|
|
81
|
+
function extractProjectId(pathname: string): string | null {
|
|
82
|
+
const match = pathname.match(PROJECT_ID_RE);
|
|
83
|
+
return match ? match[1] : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function NavLink({
|
|
87
|
+
href,
|
|
88
|
+
icon: Icon,
|
|
89
|
+
label,
|
|
90
|
+
active,
|
|
91
|
+
collapsed,
|
|
92
|
+
external,
|
|
93
|
+
}: {
|
|
94
|
+
href: string;
|
|
95
|
+
icon: typeof Boxes;
|
|
96
|
+
label: string;
|
|
97
|
+
active: boolean;
|
|
98
|
+
collapsed?: boolean;
|
|
99
|
+
external?: boolean;
|
|
100
|
+
}) {
|
|
101
|
+
const link = (
|
|
102
|
+
<Link
|
|
103
|
+
href={href}
|
|
104
|
+
target={external ? "_blank" : undefined}
|
|
105
|
+
rel={external ? "noopener noreferrer" : undefined}
|
|
106
|
+
data-testid={`nav-${label.toLowerCase().replace(/\s+/g, "-")}`}
|
|
107
|
+
className={`flex items-center text-sm transition-colors ${
|
|
108
|
+
collapsed ? "justify-center p-1.5" : "gap-2 px-2 py-1.5"
|
|
109
|
+
} ${
|
|
110
|
+
active
|
|
111
|
+
? "bg-foreground/10 text-foreground font-medium"
|
|
112
|
+
: "text-muted-foreground hover:text-foreground hover:bg-foreground/5"
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
<Icon
|
|
116
|
+
className={`shrink-0 ${active ? "h-5 w-5 text-foreground" : "h-[18px] w-[18px] text-muted-foreground"}`}
|
|
117
|
+
strokeWidth={active ? 2 : 1.5}
|
|
118
|
+
/>
|
|
119
|
+
{!collapsed && (
|
|
120
|
+
<span className="truncate flex-1 inline-flex items-center gap-1.5">
|
|
121
|
+
{label}
|
|
122
|
+
{external && (
|
|
123
|
+
<ExternalLink className="h-3 w-3 opacity-50" strokeWidth={1.5} />
|
|
124
|
+
)}
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
</Link>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (collapsed) {
|
|
131
|
+
return (
|
|
132
|
+
<Tooltip>
|
|
133
|
+
<TooltipTrigger render={link} />
|
|
134
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
135
|
+
</Tooltip>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return link;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Inner sidebar content — reused by both desktop aside and mobile sheet */
|
|
143
|
+
function SidebarContent({ collapsed = false }: { collapsed?: boolean }) {
|
|
144
|
+
const pathname = usePathname();
|
|
145
|
+
const projectId = extractProjectId(pathname);
|
|
146
|
+
const inProject = !!projectId;
|
|
147
|
+
const { toggle } = useDesktopSidebar();
|
|
148
|
+
|
|
149
|
+
// Same React Query key as the top-header → cache hit when both render
|
|
150
|
+
// (no double fetch). Only fires when we're inside a project route.
|
|
151
|
+
const { data: project } = useQuery({
|
|
152
|
+
queryKey: ["project", projectId],
|
|
153
|
+
queryFn: () =>
|
|
154
|
+
fetchControlPlane<{ name: string; orgId: string }>(
|
|
155
|
+
`/v1/projects/${projectId}`,
|
|
156
|
+
),
|
|
157
|
+
enabled: !!projectId,
|
|
158
|
+
staleTime: 60_000,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function isActive(href: string) {
|
|
162
|
+
if (inProject && href === `/projects/${projectId}`) return pathname === href;
|
|
163
|
+
return pathname.startsWith(href);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
{/* Nav */}
|
|
169
|
+
<nav
|
|
170
|
+
data-testid="sidebar-nav"
|
|
171
|
+
className={`flex-1 overflow-y-auto pt-3 ${collapsed ? "px-1" : "px-2"}`}
|
|
172
|
+
>
|
|
173
|
+
{/* Project switcher — same component as the top header (different
|
|
174
|
+
visual variant). The header switcher has been removed; this is
|
|
175
|
+
the sole entry point for switching/renaming the active project.
|
|
176
|
+
Above it: "All projects" back link so escaping the project
|
|
177
|
+
context is one click regardless of the current page. */}
|
|
178
|
+
{inProject && !collapsed && projectId && project?.name && project?.orgId && (
|
|
179
|
+
<div className="mb-5 px-1 flex flex-col gap-2.5">
|
|
180
|
+
<Link
|
|
181
|
+
href="/projects"
|
|
182
|
+
data-testid="sidebar-all-projects"
|
|
183
|
+
className="inline-flex items-center gap-1.5 px-1.5 text-[11px] text-muted-foreground/70 hover:text-foreground transition-colors"
|
|
184
|
+
>
|
|
185
|
+
<ArrowLeft className="h-3 w-3" />
|
|
186
|
+
All projects
|
|
187
|
+
</Link>
|
|
188
|
+
<ProjectSwitcher
|
|
189
|
+
projectId={projectId}
|
|
190
|
+
projectName={project.name}
|
|
191
|
+
orgId={project.orgId}
|
|
192
|
+
variant="sidebar"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
<div className="flex flex-col gap-1.5">
|
|
198
|
+
{inProject
|
|
199
|
+
? projectNav.map((group, gi) => (
|
|
200
|
+
<div key={gi} className="flex flex-col gap-1.5">
|
|
201
|
+
{group.separator && (
|
|
202
|
+
<div className={`my-2 border-t border-border ${collapsed ? "mx-1" : "mx-3"}`} />
|
|
203
|
+
)}
|
|
204
|
+
{group.heading && !collapsed && (
|
|
205
|
+
<div className="px-2 pt-1 pb-0.5 text-[10px] font-medium uppercase tracking-[0.12em] text-muted-foreground/50">
|
|
206
|
+
{group.heading}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
{group.items.map((item) => {
|
|
210
|
+
const href = `/projects/${projectId}${item.href}`;
|
|
211
|
+
return (
|
|
212
|
+
<NavLink
|
|
213
|
+
key={href}
|
|
214
|
+
href={href}
|
|
215
|
+
icon={item.icon}
|
|
216
|
+
label={item.label}
|
|
217
|
+
active={isActive(href)}
|
|
218
|
+
collapsed={collapsed}
|
|
219
|
+
external={item.external}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</div>
|
|
224
|
+
))
|
|
225
|
+
: (
|
|
226
|
+
<>
|
|
227
|
+
{!collapsed && (
|
|
228
|
+
<div className="px-2 pt-1 pb-0.5 text-[10px] font-medium uppercase tracking-[0.12em] text-muted-foreground/50">
|
|
229
|
+
Organization
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
{globalNav.map((item) => (
|
|
233
|
+
<NavLink
|
|
234
|
+
key={item.href}
|
|
235
|
+
{...item}
|
|
236
|
+
active={isActive(item.href)}
|
|
237
|
+
collapsed={collapsed}
|
|
238
|
+
/>
|
|
239
|
+
))}
|
|
240
|
+
</>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
</div>
|
|
244
|
+
</nav>
|
|
245
|
+
|
|
246
|
+
{/* Bottom — contextual Settings + collapse toggle */}
|
|
247
|
+
{(() => {
|
|
248
|
+
const settingsHref = inProject ? `/projects/${projectId}/settings` : "/settings";
|
|
249
|
+
const settingsLabel = inProject ? "Project Settings" : "Settings";
|
|
250
|
+
const settingsActive = inProject
|
|
251
|
+
? pathname === settingsHref
|
|
252
|
+
: pathname.startsWith("/settings");
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div className={`py-2 shrink-0 ${collapsed ? "px-1 space-y-0.5" : "px-2"}`}>
|
|
256
|
+
{collapsed ? (
|
|
257
|
+
<>
|
|
258
|
+
<Link
|
|
259
|
+
href={settingsHref}
|
|
260
|
+
data-testid="nav-settings"
|
|
261
|
+
aria-label={settingsLabel}
|
|
262
|
+
className={`flex items-center justify-center rounded-md p-1.5 transition-colors ${
|
|
263
|
+
settingsActive
|
|
264
|
+
? "text-foreground bg-foreground/5"
|
|
265
|
+
: "text-muted-foreground/70 hover:text-foreground hover:bg-secondary/50"
|
|
266
|
+
}`}
|
|
267
|
+
>
|
|
268
|
+
<Settings className="h-[18px] w-[18px]" strokeWidth={1.5} />
|
|
269
|
+
</Link>
|
|
270
|
+
<CollapseToggle collapsed onToggle={toggle} />
|
|
271
|
+
</>
|
|
272
|
+
) : (
|
|
273
|
+
<div className="flex items-center gap-1.5">
|
|
274
|
+
<Link
|
|
275
|
+
href={settingsHref}
|
|
276
|
+
data-testid="nav-settings"
|
|
277
|
+
className={`group flex-1 flex items-center gap-2 px-2 py-1.5 text-sm transition-colors ${
|
|
278
|
+
settingsActive
|
|
279
|
+
? "text-foreground"
|
|
280
|
+
: "text-muted-foreground/70 hover:text-foreground"
|
|
281
|
+
}`}
|
|
282
|
+
>
|
|
283
|
+
<Settings className="h-[18px] w-[18px]" strokeWidth={1.5} />
|
|
284
|
+
<span>{settingsLabel}</span>
|
|
285
|
+
</Link>
|
|
286
|
+
<CollapseToggle collapsed={false} onToggle={toggle} inline />
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
})()}
|
|
292
|
+
</>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function CollapseToggle({
|
|
297
|
+
collapsed,
|
|
298
|
+
onToggle,
|
|
299
|
+
inline,
|
|
300
|
+
}: {
|
|
301
|
+
collapsed: boolean;
|
|
302
|
+
onToggle: () => void;
|
|
303
|
+
inline?: boolean;
|
|
304
|
+
}) {
|
|
305
|
+
const Icon = collapsed ? PanelLeft : PanelRight;
|
|
306
|
+
const label = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
307
|
+
|
|
308
|
+
const btn = (
|
|
309
|
+
<button
|
|
310
|
+
onClick={onToggle}
|
|
311
|
+
aria-label={label}
|
|
312
|
+
data-testid="sidebar-toggle"
|
|
313
|
+
className={`hidden md:flex items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-secondary/50 ${
|
|
314
|
+
inline ? "p-2 shrink-0" : "p-2 w-full"
|
|
315
|
+
}`}
|
|
316
|
+
>
|
|
317
|
+
<Icon className="h-4 w-4" strokeWidth={1.5} />
|
|
318
|
+
</button>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<Tooltip>
|
|
323
|
+
<TooltipTrigger render={btn} />
|
|
324
|
+
<TooltipContent side={collapsed ? "right" : "top"}>{label}</TooltipContent>
|
|
325
|
+
</Tooltip>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Desktop sidebar — hidden on mobile, collapsible to icon strip on desktop */
|
|
330
|
+
export function Sidebar() {
|
|
331
|
+
const { open } = useDesktopSidebar();
|
|
332
|
+
const collapsed = !open;
|
|
333
|
+
return (
|
|
334
|
+
<aside
|
|
335
|
+
data-testid="sidebar"
|
|
336
|
+
data-open={open}
|
|
337
|
+
className={`hidden md:flex shrink-0 flex-col border-r border-border bg-card overflow-hidden transition-[width] duration-150 ease-out ${
|
|
338
|
+
collapsed ? "w-12" : "w-[200px]"
|
|
339
|
+
}`}
|
|
340
|
+
>
|
|
341
|
+
<SidebarContent collapsed={collapsed} />
|
|
342
|
+
</aside>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Mobile sidebar — sheet drawer from left (always expanded) */
|
|
347
|
+
export function MobileSidebar() {
|
|
348
|
+
const { open, setOpen } = useMobileSidebar();
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
352
|
+
<SheetContent
|
|
353
|
+
side="left"
|
|
354
|
+
showCloseButton={false}
|
|
355
|
+
className="w-72 p-0 bg-card border-border"
|
|
356
|
+
>
|
|
357
|
+
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
358
|
+
<div className="flex h-full flex-col">
|
|
359
|
+
<SidebarContent />
|
|
360
|
+
</div>
|
|
361
|
+
</SheetContent>
|
|
362
|
+
</Sheet>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Mobile top bar with hamburger — shown only on mobile */
|
|
367
|
+
export function MobileHeader() {
|
|
368
|
+
const { toggle } = useMobileSidebar();
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<header className="flex md:hidden h-14 items-center gap-3 border-b border-border bg-card px-4 shrink-0">
|
|
372
|
+
<button
|
|
373
|
+
onClick={toggle}
|
|
374
|
+
data-testid="mobile-menu-toggle"
|
|
375
|
+
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-secondary/50 transition-colors"
|
|
376
|
+
aria-label="Toggle navigation"
|
|
377
|
+
>
|
|
378
|
+
<Menu className="h-5 w-5" />
|
|
379
|
+
</button>
|
|
380
|
+
<Link href="/projects" className="flex items-center">
|
|
381
|
+
<Image src="/polpo-logo.svg" alt="Polpo" width={92} height={20} priority className="h-5 w-auto" />
|
|
382
|
+
</Link>
|
|
383
|
+
</header>
|
|
384
|
+
);
|
|
385
|
+
}
|