dragonflight/services/web-ui/public/screens-editor.jsx
claude f186cdeacd polish(ui): wire dead buttons across asset detail, shell, containers, cluster
Asset detail:
- Download now fetches /assets/:id/hires presigned URL and triggers a
  named browser download instead of doing nothing
- More icon now opens a kebab menu (Copy ID, Delete permanently)
- Approve button removed (no backend); audio + fullscreen icons
  in the player controls now actually toggle mute / requestFullscreen

Shell:
- Sidebar Sign-out now POSTs /auth/logout + reloads (no-op when auth disabled, by design)
- Topbar Notifications bell removed (dead, no backend)
- Topbar search wired: typing + Enter routes to Library with the term
  pre-loaded into Library's own search box
- Cluster-healthy pip now polls /metrics/home every 30s so it reflects
  real online-vs-total instead of always showing green

Editor:
- Dead Export / Publish / Mark in / Mark out / Add to timeline / Step
  buttons are now visibly disabled with explanatory titles; a PREVIEW
  badge sits next to the sequence name so the WIP state is obvious

Containers / Cluster admin:
- Logs button opens a modal with the docker tail command + Copy button
  instead of a JS alert
- Restart now shows an inline toast (pending/ok/fail) instead of alerts
- Cluster Add Node / Drain / Logs replace alert() with a styled advice
  modal that supports multi-line commands + Copy
- Dead Cluster topology Graph/List tab toggle removed (only Graph is
  implemented anyway)
2026-05-23 04:04:08 +00:00

162 lines
7.7 KiB
JavaScript

