Compare commits

...

8 commits

Author SHA1 Message Date
OpenCode
dbef15ae0a fix(library): clicking project in rail now filters assets to that project instead of navigating away 2026-06-03 04:11:32 +00:00
OpenCode
99bd6a8c9c fix(library): auto-expand all bins on load so nested children visible by default 2026-06-03 04:06:48 +00:00
OpenCode
4e6142f455 fix(web-ui): orange pulse logo (bigger, no canvas), fix library missing expandedBins state 2026-06-03 04:02:17 +00:00
OpenCode
02d502baaf fix(web-ui): restore full screens-home.jsx with DragonFlame + Home + Dashboard 2026-06-03 03:58:35 +00:00
OpenCode
00a7af7c54 feat(web-ui): nested bins tree + DragonFlame CSS restored (complete) 2026-06-03 03:48:29 +00:00
cb9ef9c14e fix(web-ui): restore correct styles-fixes.css with DragonFlame logo CSS + upload actual screens-library.jsx nested bins: styles-modal.css 2026-06-02 23:35:12 -04:00
f48a0b73ee feat(web-ui): nested bins tree in library sidebar + bin filter includes descendants: styles-fixes.css 2026-06-02 23:34:14 -04:00
463cc3694d feat(web-ui): nested bins tree, DragonFlame logo, recorder modal 2x2 grid, cleanup .bak
- Library: nested bins with expand/collapse tree in sidebar
  - buildBinTree() + collectDescendantIds() helpers
  - BinTreeNodes recursive component with hover sub-bin create (+) button
  - Selecting a parent bin shows assets from all descendant bins too
- Home: canvas DragonFlame particle animation behind logo (90 flame + 30 spark), logo 140px
- Recorder modal: source-type-grid 3-col → 2x2 so Deltacast card no longer overflows
- CSS: launcher background radial gradient taller; launcher-logo-wrap 160x200px
- Cleanup: remove capture.js.bak: screens-home.jsx
2026-06-02 23:33:58 -04:00
5 changed files with 66 additions and 58 deletions

View file

