some UI tweaking before testing in production lol

This commit is contained in:
legop3
2026-09-14 20:12:11 -04:00
parent 43274e7371
commit ef9baf6063
17 changed files with 125 additions and 61 deletions
@@ -1,7 +1,7 @@
// Complete Configuration Editor
// Purpose: Connects the one hierarchical configuration form to revision, secret, validation, and save behavior.
// Scope: Edits and saves one complete configuration document as one immutable revision.
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import CardFrame from '../../components/CardFrame/index.jsx';
import { importAdminConfigurationFile, updateConfiguration } from '../api.js';
import SchemaConfigurationForm from './SchemaConfigurationForm.jsx';
@@ -22,6 +22,8 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
const [notice, setNotice] = useState('');
const [error, setError] = useState('');
const [validationErrors, setValidationErrors] = useState([]);
const editorRef = useRef(null);
const toolbarRef = useRef(null);
useEffect(() => {
setDraft(clone(serverValue));
@@ -31,6 +33,35 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
setValidationErrors([]);
}, [revision, serverValue]);
useEffect(() => {
const editor = editorRef.current;
const toolbar = toolbarRef.current;
if (!editor || !toolbar) return undefined;
/*
The action toolbar can wrap differently at each viewport width, so a
fixed CSS offset would eventually let section headings overlap it. Feed
its real rendered height into one local CSS variable instead; every
sticky configuration CardFrame can then meet the toolbar exactly.
*/
const updateStickyOffset = () => {
editor.style.setProperty('--configuration-sticky-top', `${toolbar.offsetHeight}px`);
};
updateStickyOffset();
const observer = typeof ResizeObserver === 'function'
? new ResizeObserver(updateStickyOffset)
: null;
observer?.observe(toolbar);
window.addEventListener('resize', updateStickyOffset);
return () => {
observer?.disconnect();
window.removeEventListener('resize', updateStickyOffset);
editor.style.removeProperty('--configuration-sticky-top');
};
}, [serverValue, schema]);
const dirty = useMemo(
() => JSON.stringify(draft) !== JSON.stringify(serverValue) || Object.keys(secretOperations).length > 0,
[draft, secretOperations, serverValue],
@@ -104,21 +135,22 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
}
return (
<CardFrame title="Configuration" meta={`revision ${revision}`} clipOverflow={false} bodyClassName="p-0.5">
<div className="configuration-toolbar sticky top-0 z-20 mb-0.5 space-y-0.5 border border-neutral-500/60 bg-neutral-900/95 p-0.5 backdrop-blur">
<p className="text-xs text-slate-400">Saving applies the complete revision immediately and reloads each affected service.</p>
{/* All document actions stay together at the start of the toolbar. The
editor may use a wide canvas, but width is never used to separate a
control from the content that explains it. */}
<div className="flex flex-wrap gap-0.5">
<button type="button" className="button-dark" disabled={busy} onClick={onReload}>Reload</button>
<button type="button" className="button-dark" disabled={!dirty || busy} onClick={() => {
setDraft(clone(serverValue));
setSecretOperations({});
}}>Reset</button>
<button type="button" className="button-dark" disabled={!dirty || busy} onClick={save}>{saving ? 'Applying…' : 'Save configuration'}</button>
<div ref={editorRef} className="configuration-editor">
<CardFrame title="Configuration" meta={`revision ${revision}`} clipOverflow={false} bodyClassName="p-0.5">
<div ref={toolbarRef} className="configuration-toolbar sticky top-0 z-20 mb-0.5 space-y-0.5 border border-neutral-500/60 bg-neutral-900/95 p-0.5 backdrop-blur">
<p className="text-xs text-slate-400">Saving applies the complete revision immediately and reloads each affected service.</p>
{/* All document actions stay together at the start of the toolbar. The
editor may use a wide canvas, but width is never used to separate a
control from the content that explains it. */}
<div className="flex flex-wrap gap-0.5">
<button type="button" className="button-dark" disabled={busy} onClick={onReload}>Reload</button>
<button type="button" className="button-dark" disabled={!dirty || busy} onClick={() => {
setDraft(clone(serverValue));
setSecretOperations({});
}}>Reset</button>
<button type="button" className="button-dark" disabled={!dirty || busy} onClick={save}>{saving ? 'Applying…' : 'Save configuration'}</button>
</div>
</div>
</div>
<CardFrame title="Import legacy YAML" bodyClassName="space-y-0.5 p-1 text-sm" clipOverflow={false}>
<p className="text-sm text-slate-300">Replace this configuration from an explicitly selected legacy file. Unknown old settings and administrator accounts are ignored; current settings are validated and applied immediately.</p>
{/* Keep the picker and its action beside each other at the start of the
@@ -168,6 +200,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
secretOperations={secretOperations}
setSecretOperation={setSecretOperation}
/>
</CardFrame>
</CardFrame>
</div>
);
}
@@ -158,7 +158,7 @@ function ConfigurationFieldTemplate({
<div className="configuration-key">
{/* Boolean widgets deliberately hide their internal duplicate label, so
every scalar can use this same key column and preserve YAML order. */}
<label htmlFor={id} className="text-xs font-semibold text-slate-100">
<label htmlFor={id} className="configuration-key-label">
{label}{required ? <span className="ml-0.25 text-sky-300">*</span> : null}
</label>
</div>
@@ -215,7 +215,9 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
title={title}
color={layer.color}
clipOverflow={false}
stickyHeader
className={`configuration-card${topLevel ? ' configuration-top-level-card' : ''}`}
headerClassName="configuration-card-header"
bodyClassName="configuration-card-body"
>
{description ? <div className="configuration-branch-description">{description}</div> : null}
@@ -235,7 +237,9 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
title={`Item ${index + 1}`}
color={layer.color}
clipOverflow={false}
stickyHeader
className="configuration-card configuration-array-item"
headerClassName="configuration-card-header"
bodyClassName="configuration-card-body"
>
{hasToolbar ? (
@@ -274,7 +278,9 @@ function ConfigurationArrayTemplate({ canAdd, disabled, fieldPathId, items, onAd
meta={`${items.length} ${items.length === 1 ? 'item' : 'items'}`}
color={layer.color}
clipOverflow={false}
stickyHeader
className={`configuration-card configuration-array${topLevel ? ' configuration-top-level-card' : ''}`}
headerClassName="configuration-card-header"
bodyClassName="configuration-card-body"
>
{schema.description ? <div className="configuration-branch-description">{schema.description}</div> : null}
+13
View File
@@ -14,6 +14,13 @@
@apply min-w-0;
}
.configuration-card-header {
/* The editor toolbar owns the top edge of the viewport. Structural title
bars use its measured height so sticky CardFrames remain visible directly
beneath the actions instead of covering them at narrow widths. */
top: var(--configuration-sticky-top, 0px);
}
.configuration-card .configuration-card {
/* Indent the next structural boundary itself, not every row owned by the
parent. Since nested CardFrames repeat this rule, the offset accumulates
@@ -48,6 +55,12 @@
@apply min-w-0;
}
.configuration-key-label {
/* Keys are the primary identifiers on YAML-like rows, so they must be more
prominent than the explanatory copy in the adjacent value column. */
@apply text-base font-semibold leading-snug text-slate-100;
}
.configuration-root-description,
.configuration-branch-description,
.configuration-item-description,
+6
View File
@@ -35,6 +35,7 @@ export default function CardFrame({
actions = null,
color = null,
hideHeader = false,
stickyHeader = false,
className = '',
headerClassName = '',
bodyClassName = '',
@@ -93,6 +94,11 @@ export default function CardFrame({
<header
className={cx(
'flex items-center justify-between gap-0.5 border-b border-neutral-500/50 bg-slate-800 px-0.5 py-0.5',
// Sticky headings are opt-in because many CardFrames are short or
// live inside independently scrolling panes. Keeping the behavior
// on the shared component gives long cards a consistent title bar
// without changing the layout of existing callers.
stickyHeader && 'sticky top-0 z-10',
// 'flex items-center justify-between gap-0.5 border-b border-neutral-500/50 bg-gradient-to-r from-neutral-800 via-neutral-700 to-neutral-600 px-0.5 py-0.5',
headerClassName,
)}