From bd791462be2488e446c01d25cdefced6ae30e293 Mon Sep 17 00:00:00 2001 From: Miles Ward Date: Tue, 26 May 2026 02:32:38 -0400 Subject: [PATCH] v0.4.63: SSO row alignment, themed checkbox, dropdown affordance Three UI nits the operator caught on v0.4.62, plus the queued PXE-theme research note for the next release. - SSO header grid is now a 4-column form-row matching the Administrator account card column-for-column (display name / logo URL / metadata source / metadata URL). Switching to XML mode collapses column 4 and drops the multi-line textarea on its own full-width row below. - Native form chrome (checkboxes, scroll bars) follows the active OpenPXE theme via CSS `color-scheme`; the inline meta tag was forcing dark form controls in light mode, which is why the "Enable single sign-on" checkbox rendered as an opaque black square against the light panel. - Checkbox itself is now custom-styled (16x16 rounded square, accent fill + tick on :checked) so the chrome reads identically across both palettes and browsers, not just on whichever WebKit happens to honor `accent-color`. - dropdown arrow + disappears, which makes the "Metadata source" pick-list look like a + plain (and slightly squished) text input. Paint our own chevron via + background-image so the control still reads as a dropdown, and reserve + right-padding for it. The data-URI SVG inherits currentColor via the + `stroke` attribute so the arrow follows light/dark theme without a + second declaration. */ +label.field select { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 11px 7px; + padding-right: 30px; +} +:root[data-theme="light"] label.field select { + background-image: url("data:image/svg+xml;utf8,"); +} label.field input:focus, label.field select:focus, label.field textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent); @@ -313,7 +339,40 @@ label.check { padding: 8px 10px; margin-bottom: 6px; border: 1px solid var(--border-soft); border-radius: var(--radius); } -label.check input { accent-color: var(--accent); } +/* v0.4.63: native checkboxes used to render as opaque black squares in + light mode because the page meta declares `color-scheme: dark light` + and `accent-color` alone only repaints the *check mark* (not the + container). Take full control of the chrome so the box reads cleanly + on both palettes and the checked state lights up in our accent. */ +label.check input[type="checkbox"] { + appearance: none; -webkit-appearance: none; + width: 16px; height: 16px; flex: none; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 3px; + display: inline-grid; place-content: center; + cursor: pointer; margin: 0; + transition: background 0.1s ease, border-color 0.1s ease; +} +label.check input[type="checkbox"]:hover { border-color: var(--accent); } +label.check input[type="checkbox"]:checked { + background: var(--accent); + border-color: var(--accent); +} +label.check input[type="checkbox"]:checked::after { + /* Classic ✓ glyph built from a rotated rectangle border. Colour is + #002923 (the same near-black we use on solid-accent buttons) so the + tick stays legible against the teal fill in both themes. */ + content: ''; + width: 4px; height: 8px; + border: solid #002923; + border-width: 0 2px 2px 0; + transform: rotate(45deg) translate(-1px, -1px); +} +label.check input[type="checkbox"]:focus-visible { + outline: none; + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent); +} /* ── Drop zone ────────────────────────────────────────────────────── */ diff --git a/crates/webui/src/app.js b/crates/webui/src/app.js index 8786a0a..1d013b4 100644 --- a/crates/webui/src/app.js +++ b/crates/webui/src/app.js @@ -1025,7 +1025,11 @@ const newPwConfirm = el('input', {type:'password', autocomplete:'new-password', placeholder: 'confirm new password'}); const accountMsg = el('div', {class:'msg', style:'margin-top:8px'}); - const accountSave = el('button', {onclick: async () => { + // v0.4.63: explicit top margin so the action button sits clearly + // 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 () => { accountMsg.textContent = ''; accountMsg.className = 'msg'; if (!currentPw.value) { accountMsg.textContent = 'Current password is required.'; @@ -1130,29 +1134,39 @@ el('option', {value:'xml'}, 'Metadata XML'), ]); ssoMode.value = sso.metadata && !sso.metadata_url ? 'xml' : 'url'; + // v0.4.63: the IdP metadata URL now sits inside the 4-col header + // grid as column 4, so the SSO row is column-for-column aligned with + // the Administrator account row above. When the operator switches + // to XML mode, column 4 collapses (display:none) and the multi-line + // XML textarea takes its own full-width row below — there's no way + // to fit a 6-row textarea into a single grid cell without making + // the rest of the row look stretched. const urlWrap = el('label', {class:'field'}, [ el('span', {class:'name'}, 'IdP metadata URL'), ssoUrl, - el('span', {class:'hint'}, - 'OpenPXE will fetch this URL once SSO sign-in lands; v0.4.6 just stores it.'), ]); - const xmlWrap = el('label', {class:'field'}, [ + const xmlWrap = el('label', {class:'field', style:'margin-top:14px'}, [ el('span', {class:'name'}, 'IdP metadata XML'), ssoXml, el('span', {class:'hint'}, 'Paste the raw document from your IdP.'), ]); + // Hint that used to live under the URL field; surfaced once below + // the whole row so it doesn't compete with the in-grid layout. + const urlHint = el('p', {class:'msg', style:'margin-top:10px;margin-bottom:0'}, + 'OpenPXE will fetch the metadata URL once SSO sign-in lands; v0.4.63 stores it.'); const refreshSsoFields = () => { if (ssoMode.value === 'url') { urlWrap.style.display = ''; xmlWrap.style.display = 'none'; + urlHint.style.display = ''; } else { urlWrap.style.display = 'none'; xmlWrap.style.display = ''; + urlHint.style.display = 'none'; } }; ssoMode.onchange = refreshSsoFields; - refreshSsoFields(); const ssoMsg = el('div', {class:'msg', style:'margin-top:8px'}); - const ssoSave = el('button', {onclick: async () => { + const ssoSave = el('button', {style:'margin-top:16px', onclick: async () => { ssoMsg.textContent = ''; ssoMsg.className = 'msg'; const payload = { enabled: ssoEnabled.checked, @@ -1193,32 +1207,35 @@ ssoEnabled, el('span', {}, 'Enable single sign-on'), ]), - // 3-column header strip: display name, logo URL, metadata - // source. All three controls inherit the same border/padding/ - // focus chrome from the global `label.field input/select` - // rule, so they line up cleanly. Below: the active source - // field (URL or XML) spans the full width. - el('div', {class:'form-row cols-3'}, [ + // v0.4.63: 4-column form-row that matches the Administrator + // account card above column-for-column — display name / logo + // URL / metadata source / metadata URL. All four controls share + // the same `label.field` chrome so they line up cleanly. When + // the operator picks "Metadata XML" the URL column collapses + // and the multi-line textarea drops below the row. + el('div', {class:'form-row'}, [ el('label', {class:'field'}, [ el('span', {class:'name'}, 'IdP display name'), ssoName, - el('span', {class:'hint'}, '"Sign in with X" label on the login screen.'), ]), el('label', {class:'field'}, [ el('span', {class:'name'}, 'IdP logo URL'), ssoLogo, - el('span', {class:'hint'}, 'Optional. Shown next to the IdP name on the login button.'), ]), el('label', {class:'field'}, [ el('span', {class:'name'}, 'Metadata source'), ssoMode, ]), + urlWrap, ]), - urlWrap, xmlWrap, + urlHint, ssoSave, ssoMsg, ]), ]); + // Wire up + paint the initial visibility now that all elements + // referenced by `refreshSsoFields` are attached. + refreshSsoFields(); // ── Custom logo upload. // Single-file drop-zone; PNG/SVG/JPEG/WebP/GIF up to 2 MB. diff --git a/crates/webui/src/index.html b/crates/webui/src/index.html index 59a0240..b65e11c 100644 --- a/crates/webui/src/index.html +++ b/crates/webui/src/index.html @@ -66,7 +66,7 @@ - + diff --git a/docs/queued/ipxe-pxe-menu-theme-research.md b/docs/queued/ipxe-pxe-menu-theme-research.md new file mode 100644 index 0000000..07dbf6e --- /dev/null +++ b/docs/queued/ipxe-pxe-menu-theme-research.md @@ -0,0 +1,106 @@ +# PXE menu theme — research for next-release follow-up + +Status: queued. v0.4.63 keeps the ASCII-banner fallback + `console --picture` +compositor wired; this note captures the design for the menu-theming work +that lands once iPXE rebuilt with `IMAGE_PNG` is published. + +## How iVentoy actually does it + +iVentoy is closed-source for its menu, but the supporting bits are +public at https://github.com/ventoy/PXE — a vanilla iPXE snapshot +(`iPXE/ipxe-bd13697`) used to produce the loader binaries iVentoy +serves over TFTP (`pxeboot.efi`, `iventoy_loader_16000`, +`iventoy_loader_16000_uefi`). + +The graphical menu itself is rendered by iPXE's framebuffer console +with a baked-in PNG background via `console --picture` — same +primitive OpenPXE already uses in `crates/http-api/src/ipxe_script.rs`. +Evidence: + +- The iPXE build in `ventoy/PXE` is configured with `CONSOLE_FRAMEBUFFER` + + `IMAGE_PNG` + `CONSOLE_CMD` (the three flags `console --picture` + needs). +- iVentoy issue #11 confirms "iventoy using default 1024x768"; users + report 800x600 / 1024x768 / 1280x720 / 1280x1024 / 1920x1080 as + selectable resolutions from the iVentoy web UI **Configuration tab**, + not via EDID auto-detect. iPXE has no EDID parsing; the daemon writes + a resolution-tagged script per boot and serves the matching PNG. +- iVentoy docs explicitly state both Free and Pro editions **do not + support** modifying the boot background/title — it's baked into the + shipped PNG assets. +- Chrome is iPXE's native `menu` / `item` / `choose` widgets (single + highlight bar, no borders) painted on top of the PNG, with margins + set via `console --left/--right/--top/--bottom` to keep the text off + the logo. Not GRUB, not syslinux — UEFI iVentoy uses iPXE's + `snponly.efi` / `pxeboot.efi`, and `--picture` does work under UEFI + GOP despite older folklore. + +Do not conflate this with Ventoy-USB, which is a separate codebase and +uses GRUB2 themes (`theme.txt`, `background_ventoy.png`, `select_c.png`). + +## Rust ingredients to replicate / surpass + +Most of these already exist in the workspace. + +1. **Compositor (extend, don't replace)** — extend + `crates/iso-store/src/pxe_logo.rs` to emit per-resolution PNGs + (1024x768, 1280x1024, 1920x1080 as the v1 set). `image` + + `imageproc` crates handle scaling; `ab_glyph` / `fontdue` for raster + text (subtitle, hostname, version). One source SVG/logo, three to + five rendered PNGs cached on disk. +2. **Script generator** — `ipxe_script.rs` already emits + `console --picture … || console`. Add a `?res=` query param (or + per-MAC client hint persisted in `hosts.json`) and serve the matching + PNG plus matching `console --x --y` line. Keep the text-console + fallback already in place. +3. **Resolution selection** — iPXE exposes `${vesa-x}` / `${vesa-y}` on + BIOS; UEFI side we can probe firmware vars at chain-time. The simpler + v1 is a "low-res / hi-res" toggle in Settings plus a per-host + override — mirrors iVentoy's UX, no kernel helper needed. True EDID + parsing is overkill for the first cut. +4. **Chrome upgrades over iVentoy** — iPXE menus are limited (single + highlight, no borders). To look distinctly cooler without leaving + iPXE: paint border / title / footer **into the PNG**, leave a window + in the middle, then `console --left/--right/--top/--bottom` to inset + the iPXE menu exactly into that window. ASCII box-drawing inside the + menu remains fragile (iPXE mangles non-ASCII on some builds — already + noted in `ipxe_script.rs`). + +## Recommended architecture for the next OpenPXE release + +- Build a `pxe_theme` module beside `pxe_logo.rs`: takes operator logo + + theme tokens (accent colour, title, footer) and renders a layered + PNG (background gradient → framing chrome → logo → title bar → footer + with `${hostname}` / `${version}` / `${ip}`) at the three target + resolutions. Cache by hash of inputs. +- Serve at `/branding/pxe-menu-{w}x{h}.png`. Default 1024x768; expose a + Settings dropdown. +- In `ipxe_script.rs`, emit + `console --picture …/pxe-menu-1024x768.png --left 80 --right 80 --top 180 --bottom 60 || console`, + then the existing `menu` / `item` / `choose` block — text now lands + inside the framed window. +- Compile iPXE with `CONSOLE_FRAMEBUFFER`, `IMAGE_PNG`, `CONSOLE_CMD`, + `CONSOLE_VESAFB` (BIOS) and `CONSOLE_EFIFB` (UEFI). The v0.4.61 image + attempted this in-Docker via QEMU emulation and hit `cc1` segfaults. + The follow-up will use a Gitea Actions runner pinned to native + `linux/amd64` (an Unraid host already exists for this). +- Stretch goal: a second "theme pack" that ships a layered PNG with + subtle scanlines / grid — iPXE can't animate, but a well-designed + static composite beats iVentoy's plain centered logo handily. + +## Source URLs + +- https://github.com/ventoy/PXE +- https://github.com/ventoy/PXE/tree/master/iPXE +- https://github.com/ventoy/PXE/issues/11 — 1024x768 default +- https://github.com/ventoy/PXE/issues/59 — iVentoy iPXE EFI loader +- https://ipxe.org/cmd/console — `--picture` and compile flags +- https://github.com/ipxe/ipxe/discussions/945 — background image how-to +- https://github.com/ipxe/ipxe/discussions/802 — `CONSOLE_FRAMEBUFFER` + requirement +- https://github.com/ipxe/ipxe/discussions/1006 — picture resolution + behaviour +- https://www.iventoy.com/en/doc_edition.html — background / title not + user-customisable +- https://kingtam.win/archives/iventoy.html — third-party iPXE-based + iVentoy alternative