@ -123,7 +123,7 @@ function App() {
switch (effectiveRoute) { switch (effectiveRoute) {
case 'home': content = <Home navigate={navigate} />; break; case 'home': content = <Home navigate={navigate} />; break;
case 'dashboard': content = <Dashboard navigate={navigate} />; break; case 'dashboard': content = <Dashboard navigate={navigate} />; break;
case 'library': content = <Library navigate={navigate} onOpenAsset={setOpenAsset} openProject={openProject} onClearProject={() => setOpenProject(null)} />; break; case 'library': content = <Library navigate={navigate} onOpenAsset={setOpenAsset} openProject={openProject} onClearProject={() => setOpenProject(null)} onOpenProject={openProjectFromAnywhere} />; break;
case 'projects': content = <Projects navigate={navigate} onOpenProject={openProjectFromAnywhere} />; break; case 'projects': content = <Projects navigate={navigate} onOpenProject={openProjectFromAnywhere} />; break;
case 'upload': content = <Upload navigate={navigate} />; break; case 'upload': content = <Upload navigate={navigate} />; break;
case 'recorders': content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break; case 'recorders': content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break;

View file

@ -1,6 +1,9 @@
// screens-library.jsx // screens-library.jsx
function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
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, onOpenProject }) {
const PROJECTS = window.ZAMPP_DATA?.PROJECTS || []; const PROJECTS = window.ZAMPP_DATA?.PROJECTS || [];
const [bins, setBins] = React.useState(window.ZAMPP_DATA?.BINS || []); const [bins, setBins] = React.useState(window.ZAMPP_DATA?.BINS || []);
const BINS = bins; // legacy local name; keep so the rest of the function reads unchanged const BINS = bins; // legacy local name; keep so the rest of the function reads unchanged
@ -14,6 +17,8 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
var normalized = (list || []).map(function(b) { return { ...b, count: b.asset_count != null ? b.asset_count : (b.count || 0), icon: b.type || 'grid' }; }); 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; if (!openProject) window.ZAMPP_DATA.BINS = normalized;
setBins(normalized); setBins(normalized);
// Auto-expand all bins so nested children are always visible
setExpandedBins(function(prev) { var s = new Set(prev); normalized.forEach(function(b){ s.add(b.id); }); return s; });
}) })
.catch(function() {}); .catch(function() {});
}, [openProject]); }, [openProject]);
@ -25,20 +30,32 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
return function() { window.removeEventListener('df:bins-changed', onBinsChanged); }; return function() { window.removeEventListener('df:bins-changed', onBinsChanged); };
}, [refreshBins]); }, [refreshBins]);
const [creatingChildOf, setCreatingChildOf] = React.useState(null);
// Start with all bins expanded so nested children are visible immediately
const [expandedBins, setExpandedBins] = React.useState(() => new Set((window.ZAMPP_DATA?.BINS||[]).map(b=>b.id)));
const createBin = () => { const createBin = () => {
if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; } if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; }
setNewBinName(''); setCreatingBin(true); setCreatingChildOf(null); setNewBinName(''); setCreatingBin(true);
};
const createSubBin = (parentId) => {
if (!openProject) return;
setCreatingChildOf(parentId); setNewBinName(''); setCreatingBin(true);
};
const toggleBinExpanded = (binId) => {
setExpandedBins(prev => { const s = new Set(prev); s.has(binId) ? s.delete(binId) : s.add(binId); return s; });
}; };
const submitBin = (name) => { const submitBin = (name) => {
if (!name || !name.trim()) { setCreatingBin(false); return; } if (!name || !name.trim()) { setCreatingBin(false); setCreatingChildOf(null); return; }
setCreatingBin(false); setCreatingBin(false);
const parentId = creatingChildOf;
setCreatingChildOf(null);
window.ZAMPP_API.fetch('/bins', { window.ZAMPP_API.fetch('/bins', {
method: 'POST', method: 'POST',
body: JSON.stringify({ project_id: openProject.id, name: name.trim() }), body: JSON.stringify({ project_id: openProject.id, name: name.trim(), parent_id: parentId || null }),
}) })
.then(() => window.ZAMPP_API.fetch('/bins?project_id=' + openProject.id)) .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' })))) .then(list => { const n = (list||[]).map(b=>({...b,count:b.asset_count||0,icon:b.type||'grid'})); setBins(n); if (parentId) setExpandedBins(prev => { const s=new Set(prev); s.add(parentId); return s; }); })
.catch(e => window.alert('Could not create bin: ' + e.message)); .catch(e => window.alert('Could not create bin: ' + e.message));
}; };
const [view, setView] = React.useState('grid'); const [view, setView] = React.useState('grid');
@ -285,12 +302,13 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
assets = assets.filter(function(a) { return a.status === filter; }); assets = assets.filter(function(a) { return a.status === filter; });
} }
if (search) assets = assets.filter(function(a) { return a.name.toLowerCase().includes(search.toLowerCase()); }); if (search) assets = assets.filter(function(a) { return a.name.toLowerCase().includes(search.toLowerCase()); });
if (selectedBinId) assets = assets.filter(function(a) { return a.bin_id === selectedBinId; }); 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 activeBin = selectedBinId ? BINS.find(b => b.id === selectedBinId) : null;
const displayTitle = activeBin const displayTitle = activeBin
? (openProject ? openProject.name + ' · ' : '') + activeBin.name ? (openProject ? openProject.name + ' · ' : '') + activeBin.name
: (openProject ? openProject.name : 'All Assets'); : (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 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; const recentCount = ALL_ASSETS.filter(function(a) { return (Date.now() - new Date(a.created_at)) < 86400000; }).length;
@ -309,7 +327,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
{PROJECTS.slice(0, 8).map(function(p) { {PROJECTS.slice(0, 8).map(function(p) {
return ( return (
<div key={p.id} className={`rail-item ${openProject && openProject.id === p.id ? 'active' : ''}`} style={{ cursor: 'pointer' }} <div key={p.id} className={`rail-item ${openProject && openProject.id === p.id ? 'active' : ''}`} style={{ cursor: 'pointer' }}
onClick={function() { navigate('projects'); }} onClick={function() { if (onOpenProject) onOpenProject(p); }}
onContextMenu={function(e) { openProjectCtx(p, e); }}> onContextMenu={function(e) { openProjectCtx(p, e); }}>
<span className="rail-color-dot" style={{ background: p.color }} /> <span className="rail-color-dot" style={{ background: p.color }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
@ -329,45 +347,30 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
</button> </button>
</div> </div>
<div className="rail-list"> <div className="rail-list">
{creatingBin && (
<div style={{ padding: '4px 6px', display: 'flex', gap: 4, alignItems: 'center' }}>
<input
className="field-input"
autoFocus
value={newBinName}
onChange={function(e) { setNewBinName(e.target.value); }}
onKeyDown={function(e) {
if (e.key === 'Enter') submitBin(newBinName);
if (e.key === 'Escape') { setCreatingBin(false); }
}}
onBlur={function() { submitBin(newBinName); }}
placeholder="Bin name"
style={{ fontSize: 12, height: 26, padding: '0 6px', flex: 1 }}
/>
</div>
)}
{!creatingBin && BINS.length === 0 ? ( {!creatingBin && BINS.length === 0 ? (
<div style={{ fontSize: 11, color: 'var(--text-3)', padding: '6px 8px', fontStyle: 'italic' }}> <div style={{ fontSize: 11, color: 'var(--text-3)', padding: '6px 8px', fontStyle: 'italic' }}>
{openProject ? 'No bins yet: click + to create one.' : 'Open a project to manage bins.'} {openProject ? 'No bins yet: click + to create one.' : 'Open a project to manage bins.'}
</div> </div>
) : BINS.map(function(b) { ) : (
const isActive = selectedBinId === b.id; <BinTreeNodes nodes={binTree} depth={0}
const isDragTarget = draggingAssetId !== null && dragOverBinId === b.id; selectedBinId={selectedBinId} setSelectedBinId={setSelectedBinId}
return ( draggingAssetId={draggingAssetId} dragOverBinId={dragOverBinId}
<div key={b.id} onBinDragOver={onBinDragOver} onBinDrop={onBinDrop} onBinDragLeave={onBinDragLeave}
className={'rail-item' + (isActive ? ' active' : '') + (isDragTarget ? ' droppable' : '')} expandedBins={expandedBins} toggleBinExpanded={toggleBinExpanded}
onClick={function() { setSelectedBinId(isActive ? null : b.id); }} creatingBin={creatingBin} creatingChildOf={creatingChildOf}
onDragOver={function(e) { onBinDragOver(b.id, e); }} newBinName={newBinName} setNewBinName={setNewBinName}
onDrop={function(e) { onBinDrop(b.id, e); }} submitBin={submitBin} setCreatingBin={setCreatingBin} setCreatingChildOf={setCreatingChildOf}
onDragLeave={onBinDragLeave} createSubBin={createSubBin} openProject={openProject} />
style={{ cursor: 'pointer' }} )}
title={isActive ? 'Click to clear bin filter' : 'Filter to this bin'}> {creatingBin && creatingChildOf === null && (
<Icon name={binIcon(b.icon)} size={13} className="rail-icon" /> <div style={{ padding: '4px 6px', display: 'flex', gap: 4, alignItems: 'center' }}>
<span>{b.name}</span> <input className="field-input" autoFocus value={newBinName}
<span className="rail-count">{b.count}</span> onChange={function(e) { setNewBinName(e.target.value); }}
onKeyDown={function(e) { if (e.key==='Enter') submitBin(newBinName); if (e.key==='Escape') { setCreatingBin(false); } }}
onBlur={function() { submitBin(newBinName); }}
placeholder="Bin name" style={{ fontSize: 12, height: 26, padding: '0 6px', flex: 1 }} />
</div> </div>
); )}
})}
</div> </div>
</div> </div>
<div> <div>
@ -873,5 +876,6 @@ function DownloadWarningModal({ asset, onClose, onConfirm }) {
); );
} }
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.Library = Library;
window.AssetCard = AssetCard; window.AssetCard = AssetCard;

View file

@ -292,37 +292,38 @@
text-align: center; text-align: center;
margin-top: 8px; margin-top: 8px;
} }
/* Logo wrapper holds the animated pulse halo behind the image. */ /* Logo wrapper — large hero with orange pulse halo. */
.launcher-logo-wrap { .launcher-logo-wrap {
position: relative; position: relative;
display: inline-grid; display: inline-grid;
place-items: center; place-items: center;
width: 52px; width: 120px;
height: 52px; height: 120px;
flex-shrink: 0; flex-shrink: 0;
} }
.launcher-logo-pulse { .launcher-logo-pulse {
position: absolute; position: absolute;
width: 80px; width: 180px;
height: 80px; height: 180px;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle, var(--accent-soft) 0%, transparent 70%); background: radial-gradient(circle, rgba(232, 130, 28, 0.35) 0%, rgba(232, 130, 28, 0.08) 55%, transparent 70%);
animation: logoPulse 3s ease-in-out infinite; animation: logoPulse 2.8s ease-in-out infinite;
z-index: 0; z-index: 0;
} }
@keyframes logoPulse { @keyframes logoPulse {
0%, 100% { transform: scale(1); opacity: 0.6; } 0%, 100% { transform: scale(1); opacity: 0.7; }
50% { transform: scale(1.15); opacity: 1; } 50% { transform: scale(1.18); opacity: 1; }
} }
.launcher-logo { .launcher-logo {
position: relative; position: relative;
z-index: 1; z-index: 1;
width: 52px; width: 110px;
height: 52px; height: 110px;
object-fit: contain; object-fit: contain;
filter: filter:
brightness(0) invert(1) brightness(0) invert(1)
drop-shadow(0 0 8px rgba(232, 130, 28, 0.35)); drop-shadow(0 0 14px rgba(232, 130, 28, 0.6))
drop-shadow(0 0 4px rgba(255, 180, 60, 0.4));
animation: launcherLogoIn 400ms cubic-bezier(0.2, 0.7, 0.2, 1) both; animation: launcherLogoIn 400ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
} }
@keyframes launcherLogoIn { @keyframes launcherLogoIn {
@ -330,7 +331,7 @@
to { opacity: 1; transform: scale(1); } to { opacity: 1; transform: scale(1); }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.launcher-logo-pulse { animation: none; opacity: 0.5; } .launcher-logo-pulse { animation: none; opacity: 0.6; }
.launcher-logo { animation: none; } .launcher-logo { animation: none; }
} }

View file

@ -70,7 +70,7 @@
} }
.source-type-grid { .source-type-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 8px; gap: 8px;
} }
.source-type-card { .source-type-card {

View file

@ -1066,6 +1066,9 @@
.rail-item .rail-icon { color: var(--text-3); } .rail-item .rail-icon { color: var(--text-3); }
.rail-item .rail-count { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); } .rail-item .rail-count { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); }
.rail-color-dot { width: 8px; height: 8px; border-radius: 50%; } .rail-color-dot { width: 8px; height: 8px; border-radius: 50%; }
/* Show sub-bin create button only on hover of the parent rail-item */
.rail-item:hover .bin-add-child-btn { opacity: 1 !important; }
.bin-add-child-btn { padding: 0 2px; height: 18px; min-width: 18px; }
.library-main { .library-main {
display: flex; flex-direction: column; display: flex; flex-direction: column;