feat(ui): Dragonflight redesign — foundation JSX files: app.jsx
This commit is contained in:
parent
b6dcecb672
commit
2706903353
1 changed files with 164 additions and 0 deletions
164
services/web-ui/public/app.jsx
Normal file
164
services/web-ui/public/app.jsx
Normal 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 />);
|
||||
Loading…
Reference in a new issue