// screens-editor.jsx — Editor (timeline)
function _fmtTimecode(ms) {
const s = Math.floor(ms / 1000);
const f = Math.floor((ms % 1000) / (1000 / 30));
return String(Math.floor(s / 3600)).padStart(2, '0') + ':' +
String(Math.floor((s % 3600) / 60)).padStart(2, '0') + ':' +
String(s % 60).padStart(2, '0') + ':' +
String(f).padStart(2, '0');
}
function Editor() {
const { ASSETS } = window.ZAMPP_DATA;
const [playing, setPlaying] = React.useState(false);
const [currentMs, setCurrentMs] = React.useState(0);
React.useEffect(() => {
if (!playing) return;
const i = setInterval(() => setCurrentMs(t => t + 100), 100);
return () => clearInterval(i);
}, [playing]);
return (
<div className="editor-shell" style={{ position: 'relative' }}>
<div className="editor-topbar">
<span style={{ fontWeight: 600, fontSize: 13 }}>New sequence</span>
<span className="badge dev" style={{ marginLeft: 8 }}>PREVIEW</span>
<div style={{ flex: 1 }} />
<button className="btn ghost sm" disabled title="Export not yet implemented — use the Premiere panel for now"><Icon name="download" />Export</button>
<button className="btn primary sm" disabled title="Publish to MAM not yet implemented">Publish</button>
</div>
<div className="editor-body">
<aside className="editor-bins">
<div style={{ padding: '12px 12px 8px', display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: 12 }}>Project bin</span>
<span style={{ flex: 1 }} />
<button className="icon-btn" disabled title="Bin search not yet implemented"><Icon name="search" size={12} /></button>
</div>
<div style={{ padding: '0 8px 12px', display: 'flex', flexDirection: 'column', gap: 4 }}>
{ASSETS.length === 0 ? (
<div style={{ padding: '16px 4px', color: 'var(--text-3)', fontSize: 12 }}>No assets in library.</div>
) : ASSETS.slice(0, 12).map(a => (
<div key={a.id} className="editor-bin-item">
<div className="editor-bin-thumb"><AssetThumb asset={a} /></div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 11.5, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.name}</div>
<div className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>{a.duration}</div>
</div>
</div>
))}
</div>
</aside>
<div className="editor-viewer">
<div className="editor-canvas">
<FauxFrame />
{!playing && (
<button className="player-play-overlay" onClick={() => setPlaying(true)}>
<Icon name="play" size={28} />
</button>
)}
<div className="player-tc"><span className="mono">{_fmtTimecode(currentMs)}</span></div>
</div>
<div className="editor-transport">
<button className="icon-btn" onClick={() => setCurrentMs(0)} title="Go to start"><Icon name="arrowLeft" size={14} /></button>
<button className="icon-btn" onClick={() => setPlaying(p => !p)} title={playing ? 'Pause' : 'Play'}>
<Icon name={playing ? 'pause' : 'play'} size={14} />
</button>
<button className="icon-btn" disabled title="Step forward — not yet implemented"><Icon name="arrowRight" size={14} /></button>
<span style={{ flex: 1 }} />
<button className="btn ghost sm" disabled title="In/out trim points — not yet implemented">Mark in</button>
<button className="btn ghost sm" disabled title="In/out trim points — not yet implemented">Mark out</button>
<button className="btn subtle sm" disabled title="Timeline editing — not yet implemented">Add to timeline</button>
</div>
</div>
<aside className="editor-insp">
<div style={{ padding: '12px', fontSize: 12, fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Inspector</div>
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
<InspGroup title="Transform">
<InspRow label="Position" value="0, 0" />
<InspRow label="Scale" value="100%" />
<InspRow label="Rotation" value="0°" />
</InspGroup>
<InspGroup title="Color">
<InspRow label="Exposure" value="0.0" />
<InspRow label="Contrast" value="0.0" />
<InspRow label="Saturation" value="0.0" />
</InspGroup>
<InspGroup title="Audio">
<InspRow label="Level" value="0.0 dB" />
<InspRow label="Pan" value="C" />
</InspGroup>
</div>
</aside>
</div>
<EditorTimeline currentMs={currentMs} total={60} clips={[]} />
{/* IN DEVELOPMENT overlay */}
<div style={{
position: 'absolute', inset: 0, zIndex: 20,
display: 'flex', alignItems: 'center', justifyContent: 'center',
backdropFilter: 'blur(6px)', background: 'rgba(10,12,16,0.82)',
pointerEvents: 'all',
}}>
<div style={{
background: 'var(--bg-1)', border: '1px solid var(--border-stronger)',
borderRadius: 14, padding: '48px 56px', textAlign: 'center',
maxWidth: 440, boxShadow: '0 24px 80px rgba(0,0,0,0.6)',
}}>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.14em', color: 'var(--warning)', textTransform: 'uppercase', marginBottom: 20 }}>In Development</div>
<div style={{ fontSize: 22, fontWeight: 700, marginBottom: 14, letterSpacing: '-0.02em', color: 'var(--text-1)' }}>Non-linear Editor</div>
<div style={{ fontSize: 13, color: 'var(--text-3)', lineHeight: 1.75 }}>
Timeline editing, multi-track audio mixing,<br />
GPU-accelerated export, and color grading<br />
are coming in a future release.
</div>
</div>
</div>
</div>
);
}
function InspGroup({ title, children }) {
return (
<div>
<div className="muted" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, marginBottom: 6 }}>{title}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>{children}</div>
</div>
);
}
function InspRow({ label, value }) {
return (
<div style={{ display: 'grid', gridTemplateColumns: '70px 1fr', alignItems: 'center', fontSize: 11.5 }}>
<span style={{ color: 'var(--text-3)' }}>{label}</span>
<span className="mono" style={{ background: 'var(--bg-2)', padding: '3px 6px', borderRadius: 4, border: '1px solid var(--border)' }}>{value}</span>
</div>
);
}
function EditorTimeline({ currentMs, total = 60, clips = [] }) {
const playheadPct = total > 0 ? ((currentMs / 1000) / total) * 100 : 0;
return (
<div className="editor-timeline">
<div className="editor-timeline-head">
<span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>Timeline</span>
<span style={{ flex: 1 }} />
</div>
<div className="timeline-ruler">
{Array.from({ length: 13 }).map((_, i) => (
<div key={i} className="ruler-tick">
{i % 2 === 0 && <span className="mono">{`00:${String(i * 5).padStart(2, '0')}`}</span>}
</div>
))}
</div>
{clips.length === 0 && (
<div style={{ padding: '20px 40px', color: 'var(--text-3)', fontSize: 12 }}>Drop assets from the bin to build a sequence.</div>
)}
<div className="timeline-playhead" style={{ left: `calc(40px + (100% - 40px) * ${playheadPct / 100})` }} />
</div>
);
}
window.Editor = Editor;