Unify colour scheme and CSS as one Style list in settings UI (ticket 047)
The settings panel had two separate sections — "Colour scheme" (built-in palettes) and "CSS" (imported/linked stylesheets) — even though the data model (tickets 012/020) already treats both as equally-selectable themes via custom_theme_id. Merge them into one "Style" section presenting a single list: built-in scheme pills plus imported/linked entries, any of which can be a palette swap or a full layout override, with copy that no longer implies "colour scheme" and "CSS" are separate features. Traced and manually verified (via a running dev server) that a user-supplied stylesheet is not constrained to --c-* variable overrides: import_css/get_css_content in src/api.rs and src/settings.rs store and serve the raw content unmodified (no sanitization or selector scoping), and applyCustomThemeCss injects it as a plain unscoped <link> tag — a stylesheet with e.g. `flex-direction`/`font-size` rules on real selectors applies exactly as authored, alongside the required --c-* declarations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7c331e76b1
commit
5b31f51526
@@ -50,9 +50,10 @@ const SECTIONS: DocSection[] = [
|
||||
</ul>
|
||||
<p className="font-semibold text-text">Settings</p>
|
||||
<p>
|
||||
One <em>settings</em> file per machine holds the colour scheme, imported/linked CSS, the ntfy sink, and
|
||||
which profile is active. Settings only tracks profile pointers — the profiles themselves (params, metrics,
|
||||
dashboards) are stored separately, next to the settings file by default.
|
||||
One <em>settings</em> file per machine holds the active style (a built-in colour scheme or an
|
||||
imported/linked stylesheet — see below), the ntfy sink, and which profile is active. Settings only tracks
|
||||
profile pointers — the profiles themselves (params, metrics, dashboards) are stored separately, next to the
|
||||
settings file by default.
|
||||
</p>
|
||||
<p className="font-semibold text-text">Profiles</p>
|
||||
<p>
|
||||
@@ -424,9 +425,10 @@ const SECTIONS: DocSection[] = [
|
||||
body: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
The Settings panel controls process-wide state: colour scheme, the ntfy sink alert rules deliver to, the
|
||||
max number of logs kept in memory (blank means unlimited), and imported/linked CSS. Changes apply
|
||||
immediately and persist to the settings file on disk. There is no save button.
|
||||
The Settings panel controls process-wide state: the active style — one unified list of built-in colour
|
||||
schemes and any imported/linked stylesheets, any of which can be a simple palette or a full layout
|
||||
redesign — the ntfy sink alert rules deliver to, and the max number of logs kept in memory (blank means
|
||||
unlimited). Changes apply immediately and persist to the settings file on disk. There is no save button.
|
||||
</p>
|
||||
<p className="font-semibold text-text">Worked example</p>
|
||||
<p>
|
||||
|
||||
+117
-112
@@ -258,8 +258,10 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
setCssUploadError(null)
|
||||
}
|
||||
|
||||
// Every imported/linked stylesheet is always a selectable theme (ticket 020), so it's always
|
||||
// validated against the canonical `--c-*` variable set before it's sent.
|
||||
// Every imported/linked stylesheet is always a selectable style, listed alongside the built-in
|
||||
// colour schemes (ticket 020/047), so it's always validated against the canonical `--c-*`
|
||||
// variable set before it's sent — that's a floor, not a ceiling: the stylesheet is free to also
|
||||
// carry rules for any other selector, up to and including a full layout redesign.
|
||||
async function handleCssImport(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setCssUploadError(null)
|
||||
@@ -300,8 +302,14 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
return (
|
||||
<div className="flex flex-col gap-5 text-sm text-text-secondary">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-text-subtle">Colour scheme</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-subtle">Style</h3>
|
||||
<span className="text-[11px] text-text-faint">
|
||||
one list of interchangeable styles — a built-in palette or your own imported/linked stylesheet. Any of
|
||||
them can be a simple colour swap or a full layout redesign; only one is active at a time.
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-3 flex flex-wrap gap-1">
|
||||
{COLOR_SCHEMES.map((scheme) => (
|
||||
<div key={scheme} className="flex items-center">
|
||||
<button
|
||||
@@ -329,6 +337,111 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{panel.cssEntries.length === 0 ? (
|
||||
<div className="mb-2 text-xs text-text-faint">No styles imported or linked yet.</div>
|
||||
) : (
|
||||
<ul className="mb-2 flex flex-col gap-1">
|
||||
{panel.cssEntries.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between rounded border border-border px-2 py-1 text-xs"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.setCustomTheme(entry.id)}
|
||||
className={`block w-full truncate text-left capitalize ${
|
||||
panel.activeCustomTheme?.id === entry.id ? 'text-text' : 'text-text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
{entry.filename ?? entry.path_or_url}
|
||||
</button>
|
||||
<div className="text-[10px] uppercase text-text-faint">{entry.kind}</div>
|
||||
</div>
|
||||
<div className="ml-2 flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadCssEntry(entry)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-text"
|
||||
aria-label={`Download ${entry.filename ?? entry.path_or_url}`}
|
||||
title="Download as .css"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
{entry.kind === 'imported' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeCssEntry(entry.id)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
||||
aria-label={`Delete ${entry.filename}`}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-text-faintest" title="Linked CSS isn't deletable here">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<p className="mb-2 text-[11px] text-text-faint">
|
||||
Add your own style below — a stylesheet, uploaded or linked by URL/path. It must at least define every{' '}
|
||||
<code>--c-*</code> variable the built-ins do, but nothing stops it from also targeting any other selector
|
||||
in the app (layout, spacing, fonts, anything) — it isn't limited to palette overrides.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleCssImport} className="flex flex-col gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".css,text/css"
|
||||
onChange={handleCssFileChange}
|
||||
className="text-xs text-text-muted file:mr-2 file:rounded file:border file:border-border-soft file:bg-surface-2 file:px-2 file:py-1 file:text-text"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter name"
|
||||
value={cssFilename}
|
||||
onChange={(e) => setCssFilename(e.target.value)}
|
||||
className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Paste CSS content here"
|
||||
value={cssContent}
|
||||
onChange={(e) => setCssContent(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-y rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
{cssUploadError && <div className="text-xs text-danger-text-strong">{cssUploadError}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleCssLink} className="mt-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://example.com/style.css"
|
||||
value={cssLinkUrl}
|
||||
onChange={(e) => setCssLinkUrl(e.target.value)}
|
||||
className="min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -384,114 +497,6 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-subtle">CSS</h3>
|
||||
<span className="text-[11px] text-text-faint">
|
||||
imported/linked stylesheets — each one becomes a selectable alternative to the built-in colour schemes
|
||||
above
|
||||
</span>
|
||||
</div>
|
||||
{panel.cssEntries.length === 0 ? (
|
||||
<div className="mb-2 text-xs text-text-faint">No CSS configured.</div>
|
||||
) : (
|
||||
<ul className="mb-2 flex flex-col gap-1">
|
||||
{panel.cssEntries.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between rounded border border-border px-2 py-1 text-xs"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.setCustomTheme(entry.id)}
|
||||
className={`block w-full truncate text-left capitalize ${
|
||||
panel.activeCustomTheme?.id === entry.id ? 'text-text' : 'text-text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
{entry.filename ?? entry.path_or_url}
|
||||
</button>
|
||||
<div className="text-[10px] uppercase text-text-faint">{entry.kind}</div>
|
||||
</div>
|
||||
<div className="ml-2 flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadCssEntry(entry)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-text"
|
||||
aria-label={`Download ${entry.filename ?? entry.path_or_url}`}
|
||||
title="Download as .css"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
{entry.kind === 'imported' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeCssEntry(entry.id)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
||||
aria-label={`Delete ${entry.filename}`}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-text-faintest" title="Linked CSS isn't deletable here">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleCssImport} className="flex flex-col gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".css,text/css"
|
||||
onChange={handleCssFileChange}
|
||||
className="text-xs text-text-muted file:mr-2 file:rounded file:border file:border-border-soft file:bg-surface-2 file:px-2 file:py-1 file:text-text"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter name"
|
||||
value={cssFilename}
|
||||
onChange={(e) => setCssFilename(e.target.value)}
|
||||
className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Paste CSS content here"
|
||||
value={cssContent}
|
||||
onChange={(e) => setCssContent(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-y rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
{cssUploadError && <div className="text-xs text-danger-text-strong">{cssUploadError}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleCssLink} className="mt-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://example.com/style.css"
|
||||
value={cssLinkUrl}
|
||||
onChange={(e) => setCssLinkUrl(e.target.value)}
|
||||
className="min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user