feat(ui): Dragonflight redesign — foundation JSX files: app.jsx

This commit is contained in:
Zac Gaetano 2026-05-22 08:13:03 -04:00
parent b6dcecb672
commit 2706903353

View file

@ -0,0 +1,164 @@
// app.jsx main shell wiring all screens together + tweaks
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "#5B7CFA",
"density": "comfortable",
"gridSize": "md",
"showSearch": true,
"sidebarMode": "expanded"
}/*EDITMODE-END*/;
function App() {
const [route, setRoute] = React.useState("home");
const [openAsset, setOpenAsset] = React.useState(null);
const [openProject, setOpenProject] = React.useState(null);
const [showNewRecorder, setShowNewRecorder] = React.useState(false);
const [t, setTweak] = (window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}]);
React.useEffect(() => {
document.documentElement.style.setProperty("--accent", t.accent);
document.documentElement.style.setProperty("--accent-soft", hexToRgba(t.accent, 0.14));
document.documentElement.style.setProperty("--accent-soft-2", hexToRgba(t.accent, 0.22));
document.documentElement.style.setProperty("--accent-text", lighten(t.accent, 0.25));
document.documentElement.style.setProperty("--accent-hover", lighten(t.accent, 0.08));
}, [t.accent]);
const navigate = (id) => {
setOpenAsset(null);
setRoute(id);
};
const crumbs = React.useMemo(() => {
if (openAsset) return [
{ label: "Library", to: "library" },
{ label: openAsset.project, to: "library" },
{ label: openAsset.name },
];
if (openProject) return [
{ label: "Projects", to: "projects" },
{ label: openProject.name },
];
const labels = {
home: ["Home"],
library: ["Library", "Protour 2026"],
projects: ["Projects"],
upload: ["Ingest", "Upload"],
recorders: ["Ingest", "Recorders"],
capture: ["Ingest", "Capture"],
monitors: ["Ingest", "Monitors"],
jobs: ["Jobs"],
editor: ["Editor"],
users: ["Admin", "Users & Groups"],
tokens: ["Admin", "Tokens"],
containers: ["Admin", "Containers"],
cluster: ["Admin", "Cluster"],
settings: ["Admin", "Settings"],
};
return (labels[route] || ["Home"]).map((label, i, arr) => i < arr.length - 1 ? { label } : { label });
}, [route, openAsset, openProject]);
let content;
if (openAsset) {
content = <AssetDetail asset={openAsset} onClose={() => setOpenAsset(null)} />;
} else {
switch (route) {
case "home": content = <Home navigate={navigate} />; break;
case "library": content = <Library navigate={navigate} onOpenAsset={setOpenAsset} project={openProject?.name || "Protour 2026"} />; break;
case "projects": content = <Projects navigate={navigate} onOpenProject={(p) => { setOpenProject(p); setRoute("library"); }} />; break;
case "upload": content = <Upload navigate={navigate} />; break;
case "recorders": content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break;
case "capture": content = <Capture navigate={navigate} />; break;
case "monitors": content = <Monitors navigate={navigate} />; break;
case "jobs": content = <Jobs navigate={navigate} />; break;
case "editor": content = <Editor />; break;
case "users": content = <Users />; break;
case "tokens": content = <Tokens />; break;
case "containers": content = <Containers />; break;
case "cluster": content = <Cluster />; break;
case "settings": content = <Settings />; break;
default: content = <Home navigate={navigate} />;
}
}
return (
<div className="app" data-density={t.density} data-grid-size={t.gridSize} data-sidebar={t.sidebarMode}>
<Sidebar active={openAsset ? "library" : route} onNavigate={navigate} />
<div className="main">
{!openAsset && <Topbar crumbs={crumbs} onNavigate={navigate} />}
{content}
</div>
{showNewRecorder && <NewRecorderModal open={showNewRecorder} onClose={() => setShowNewRecorder(false)} />}
{window.TweaksPanel && (
<window.TweaksPanel title="Tweaks">
<window.TweakSection label="Theme">
<window.TweakColor
label="Accent"
value={t.accent}
onChange={(v) => setTweak("accent", v)}
options={["#5B7CFA", "#7C5CFF", "#2DD4A8", "#FF5B5B", "#F5A623", "#E8E8E8"]}
/>
</window.TweakSection>
<window.TweakSection label="Density">
<window.TweakRadio
label="Density"
value={t.density}
onChange={(v) => setTweak("density", v)}
options={[{ value: "comfortable", label: "Comfy" }, { value: "compact", label: "Compact" }]}
/>
<window.TweakRadio
label="Grid size"
value={t.gridSize}
onChange={(v) => setTweak("gridSize", v)}
options={[{ value: "sm", label: "S" }, { value: "md", label: "M" }, { value: "lg", label: "L" }]}
/>
</window.TweakSection>
<window.TweakSection label="Navigation">
<window.TweakRadio
label="Sidebar"
value={t.sidebarMode}
onChange={(v) => setTweak("sidebarMode", v)}
options={[{ value: "expanded", label: "Expanded" }, { value: "collapsed", label: "Icons" }]}
/>
</window.TweakSection>
<window.TweakSection label="Quick jump">
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
{[
["home", "Home"], ["library", "Library"], ["recorders", "Recorders"],
["capture", "Capture"], ["monitors", "Monitors"], ["jobs", "Jobs"],
["editor", "Editor"], ["cluster", "Cluster"],
].map(([k, l]) => (
<button key={k} className="btn ghost sm" style={{ justifyContent: "flex-start", border: "1px solid var(--border)" }} onClick={() => navigate(k)}>
{l}
</button>
))}
<button className="btn ghost sm" style={{ justifyContent: "flex-start", border: "1px solid var(--border)", gridColumn: "1 / -1" }} onClick={() => { navigate("library"); setTimeout(() => setOpenAsset(window.ZAMPP_DATA.ASSETS[1]), 50); }}>
Open asset detail
</button>
<button className="btn ghost sm" style={{ justifyContent: "flex-start", border: "1px solid var(--border)", gridColumn: "1 / -1" }} onClick={() => { navigate("recorders"); setTimeout(() => setShowNewRecorder(true), 50); }}>
Open "New recorder" modal
</button>
</div>
</window.TweakSection>
</window.TweaksPanel>
)}
</div>
);
}
function hexToRgba(hex, a) {
const h = hex.replace("#", "");
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
function lighten(hex, amt) {
const h = hex.replace("#", "");
const r = Math.min(255, parseInt(h.slice(0, 2), 16) + Math.round(amt * 255));
const g = Math.min(255, parseInt(h.slice(2, 4), 16) + Math.round(amt * 255));
const b = Math.min(255, parseInt(h.slice(4, 6), 16) + Math.round(amt * 255));
return `rgb(${r}, ${g}, ${b})`;
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);