diff --git a/Cargo.lock b/Cargo.lock index 57e5796..3ce37a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2251,7 +2251,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openpxe" -version = "0.5.2" +version = "0.5.3" dependencies = [ "anyhow", "axum", @@ -2273,7 +2273,7 @@ dependencies = [ [[package]] name = "openpxe-core" -version = "0.5.2" +version = "0.5.3" dependencies = [ "anyhow", "base64", @@ -2299,7 +2299,7 @@ dependencies = [ [[package]] name = "openpxe-dhcp-proxy" -version = "0.5.2" +version = "0.5.3" dependencies = [ "anyhow", "bytes", @@ -2313,7 +2313,7 @@ dependencies = [ [[package]] name = "openpxe-http-api" -version = "0.5.2" +version = "0.5.3" dependencies = [ "anyhow", "axum", @@ -2349,7 +2349,7 @@ dependencies = [ [[package]] name = "openpxe-ipxe-assets" -version = "0.5.2" +version = "0.5.3" dependencies = [ "openpxe-core", "rust-embed", @@ -2359,7 +2359,7 @@ dependencies = [ [[package]] name = "openpxe-iso-store" -version = "0.5.2" +version = "0.5.3" dependencies = [ "anyhow", "bcrypt", @@ -2386,7 +2386,7 @@ dependencies = [ [[package]] name = "openpxe-tftp" -version = "0.5.2" +version = "0.5.3" dependencies = [ "anyhow", "bytes", @@ -2400,7 +2400,7 @@ dependencies = [ [[package]] name = "openpxe-webui" -version = "0.5.2" +version = "0.5.3" [[package]] name = "p256" diff --git a/Cargo.toml b/Cargo.toml index dfa8d42..9e6076a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.5.2" +version = "0.5.3" edition = "2021" rust-version = "1.95" license = "MIT OR Apache-2.0" diff --git a/crates/webui/src/app.css b/crates/webui/src/app.css index fc11c2e..fb85d98 100644 --- a/crates/webui/src/app.css +++ b/crates/webui/src/app.css @@ -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; diff --git a/crates/webui/src/app.js b/crates/webui/src/app.js index 5ee77a3..eb541b2 100644 --- a/crates/webui/src/app.js +++ b/crates/webui/src/app.js @@ -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.