v0.5.3: dark-mode branding preview + unified button spacing

UI polish:
- Settings → Branding: each logo swatch now previews on a background
  matching where the mark lands (light page / dark page / dark PXE screen)
  regardless of the current page theme, so the Dark slot reads as dark
  even while viewing Settings in light mode.
- Site-wide button spacing: add one rule (`.card .body > button`) giving
  every primary card action button the same gap above it, and drop the
  ad-hoc per-button inline margins (14/16/6px) so the look is uniform.
  Fixes the Hosts → "Bind MAC to target" button butting against the form.

(Boot-menu highlight intentionally unchanged — a rotating-RGB highlight
isn't possible in iPXE's static single-draw menu; deferred to a future
custom-renderer effort.)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-06-03 02:50:51 -04:00
co-authored by Claude Opus 4.8
parent a906c47f53
commit 7358013093
4 changed files with 30 additions and 18 deletions
+8
View File
@@ -304,6 +304,14 @@ button.ghost { background: transparent; color: var(--fg); border: 1px solid var(
button.ghost:hover { background: var(--bg-panel-2); color: var(--fg); }
button.danger { background: transparent; color: var(--err); border: 1px solid color-mix(in srgb, var(--err) 35%, transparent); }
button.danger:hover { background: color-mix(in srgb, var(--err) 14%, transparent); color: var(--err); }
/* v0.5.3: unified spacing for a card's primary action button(s). Any
button that sits as a direct child of a card body (Save, Bind, Add,
Launch, …) gets the same gap above it so it never butts against the
form. Inline buttons inside table rows / toolbars / logo slots /
modal action bars are nested deeper, so the `>` keeps them untouched.
Adjacent action buttons on one row (e.g. Save + Send test) share the
margin and stay aligned. */
.card .body > button { margin-top: 16px; }
label.field {
display: grid; gap: 4px; margin-bottom: 14px;
+13 -9
View File
@@ -904,7 +904,7 @@
protoSelect.addEventListener('change', syncProto);
// One add button; dispatches to the selected protocol's endpoint.
const addShare = el('button', {style:'margin-top:14px', onclick: async () => {
const addShare = el('button', {onclick: async () => {
if (protoSelect.value === 'smb') {
if (!smbServer.value || !smbShare.value) {
shareMsg.replaceChildren(document.createTextNode('Server and share name are required.'));
@@ -1411,7 +1411,7 @@
// beneath the input row instead of butting against the password
// fields. Mirrors the `Save SSO settings` button below for visual
// parity between the two settings cards.
const accountSave = el('button', {style:'margin-top:6px', onclick: async () => {
const accountSave = el('button', {onclick: async () => {
accountMsg.textContent = ''; accountMsg.className = 'msg';
if (!currentPw.value) {
accountMsg.textContent = 'Current password is required.';
@@ -1549,7 +1549,7 @@
};
ssoMode.onchange = refreshSsoFields;
const ssoMsg = el('div', {class:'msg', style:'margin-top:8px'});
const ssoSave = el('button', {style:'margin-top:16px', onclick: async () => {
const ssoSave = el('button', {onclick: async () => {
ssoMsg.textContent = ''; ssoMsg.className = 'msg';
const payload = {
enabled: ssoEnabled.checked,
@@ -1630,13 +1630,17 @@
const logoMsg = el('div', {class:'msg', style:'margin-top:10px'});
const bust = '?v=' + Date.now(); // bust the preview cache after a change
const brandingPresence = status.branding || { light:false, dark:false, client:false };
// Each swatch previews on a background matching where the mark
// lands (light page / dark page / dark PXE screen), independent of
// the operator's current page theme — so the Dark slot always reads
// as dark even while viewing Settings in light mode.
const slotDefs = [
{ slot:'light', title:'Light mode', preview:'/assets/logo.svg?theme=light' + '&' + bust.slice(1),
hint:'Shown on light-theme pages.', accept:'image/svg+xml,image/png,image/jpeg,image/webp,image/gif' },
swatchBg:'#f4f5f7', hint:'Shown on light-theme pages.', accept:'image/svg+xml,image/png,image/jpeg,image/webp,image/gif' },
{ slot:'dark', title:'Dark mode', preview:'/assets/logo.svg?theme=dark' + '&' + bust.slice(1),
hint:'Shown on dark-theme pages.', accept:'image/svg+xml,image/png,image/jpeg,image/webp,image/gif' },
swatchBg:'#0e1014', hint:'Shown on dark-theme pages.', accept:'image/svg+xml,image/png,image/jpeg,image/webp,image/gif' },
{ slot:'client', title:'Client', preview:'/branding/pxe-logo' + bust,
hint:'Above the PXE boot menu.', accept:'image/png,image/jpeg,image/webp,image/gif' },
swatchBg:'#0e1014', hint:'Above the PXE boot menu.', accept:'image/png,image/jpeg,image/webp,image/gif' },
];
const slotCol = (def) => {
const set = !!brandingPresence[def.slot];
@@ -1662,7 +1666,7 @@
el('span', {class:'name'}, def.title),
set ? el('span', {class:'tag ok'}, 'set') : el('span', {class:'tag'}, 'default'),
]),
el('div', {class:'swatch', style: def.slot === 'light' ? 'background:#f4f5f7' : ''},
el('div', {class:'swatch', style:'background:' + def.swatchBg},
el('img', {src: def.preview, alt: def.title + ' logo'})),
el('div', {class:'logo-slot-hint'}, def.hint),
el('div', {style:'display:flex;gap:6px;flex-wrap:wrap'}, [
@@ -1795,13 +1799,13 @@
smtp_implicit_tls: sTls.checked,
});
const saveBtn = el('button', {style:'margin-top:16px', onclick: async () => {
const saveBtn = el('button', {onclick: async () => {
nMsg.textContent = 'Saving…'; nMsg.className = 'msg';
const r = await putJSON('/api/notify', collectNotify());
if (r.ok) { nMsg.textContent = 'Saved.'; nMsg.className = 'msg ok'; render('settings'); }
else { nMsg.textContent = 'Save failed: ' + (await r.text()); nMsg.className = 'msg err'; }
}}, 'Save notification settings');
const testBtn = el('button', {class:'ghost', style:'margin-top:16px;margin-left:8px',
const testBtn = el('button', {class:'ghost', style:'margin-left:8px',
onclick: async () => {
nMsg.textContent = 'Sending test…'; nMsg.className = 'msg';
// Save first so the test uses exactly what's on screen.