diff --git a/services/web-ui/public/screens-library.jsx b/services/web-ui/public/screens-library.jsx
new file mode 100644
index 0000000..9e9f8ec
--- /dev/null
+++ b/services/web-ui/public/screens-library.jsx
@@ -0,0 +1,156 @@
+// screens-library.jsx — library / asset grid + asset detail
+const { ASSETS: ALL_ASSETS, BINS, COMMENTS, PROJECTS } = window.ZAMPP_DATA;
+
+function Library({ navigate, onOpenAsset, project = "Protour 2026" }) {
+ const [bin, setBin] = React.useState("b1");
+ const [view, setView] = React.useState("grid");
+ const [filter, setFilter] = React.useState("all");
+ const [search, setSearch] = React.useState("");
+
+ let assets = ALL_ASSETS;
+ if (filter !== "all") assets = assets.filter(a => a.status === filter);
+ if (search) assets = assets.filter(a => a.name.toLowerCase().includes(search.toLowerCase()));
+
+ return (
+
+
+
+
+
+
{project}
+
· {assets.length} assets
+
+
+
+ setSearch(e.target.value)} placeholder="Filter assets…" />
+
+
+ {["all", "ready", "processing", "live", "error"].map(f => (
+
+ ))}
+
+
+
+
+
+
+
+
+ {view === "grid" ? (
+
+ {assets.map(a =>
onOpenAsset(a)} />)}
+
+ ) : (
+
+
+
+
Name
+
Duration
+
Resolution
+
Codec
+
Size
+
Updated
+
+
+ {assets.map(a => (
+
onOpenAsset(a)}>
+
+
+
{a.name}
+
+
+ {a.status}
+ {a.comments > 0 && · {a.comments} comments}
+
+
+
{a.duration}
+
{a.res}
+
{a.codec}
+
{a.size}
+
{a.updated}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+function AssetCard({ asset, onOpen }) {
+ return (
+
+
+
+ {asset.status === "live" && LIVE}
+ {asset.status === "processing" && Proxy {asset.progress}%}
+ {asset.status === "error" && Error}
+
+ {asset.type === "video" &&
{asset.duration}
}
+ {asset.status === "processing" && (
+
+ )}
+
+
{asset.name}
+
+ {asset.res}
+ ·
+ {asset.size}
+ {asset.comments > 0 && (
+
+ {asset.comments}
+
+ )}
+
+
+
+ );
+}
+
+function binIcon(name) {
+ return { grid: "library", live: "record", film: "film", proxy: "proxy", audio: "audio", package: "package" }[name] || "folder";
+}
+
+window.Library = Library;
+window.AssetCard = AssetCard;