feat(web/mazenet): glide transitions for service fleet + inspector panels

This commit is contained in:
2026-04-22 18:16:17 -04:00
parent e0231bf990
commit 1674316788
4 changed files with 52 additions and 33 deletions

View File

@@ -27,12 +27,14 @@ interface Props {
onAddDecky?: (netId: string) => void; onAddDecky?: (netId: string) => void;
setSelection?: (sel: Selection) => void; setSelection?: (sel: Selection) => void;
pendingChanges?: number; pendingChanges?: number;
className?: string;
} }
const Inspector: React.FC<Props> = ({ const Inspector: React.FC<Props> = ({
selection, nets, nodes, edges, topologyStatus, onClose, selection, nets, nodes, edges, topologyStatus, onClose,
onDeleteNet, onDeleteNode, onDeleteEdge, onRemoveService, onAddDecky, setSelection, onDeleteNet, onDeleteNode, onDeleteEdge, onRemoveService, onAddDecky, setSelection,
pendingChanges = 0, pendingChanges = 0,
className = '',
}) => { }) => {
const net = selection?.type === 'net' ? nets.find((n) => n.id === selection.id) : undefined; const net = selection?.type === 'net' ? nets.find((n) => n.id === selection.id) : undefined;
const node = selection?.type === 'node' ? nodes.find((n) => n.id === selection.id) : undefined; const node = selection?.type === 'node' ? nodes.find((n) => n.id === selection.id) : undefined;
@@ -58,7 +60,7 @@ const Inspector: React.FC<Props> = ({
const isObserved = node?.kind === 'observed'; const isObserved = node?.kind === 'observed';
return ( return (
<aside className="maze-inspector"> <aside className={`maze-inspector ${className}`}>
<div className="maze-inspector-title"> <div className="maze-inspector-title">
<Crosshair size={12} className="violet-accent" /> <Crosshair size={12} className="violet-accent" />
<span>INSPECTOR</span> <span>INSPECTOR</span>

View File

@@ -63,7 +63,21 @@ body.maze-fullscreen .maze-shell {
min-height: 0; min-height: 0;
margin: 0 -32px -32px; margin: 0 -32px -32px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
transition: grid-template-columns 260ms cubic-bezier(0.4, 0, 0.2, 1);
} }
.maze-palette,
.maze-inspector {
transition: opacity 200ms ease, transform 260ms cubic-bezier(0.4, 0, 0.2, 1);
min-width: 0;
}
.maze-palette.collapsed,
.maze-inspector.collapsed {
opacity: 0;
pointer-events: none;
overflow: hidden;
}
.maze-palette.collapsed { transform: translateX(-8px); }
.maze-inspector.collapsed { transform: translateX(8px); }
/* ── Palette ────────────────────────────────── */ /* ── Palette ────────────────────────────────── */
.maze-palette { .maze-palette {

View File

@@ -626,12 +626,15 @@ const MazeNET: React.FC = () => {
<div <div
className="maze-shell" className="maze-shell"
style={{ style={{
gridTemplateColumns: `${paletteOpen ? '240px ' : ''}1fr${inspectorOpen ? ' 320px' : ''}`, gridTemplateColumns: `${paletteOpen ? '240px' : '0px'} 1fr ${inspectorOpen ? '320px' : '0px'}`,
}} }}
> >
{paletteOpen && ( <Palette
<Palette services={services} archetypes={archetypes} startPaletteDrag={interaction.startPaletteDrag} /> services={services}
)} archetypes={archetypes}
startPaletteDrag={interaction.startPaletteDrag}
className={paletteOpen ? '' : 'collapsed'}
/>
<Canvas <Canvas
ref={canvasRef} ref={canvasRef}
nets={nets} nets={nets}
@@ -673,32 +676,31 @@ const MazeNET: React.FC = () => {
{interaction.paletteDrag.label} {interaction.paletteDrag.label}
</div> </div>
)} )}
{inspectorOpen && ( <Inspector
<Inspector selection={selection}
selection={selection} setSelection={setSelection}
setSelection={setSelection} nets={nets}
nets={nets} nodes={nodes}
nodes={nodes} edges={edges}
edges={edges} topologyStatus={topoStatus}
topologyStatus={topoStatus} onClose={() => setInspectorOpen(false)}
onClose={() => setInspectorOpen(false)} onDeleteNet={removeNet}
onDeleteNet={removeNet} onDeleteNode={removeNode}
onDeleteNode={removeNode} onDeleteEdge={removeEdge}
onDeleteEdge={removeEdge} onRemoveService={removeServiceFromNode}
onRemoveService={removeServiceFromNode} onAddDecky={(netId) => {
onAddDecky={(netId) => { const net = nets.find((n) => n.id === netId);
const net = nets.find((n) => n.id === netId); if (!net) return;
if (!net) return; onPaletteDrop(
onPaletteDrop( { kind: 'archetype', slug: archetypes[0]?.slug ?? 'deaddeck',
{ kind: 'archetype', slug: archetypes[0]?.slug ?? 'deaddeck', services: archetypes[0]?.services.slice(0, 2) ?? [],
services: archetypes[0]?.services.slice(0, 2) ?? [], label: archetypes[0]?.name ?? 'DECKY',
label: archetypes[0]?.name ?? 'DECKY', clientX: 0, clientY: 0 },
clientX: 0, clientY: 0 }, { x: net.x + 40, y: net.y + 60 }, netId, null,
{ x: net.x + 40, y: net.y + 60 }, netId, null, );
); }}
}} className={inspectorOpen ? '' : 'collapsed'}
/> />
)}
</div> </div>
</div> </div>
); );

View File

@@ -22,9 +22,10 @@ interface Props {
services: ServiceDef[]; services: ServiceDef[];
archetypes: Archetype[]; archetypes: Archetype[];
startPaletteDrag: (d: Omit<PaletteDrag, 'clientX' | 'clientY'>, e: React.MouseEvent) => void; startPaletteDrag: (d: Omit<PaletteDrag, 'clientX' | 'clientY'>, e: React.MouseEvent) => void;
className?: string;
} }
const Palette: React.FC<Props> = ({ services, archetypes, startPaletteDrag }) => { const Palette: React.FC<Props> = ({ services, archetypes, startPaletteDrag, className = '' }) => {
const start = (d: Omit<PaletteDrag, 'clientX' | 'clientY'>) => const start = (d: Omit<PaletteDrag, 'clientX' | 'clientY'>) =>
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (e.button !== 0) return; if (e.button !== 0) return;
@@ -33,7 +34,7 @@ const Palette: React.FC<Props> = ({ services, archetypes, startPaletteDrag }) =>
}; };
return ( return (
<div className="maze-palette"> <div className={`maze-palette ${className}`}>
<div className="palette-group"> <div className="palette-group">
<label> NETWORKS</label> <label> NETWORKS</label>
<div className="palette-item" onMouseDown={start({ kind: 'network-subnet', slug: 'subnet', label: 'SUBNET' })}> <div className="palette-item" onMouseDown={start({ kind: 'network-subnet', slug: 'subnet', label: 'SUBNET' })}>