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:
parent
c03b7ef491
commit
b393eca960
1 changed files with 64 additions and 78 deletions
|
|
@ -163,10 +163,10 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
|
||||
/* Dragon icon in login */
|
||||
.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-vpm-logo{height:22px;object-fit:contain;opacity:.55;margin-top:.15rem}
|
||||
[data-theme="light"] .login-vpm-logo{filter:invert(1)}
|
||||
.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}
|
||||
.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-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{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-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-vpm-logo{height:20px;object-fit:contain;opacity:.5;flex-shrink:0;border-left:1px solid var(--border);padding-left:.85rem;margin-left:.1rem}
|
||||
[data-theme="light"] .header-vpm-logo{filter:invert(1)}
|
||||
.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{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-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}
|
||||
|
|
@ -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-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 */
|
||||
.tree-root-bar{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:.6rem}
|
||||
.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}
|
||||
.tree-chip:hover{border-color:var(--border-bright);background:var(--bg-card)}
|
||||
.tree-chip.active{border-color:var(--blue-bright);background:var(--accent-glow);color:var(--blue-bright)}
|
||||
.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}
|
||||
.tree-chip .rm:hover{opacity:1;background:var(--error-bg);color:var(--error)}
|
||||
.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}
|
||||
.tree-chip-none.active{border-color:var(--blue-bright);background:var(--accent-glow);color:var(--blue-bright);font-style:normal}
|
||||
/* FOLDER TREE SELECTOR */
|
||||
.folder-tree-box{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:.55rem}
|
||||
.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)}
|
||||
.folder-tree-row:last-child{border-bottom:none}
|
||||
.folder-tree-row:hover{background:var(--bg-card-hover)}
|
||||
.folder-tree-row.active{background:var(--accent-glow);border-left:3px solid var(--blue-bright)}
|
||||
.folder-tree-row.active .ftr-name{color:var(--blue-bright)}
|
||||
.folder-tree-row .ftr-icon{font-size:.78rem;flex-shrink:0;width:16px;text-align:center}
|
||||
.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-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)}
|
||||
|
|
@ -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-box">
|
||||
<div class="login-icon-wrap">
|
||||
<img class="login-dragon-icon" src="/dragon-icon.png" alt="Wild Dragon"/>
|
||||
<img class="login-wordmark" src="/wilddragon-logo.png" alt="Wild Dragon"/>
|
||||
<div class="login-product">Dragon Wind · Broadcast Platform</div>
|
||||
<img class="login-vpm-logo" src="/vpm-logo.png" alt="VPM"/>
|
||||
<div class="login-vpm-text">VPM</div>
|
||||
<div class="login-vpm-sub">Broadcast Upload Portal</div>
|
||||
</div>
|
||||
<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"/>
|
||||
|
|
@ -440,10 +441,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<img class="header-dragon" src="/dragon-icon.png" alt="Wild Dragon"/>
|
||||
<img class="header-wordmark" src="/wilddragon-logo.png" alt="Wild Dragon"/>
|
||||
<span class="header-product-tag">Dragon Wind</span>
|
||||
<img class="header-vpm-logo" src="/vpm-logo.png" alt="VPM"/>
|
||||
<span class="header-vpm-text">VPM</span>
|
||||
<span class="header-product-tag">Upload Portal</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<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 class="section-title">Destination Folder</div>
|
||||
<div class="tree-root-bar" id="prefix-chips"></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="folder-tree-box" id="folder-tree-box"></div>
|
||||
<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()"/>
|
||||
<button class="btn-small" onclick="addFolder()">+ Add</button>
|
||||
|
|
@ -790,7 +783,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
// ============================================================
|
||||
// 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);
|
||||
function toggleTheme() {
|
||||
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 = 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
|
||||
|
|
@ -944,68 +946,52 @@ async function loadFolders() {
|
|||
try {
|
||||
const d = await api('GET','/api/folders');
|
||||
folderTree = d.tree || [];
|
||||
renderPrefixChips();
|
||||
renderTree(folderTree, document.getElementById('tree-inner'), []);
|
||||
renderFolderTree();
|
||||
} catch(e) { console.error('loadFolders:',e); }
|
||||
}
|
||||
|
||||
function renderPrefixChips() {
|
||||
const bar = document.getElementById('prefix-chips');
|
||||
bar.innerHTML = '';
|
||||
const none = document.createElement('div');
|
||||
none.className = 'tree-chip-none' + (selectedPrefix==='' ? ' active' : '');
|
||||
none.textContent = 'No Prefix (root)';
|
||||
none.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderPrefixChips(); };
|
||||
bar.appendChild(none);
|
||||
function addChips(nodes, pathArr) {
|
||||
function renderFolderTree() {
|
||||
const box = document.getElementById('folder-tree-box');
|
||||
if (!box) return;
|
||||
box.innerHTML = '';
|
||||
// Root row
|
||||
const rootRow = document.createElement('div');
|
||||
rootRow.className = 'folder-tree-row' + (selectedPrefix==='' ? ' active' : '');
|
||||
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>`;
|
||||
rootRow.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderFolderTree(); };
|
||||
box.appendChild(rootRow);
|
||||
function addRows(nodes, pathArr, container) {
|
||||
nodes.forEach(n => {
|
||||
const fullPath = [...pathArr, n.name];
|
||||
const key = fullPath.join('/');
|
||||
const chip = document.createElement('div');
|
||||
chip.className = 'tree-chip' + (selectedPrefix===key ? ' active' : '');
|
||||
chip.innerHTML = `${n.children.length?'📁':'📄'} ${n.name}`;
|
||||
const indent = pathArr.length;
|
||||
const row = document.createElement('div');
|
||||
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') {
|
||||
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); };
|
||||
chip.appendChild(rm);
|
||||
row.appendChild(rm);
|
||||
}
|
||||
chip.onclick = e => { if (e.target.classList.contains('rm')) return; selectedPrefix=key; updatePrefixDisplay(); renderPrefixChips(); };
|
||||
bar.appendChild(chip);
|
||||
if (n.children.length) addChips(n.children, fullPath);
|
||||
row.onclick = e => { if (e.target.classList.contains('ftr-rm')) return; selectedPrefix=key; updatePrefixDisplay(); renderFolderTree(); };
|
||||
container.appendChild(row);
|
||||
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 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() {
|
||||
const input = document.getElementById('new-folder-input');
|
||||
const name = input.value.trim();
|
||||
|
|
|
|||
Loading…
Reference in a new issue