// screens-library.jsx
function buildBinTree(f){const m={};f.forEach(b=>{m[b.id]={...b,children:[]};});const r=[];f.forEach(b=>{if(b.parent_id&&m[b.parent_id])m[b.parent_id].children.push(m[b.id]);else r.push(m[b.id]);});return r;}
function collectDescendantIds(id,f){const s=new Set([id]);let c=true;while(c){c=false;f.forEach(b=>{if(b.parent_id&&s.has(b.parent_id)&&!s.has(b.id)){s.add(b.id);c=true;}});}return s;}
function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
const PROJECTS = window.ZAMPP_DATA?.PROJECTS || [];
const [bins, setBins] = React.useState(window.ZAMPP_DATA?.BINS || []);
const BINS = bins; // legacy local name; keep so the rest of the function reads unchanged
// Re-fetch bins on mount + whenever the open project changes; surfaces
// every-project bins when the global view is on, project-scoped otherwise.
var refreshBins = React.useCallback(function() {
var qs2 = openProject ? '?project_id=' + openProject.id : '';
window.ZAMPP_API.fetch('/bins' + qs2)
.then(function(list) {
var normalized = (list || []).map(function(b) { return { ...b, count: b.asset_count != null ? b.asset_count : (b.count || 0), icon: b.type || 'grid' }; });
if (!openProject) window.ZAMPP_DATA.BINS = normalized;
setBins(normalized);
})
.catch(function() {});
}, [openProject]);
React.useEffect(function() {
refreshBins();
var onBinsChanged = function() { refreshBins(); };
window.addEventListener('df:bins-changed', onBinsChanged);
return function() { window.removeEventListener('df:bins-changed', onBinsChanged); };
}, [refreshBins]);
const createBin = () => {
if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; }
setNewBinName(''); setCreatingBin(true);
};
const submitBin = (name) => {
if (!name || !name.trim()) { setCreatingBin(false); return; }
setCreatingBin(false);
window.ZAMPP_API.fetch('/bins', {
method: 'POST',
body: JSON.stringify({ project_id: openProject.id, name: name.trim() }),
})
.then(() => window.ZAMPP_API.fetch('/bins?project_id=' + openProject.id))
.then(list => setBins((list || []).map(b => ({ ...b, count: b.asset_count || 0, icon: b.type || 'grid' }))))
.catch(e => window.alert('Could not create bin: ' + e.message));
};
const [view, setView] = React.useState('grid');
const [filter, setFilter] = React.useState('all'); // 'all', 'ready', 'processing', 'live', 'error', 'recent'
const [search, setSearch] = React.useState(window._dfPendingSearch || '');
React.useEffect(() => { delete window._dfPendingSearch; }, []);
// Local state lets us re-render after delete / move-to-bin without forcing
// a full app reload - keeps ZAMPP_DATA in sync as the cache of record.
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
const [renamingAsset, setRenamingAsset] = React.useState(null);
const [confirm, confirmModal] = window.useConfirm();
// Asset queued for hi-res download. Null means no modal showing. Set when
// the user clicks Download and has NOT dismissed the "are you sure" warning.
const [pendingDownload, setPendingDownload] = React.useState(null);
// Single entry point for hi-res downloads from the library. If the user
// ticked "Don't show again" in the warning modal, skip straight to the
// download; otherwise pop the modal with this asset queued. The modal's
// confirm handler calls runDownload directly.
const requestDownload = function(asset) {
if (!asset || !asset.original_s3_key) return;
try {
if (localStorage.getItem('df.lib.download.warnDismissed') === '1') {
runDownload(asset);
return;
}
} catch (_) { /* localStorage unavailable: show modal to be safe */ }
setPendingDownload(asset);
};
const [creatingBin, setCreatingBin] = React.useState(false);
const [newBinName, setNewBinName] = React.useState('');
const [draggingAssetId, setDraggingAssetId] = React.useState(null);
const [dragOverBinId, setDragOverBinId] = React.useState(null);
const [recentlyMovedId, setRecentlyMovedId] = React.useState(null);
// Rename project state
const [renamingProject, setRenamingProject] = React.useState(null);
const [projVersion, setProjVersion] = React.useState(0);
// Multi-select state
const [selectedAssets, setSelectedAssets] = React.useState(new Set());
const [selectionMode, setSelectionMode] = React.useState(false);
const refreshAssets = React.useCallback(() => {
window.ZAMPP_API.refreshAssets()
.then(function(normalized) {
setAllAssets(normalized);
})
.catch(() => {});
}, []);
const toggleSelection = function(assetId, e) {
if (e) e.stopPropagation();
setSelectedAssets(function(prev) {
var next = new Set(prev);
if (next.has(assetId)) next.delete(assetId);
else next.add(assetId);
return next;
});
};
const selectAll = function() {
setSelectedAssets(new Set(assets.map(function(a) { return a.id; })));
};
const clearSelection = function() {
setSelectedAssets(new Set());
setSelectionMode(false);
};
const bulkMoveToBin = async function(binId) {
var ids = Array.from(selectedAssets);
if (ids.length === 0) return;
var targetBin = bins.find(function(b) { return b.id === binId; });
for (var i = 0; i < ids.length; i++) {
var asset = allAssets.find(function(a) { return a.id === ids[i]; });
if (asset && targetBin && asset.project_id && targetBin.project_id && asset.project_id !== targetBin.project_id) {
alert('Cannot move assets to a bin in a different project.');
return;
}
}
try {
await Promise.all(ids.map(function(id) {
return window.ZAMPP_API.fetch('/assets/' + id, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) });
}));
refreshAssets();
window.dispatchEvent(new Event('df:bins-changed'));
clearSelection();
} catch (e) {
alert('Bulk move failed: ' + e.message);
}
};
const bulkDelete = async function() {
var ids = Array.from(selectedAssets);
if (ids.length === 0) return;
if (!(await confirm({
title: 'Delete ' + ids.length + ' assets?',
message: 'Delete ' + ids.length + ' assets permanently?\nThis removes the database rows and S3 objects.\nThis cannot be undone.',
}))) return;
try {
await Promise.all(ids.map(function(id) {
return window.ZAMPP_API.fetch('/assets/' + id + '?hard=true', { method: 'DELETE' });
}));
refreshAssets();
clearSelection();
} catch (e) {
alert('Bulk delete failed: ' + e.message);
}
};
const deleteAsset = React.useCallback(async (asset) => {
if (!(await confirm({
title: 'Delete asset?',
message: 'Delete "' + (asset.display_name || asset.name) + '" permanently?\nThis removes the database row and the S3 objects.\nThis cannot be undone.',
}))) return;
window.ZAMPP_API.fetch('/assets/' + asset.id + '?hard=true', { method: 'DELETE' })
.then(refreshAssets)
.catch(function(e) { alert('Delete failed: ' + e.message); });
}, [confirm, refreshAssets]);
// Auto-refresh: poll the library while it's open so live recordings flip
// to 'ready' (with thumbnail) without a manual reload. Also pull once on
// mount so uploads/imports created on other screens appear immediately.
const hasLive = React.useMemo(
() => allAssets.some(a => a.status === 'live' || a.status === 'processing' || a.status === 'ingesting'),
[allAssets]
);
React.useEffect(() => {
refreshAssets();
const tick = hasLive ? 4000 : 15000;
const id = setInterval(refreshAssets, tick);
const onAssetsChanged = () => refreshAssets();
window.addEventListener('df:assets-changed', onAssetsChanged);
return () => {
clearInterval(id);
window.removeEventListener('df:assets-changed', onAssetsChanged);
};
}, [hasLive, refreshAssets]);
// Dismiss the context menu on any outside click (capture phase so clicking
// a menu item still fires before the menu unmounts).
React.useEffect(() => {
if (!ctxMenu) return;
const close = () => setCtxMenu(null);
window.addEventListener('click', close);
window.addEventListener('contextmenu', close);
window.addEventListener('scroll', close, true);
return () => {
window.removeEventListener('click', close);
window.removeEventListener('contextmenu', close);
window.removeEventListener('scroll', close, true);
};
}, [ctxMenu]);
const openCtx = (asset, e) => {
e.preventDefault();
e.stopPropagation();
setCtxMenu({ asset, x: e.clientX, y: e.clientY });
};
// Drag-and-drop: asset → bin
const onAssetDragStart = function(assetId, e) {
e.dataTransfer.setData('text/plain', assetId);
e.dataTransfer.effectAllowed = 'move';
setDraggingAssetId(assetId);
};
const onBinDragOver = function(binId, e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragOverBinId !== binId) setDragOverBinId(binId);
};
const onBinDragLeave = function() {
setDragOverBinId(null);
};
const onBinDrop = function(binId, e) {
e.preventDefault();
setDraggingAssetId(null);
var assetId = e.dataTransfer.getData('text/plain');
if (!assetId) return;
// Guard against cross-project moves
var asset = allAssets.find(function(a) { return a.id === assetId; });
var targetBin = bins.find(function(b) { return b.id === binId; });
if (asset && targetBin && asset.project_id && targetBin.project_id && asset.project_id !== targetBin.project_id) {
alert('Cannot move asset to a bin in a different project.');
return;
}
window.ZAMPP_API.fetch('/assets/' + assetId, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) })
.then(function() {
setRecentlyMovedId(assetId);
refreshAssets();
window.dispatchEvent(new Event('df:bins-changed'));
setTimeout(function() { setRecentlyMovedId(null); }, 2000);
})
.catch(function(e2) { alert('Move failed: ' + e2.message); });
};
// Project rename
const renameProject = function(p) { setRenamingProject(p); };
// Close drag state when drag ends anywhere
React.useEffect(function() {
if (!draggingAssetId) return;
var onEnd = function() { setDraggingAssetId(null); };
window.addEventListener('dragend', onEnd);
return function() { window.removeEventListener('dragend', onEnd); };
}, [draggingAssetId]);
// Project context menu state
var [projectCtx, setProjectCtx] = React.useState(null);
React.useEffect(function() {
if (!projectCtx) return;
var close = function() { setProjectCtx(null); };
// #49: add contextmenu + scroll dismiss so right-clicking another project
// or scrolling away doesn't leave the menu orphaned
window.addEventListener('click', close);
window.addEventListener('contextmenu', close);
window.addEventListener('scroll', close, true);
return function() {
window.removeEventListener('click', close);
window.removeEventListener('contextmenu', close);
window.removeEventListener('scroll', close, true);
};
}, [projectCtx]);
var openProjectCtx = function(p, e) {
e.preventDefault();
e.stopPropagation();
setProjectCtx({ project: p, x: e.clientX, y: e.clientY });
};
const [selectedBinId, setSelectedBinId] = React.useState(null);
// Clear bin filter on project change so a stale id doesn't hide everything.
React.useEffect(() => { setSelectedBinId(null); }, [openProject?.id]);
let assets = openProject
? allAssets.filter(function(a) { return a.project_id === openProject.id; })
: allAssets;
const ALL_ASSETS = allAssets;
if (filter === 'recent') {
assets = assets.filter(function(a) { return (Date.now() - new Date(a.created_at)) < 86400000; });
} else if (filter !== 'all') {
assets = assets.filter(function(a) { return a.status === filter; });
}
if (search) assets = assets.filter(function(a) { return a.name.toLowerCase().includes(search.toLowerCase()); });
if (selectedBinId) { var sids=collectDescendantIds(selectedBinId,BINS); assets=assets.filter(function(a){return sids.has(a.bin_id);}); }
const activeBin = selectedBinId ? BINS.find(b => b.id === selectedBinId) : null;
const displayTitle = activeBin
? (openProject ? openProject.name + ' · ' : '') + activeBin.name
: (openProject ? openProject.name : 'All Assets');
const binTree=React.useMemo(function(){return buildBinTree(BINS);},[BINS]);
const errorCount = ALL_ASSETS.filter(function(a) { return a.status === 'error'; }).length;
const recentCount = ALL_ASSETS.filter(function(a) { return (Date.now() - new Date(a.created_at)) < 86400000; }).length;
return (
{confirmModal}
Projects
All projects
{ALL_ASSETS.length}
{PROJECTS.slice(0, 8).map(function(p) {
return (
{p.name}
{p.assets}
);
})}
Bins
{!creatingBin && BINS.length === 0 ? (
{openProject ? 'No bins yet: click + to create one.' : 'Open a project to manage bins.'}
) : (
)}
{creatingBin && creatingChildOf === null && (
)}
Smart filters
{errorCount > 0 &&
Errors {errorCount}
}
Last 24h {recentCount}
Ready {ALL_ASSETS.filter(function(a) { return a.status === 'ready'; }).length}
{displayTitle}
· {assets.length} assets
{selectionMode && selectedAssets.size > 0 && (
{selectedAssets.size} selected
{BINS.length > 0 && (
Move to bin…
{BINS.filter(function(b) { return !openProject || b.project_id === openProject.id; }).map(function(b) {
return {b.name} ;
})}
)}
Delete
)}
Select
{['all', 'ready', 'processing', 'live', 'error', 'pending_migration'].map(function(f) {
return (
{f === 'all' ? 'All' : f === 'pending_migration' ? 'Pending' : f[0].toUpperCase() + f.slice(1)}
);
})}
Upload
{assets.length === 0 ? (
No assets match this filter.
) : view === 'grid' ? (
{assets.map(function(a) {
return
;
})}
) : (
{selectionMode &&
}
Name
Duration
Resolution
Codec
Size
Updated
{assets.map(function(a) {
return (
{selectionMode && (
)}
{a.duration}
{a.res}
{a.codec || '·'}
{a.size}
{a.updated}
);
})}
)}
{ctxMenu && (
)}
{pendingDownload && (
)}
{renamingAsset && (
)}
{projectCtx && (
)}
{renamingProject && window.RenameProjectModal && (
React.createElement(window.RenameProjectModal, {
project: renamingProject,
onClose: function() { setRenamingProject(null); },
onSaved: function() {
setRenamingProject(null);
// Re-fetch projects and update ZAMPP_DATA so the rail refreshes
window.ZAMPP_API.fetch('/projects').then(function(list) {
if (Array.isArray(list)) {
window.ZAMPP_DATA.PROJECTS = list.map(function(p, i) {
return {
...p,
color: (window.ZAMPP_DATA.PROJECTS.find(function(x) { return x.id === p.id; }) || {}).color
|| window.PROJECT_COLORS?.[i % (window.PROJECT_COLORS?.length || 1)]
|| 'var(--accent)',
assets: (window.ZAMPP_DATA?.ASSETS || []).filter(function(a) { return a.project_id === p.id; }).length,
updated: window.ZAMPP_API.fmtRelative(p.updated_at),
};
});
setProjVersion(function(v) { return v + 1; });
}
}).catch(function() {});
}
})
)}
);
}
// Hi-res download trigger shared by the card and the context menu. Resolves
// a presigned S3 URL via /assets/:id/hires (returns { url, filename, ext })
// and clicks a hidden anchor so the browser does the download. The download
// itself is direct S3 - never proxied through mam-api - so big files don't
// touch the API container.
function runDownload(asset) {
if (!asset || !asset.id) return;
return window.ZAMPP_API.fetch('/assets/' + asset.id + '/hires')
.then(function(r) {
if (!r || !r.url) { alert('No hi-res source available for this asset.'); return; }
const a = document.createElement('a');
a.href = r.url;
a.download = r.filename || ((asset.display_name || asset.name || asset.id) + '.' + (r.ext || 'mov'));
a.target = '_blank';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
})
.catch(function(e) { alert('Download failed: ' + (e.message || 'unknown error')); });
}
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename, onDownload, onDelete }) {
const ref = React.useRef(null);
// Pin the menu inside the viewport even if the user right-clicked near
// the bottom-right edge of the grid.
const [pos, setPos] = React.useState({ left: x, top: y });
React.useLayoutEffect(() => {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
const margin = 8;
let nx = x, ny = y;
if (x + r.width + margin > window.innerWidth) nx = window.innerWidth - r.width - margin;
if (y + r.height + margin > window.innerHeight) ny = window.innerHeight - r.height - margin;
setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) });
}, [x, y]);
const rename = function() { if (onRename) onRename(asset); else onClose(); };
const promoteToS3 = function() {
onClose();
window.ZAMPP_API.fetch('/assets/' + asset.id + '/promote', { method: 'POST' })
.then(function() {
if (onChanged) onChanged();
window.alert('Promotion job queued. The file is being uploaded to S3 in the background.');
})
.catch(function(e) { alert('Promotion failed: ' + e.message); });
};
const moveToBin = function(binId) {
onClose();
window.ZAMPP_API.fetch('/assets/' + asset.id, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) })
.then(function() { onChanged(); window.dispatchEvent(new Event('df:bins-changed')); })
.catch(function(e) { alert('Move failed: ' + e.message); });
};
const copyId = function() {
onClose();
if (navigator.clipboard) navigator.clipboard.writeText(asset.id).catch(function() {});
};
const remove = function() {
if (onDelete) { onDelete(asset); return; }
onClose();
};
return (
{asset.display_name || asset.name}
Open
Rename…
{asset.original_s3_key && onDownload && (
Download original…
)}
{asset.status === 'pending_migration' && (
Move to S3
)}
{(bins && bins.length > 0) ? (
<>
Move to bin
{bins
.filter(function(b) { return !asset.project_id || b.project_id === asset.project_id; })
.slice(0, 10)
.map(function(b) {
const isCurrent = asset.bin_id === b.id;
return (
{b.name}{isCurrent && current }
);
})}
{asset.bin_id && (
Remove from bin
)}
>
) : (
No bins: create one inside a project
)}
Copy asset ID
Delete permanently
);
}
function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable, selectionMode, isSelected, onToggleSelect }) {
const [hoverStream, setHoverStream] = React.useState(null);
const [hovered, setHovered] = React.useState(false);
const timerRef = React.useRef(null);
const hlsRef = React.useRef(null);
const videoRef = React.useRef(null);
const handleMouseEnter = function() {
timerRef.current = setTimeout(function() {
setHovered(true);
if (!hoverStream) {
window.ZAMPP_API.fetch('/assets/' + asset.id + '/stream')
.then(function(r) { if (r && r.url) setHoverStream(r); })
.catch(function() {});
}
}, 350);
};
const handleMouseLeave = function() {
clearTimeout(timerRef.current);
setHovered(false);
};
// HLS wiring
React.useEffect(function() {
if (!hovered || !hoverStream || hoverStream.type !== 'hls' || !videoRef.current) return;
if (!window.Hls) return;
hlsRef.current = new window.Hls({ maxBufferLength: 10 });
hlsRef.current.loadSource(hoverStream.url);
hlsRef.current.attachMedia(videoRef.current);
return function() {
if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
};
}, [hovered, hoverStream]);
const showVideo = hovered && hoverStream;
return (
{selectionMode && (
)}
{showVideo && (
)}
{/* Status badges and duration: inside the relative wrapper so
position:absolute is anchored to the thumbnail, not the card (#52) */}
{asset.status === 'live' && LIVE }
{asset.status === 'processing' && Processing }
{asset.status === 'error' && Error }
{asset.status === 'pending_migration' && SMB }
{/* Hi-res download trigger: only shown when the asset has an
original_s3_key (everything queued through ingest / conform).
Hidden until card hover, lives in top-right of the thumb. */}
{asset.original_s3_key && onDownload && (
)}
{(asset.type === 'video' || !asset.type) && asset.duration !== '·' &&
{asset.duration}
}
{asset.name}
{asset.res}
·
{asset.size}
);
}
function ProjectContextMenu({ project, x, y, onClose, onRename }) {
var ref = React.useRef(null);
var [pos, setPos] = React.useState({ left: x, top: y });
React.useLayoutEffect(function() {
if (!ref.current) return;
var r = ref.current.getBoundingClientRect();
var margin = 8;
var nx = x, ny = y;
if (x + r.width + margin > window.innerWidth) nx = window.innerWidth - r.width - margin;
if (y + r.height + margin > window.innerHeight) ny = window.innerHeight - r.height - margin;
setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) });
}, [x, y]);
return (
{project.name}
Rename project…
Delete project
);
}
function binIcon(name) {
return { grid: 'library', live: 'record', film: 'film', proxy: 'proxy', audio: 'audio', package: 'package' }[name] || 'folder';
}
function RenameAssetModal({ asset, onClose, onSaved }) {
const [name, setName] = React.useState(asset.display_name || asset.name || '');
const [saving, setSaving] = React.useState(false);
const [err, setErr] = React.useState(null);
const original = asset.display_name || asset.name || '';
const submit = function() {
const trimmed = name.trim();
if (!trimmed || trimmed === original) { onClose(); return; }
setSaving(true); setErr(null);
window.ZAMPP_API.fetch('/assets/' + asset.id, { method: 'PATCH', body: JSON.stringify({ display_name: trimmed }) })
.then(onSaved)
.catch(function(e) { setSaving(false); setErr(e.message); });
};
return (
Display name
{err &&
{err}
}
Cancel
{saving ? 'Saving…' : 'Rename'}
);
}
// First-run-style warning before a hi-res download. Tells the operator the
// file is full-length, may be multi-GB, and that speed depends on their
// connection. "Don't show again on this device" persists the dismissal so
// subsequent downloads (and every Download click after dismissal) skip
// straight to the file. Settings → Account exposes a control to re-enable.
function DownloadWarningModal({ asset, onClose, onConfirm }) {
const [dismiss, setDismiss] = React.useState(false);
const name = asset.display_name || asset.name || asset.id;
return (
);
}
function BinTreeNodes(p){var nodes=p.nodes,depth=p.depth,selId=p.selectedBinId,setSel=p.setSelectedBinId;var drag=p.draggingAssetId,over=p.dragOverBinId;var onOver=p.onBinDragOver,onDrop=p.onBinDrop,onLeave=p.onBinDragLeave;var expanded=p.expandedBins,toggle=p.toggleBinExpanded;var creat=p.creatingBin,parentOf=p.creatingChildOf;var newName=p.newBinName,setName=p.setNewBinName;var submit=p.submitBin,setCreat=p.setCreatingBin,setParent=p.setCreatingChildOf;var createSub=p.createSubBin,proj=p.openProject;if(!nodes||!nodes.length)return null;return nodes.map(function(b){var act=selId===b.id,idt=drag!==null&&over===b.id,hasC=b.children&&b.children.length>0,exp=expanded.has(b.id),isCk=creat&&parentOf===b.id,ind=depth*14;return React.createElement(React.Fragment,{key:b.id},React.createElement("div",{className:"rail-item"+(act?" active":"")+(idt?" droppable":""),onClick:function(){setSel(act?null:b.id);},onDragOver:function(e){onOver(b.id,e);},onDrop:function(e){onDrop(b.id,e);},onDragLeave:onLeave,style:{cursor:"pointer",paddingLeft:8+ind}},React.createElement("span",{style:{display:"inline-flex",alignItems:"center",width:16,height:16,flexShrink:0,marginRight:2,color:hasC?"var(--text-3)":"transparent",transition:"transform 120ms",transform:hasC&&exp?"rotate(90deg)":"none"},onClick:function(e){if(!hasC)return;e.stopPropagation();toggle(b.id);}},hasC&&React.createElement("svg",{width:8,height:8,viewBox:"0 0 8 8",fill:"currentColor"},React.createElement("path",{d:"M2 1l4 3-4 3V1z"}))),React.createElement(Icon,{name:binIcon(b.icon),size:13,className:"rail-icon",style:{marginRight:4}}),React.createElement("span",{style:{flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},b.name),React.createElement("span",{className:"rail-count"},b.count),proj&&React.createElement("button",{className:"icon-btn bin-add-child-btn","aria-label":"Create sub-bin",onClick:function(e){e.stopPropagation();createSub(b.id);},style:{opacity:0,transition:"opacity 100ms",marginLeft:2,flexShrink:0},onFocus:function(e){e.currentTarget.style.opacity="1";},onBlur:function(e){e.currentTarget.style.opacity="";}},React.createElement(Icon,{name:"plus",size:10}))),isCk&&React.createElement("div",{style:{paddingLeft:8+ind+14,paddingRight:6,paddingTop:4,paddingBottom:4,display:"flex",gap:4,alignItems:"center"}},React.createElement("input",{className:"field-input",autoFocus:true,value:newName,onChange:function(e){setName(e.target.value);},onKeyDown:function(e){if(e.key==="Enter")submit(newName);if(e.key==="Escape"){setCreat(false);setParent(null);}},onBlur:function(){submit(newName);},placeholder:"Sub-bin name",style:{fontSize:12,height:26,padding:"0 6px",flex:1}})),hasC&&exp&&React.createElement(BinTreeNodes,Object.assign({},p,{nodes:b.children,depth:depth+1})));});}
window.Library = Library;
window.AssetCard = AssetCard;