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`. - <select> dropdowns get a hand-drawn chevron via background-image SVG; with `-webkit-appearance: none` the native arrow had disappeared, making "Metadata source" look squished next to the inputs beside it. - Update credentials + Save SSO settings buttons get explicit top margins so they sit clearly under their input rows instead of butting against the field beneath. - `docs/queued/ipxe-pxe-menu-theme-research.md` captures findings on how iVentoy paints its boot menu (iPXE `console --picture` with baked-in per-resolution PNGs, no EDID auto-detect) and the recommended Rust architecture for the follow-up release. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
89c02810c9
commit
bd791462be
@@ -34,9 +34,18 @@
|
||||
--topbar-h: 56px;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
|
||||
/* v0.4.63: tie native form-control rendering (checkboxes, scroll bars,
|
||||
date pickers) to the active OpenPXE theme. Without this, the inline
|
||||
`<meta name="color-scheme" content="dark light">` in index.html forces
|
||||
dark form chrome in *both* themes — so the SSO "Enable single sign-on"
|
||||
checkbox renders as an opaque black square against the light-mode
|
||||
panel, ignoring our accent-color hint. CSS `color-scheme` overrides
|
||||
the meta and tracks `data-theme` correctly. */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
/* Light palette — high-contrast neutral, accent unchanged for brand
|
||||
consistency. Designed against Netbox Labs's reference screenshot:
|
||||
near-white surfaces, soft grey dividers, dark text. */
|
||||
@@ -304,6 +313,23 @@ label.field textarea {
|
||||
font-size: 14px; line-height: 1.4;
|
||||
box-shadow: none; -webkit-appearance: none; appearance: none;
|
||||
}
|
||||
/* v0.4.63: with `appearance: none`, the native <select> 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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%239aa0a6' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='1.5,1.5 6,6 10.5,1.5'/></svg>");
|
||||
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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%235a6377' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='1.5,1.5 6,6 10.5,1.5'/></svg>");
|
||||
}
|
||||
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 ────────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
+32
-15
@@ -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 <EntityDescriptor>…</EntityDescriptor> 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.
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<!-- The brand badge at the top can be overridden by operator-uploaded
|
||||
logos; keep "OpenPXE v…" pinned in the footer so the backend
|
||||
identity is always visible regardless of branding. -->
|
||||
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.61</span></div>
|
||||
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.63</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user