Skip to content

Commit e490d91

Browse files
feat(web): add global metadata visibility toggle
Introduce MetadataContext + useMetadata hook backed by localStorage and wire a Braces button into the sidebar footer that toggles raw metadata visibility across the app. Replace per-page collapsible metadata in PeerDetail and WorkspaceDetail with a global animated reveal styled distinctly (warning-colored card) to signal raw payload. Also adopts the shared Breadcrumb in both detail pages.
1 parent 62cae68 commit e490d91

6 files changed

Lines changed: 110 additions & 94 deletions

File tree

packages/web/src/components/layout/Sidebar.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Link, useMatchRoute } from "@tanstack/react-router";
22
import { AnimatePresence, motion } from "framer-motion";
33
import {
44
Boxes,
5+
Braces,
56
Check,
67
ChevronRight,
78
ChevronsUpDown,
@@ -21,6 +22,7 @@ import { HealthDot } from "@/components/shared/HealthDot";
2122
import { useDemo } from "@/hooks/useDemo";
2223
import { useHealthStatus } from "@/hooks/useHealthStatus";
2324
import { useInstances } from "@/hooks/useInstances";
25+
import { useMetadata } from "@/hooks/useMetadata";
2426
import { useTheme } from "@/hooks/useTheme";
2527
import { COLOR } from "@/lib/constants";
2628

@@ -42,6 +44,7 @@ export function Sidebar() {
4244
const { instances, active, activate } = useInstances();
4345
const { theme, toggle } = useTheme();
4446
const { demo, toggle: toggleDemo, mask } = useDemo();
47+
const { showMetadata, toggle: toggleMeta } = useMetadata();
4548
const { data: health } = useHealthStatus();
4649
const [switcherOpen, setSwitcherOpen] = useState(false);
4750
const switcherRef = useRef<HTMLDivElement | null>(null);
@@ -285,7 +288,7 @@ export function Sidebar() {
285288
</AnimatePresence>
286289
</nav>
287290

288-
{/* Theme toggle + footer */}
291+
{/* Footer — version, demo, metadata, theme */}
289292
<div
290293
className="px-3 sm:px-5 py-3 flex items-center justify-between"
291294
style={{ borderTop: "1px solid var(--border)" }}
@@ -311,6 +314,19 @@ export function Sidebar() {
311314
<Eye className="w-3.5 h-3.5" strokeWidth={1.5} />
312315
)}
313316
</button>
317+
<button
318+
type="button"
319+
onClick={toggleMeta}
320+
className="w-7 h-7 rounded-md flex items-center justify-center transition-colors"
321+
style={{
322+
background: showMetadata ? "rgba(245,158,11,0.1)" : "var(--surface)",
323+
border: `1px solid ${showMetadata ? "rgba(245,158,11,0.3)" : "var(--border)"}`,
324+
color: showMetadata ? COLOR.warning : "var(--text-3)",
325+
}}
326+
title={showMetadata ? "Hide raw metadata" : "Show raw metadata"}
327+
>
328+
<Braces className="w-3.5 h-3.5" strokeWidth={1.5} />
329+
</button>
314330
<button
315331
type="button"
316332
onClick={toggle}

packages/web/src/components/peers/PeerDetail.tsx

Lines changed: 26 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
1-
import { Link, useNavigate, useParams } from "@tanstack/react-router";
1+
import { useNavigate, useParams } from "@tanstack/react-router";
22
import { AnimatePresence, motion } from "framer-motion";
3-
import {
4-
ChevronDown,
5-
Eye,
6-
EyeOff,
7-
MessageCircle,
8-
Save,
9-
Search,
10-
User,
11-
Users,
12-
X,
13-
} from "lucide-react";
3+
import { Eye, EyeOff, MessageCircle, Save, Search, User, Users, X } from "lucide-react";
144
import { useState } from "react";
155
import {
166
usePeer,
@@ -20,6 +10,7 @@ import {
2010
useSearchPeer,
2111
useSetPeerCard,
2212
} from "@/api/queries";
13+
import { Breadcrumb } from "@/components/layout/Breadcrumb";
2314
import { Badge } from "@/components/shared/Badge";
2415
import { ErrorAlert } from "@/components/shared/ErrorAlert";
2516
import { JsonViewer } from "@/components/shared/JsonViewer";
@@ -38,10 +29,12 @@ import {
3829
SectionHeading,
3930
} from "@/components/ui/typography";
4031
import { useDemo } from "@/hooks/useDemo";
32+
import { useMetadata } from "@/hooks/useMetadata";
4133
import { COLOR } from "@/lib/constants";
4234

4335
export function PeerDetail() {
4436
const { mask } = useDemo();
37+
const { showMetadata } = useMetadata();
4538
const { workspaceId, peerId } = useParams({ strict: false }) as {
4639
workspaceId: string;
4740
peerId: string;
@@ -65,7 +58,6 @@ export function PeerDetail() {
6558

6659
const [cardDraft, setCardDraft] = useState<string | null>(null);
6760
const [searchQuery, setSearchQuery] = useState("");
68-
const [metaExpanded, setMetaExpanded] = useState(false);
6961

7062
const observeMe = (peer as { configuration?: { observe_me?: boolean } } | undefined)
7163
?.configuration?.observe_me;
@@ -79,27 +71,7 @@ export function PeerDetail() {
7971
return (
8072
<div className="page-container page-container--xl">
8173
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }}>
82-
<div className="flex items-center gap-2 text-xs mb-4" style={{ color: "var(--text-3)" }}>
83-
<Link to="/workspaces" className="hover:underline">
84-
Workspaces
85-
</Link>
86-
<span>/</span>
87-
<Link
88-
to="/workspaces/$workspaceId"
89-
params={{ workspaceId } as never}
90-
className="hover:underline font-mono"
91-
>
92-
{mask(workspaceId)}
93-
</Link>
94-
<span>/</span>
95-
<Link
96-
to="/workspaces/$workspaceId/peers"
97-
params={{ workspaceId } as never}
98-
className="hover:underline"
99-
>
100-
Peers
101-
</Link>
102-
</div>
74+
<Breadcrumb />
10375

10476
<div className="flex items-start justify-between gap-4">
10577
<div>
@@ -377,47 +349,29 @@ export function PeerDetail() {
377349
)}
378350
</motion.div>
379351

380-
{/* Metadata — collapsible */}
381-
<motion.div
382-
initial={{ opacity: 0, y: 8 }}
383-
animate={{ opacity: 1, y: 0 }}
384-
transition={{ delay: 0.25 }}
385-
className="rounded-xl theme-card overflow-hidden"
386-
>
387-
<button
388-
type="button"
389-
onClick={() => setMetaExpanded((v) => !v)}
390-
className="w-full flex items-center justify-between px-5 py-4"
391-
style={{ color: "var(--text-3)" }}
392-
>
393-
<SectionHeading className="mb-0">Metadata</SectionHeading>
352+
{/* Metadata — global toggle */}
353+
<AnimatePresence>
354+
{showMetadata && (
394355
<motion.div
395-
animate={{ rotate: metaExpanded ? 0 : -90 }}
396-
transition={{ duration: 0.15 }}
356+
initial={{ opacity: 0, height: 0 }}
357+
animate={{ opacity: 1, height: "auto" }}
358+
exit={{ opacity: 0, height: 0 }}
359+
transition={{ duration: 0.2 }}
360+
className="overflow-hidden"
397361
>
398-
<ChevronDown
399-
className="w-4 h-4"
400-
strokeWidth={2}
401-
style={{ color: COLOR.dimText }}
402-
/>
403-
</motion.div>
404-
</button>
405-
<AnimatePresence initial={false}>
406-
{metaExpanded && (
407-
<motion.div
408-
initial={{ height: 0, opacity: 0 }}
409-
animate={{ height: "auto", opacity: 1 }}
410-
exit={{ height: 0, opacity: 0 }}
411-
transition={{ duration: 0.2 }}
412-
className="overflow-hidden"
362+
<div
363+
className="rounded-xl p-5"
364+
style={{
365+
background: "rgba(245,158,11,0.04)",
366+
border: "1px solid rgba(245,158,11,0.2)",
367+
}}
413368
>
414-
<div className="px-5 pb-5">
415-
<JsonViewer data={peer.metadata} maxHeight="300px" />
416-
</div>
417-
</motion.div>
418-
)}
419-
</AnimatePresence>
420-
</motion.div>
369+
<SectionHeading style={{ color: COLOR.warning }}>Metadata</SectionHeading>
370+
<JsonViewer data={peer.metadata} maxHeight="300px" />
371+
</div>
372+
</motion.div>
373+
)}
374+
</AnimatePresence>
421375
</>
422376
)}
423377
</div>

packages/web/src/components/workspaces/WorkspaceDetail.tsx

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Link, useNavigate, useParams } from "@tanstack/react-router";
22
import { AnimatePresence, motion } from "framer-motion";
33
import {
4-
ArrowLeft,
54
Boxes,
65
ChevronDown,
76
CircleDot,
@@ -14,6 +13,7 @@ import {
1413
} from "lucide-react";
1514
import { useState } from "react";
1615
import { useDeleteWorkspace, useQueueStatus, useScheduleDream, useWorkspace } from "@/api/queries";
16+
import { Breadcrumb } from "@/components/layout/Breadcrumb";
1717
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
1818
import { ErrorAlert } from "@/components/shared/ErrorAlert";
1919
import { JsonViewer } from "@/components/shared/JsonViewer";
@@ -22,6 +22,7 @@ import { Button } from "@/components/ui/button";
2222
import { Body, Caption, PageTitle, SectionHeading } from "@/components/ui/typography";
2323
import { ScheduleDreamModal } from "@/components/workspaces/ScheduleDreamModal";
2424
import { useDemo } from "@/hooks/useDemo";
25+
import { useMetadata } from "@/hooks/useMetadata";
2526
import { COLOR } from "@/lib/constants";
2627

2728
const NAV_SECTIONS = [
@@ -53,6 +54,7 @@ const NAV_SECTIONS = [
5354

5455
export function WorkspaceDetail() {
5556
const { mask } = useDemo();
57+
const { showMetadata } = useMetadata();
5658
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
5759
const navigate = useNavigate();
5860

@@ -74,14 +76,7 @@ export function WorkspaceDetail() {
7476
return (
7577
<div className="page-container page-container--wide">
7678
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }}>
77-
<Link
78-
to="/workspaces"
79-
className="inline-flex items-center gap-1.5 text-xs mb-4 transition-colors"
80-
style={{ color: "var(--text-3)" }}
81-
>
82-
<ArrowLeft className="w-3 h-3" strokeWidth={1.5} />
83-
Workspaces
84-
</Link>
79+
<Breadcrumb />
8580
<div className="flex items-start justify-between gap-4 mb-1">
8681
<div className="flex items-center gap-2 min-w-0">
8782
<Boxes
@@ -125,7 +120,7 @@ export function WorkspaceDetail() {
125120
<Link
126121
to={`/workspaces/$workspaceId/${s.to}` as never}
127122
params={{ workspaceId } as never}
128-
className="block rounded-xl p-5 group transition-all theme-card"
123+
className="block h-full rounded-xl p-5 group transition-all theme-card"
129124
>
130125
<Icon
131126
className="w-5 h-5 mb-3"
@@ -309,16 +304,29 @@ export function WorkspaceDetail() {
309304
</motion.div>
310305
)}
311306

312-
{/* Metadata */}
313-
<motion.div
314-
initial={{ opacity: 0 }}
315-
animate={{ opacity: 1 }}
316-
transition={{ delay: 0.38 }}
317-
className="rounded-xl p-5 theme-card"
318-
>
319-
<SectionHeading>Metadata</SectionHeading>
320-
<JsonViewer data={workspace.metadata} />
321-
</motion.div>
307+
{/* Metadata — global toggle */}
308+
<AnimatePresence>
309+
{showMetadata && (
310+
<motion.div
311+
initial={{ opacity: 0, height: 0 }}
312+
animate={{ opacity: 1, height: "auto" }}
313+
exit={{ opacity: 0, height: 0 }}
314+
transition={{ duration: 0.2 }}
315+
className="overflow-hidden"
316+
>
317+
<div
318+
className="rounded-xl p-5"
319+
style={{
320+
background: "rgba(245,158,11,0.04)",
321+
border: "1px solid rgba(245,158,11,0.2)",
322+
}}
323+
>
324+
<SectionHeading style={{ color: COLOR.warning }}>Metadata</SectionHeading>
325+
<JsonViewer data={workspace.metadata} />
326+
</div>
327+
</motion.div>
328+
)}
329+
</AnimatePresence>
322330
</div>
323331
)}
324332
</div>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createContext, type ReactNode, useContext, useState } from "react";
2+
3+
const STORAGE_KEY = "openconcho:show-metadata";
4+
5+
interface MetadataContextValue {
6+
showMetadata: boolean;
7+
toggle: () => void;
8+
}
9+
10+
const MetadataContext = createContext<MetadataContextValue | null>(null);
11+
12+
export function MetadataProvider({ children }: { children: ReactNode }) {
13+
const [showMetadata, setShowMetadata] = useState<boolean>(
14+
() => localStorage.getItem(STORAGE_KEY) === "true",
15+
);
16+
17+
function toggle() {
18+
setShowMetadata((v) => {
19+
const next = !v;
20+
localStorage.setItem(STORAGE_KEY, String(next));
21+
return next;
22+
});
23+
}
24+
25+
return (
26+
<MetadataContext.Provider value={{ showMetadata, toggle }}>{children}</MetadataContext.Provider>
27+
);
28+
}
29+
30+
export function useMetadataContext(): MetadataContextValue {
31+
const ctx = useContext(MetadataContext);
32+
if (!ctx) throw new Error("useMetadataContext must be used within MetadataProvider");
33+
return ctx;
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useMetadataContext as useMetadata } from "@/context/MetadataContext";

packages/web/src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createRouter, RouterProvider } from "@tanstack/react-router";
33
import { StrictMode } from "react";
44
import { createRoot } from "react-dom/client";
55
import { DemoProvider } from "./context/DemoContext";
6+
import { MetadataProvider } from "./context/MetadataContext";
67
import { initDeepLinks } from "./lib/deep-link";
78
import { routeTree } from "./routeTree.gen";
89
import "./index.css";
@@ -37,7 +38,9 @@ createRoot(root).render(
3738
<StrictMode>
3839
<QueryClientProvider client={queryClient}>
3940
<DemoProvider>
40-
<RouterProvider router={router} />
41+
<MetadataProvider>
42+
<RouterProvider router={router} />
43+
</MetadataProvider>
4144
</DemoProvider>
4245
</QueryClientProvider>
4346
</StrictMode>,

0 commit comments

Comments
 (0)