Add customizable home layout editing and settings

Introduces an edit mode on the home page allowing users to drag and reposition the greeting, search bar, top sites, and 'at a glance' widget. Adds persistent storage for these layout preferences and a settings panel for fine-tuning positions and widget corner. Updates CSS, HTML, and JS to support edit UI, drag logic, and settings integration.
This commit is contained in:
2025-09-06 21:46:08 +12:00
parent 285fc44124
commit 5e3b99fdd6
5 changed files with 302 additions and 0 deletions
+30
View File
@@ -14,6 +14,9 @@
--primary: #7B2EFF; --primary: #7B2EFF;
--accent: #00C6FF; --accent: #00C6FF;
--text: #E0E0E0; --text: #E0E0E0;
--home-greeting-y: 12vh; /* user adjustable */
--home-search-y: 22vh; /* user adjustable */
--home-bookmarks-y: 40vh; /* user adjustable */
} }
/* Base reset */ /* Base reset */
@@ -46,6 +49,20 @@ body, html {
padding: 4rem 2rem 2rem; padding: 4rem 2rem 2rem;
} }
.edit-btn { position: absolute; top: 16px; right: 16px; z-index: 5; background: rgba(255,255,255,0.1); color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:8px; padding:6px 10px; cursor:pointer; backdrop-filter: blur(6px); }
.edit-btn[aria-pressed="true"] { background: rgba(255,255,255,0.22); }
.edit-mode .edit-btn { display:none; }
.edit-mode .greeting-title, .edit-mode .search-container, .edit-mode .top-sites-card, .edit-mode .glance { outline: 2px dashed rgba(255,255,255,0.35); outline-offset: 4px; cursor: grab; }
.edit-mode .glance.dragging { cursor: grabbing; }
/* Edit toolbar */
.edit-toolbar { position: fixed; top: 16px; right: 16px; display:none; gap:10px; z-index:6; backdrop-filter: blur(8px); background: rgba(8,10,16,0.5); border:1px solid rgba(255,255,255,0.15); padding:8px 10px; border-radius:12px; box-shadow: 0 12px 30px -14px rgba(0,0,0,.7); }
.edit-mode .edit-toolbar { display:flex; }
.edit-toolbar[hidden] { display: none !important; }
.edit-toolbar .btn { min-width:90px; padding:8px 12px; border-radius:8px; border:1px solid transparent; color:#fff; cursor:pointer; }
.edit-toolbar .btn.primary { background: linear-gradient(135deg, var(--accent), var(--primary)); }
.edit-toolbar .btn.secondary { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.2); }
/* Greeting hero title */ /* Greeting hero title */
.greeting-title { .greeting-title {
font-size: clamp(2rem, 5vw, 3.5rem); font-size: clamp(2rem, 5vw, 3.5rem);
@@ -54,6 +71,8 @@ body, html {
color: #cfd4ff; color: #cfd4ff;
text-shadow: 0 4px 22px rgba(0,0,0,0.6); text-shadow: 0 4px 22px rgba(0,0,0,0.6);
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
position: relative;
top: var(--home-greeting-y);
} }
@@ -86,6 +105,8 @@ body, html {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
width: 550px; /* Increased width for the new button */ width: 550px; /* Increased width for the new button */
max-width: 95vw; max-width: 95vw;
position: relative;
top: var(--home-search-y);
} }
/* Search bar */ /* Search bar */
@@ -215,6 +236,8 @@ body, html {
border: 1px solid rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.12);
box-shadow: 0 18px 50px -20px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06); box-shadow: 0 18px 50px -20px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
position: relative;
top: var(--home-bookmarks-y);
} }
.top-sites-header { .top-sites-header {
display:flex; align-items:center; justify-content:space-between; display:flex; align-items:center; justify-content:space-between;
@@ -456,11 +479,18 @@ body[data-theme="dark"] .bookmark .material-symbols-outlined,
/* At a glance widget */ /* At a glance widget */
.glance { position: fixed; right: 22px; bottom: 22px; } .glance { position: fixed; right: 22px; bottom: 22px; }
.glance.pos-br { right:22px; bottom:22px; left:auto; top:auto; }
.glance.pos-bl { left:22px; bottom:22px; right:auto; top:auto; }
.glance.pos-tr { right:22px; top:22px; left:auto; bottom:auto; }
.glance.pos-tl { left:22px; top:22px; right:auto; bottom:auto; }
.glance-card { .glance-card {
min-width: 280px; background: rgba(12,16,26,0.55); border: 1px solid rgba(255,255,255,0.1); min-width: 280px; background: rgba(12,16,26,0.55); border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px; padding: 1rem; box-shadow: 0 14px 40px -18px rgba(0,0,0,.8), inset 0 1px 0 rgba(255,255,255,0.05); border-radius: 16px; padding: 1rem; box-shadow: 0 14px 40px -18px rgba(0,0,0,.8), inset 0 1px 0 rgba(255,255,255,0.05);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
.glance { transition: transform 0.06s linear; will-change: transform; }
.glance.dragging { transform: translate3d(var(--drag-x, 0px), var(--drag-y, 0px), 0) scale(1.02); }
.glance.dragging .glance-card { box-shadow: 0 24px 60px -24px rgba(0,0,0,.9), 0 0 0 2px rgba(255,255,255,.12) inset; }
.glance-title { font-size: .95rem; color: #dfe3ff; opacity: .9; margin-bottom: .65rem; } .glance-title { font-size: .95rem; color: #dfe3ff; opacity: .9; margin-bottom: .65rem; }
.glance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; } .glance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
.glance-tile { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: .6rem .75rem; text-align:left; } .glance-tile { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: .6rem .75rem; text-align:left; }
+7
View File
@@ -12,6 +12,7 @@
</head> </head>
<body> <body>
<div class="home-container"> <div class="home-container">
<button id="editLayoutBtn" class="edit-btn" aria-pressed="false" title="Edit layout">Edit</button>
<!-- Dynamic greeting replaces the large logo for a cleaner hero look --> <!-- Dynamic greeting replaces the large logo for a cleaner hero look -->
<h1 id="greeting" class="greeting-title">Welcome</h1> <h1 id="greeting" class="greeting-title">Welcome</h1>
@@ -75,6 +76,12 @@
</div> </div>
</aside> </aside>
<!-- Edit mode toolbar -->
<div id="editToolbar" class="edit-toolbar" hidden>
<button id="cancelEditBtn" class="btn secondary" aria-label="Cancel layout edits">Cancel</button>
<button id="saveEditBtn" class="btn primary" aria-label="Save layout edits">Save</button>
</div>
<!-- Popup for adding a bookmark --> <!-- Popup for adding a bookmark -->
<div id="addPopup" class="popup hidden"> <div id="addPopup" class="popup hidden">
<div class="popup-inner"> <div class="popup-inner">
+194
View File
@@ -21,6 +21,14 @@ const greetingEl = document.getElementById('greeting');
const resetTopSitesBtn = document.getElementById('resetTopSites'); const resetTopSitesBtn = document.getElementById('resetTopSites');
const clockEl = document.getElementById('clock'); const clockEl = document.getElementById('clock');
const weatherEl = document.getElementById('weather'); const weatherEl = document.getElementById('weather');
const glanceEl = document.querySelector('.glance');
const searchContainerEl = document.querySelector('.search-container');
const topSitesEl = document.querySelector('.top-sites-card');
const editBtn = document.getElementById('editLayoutBtn');
const greetingTitleEl = document.getElementById('greeting');
const editToolbar = document.getElementById('editToolbar');
const saveEditBtn = document.getElementById('saveEditBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
let selectedIcon = initialIcons[0]; let selectedIcon = initialIcons[0];
let availableIcons = initialIcons; let availableIcons = initialIcons;
let currentIconSetKey = 'material'; let currentIconSetKey = 'material';
@@ -678,3 +686,189 @@ window.addEventListener('storage', (e) => {
loadWeather(); loadWeather();
} }
}); });
// ---- Home layout preferences ----
const HOME_SEARCH_Y_KEY = 'nebula-home-search-y';
const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y';
const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner';
const HOME_GREETING_Y_KEY = 'nebula-home-greeting-y';
function applyHomeLayoutPrefs() {
try {
const root = document.documentElement;
const greetY = Number(localStorage.getItem(HOME_GREETING_Y_KEY) || 12);
const searchY = Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22);
const bmY = Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40);
root.style.setProperty('--home-greeting-y', `${greetY}vh`);
root.style.setProperty('--home-search-y', `${searchY}vh`);
root.style.setProperty('--home-bookmarks-y', `${bmY}vh`);
const corner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
if (glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${corner}`);
}
} catch (e) { console.warn('applyHomeLayoutPrefs failed', e); }
}
applyHomeLayoutPrefs();
// React to settings updates via storage or host messages
window.addEventListener('storage', (e) => {
if (!e) return;
if ([HOME_SEARCH_Y_KEY, HOME_BOOKMARKS_Y_KEY, HOME_GLANCE_CORNER_KEY].includes(e.key)) {
applyHomeLayoutPrefs();
}
});
if (window.electronAPI && typeof window.electronAPI.on === 'function') {
window.electronAPI.on('settings-update', (payload) => {
if (!payload) return;
if (payload.searchY != null) document.documentElement.style.setProperty('--home-search-y', `${payload.searchY}vh`);
if (payload.bookmarksY != null) document.documentElement.style.setProperty('--home-bookmarks-y', `${payload.bookmarksY}vh`);
if (payload.glanceCorner && glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${payload.glanceCorner}`);
}
});
}
// ---- Edit mode drag support ----
let editMode = false;
let snapshot = null; // stores values before edits
function setEditMode(on) {
editMode = !!on;
document.body.classList.toggle('edit-mode', editMode);
if (editBtn) editBtn.setAttribute('aria-pressed', String(editMode));
if (editToolbar) editToolbar.hidden = !editMode;
if (editMode) {
// Take a snapshot of current persisted values
snapshot = {
greetY: Number(localStorage.getItem('nebula-home-greeting-y') || 12),
searchY: Number(localStorage.getItem('nebula-home-search-y') || 22),
bmY: Number(localStorage.getItem('nebula-home-bookmarks-y') || 40),
corner: localStorage.getItem('nebula-home-glance-corner') || 'br'
};
} else {
snapshot = null;
}
}
if (editBtn) {
editBtn.addEventListener('click', () => setEditMode(!editMode));
}
function vhFromPx(px) { return (px / window.innerHeight) * 100; }
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
function makeDragY(el, storageKey, cssVar) {
if (!el) return;
let startY = 0; let startTop = 0; let dragging = false;
const onDown = (ev) => {
if (!editMode) return; dragging = true;
startY = (ev.touches ? ev.touches[0].clientY : ev.clientY);
// current computed var
const current = Number((getComputedStyle(document.documentElement).getPropertyValue(cssVar) || '0vh').replace('vh',''));
startTop = isNaN(current) ? 0 : current;
ev.preventDefault();
};
const onMove = (ev) => {
if (!dragging) return;
const y = (ev.touches ? ev.touches[0].clientY : ev.clientY);
const deltaPx = y - startY;
const deltaVh = vhFromPx(deltaPx);
const next = clamp(startTop + deltaVh, 0, 90);
document.documentElement.style.setProperty(cssVar, `${next}vh`);
};
const onUp = () => {
if (!dragging) return; dragging = false;
// Don't persist here; only on Save. Values still applied via CSS var.
};
el.addEventListener('mousedown', onDown);
el.addEventListener('touchstart', onDown, { passive:false });
window.addEventListener('mousemove', onMove);
window.addEventListener('touchmove', onMove, { passive:false });
window.addEventListener('mouseup', onUp);
window.addEventListener('touchend', onUp);
}
function makeDragGlance(el) {
if (!el) return;
let dragging = false; let start;
const onDown = (ev) => {
if (!editMode) return; dragging = true; el.classList.add('dragging');
const p = ev.touches?ev.touches[0]:ev; start = { x:p.clientX, y:p.clientY };
// reset any prior drag offsets
el.style.setProperty('--drag-x','0px'); el.style.setProperty('--drag-y','0px');
ev.preventDefault();
};
const onMove = (ev) => {
if (!dragging) return; const p = ev.touches?ev.touches[0]:ev;
const dx = p.clientX - start.x; const dy = p.clientY - start.y;
el.style.setProperty('--drag-x', `${dx}px`);
el.style.setProperty('--drag-y', `${dy}px`);
};
const onUp = (ev) => {
if (!dragging) return; dragging = false; el.classList.remove('dragging');
el.style.removeProperty('--drag-x'); el.style.removeProperty('--drag-y');
const p = ev.changedTouches?ev.changedTouches[0]:ev;
const x = p.clientX; const y = p.clientY;
// snap to nearest corner
const left = x < window.innerWidth/2;
const top = y < window.innerHeight/2;
const corner = top ? (left ? 'tl' : 'tr') : (left ? 'bl' : 'br');
// Only store corner on Save; temporarily apply class for preview
if (glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${corner}`);
// Stash pending corner choice on the element during edit mode
glanceEl.dataset.pendingCorner = corner;
}
};
el.addEventListener('mousedown', onDown);
el.addEventListener('touchstart', onDown, { passive:false });
window.addEventListener('mousemove', onMove);
window.addEventListener('touchmove', onMove, { passive:false });
window.addEventListener('mouseup', onUp);
window.addEventListener('touchend', onUp);
}
makeDragY(searchContainerEl, 'nebula-home-search-y', '--home-search-y');
makeDragY(topSitesEl, 'nebula-home-bookmarks-y', '--home-bookmarks-y');
makeDragGlance(glanceEl);
makeDragY(greetingTitleEl, 'nebula-home-greeting-y', '--home-greeting-y');
// Save/Cancel handlers
if (saveEditBtn) saveEditBtn.addEventListener('click', () => {
// Persist current CSS variable values and pending corner
const rootStyle = getComputedStyle(document.documentElement);
const getVh = (v) => Math.round(Number((v || '0vh').replace('vh','')));
const gy = getVh(rootStyle.getPropertyValue('--home-greeting-y'));
const sy = getVh(rootStyle.getPropertyValue('--home-search-y'));
const by = getVh(rootStyle.getPropertyValue('--home-bookmarks-y'));
try {
localStorage.setItem('nebula-home-greeting-y', String(gy));
localStorage.setItem('nebula-home-search-y', String(sy));
localStorage.setItem('nebula-home-bookmarks-y', String(by));
} catch {}
const corner = glanceEl?.dataset?.pendingCorner || localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
try { localStorage.setItem(HOME_GLANCE_CORNER_KEY, corner); } catch {}
if (glanceEl) delete glanceEl.dataset.pendingCorner;
setEditMode(false);
});
if (cancelEditBtn) cancelEditBtn.addEventListener('click', () => {
// Revert CSS vars and glance corner to snapshot
if (snapshot) {
document.documentElement.style.setProperty('--home-greeting-y', `${snapshot.greetY}vh`);
document.documentElement.style.setProperty('--home-search-y', `${snapshot.searchY}vh`);
document.documentElement.style.setProperty('--home-bookmarks-y', `${snapshot.bmY}vh`);
if (glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${snapshot.corner}`);
delete glanceEl.dataset.pendingCorner;
}
} else {
applyHomeLayoutPrefs();
}
setEditMode(false);
});
+20
View File
@@ -409,6 +409,26 @@
<input type="radio" name="layout" value="compact"> <input type="radio" name="layout" value="compact">
<span>Compact View</span> <span>Compact View</span>
</label> </label>
<hr style="opacity:.2; margin:10px 0;">
<div style="display:grid; grid-template-columns: 1fr; gap:10px;">
<label style="display:flex; align-items:center; gap:10px;">
<span style="min-width:160px;">Search Y position</span>
<input type="range" id="home-search-y" min="5" max="85" step="1" value="22">
<span id="home-search-y-val" style="opacity:.8; width:48px; text-align:right;">22vh</span>
</label>
<label style="display:flex; align-items:center; gap:10px;">
<span style="min-width:160px;">Top Sites Y position</span>
<input type="range" id="home-bookmarks-y" min="10" max="90" step="1" value="40">
<span id="home-bookmarks-y-val" style="opacity:.8; width:48px; text-align:right;">40vh</span>
</label>
</div>
<div style="margin-top:8px;">
<div style="margin-bottom:6px;">At a glance corner</div>
<label style="margin-right:12px;"><input type="radio" name="home-glance-corner" value="br" checked> BottomRight</label>
<label style="margin-right:12px;"><input type="radio" name="home-glance-corner" value="bl"> BottomLeft</label>
<label style="margin-right:12px;"><input type="radio" name="home-glance-corner" value="tr"> TopRight</label>
<label style="margin-right:12px;"><input type="radio" name="home-glance-corner" value="tl"> TopLeft</label>
</div>
</div> </div>
</div> </div>
+51
View File
@@ -8,6 +8,9 @@ const statusDiv = document.getElementById('status');
const statusText = document.getElementById('status-text'); const statusText = document.getElementById('status-text');
const TAB_STORAGE_KEY = 'nebula-settings-active-tab'; const TAB_STORAGE_KEY = 'nebula-settings-active-tab';
const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f' const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f'
const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; // number (vh)
const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh)
const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl'
function showStatus(message) { function showStatus(message) {
if (statusText && statusDiv) { if (statusText && statusDiv) {
@@ -116,6 +119,54 @@ window.addEventListener('DOMContentLoaded', () => {
} }
})); }));
} catch (e) { console.warn('Weather unit setup failed', e); } } catch (e) { console.warn('Weather unit setup failed', e); }
// Home layout controls
try {
const searchRange = document.getElementById('home-search-y');
const searchVal = document.getElementById('home-search-y-val');
const bmRange = document.getElementById('home-bookmarks-y');
const bmVal = document.getElementById('home-bookmarks-y-val');
const cornerRadios = document.querySelectorAll('input[name="home-glance-corner"]');
const initNum = (key, def, input, label) => {
const v = Number(localStorage.getItem(key) || def);
if (input) input.value = String(v);
if (label) label.textContent = v + 'vh';
return v;
};
initNum(HOME_SEARCH_Y_KEY, 22, searchRange, searchVal);
initNum(HOME_BOOKMARKS_Y_KEY, 40, bmRange, bmVal);
const storedCorner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
cornerRadios.forEach(r => r.checked = (r.value === storedCorner));
const notify = () => {
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('settings-update', {
searchY: Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22),
bookmarksY: Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40),
glanceCorner: localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br'
});
}
};
if (searchRange) searchRange.addEventListener('input', () => {
const val = Number(searchRange.value);
searchVal.textContent = val + 'vh';
localStorage.setItem(HOME_SEARCH_Y_KEY, String(val));
notify();
});
if (bmRange) bmRange.addEventListener('input', () => {
const val = Number(bmRange.value);
bmVal.textContent = val + 'vh';
localStorage.setItem(HOME_BOOKMARKS_Y_KEY, String(val));
notify();
});
cornerRadios.forEach(r => r.addEventListener('change', () => {
const val = document.querySelector('input[name="home-glance-corner"]:checked')?.value || 'br';
localStorage.setItem(HOME_GLANCE_CORNER_KEY, val);
notify();
}));
} catch (e) { console.warn('Home layout control setup failed', e); }
}); });
// Tabs: simple controller // Tabs: simple controller