refactor(ui): compose Team Memory from the shadcn/ruixen primitives

Per the unifying template (npx shadcn add ruixen.com/r/[component]): the chrome
now uses Button + Badge + the app's Tailwind utility patterns (matching StatPill/
App.tsx) inside the page shell, instead of a hand-rolled .pm-* stylesheet. Only
the SVG node-link canvas stays bespoke (3 SVG-only CSS rules). No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb-iam
2026-06-27 19:46:48 -07:00
parent 7c572fbbcb
commit a49501b3c4
+75 -92
View File
@@ -1,6 +1,8 @@
import { useEffect, useMemo, useState, type CSSProperties } from 'react'; import { useEffect, useMemo, useState, type CSSProperties } from 'react';
import type { PodGraph, PodGraphNode, PodGraphEdge, PodGraphNodeKind } from '@podman/shared'; import type { PodGraph, PodGraphNode, PodGraphEdge, PodGraphNodeKind } from '@podman/shared';
import { fetchPodGraph } from '../lib/graph.js'; import { fetchPodGraph } from '../lib/graph.js';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
type Mode = 'risk' | 'learn' | 'all'; type Mode = 'risk' | 'learn' | 'all';
@@ -142,102 +144,70 @@ export function GraphView({ podId, onClose }: { podId: string; onClose: () => vo
setSelected(null); setSelected(null);
} }
const toggleVariant = (m: Mode) => (mode === m && !selected ? 'default' : 'outline');
return ( return (
<div className="pm-graph"> <div className="min-h-screen bg-background text-foreground">
<div className="mx-auto w-full max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<style>{` <style>{`
.pm-graph{font-family:inherit;background:var(--card);color:var(--foreground);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.pm-graph *{box-sizing:border-box}
.pm-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border)}
.pm-ttl{font-weight:600;font-size:16px;color:var(--foreground)}
.pm-sub{font-size:12px;color:var(--muted-foreground);margin-top:3px}
.pm-x{background:transparent;border:1px solid var(--border);color:var(--foreground);font-size:13px;padding:6px 12px;border-radius:8px;cursor:pointer}
.pm-x:hover{background:var(--accent)}
.pm-bar{display:flex;gap:8px;padding:12px 16px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.pm-btn{font-size:13px;color:var(--foreground);background:transparent;border:1px solid var(--border);padding:6px 12px;cursor:pointer;border-radius:8px}
.pm-btn:hover{background:var(--accent)}
.pm-btn.on{background:var(--primary);border-color:var(--primary);color:var(--primary-foreground)}
.pm-grid{display:grid;grid-template-columns:190px 1fr 250px}
.pm-col{padding:14px}
.pm-railR{border-left:1px solid var(--border);background:var(--muted)}
.pm-st{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-foreground);margin:2px 0 12px;font-weight:500}
.pm-kpi{border:1px solid var(--border);background:var(--card);border-radius:10px;padding:11px 12px;margin-bottom:10px}
.pm-num{font-weight:600;font-size:26px;line-height:1;font-variant-numeric:tabular-nums;color:var(--foreground)}
.pm-klab{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground);margin-top:6px;font-weight:500}
.pm-kdet{font-size:11px;color:var(--muted-foreground);margin-top:5px;line-height:1.45}
.pm-canvas{background:var(--card);border-left:1px solid var(--border);border-right:1px solid var(--border);min-height:472px}
.pm-canvas svg{width:100%;height:auto;display:block}
.pm-node{cursor:pointer} .pm-node{cursor:pointer}
.pm-lbl{font-weight:500;font-size:11px;fill:var(--foreground)} .pm-lbl{fill:var(--foreground);font-size:11px;font-weight:500}
.pm-dim{opacity:.18;transition:opacity .25s} .pm-dim{opacity:.18;transition:opacity .25s}
.pm-dkind{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-foreground)}
.pm-dname{font-weight:600;font-size:18px;margin:5px 0 10px;color:var(--foreground)}
.pm-drow{display:flex;justify-content:space-between;font-size:13px;padding:7px 0;border-bottom:1px solid var(--border);color:var(--muted-foreground)}
.pm-drow b{color:var(--foreground);font-weight:500}
.pm-note{font-size:13px;color:var(--muted-foreground);line-height:1.55;margin-top:10px}
.pm-legend{display:flex;gap:14px;flex-wrap:wrap;padding:10px 16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted-foreground)}
.pm-lg{display:flex;align-items:center;gap:6px}
.pm-sw{width:13px;height:13px;display:inline-block}
@media(max-width:760px){.pm-grid{grid-template-columns:1fr}.pm-railR{border-left:0;border-top:1px solid var(--border)}.pm-canvas{border:0;border-top:1px solid var(--border)}}
`}</style> `}</style>
<div className="pm-hd"> <div className="overflow-hidden rounded-xl border bg-card text-card-foreground">
<div className="flex items-center justify-between border-b px-5 py-4">
<div> <div>
<div className="pm-ttl">Team memory</div> <h2 className="text-base font-medium">Team memory</h2>
<div className="pm-sub">What PodMan learned · {podId}</div> <p className="mt-0.5 text-xs text-muted-foreground">What PodMan learned · {podId}</p>
</div> </div>
<button className="pm-x" onClick={onClose}> <Button variant="outline" size="sm" onClick={onClose}>
Pods Pods
</button> </Button>
</div> </div>
<div className="pm-bar"> <div className="flex flex-wrap gap-2 border-b px-4 py-3">
<button <Button variant={toggleVariant('risk')} size="sm" onClick={() => pick('risk')}>
className={`pm-btn ${mode === 'risk' && !selected ? 'on' : ''}`}
onClick={() => pick('risk')}
>
Risk path Risk path
</button> </Button>
<button <Button variant={toggleVariant('learn')} size="sm" onClick={() => pick('learn')}>
className={`pm-btn ${mode === 'learn' && !selected ? 'on' : ''}`}
onClick={() => pick('learn')}
>
Learning edges Learning edges
</button> </Button>
<button <Button variant={toggleVariant('all')} size="sm" onClick={() => pick('all')}>
className={`pm-btn ${mode === 'all' && !selected ? 'on' : ''}`}
onClick={() => pick('all')}
>
Whole graph Whole graph
</button> </Button>
</div> </div>
{error && ( {error && <p className="px-4 py-4 text-sm text-destructive">Graph error: {error}</p>}
<p style={{ padding: '16px', color: 'var(--destructive)', fontSize: 13 }}>
Graph error: {error}
</p>
)}
{!graph && !error && ( {!graph && !error && (
<p style={{ padding: '16px', color: 'var(--muted-foreground)', fontSize: 13 }}> <p className="px-4 py-4 text-sm text-muted-foreground">Loading graph</p>
Loading graph
</p>
)} )}
{graph && ( {graph && (
<> <>
<div className="pm-grid"> <div className="grid lg:grid-cols-[190px_1fr_250px]">
<div className="pm-col"> <div className="space-y-3 p-4">
<div className="pm-st">Workflow metrics</div> <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Workflow metrics
</p>
{graph.metrics.map((m) => ( {graph.metrics.map((m) => (
<div className="pm-kpi" key={m.label}> <div key={m.label} className="rounded-lg border bg-card px-3 py-2.5">
<div className="pm-num">{m.value}</div> <p className="text-2xl font-medium tabular-nums">{m.value}</p>
<div className="pm-klab">{m.label}</div> <p className="mt-1 text-xs font-medium uppercase text-muted-foreground">
<div className="pm-kdet">{m.detail}</div> {m.label}
</p>
<p className="mt-1 text-xs leading-snug text-muted-foreground">{m.detail}</p>
</div> </div>
))} ))}
</div> </div>
<div className="pm-canvas"> <div className="min-h-[472px] border-y bg-card lg:border-x lg:border-y-0">
<svg viewBox="0 0 720 472" role="img" aria-label="PodMan team-memory graph"> <svg
viewBox="0 0 720 472"
role="img"
aria-label="PodMan team-memory graph"
className="block h-auto w-full"
>
{graph.edges.map((e) => { {graph.edges.map((e) => {
const a = nodeById.get(e.source); const a = nodeById.get(e.source);
const b = nodeById.get(e.target); const b = nodeById.get(e.target);
@@ -282,53 +252,66 @@ export function GraphView({ podId, onClose }: { podId: string; onClose: () => vo
</svg> </svg>
</div> </div>
<div className="pm-col pm-railR"> <div className="border-t bg-muted p-4 lg:border-l lg:border-t-0">
{sel ? ( {sel ? (
<> <>
<div className="pm-dkind">{sel.kind}</div> <p className="text-xs uppercase tracking-wide text-muted-foreground">
<div className="pm-dname">{sel.label}</div> {sel.kind}
<div className="pm-drow"> </p>
<h3 className="mb-3 mt-1 text-lg font-medium">{sel.label}</h3>
<div className="flex items-center justify-between border-b py-1.5 text-sm text-muted-foreground">
<span>Status</span> <span>Status</span>
<b style={{ color: statusColor(sel.status) }}>{sel.status}</b> <Badge variant="outline" style={{ color: statusColor(sel.status) }}>
{sel.status}
</Badge>
</div> </div>
<div className="pm-drow"> <div className="flex items-center justify-between border-b py-1.5 text-sm text-muted-foreground">
<span>Relationships</span> <span>Relationships</span>
<b>{relCount}</b> <span className="font-medium text-foreground">{relCount}</span>
</div> </div>
<div className="pm-note">{sel.summary}</div> <p className="mt-2.5 text-sm leading-relaxed text-muted-foreground">
{sel.summary}
</p>
</> </>
) : ( ) : (
<> <>
<div className="pm-dkind">Continual learning</div> <p className="text-xs uppercase tracking-wide text-muted-foreground">
<div className="pm-dname">It learned</div> Continual learning
<div className="pm-note"> </p>
The violet <b style={{ color: VIOLET }}>learned_from</b> edges are ownership <h3 className="mb-3 mt-1 text-lg font-medium">It learned</h3>
PodMan retained from accepted interventions the graph gets sharper every <p className="text-sm leading-relaxed text-muted-foreground">
session. Click any node to trace its relationships. The violet{' '}
</div> <span className="font-medium" style={{ color: VIOLET }}>
learned_from
</span>{' '}
edges are ownership PodMan retained from accepted interventions the graph
gets sharper every session. Click any node to trace its relationships.
</p>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="pm-legend"> <div className="flex flex-wrap gap-3 border-t px-4 py-2.5 text-xs text-muted-foreground">
{LEGEND.map((l) => ( {LEGEND.map((l) => (
<span className="pm-lg" key={l.label}> <span key={l.label} className="flex items-center gap-1.5">
<span className="pm-sw" style={l.swatch} /> <span className="inline-block size-3" style={l.swatch} />
{l.label} {l.label}
</span> </span>
))} ))}
<span className="pm-lg"> <span className="flex items-center gap-1.5">
<span className="pm-sw" style={{ background: RED, height: 3 }} /> <span className="inline-block h-[3px] w-3" style={{ background: RED }} />
collides collides
</span> </span>
<span className="pm-lg"> <span className="flex items-center gap-1.5">
<span className="pm-sw" style={{ background: VIOLET, height: 3 }} /> <span className="inline-block h-[3px] w-3" style={{ background: VIOLET }} />
learned_from learned_from
</span> </span>
</div> </div>
</> </>
)} )}
</div> </div>
</div>
</div>
); );
} }