Redesign folder UI, VPM branding, and auto theme

- Replace chip-pill folder selector with clean vertical tree list
- VPM text wordmark replaces vpm-logo.png in login + header (no PNG/invert hack)
- Wild Dragon logo/icon retained only on favicon and splash animation
- Auto-detect prefers-color-scheme on first load (no longer defaults to dark)
- System theme changes update UI if user hasn't manually toggled
- Remove dragon icon from login card and app header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-04-06 20:26:51 -04:00
parent c03b7ef491
commit b393eca960

View file

@ -163,10 +163,10 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
/* Dragon icon in login */ /* Dragon icon in login */
.login-icon-wrap{display:flex;flex-direction:column;align-items:center;gap:.5rem;margin-bottom:1.5rem} .login-icon-wrap{display:flex;flex-direction:column;align-items:center;gap:.5rem;margin-bottom:1.5rem}
.login-dragon-icon{width:72px;height:72px;object-fit:contain;filter:drop-shadow(0 4px 16px rgba(30,75,216,.4));animation:dragonFloat 4s ease-in-out infinite} .login-dragon-icon{display:none}
.login-wordmark{display:none} .login-wordmark{display:none}
.login-vpm-logo{height:22px;object-fit:contain;opacity:.55;margin-top:.15rem} .login-vpm-text{font-family:'Outfit',sans-serif;font-size:2.4rem;font-weight:800;letter-spacing:-.04em;color:var(--text-primary);line-height:1}
[data-theme="light"] .login-vpm-logo{filter:invert(1)} .login-vpm-sub{font-size:.62rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.18em;margin-top:.3rem}
.login-product{font-size:.65rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.18em;margin-top:.25rem} .login-product{font-size:.65rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.18em;margin-top:.25rem}
.login-field{width:100%;padding:.75rem 1rem;font-family:'Outfit',sans-serif;font-size:.9rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:10px;color:var(--text-primary);outline:none;transition:all .2s;margin-bottom:.75rem} .login-field{width:100%;padding:.75rem 1rem;font-family:'Outfit',sans-serif;font-size:.9rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:10px;color:var(--text-primary);outline:none;transition:all .2s;margin-bottom:.75rem}
@ -186,12 +186,10 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
/* HEADER */ /* HEADER */
.header{display:flex;align-items:center;justify-content:space-between;padding:.8rem 2rem;border-bottom:1px solid var(--border);background:rgba(6,8,14,.9);backdrop-filter:blur(20px);position:sticky;top:0;z-index:100} .header{display:flex;align-items:center;justify-content:space-between;padding:.8rem 2rem;border-bottom:1px solid var(--border);background:rgba(6,8,14,.9);backdrop-filter:blur(20px);position:sticky;top:0;z-index:100}
.header-left{display:flex;align-items:center;gap:.85rem;min-width:0;flex:1} .header-left{display:flex;align-items:center;gap:.85rem;min-width:0;flex:1}
.header-dragon{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 2px 8px rgba(30,75,216,.5));flex-shrink:0} .header-dragon{display:none}
.header-wordmark{display:none} .header-wordmark{display:none}
.header-vpm-logo{height:20px;object-fit:contain;opacity:.5;flex-shrink:0;border-left:1px solid var(--border);padding-left:.85rem;margin-left:.1rem} .header-vpm-text{font-family:'Outfit',sans-serif;font-size:1.25rem;font-weight:800;letter-spacing:-.03em;color:var(--text-primary);flex-shrink:0}
[data-theme="light"] .header-vpm-logo{filter:invert(1)}
[data-theme="light"] .header{background:rgba(240,242,247,.9)} [data-theme="light"] .header{background:rgba(240,242,247,.9)}
[data-theme="light"] .header-dragon{filter:none}
.header-product-tag{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--dragon-bright);text-transform:uppercase;letter-spacing:.15em;background:var(--dragon-glow);border:1px solid rgba(224,92,26,.25);border-radius:4px;padding:.15rem .45rem;flex-shrink:0} .header-product-tag{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--dragon-bright);text-transform:uppercase;letter-spacing:.15em;background:var(--dragon-glow);border:1px solid rgba(224,92,26,.25);border-radius:4px;padding:.15rem .45rem;flex-shrink:0}
.header-right{display:flex;align-items:center;gap:.85rem;flex-shrink:0} .header-right{display:flex;align-items:center;gap:.85rem;flex-shrink:0}
.header-user{font-size:.75rem;color:var(--text-secondary);font-family:'JetBrains Mono',monospace} .header-user{font-size:.75rem;color:var(--text-secondary);font-family:'JetBrains Mono',monospace}
@ -230,15 +228,20 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
.mode-badge{font-size:.62rem;padding:.12rem .4rem;border-radius:4px;font-weight:700;text-transform:uppercase;background:rgba(255,255,255,.06)} .mode-badge{font-size:.62rem;padding:.12rem .4rem;border-radius:4px;font-weight:700;text-transform:uppercase;background:rgba(255,255,255,.06)}
.mode-desc{font-size:.78rem;color:var(--text-dim);margin-bottom:1.5rem;padding:.65rem .9rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;line-height:1.5} .mode-desc{font-size:.78rem;color:var(--text-dim);margin-bottom:1.5rem;padding:.65rem .9rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;line-height:1.5}
/* FOLDER CHIPS */ /* FOLDER TREE SELECTOR */
.tree-root-bar{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:.6rem} .folder-tree-box{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:.55rem}
.tree-chip{display:inline-flex;align-items:center;gap:.4rem;padding:.38rem .8rem;font-family:'JetBrains Mono',monospace;font-size:.76rem;border-radius:20px;cursor:pointer;transition:all .15s;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-secondary);user-select:none} .folder-tree-row{display:flex;align-items:center;gap:.5rem;padding:.42rem .75rem;cursor:pointer;transition:background .12s;user-select:none;border-bottom:1px solid var(--border)}
.tree-chip:hover{border-color:var(--border-bright);background:var(--bg-card)} .folder-tree-row:last-child{border-bottom:none}
.tree-chip.active{border-color:var(--blue-bright);background:var(--accent-glow);color:var(--blue-bright)} .folder-tree-row:hover{background:var(--bg-card-hover)}
.tree-chip .rm{font-size:.6rem;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:transparent;border:none;color:inherit;cursor:pointer;opacity:.5;transition:all .15s} .folder-tree-row.active{background:var(--accent-glow);border-left:3px solid var(--blue-bright)}
.tree-chip .rm:hover{opacity:1;background:var(--error-bg);color:var(--error)} .folder-tree-row.active .ftr-name{color:var(--blue-bright)}
.tree-chip-none{display:inline-flex;align-items:center;padding:.38rem .8rem;font-family:'JetBrains Mono',monospace;font-size:.76rem;border-radius:20px;cursor:pointer;transition:all .15s;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-dim);user-select:none;font-style:italic} .folder-tree-row .ftr-icon{font-size:.78rem;flex-shrink:0;width:16px;text-align:center}
.tree-chip-none.active{border-color:var(--blue-bright);background:var(--accent-glow);color:var(--blue-bright);font-style:normal} .folder-tree-row .ftr-name{font-family:'JetBrains Mono',monospace;font-size:.76rem;color:var(--text-secondary);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.folder-tree-row .ftr-rm{font-size:.6rem;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:transparent;border:none;color:inherit;cursor:pointer;opacity:.4;transition:all .15s;flex-shrink:0}
.folder-tree-row .ftr-rm:hover{opacity:1;background:var(--error-bg);color:var(--error)}
.folder-tree-children{padding-left:1.2rem}
/* keep add-row / chip styles for admin panel reuse */
.tree-chip{display:none}.tree-chip-none{display:none}.tree-root-bar{display:none}
.add-row{display:flex;gap:.4rem;align-items:center} .add-row{display:flex;gap:.4rem;align-items:center}
.add-input{flex:1;padding:.38rem .7rem;font-family:'JetBrains Mono',monospace;font-size:.76rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);outline:none;transition:all .2s} .add-input{flex:1;padding:.38rem .7rem;font-family:'JetBrains Mono',monospace;font-size:.76rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);outline:none;transition:all .2s}
.add-input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(30,75,216,.12)} .add-input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(30,75,216,.12)}
@ -420,10 +423,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
<div class="login-screen hidden" id="login-screen"> <div class="login-screen hidden" id="login-screen">
<div class="login-box"> <div class="login-box">
<div class="login-icon-wrap"> <div class="login-icon-wrap">
<img class="login-dragon-icon" src="/dragon-icon.png" alt="Wild Dragon"/> <div class="login-vpm-text">VPM</div>
<img class="login-wordmark" src="/wilddragon-logo.png" alt="Wild Dragon"/> <div class="login-vpm-sub">Broadcast Upload Portal</div>
<div class="login-product">Dragon Wind · Broadcast Platform</div>
<img class="login-vpm-logo" src="/vpm-logo.png" alt="VPM"/>
</div> </div>
<input class="login-field" id="login-user" type="text" placeholder="Username" autocomplete="username"/> <input class="login-field" id="login-user" type="text" placeholder="Username" autocomplete="username"/>
<input class="login-field" id="login-pass" type="password" placeholder="Password" autocomplete="current-password"/> <input class="login-field" id="login-pass" type="password" placeholder="Password" autocomplete="current-password"/>
@ -440,10 +441,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
<!-- HEADER --> <!-- HEADER -->
<div class="header"> <div class="header">
<div class="header-left"> <div class="header-left">
<img class="header-dragon" src="/dragon-icon.png" alt="Wild Dragon"/> <span class="header-vpm-text">VPM</span>
<img class="header-wordmark" src="/wilddragon-logo.png" alt="Wild Dragon"/> <span class="header-product-tag">Upload Portal</span>
<span class="header-product-tag">Dragon Wind</span>
<img class="header-vpm-logo" src="/vpm-logo.png" alt="VPM"/>
</div> </div>
<div class="header-right"> <div class="header-right">
<span class="header-user" id="header-user"></span> <span class="header-user" id="header-user"></span>
@ -479,13 +478,7 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
<div style="margin-bottom:1.5rem"> <div style="margin-bottom:1.5rem">
<div class="section-title">Destination Folder</div> <div class="section-title">Destination Folder</div>
<div class="tree-root-bar" id="prefix-chips"></div> <div class="folder-tree-box" id="folder-tree-box"></div>
<button class="tree-toggle" id="tree-toggle-btn" onclick="toggleTree()">
<span class="arrow"></span> Browse Subfolders
</button>
<div class="tree-panel" id="tree-panel">
<div class="tree-inner" id="tree-inner"></div>
</div>
<div class="add-row" style="margin-top:.55rem" id="add-folder-row"> <div class="add-row" style="margin-top:.55rem" id="add-folder-row">
<input class="add-input" id="new-folder-input" placeholder="New folder name…" onkeydown="if(event.key==='Enter')addFolder()"/> <input class="add-input" id="new-folder-input" placeholder="New folder name…" onkeydown="if(event.key==='Enter')addFolder()"/>
<button class="btn-small" onclick="addFolder()">+ Add</button> <button class="btn-small" onclick="addFolder()">+ Add</button>
@ -790,7 +783,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
// ============================================================ // ============================================================
// THEME // THEME
// ============================================================ // ============================================================
const savedTheme = localStorage.getItem('dw_theme') || 'dark'; const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const savedTheme = localStorage.getItem('dw_theme') || (systemDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', savedTheme); document.documentElement.setAttribute('data-theme', savedTheme);
function toggleTheme() { function toggleTheme() {
const t = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; const t = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
@ -799,6 +793,14 @@ function toggleTheme() {
document.getElementById('theme-btn').textContent = t === 'dark' ? '🌙' : '☀️'; document.getElementById('theme-btn').textContent = t === 'dark' ? '🌙' : '☀️';
} }
document.getElementById('theme-btn').textContent = savedTheme === 'dark' ? '🌙' : '☀️'; document.getElementById('theme-btn').textContent = savedTheme === 'dark' ? '🌙' : '☀️';
// Track system changes if user hasn't manually set a preference
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('dw_theme')) {
const t = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', t);
document.getElementById('theme-btn').textContent = t === 'dark' ? '🌙' : '☀️';
}
});
// ============================================================ // ============================================================
// STATE // STATE
@ -944,68 +946,52 @@ async function loadFolders() {
try { try {
const d = await api('GET','/api/folders'); const d = await api('GET','/api/folders');
folderTree = d.tree || []; folderTree = d.tree || [];
renderPrefixChips(); renderFolderTree();
renderTree(folderTree, document.getElementById('tree-inner'), []);
} catch(e) { console.error('loadFolders:',e); } } catch(e) { console.error('loadFolders:',e); }
} }
function renderPrefixChips() { function renderFolderTree() {
const bar = document.getElementById('prefix-chips'); const box = document.getElementById('folder-tree-box');
bar.innerHTML = ''; if (!box) return;
const none = document.createElement('div'); box.innerHTML = '';
none.className = 'tree-chip-none' + (selectedPrefix==='' ? ' active' : ''); // Root row
none.textContent = 'No Prefix (root)'; const rootRow = document.createElement('div');
none.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderPrefixChips(); }; rootRow.className = 'folder-tree-row' + (selectedPrefix==='' ? ' active' : '');
bar.appendChild(none); rootRow.innerHTML = `<span class="ftr-icon">🏠</span><span class="ftr-name" style="font-style:${selectedPrefix===''?'normal':'italic'};color:${selectedPrefix===''?'':'var(--text-dim)'}">Root (no folder)</span>`;
function addChips(nodes, pathArr) { rootRow.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderFolderTree(); };
box.appendChild(rootRow);
function addRows(nodes, pathArr, container) {
nodes.forEach(n => { nodes.forEach(n => {
const fullPath = [...pathArr, n.name]; const fullPath = [...pathArr, n.name];
const key = fullPath.join('/'); const key = fullPath.join('/');
const chip = document.createElement('div'); const indent = pathArr.length;
chip.className = 'tree-chip' + (selectedPrefix===key ? ' active' : ''); const row = document.createElement('div');
chip.innerHTML = `${n.children.length?'📁':'📄'} ${n.name}`; row.className = 'folder-tree-row' + (selectedPrefix===key ? ' active' : '');
row.style.paddingLeft = (0.75 + indent * 1.2) + 'rem';
const icon = n.children && n.children.length ? '📁' : '📄';
row.innerHTML = `<span class="ftr-icon">${icon}</span><span class="ftr-name">${esc(n.name)}</span>`;
if (currentRole === 'admin') { if (currentRole === 'admin') {
const rm = document.createElement('button'); const rm = document.createElement('button');
rm.className='rm'; rm.textContent='×'; rm.className = 'ftr-rm'; rm.textContent = '×';
rm.title = 'Delete folder';
rm.onclick = e => { e.stopPropagation(); deleteFolder(fullPath); }; rm.onclick = e => { e.stopPropagation(); deleteFolder(fullPath); };
chip.appendChild(rm); row.appendChild(rm);
} }
chip.onclick = e => { if (e.target.classList.contains('rm')) return; selectedPrefix=key; updatePrefixDisplay(); renderPrefixChips(); }; row.onclick = e => { if (e.target.classList.contains('ftr-rm')) return; selectedPrefix=key; updatePrefixDisplay(); renderFolderTree(); };
bar.appendChild(chip); container.appendChild(row);
if (n.children.length) addChips(n.children, fullPath); if (n.children && n.children.length) addRows(n.children, fullPath, container);
}); });
} }
addChips(folderTree, []); addRows(folderTree, [], box);
} }
// Legacy aliases so other code still works
function renderPrefixChips() { renderFolderTree(); }
function renderTree() {}
function toggleTree() {}
function updatePrefixDisplay() { document.getElementById('prefix-display').textContent = selectedPrefix || '(root)'; } function updatePrefixDisplay() { document.getElementById('prefix-display').textContent = selectedPrefix || '(root)'; }
function toggleTree() {
document.getElementById('tree-toggle-btn').classList.toggle('open');
document.getElementById('tree-panel').classList.toggle('open');
}
function renderTree(nodes, container, pathArr) {
container.innerHTML = '';
if (!nodes.length) { container.innerHTML='<div style="color:var(--text-dim);font-size:.8rem;padding:.5rem">No subfolders</div>'; return; }
nodes.forEach(n => {
const fullPath = [...pathArr, n.name];
const key = fullPath.join('/');
const div = document.createElement('div');
div.style.paddingLeft = pathArr.length ? '1.2rem' : '0';
div.style.marginBottom = '.3rem';
const row = document.createElement('div');
row.style.cssText='display:flex;align-items:center;gap:.4rem;cursor:pointer;padding:.25rem .4rem;border-radius:6px;transition:background .15s';
row.onmouseenter=()=>row.style.background='var(--bg-card-hover)';
row.onmouseleave=()=>row.style.background='';
row.innerHTML=`<span style="font-size:.72rem;color:var(--text-dim)">${n.children.length?'📁':'📄'}</span><span style="font-family:'JetBrains Mono',monospace;font-size:.76rem;color:var(--text-secondary)">${n.name}</span>`;
row.onclick=()=>{ selectedPrefix=key; updatePrefixDisplay(); renderPrefixChips(); };
div.appendChild(row);
if (n.children.length) renderTree(n.children, div, fullPath);
container.appendChild(div);
});
}
async function addFolder() { async function addFolder() {
const input = document.getElementById('new-folder-input'); const input = document.getElementById('new-folder-input');
const name = input.value.trim(); const name = input.value.trim();