/* =========================================================================== launch-hubs-c.jsx — Workspace + Settings hubs. =========================================================================== */ const { LccPage: LccPageC, LccTabs: LccTabsC, LCC_TABS: TABS_C, Section: SectionC, Avatar: AvatarC, Bar: BarC, ENGINE_COLORS: EC } = window; /* --------------------------------------------------------------------------- WORKSPACE — Team · Checklists · Files · Timeline. --------------------------------------------------------------------------- */ function TeamRoster() { const [team, setTeam] = window.useDB("launchTeam"); const [tasks] = window.useDB("launchTasks"); const openFor = (m) => tasks.filter((t) => t.owner === (m.name || "").split(" ")[0] && t.status !== "done").length; const loadTone = { high: "#F25BA7", med: "#E0B638", low: "#10B5C7" }; const PALETTE = ["#C49800", "#B5006A", "#007A8A", "#4C1285", "#00A8BC", "#D8288C", "#7A4FBF"]; const [edit, setEdit] = React.useState(null); const save = () => { setTeam((xs) => edit.isNew ? [...xs, { ...edit }] : xs.map((m) => m.id === edit.id ? { ...edit } : m)); if (window.argtPing) window.argtPing(edit.name + " saved"); setEdit(null); }; const del = (id) => { setTeam((xs) => xs.filter((m) => m.id !== id)); setEdit(null); }; const add = () => setEdit({ id: Date.now(), name: "", role: "", load: "med", color: PALETTE[team.length % PALETTE.length], isNew: true }); return (
{team.length} TEAMMATES · TAP TO EDIT
{team.map((m) => (
setEdit({ ...m })} title="Tap to edit" style={{ display: "flex", flexDirection: "column", gap: 12, cursor: "pointer" }}>
{m.name}
{m.role}
Open tasks{openFor(m)}
Workload{m.load}
))}
setEdit(null)} onSubmit={save} submitLabel={edit && edit.isNew ? "Add" : "Save"}> {edit && (<> setEdit({ ...edit, name: e.target.value })} /> setEdit({ ...edit, role: e.target.value })} />
{PALETTE.map((col) =>
{!edit.isNew && } )}
); } function Checklists() { const team = window.ARGT_DB.get("launchTeam") || []; const [items, setItems] = window.useDB("checklists"); const [edit, setEdit] = React.useState(null); const done = items.filter((i) => i.done).length; const toggle = (id) => setItems((xs) => xs.map((x) => x.id === id ? { ...x, done: !x.done } : x)); const move = (i, dir) => setItems((xs) => { const j = i + dir; if (j < 0 || j >= xs.length) return xs; const c = xs.slice(); const t = c[i]; c[i] = c[j]; c[j] = t; return c; }); const remove = (id) => setItems((xs) => xs.filter((x) => x.id !== id)); const saveEdit = () => { setItems((xs) => xs.map((x) => x.id === edit.id ? { ...edit } : x)); if (window.argtPing) window.argtPing("Checklist item updated"); setEdit(null); }; const mvBtn = { width: 22, height: 16, display: "grid", placeItems: "center", background: "var(--pvt-surface-light)", border: "1px solid var(--pvt-border-light)", borderRadius: 5, color: "var(--pvt-text-dim)", fontSize: 8, cursor: "pointer", padding: 0, fontFamily: "var(--pvt-font-body)" }; return ( {done}/{items.length} DONE}>
{items.map((it, i) => (
toggle(it.id)} title="Toggle done" style={{ width: 22, height: 22, borderRadius: 7, border: "2px solid " + (it.done ? "var(--role-accent)" : "var(--pvt-border-light)"), background: it.done ? "var(--role-accent)" : "transparent", display: "grid", placeItems: "center", color: "#fff", fontSize: 13, fontWeight: 800, cursor: "pointer", flexShrink: 0 }}>{it.done ? "✓" : ""} setEdit({ ...it })} title="Click to edit" style={{ fontSize: 13.5, textDecoration: it.done ? "line-through" : "none", cursor: "pointer" }}>{it.label} {it.cadence} {it.due}{it.owner}
))}
setEdit(null)} onSubmit={saveEdit} submitLabel="Save"> {edit && (<> setEdit({ ...edit, label: e.target.value })} />
setEdit({ ...edit, due: e.target.value })} />
)}
); } function Files() { const [files, setFiles] = window.useDB("launchFiles"); const fmt = (b) => b >= 1e6 ? (b / 1e6).toFixed(1) + " MB" : (b / 1e3).toFixed(0) + " KB"; const [edit, setEdit] = React.useState(null); const save = () => { setFiles((xs) => xs.map((f) => f.id === edit.id ? { ...f, name: edit.name } : f)); if (window.argtPing) window.argtPing("Renamed to " + edit.name); setEdit(null); }; const del = (id) => { setFiles((xs) => xs.filter((f) => f.id !== id)); setEdit(null); }; const addFolder = () => setFiles((xs) => [{ id: "f" + Date.now(), type: "folder", name: "New folder", parentId: null, items: 0, sizeBytes: 0, updatedAt: "now" }, ...xs]); return ( }>
{files.map((f) => (
setEdit({ ...f })} title="Tap to open" style={{ minHeight: 120, cursor: "pointer" }}>
{f.type === "folder" ? "▣" : (f.mime || "·").toUpperCase().slice(0, 3)}
{f.name}
{f.type === "folder" ? f.items + " items" : fmt(f.sizeBytes)} · {f.updatedAt}
))}
setEdit(null)} onSubmit={save} submitLabel="Save"> {edit && (<> setEdit({ ...edit, name: e.target.value })} />
{edit.type === "folder" ? edit.items + " items" : fmt(edit.sizeBytes)} · updated {edit.updatedAt}
)}
); } function Timeline() { const [weeks] = window.useDB("timeline"); const tw = (window.ARGT_DB.get("launchSettings") || {}).timelineWeeks || 20; const tone = { doing: "#10B5C7", todo: "var(--pvt-text-dim)", done: "#D8288C" }; const span = weeks.length ? weeks[0].week + " → " + weeks[weeks.length - 1].week : "—"; return ( {span} · EDIT IN SETTINGS}>
{weeks.map((w, i, arr) => (
{w.week}
{i < arr.length - 1 && }
{w.label}
{w.milestones.map((m, j) => {m})} {w.engine}
))}
); } function WorkspaceHub({ tab, go }) { const [, setItems] = window.useDB("checklists"); const team = window.ARGT_DB.get("launchTeam") || []; const [showAdd, setShowAdd] = React.useState(false); const [d, setD] = React.useState({ label: "", owner: "Kam", due: "" }); const add = () => { if (!d.label.trim()) return; setItems((xs) => [...xs, { id: "ch" + Date.now(), label: d.label.trim(), owner: d.owner, due: d.due || "TBD", cadence: "once", done: false }]); if (window.argtPing) window.argtPing("Checklist item added"); setShowAdd(false); setD({ label: "", owner: "Kam", due: "" }); }; return ( setShowAdd(true)}>New checklist}> go("workspace", t)} /> {tab === "team" && } {tab === "checklists" && } {tab === "files" && } {tab === "timeline" && } setShowAdd(false)} onSubmit={add} submitLabel="Add item"> setD({ ...d, label: e.target.value })} />
setD({ ...d, due: e.target.value })} />
); } /* --------------------------------------------------------------------------- SETTINGS — Festival · Integrations · Notifications · Roles · Storage. --------------------------------------------------------------------------- */ function inputStyleC() { return { background: "var(--pvt-surface-mid)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-md)", color: "var(--pvt-text)", padding: "12px 14px", fontSize: 13, fontFamily: "var(--pvt-font-body)", outline: "none", width: "100%" }; } function Field({ label, value }) { return (
); } /* Superseded by launch-settings.jsx (LccSettings). Kept dormant; not registered. */ function SettingsHubLegacyUnused({ tab, go }) { const s = window.ARGT_DB.get("launchSettings"); const [notifs, setNotifs] = window.useDB("launchSettings"); const [access, setAccess] = window.useDB("launchAccess"); const toggleN = (k) => setNotifs((x) => ({ ...x, notifications: { ...x.notifications, [k]: !x.notifications[k] } })); const [invEmail, setInvEmail] = React.useState(""); const [invRole, setInvRole] = React.useState("Editor"); const sendInvite = () => { if (!invEmail.trim()) return; setAccess((a) => ({ ...a, invites: [...(a.invites || []), { id: "inv" + Date.now(), email: invEmail.trim(), role: invRole, status: "pending", sent: "now" }] })); if (window.argtPing) window.argtPing("Invite sent to " + invEmail.trim()); setInvEmail(""); }; const setPersonAccess = (id, val) => setNotifs((x) => ({ ...x, roles: x.roles.map((r) => r.id === id ? { ...r, access: val } : r) })); return ( Save changes}> go("settings", t)} /> {tab === "festival" && (
)} {tab === "integrations" && (
{s.integrations.map((ig) => (
{ig.name} {ig.purpose} {ig.status === "connected" ? "Connected" : "Action needed"}
))}
)} {tab === "notifications" && (
{Object.entries(s.notifications).map(([k, v]) => (
toggleN(k)}> {k} {v ? "On" : "Off"}
))}
)} {tab === "roles" && (
setInvEmail(e.target.value)} />

Admins can open Settings and edit the festival profile. Editors work the operational hubs. Viewers are read-only. Only Owner and Admin can reach this configuration surface.

{s.roles.map((r) => (
{r.name} {r.scope} {r.access === "Owner" ? ( Owner ) : ( )}
))}
{(access.invites || []).length > 0 && ( {access.invites.length} OUTSTANDING}>
{access.invites.map((iv) => (
{iv.email} {iv.role} sent {iv.sent} Pending
))}
)}
)} {tab === "storage" && (
Used{s.storage.usedGb} GB of {s.storage.totalGb} GB

Media library, contracts, stage plots, and creative. Upgrade adds 200 GB blocks.

)}
); } window.LCC_PAGES = Object.assign(window.LCC_PAGES || {}, { workspace: (tab, go) => , });