diff --git a/services/web-ui/public/screens-home.jsx b/services/web-ui/public/screens-home.jsx index 14eeaa3..5a11986 100644 --- a/services/web-ui/public/screens-home.jsx +++ b/services/web-ui/public/screens-home.jsx @@ -17,146 +17,6 @@ // // Anything that would just say "all clear" is hidden, not rendered. -// screens-home.jsx -// -// Two routes share this file: -// -// • Home - the launcher. Big-button entry into each section of the MAM. -// -// • Dashboard - the operations view. Rebuilt as a control-room status board. - -// ─── DragonFlame ───────────────────────────────────────────── -// Canvas-based particle flame rendered behind the logo. Each particle rises -// from the base, fades as it climbs, and shifts hue from deep orange → yellow. -// Spectral shimmer is added via a secondary "spark" layer with lighter colors. -function DragonFlame() { - const canvasRef = React.useRef(null); - const rafRef = React.useRef(null); - - React.useEffect(function() { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - - // Particle pool - const W = 160, H = 200; - canvas.width = W; - canvas.height = H; - - // Particle factory - function mkParticle(isSpark) { - const x = W * 0.5 + (Math.random() - 0.5) * (isSpark ? 30 : 50); - return { - x, - y: H - 10 - Math.random() * 20, - vx: (Math.random() - 0.5) * (isSpark ? 0.6 : 0.9), - vy: -(0.8 + Math.random() * (isSpark ? 2.2 : 1.6)), - life: 0, - maxLife: 50 + Math.random() * (isSpark ? 30 : 50), - size: isSpark ? 1 + Math.random() * 2 : 3 + Math.random() * 5, - spark: isSpark, - wobble: (Math.random() - 0.5) * 0.04, - }; - } - - const COUNT = 90, SPARK_COUNT = 30; - const particles = Array.from({ length: COUNT }, function() { return mkParticle(false); }); - const sparks = Array.from({ length: SPARK_COUNT }, function() { return mkParticle(true); }); - - function reset(p) { - var n = mkParticle(p.spark); - Object.assign(p, n); - } - - function draw() { - ctx.clearRect(0, 0, W, H); - - // Draw glow base - const grad = ctx.createRadialGradient(W * 0.5, H - 5, 0, W * 0.5, H - 5, 55); - grad.addColorStop(0, 'rgba(255,120,0,0.18)'); - grad.addColorStop(0.5, 'rgba(255,70,0,0.07)'); - grad.addColorStop(1, 'transparent'); - ctx.fillStyle = grad; - ctx.beginPath(); - ctx.ellipse(W * 0.5, H - 5, 55, 30, 0, 0, Math.PI * 2); - ctx.fill(); - - // Update + draw each particle - var all = particles.concat(sparks); - all.forEach(function(p) { - p.life += 1; - if (p.life >= p.maxLife) { reset(p); return; } - - var t = p.life / p.maxLife; // 0 → 1 - // Gentle horizontal drift (wobble) - p.vx += p.wobble; - p.vx *= 0.98; - p.x += p.vx; - p.y += p.vy; - // Decelerate rise near end - p.vy *= 0.99; - - var alpha = p.spark - ? Math.sin(t * Math.PI) * 0.85 - : (t < 0.2 ? t / 0.2 : 1 - (t - 0.2) / 0.8) * 0.7; - - // Colour: deep orange (0°) → orange (20°) → yellow (50°) as t rises - var hue = p.spark - ? 40 + t * 20 // sparks: golden yellow - : 10 + t * 45; // flame: orange→yellow - var sat = 100; - var lgt = p.spark ? 70 + t * 20 : 50 + t * 20; - - ctx.save(); - ctx.globalAlpha = alpha; - if (p.spark) { - // Sparks: small bright dots - ctx.fillStyle = 'hsl(' + hue + ',' + sat + '%,' + lgt + '%)'; - ctx.beginPath(); - ctx.arc(p.x, p.y, p.size * (1 - t * 0.5), 0, Math.PI * 2); - ctx.fill(); - } else { - // Flame particles: soft blurred ellipses - var size = p.size * (1 - t * 0.6); - var g2 = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size * 2.2); - g2.addColorStop(0, 'hsla(' + hue + ',' + sat + '%,' + lgt + '%,1)'); - g2.addColorStop(0.4, 'hsla(' + (hue - 8) + ',' + sat + '%,' + (lgt - 10) + '%,0.5)'); - g2.addColorStop(1, 'transparent'); - ctx.fillStyle = g2; - ctx.beginPath(); - ctx.ellipse(p.x, p.y, size * 2.2, size * 3.5, 0, 0, Math.PI * 2); - ctx.fill(); - } - ctx.restore(); - }); - - rafRef.current = requestAnimationFrame(draw); - } - - // Check reduced-motion preference - var mq = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)'); - if (mq && mq.matches) { - // Static glow only - var sg = ctx.createRadialGradient(W * 0.5, H * 0.65, 0, W * 0.5, H * 0.65, 80); - sg.addColorStop(0, 'rgba(255,130,0,0.22)'); - sg.addColorStop(0.6, 'rgba(255,70,0,0.08)'); - sg.addColorStop(1, 'transparent'); - ctx.fillStyle = sg; - ctx.fillRect(0, 0, W, H); - } else { - draw(); - } - - return function() { - if (rafRef.current) cancelAnimationFrame(rafRef.current); - }; - }, []); - - return React.createElement('span', { className: 'launcher-logo-pulse' }, - React.createElement('canvas', { ref: canvasRef }) - ); -} - function Home({ navigate }) { const [showDownloads, setShowDownloads] = React.useState(false); @@ -274,7 +134,7 @@ function Home({ navigate }) {
{
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) => {
- if (!name || !name.trim()) { setCreatingBin(false); return; }
+ if (!name || !name.trim()) { setCreatingBin(false); setCreatingChildOf(null); return; }
setCreatingBin(false);
+ const parentId = creatingChildOf;
+ setCreatingChildOf(null);
window.ZAMPP_API.fetch('/bins', {
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(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));
};
const [view, setView] = React.useState('grid');
diff --git a/services/web-ui/public/styles-fixes.css b/services/web-ui/public/styles-fixes.css
index cd70853..960d904 100644
--- a/services/web-ui/public/styles-fixes.css
+++ b/services/web-ui/public/styles-fixes.css
@@ -292,37 +292,38 @@
text-align: center;
margin-top: 8px;
}
-/* Logo wrapper holds the animated pulse halo behind the image. */
+/* Logo wrapper — large hero with orange pulse halo. */
.launcher-logo-wrap {
position: relative;
display: inline-grid;
place-items: center;
- width: 52px;
- height: 52px;
+ width: 120px;
+ height: 120px;
flex-shrink: 0;
}
.launcher-logo-pulse {
position: absolute;
- width: 80px;
- height: 80px;
+ width: 180px;
+ height: 180px;
border-radius: 50%;
- background: radial-gradient(circle, var(--accent-soft) 0%, transparent 70%);
- animation: logoPulse 3s ease-in-out infinite;
+ background: radial-gradient(circle, rgba(232, 130, 28, 0.35) 0%, rgba(232, 130, 28, 0.08) 55%, transparent 70%);
+ animation: logoPulse 2.8s ease-in-out infinite;
z-index: 0;
}
@keyframes logoPulse {
- 0%, 100% { transform: scale(1); opacity: 0.6; }
- 50% { transform: scale(1.15); opacity: 1; }
+ 0%, 100% { transform: scale(1); opacity: 0.7; }
+ 50% { transform: scale(1.18); opacity: 1; }
}
.launcher-logo {
position: relative;
z-index: 1;
- width: 52px;
- height: 52px;
+ width: 110px;
+ height: 110px;
object-fit: contain;
filter:
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;
}
@keyframes launcherLogoIn {
@@ -330,7 +331,7 @@
to { opacity: 1; transform: scale(1); }
}
@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; }
}