From 5e8cb77e79178df3c7ed0df7a0628b79bb5092c3 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:56:35 -0500 Subject: [PATCH] Polish Control UI quick settings layout Polish the Control UI quick settings dashboard layout. - Rework quick settings into a 12-column desktop grid with matched top-row card heights. - Pair Personal with a right-side Appearance/Automations stack on large screens while preserving tablet/mobile ordering. - Add render/style guards plus an Unreleased changelog entry crediting @BunsDev. Validated with focused UI tests, formatting, git diff checks, local changed gate, and full PR CI. --- CHANGELOG.md | 1 + ui/src/styles/config-quick.css | 106 ++++++++++++++++++++------- ui/src/styles/config-quick.test.ts | 19 ++++- ui/src/ui/views/config-quick.test.ts | 10 ++- ui/src/ui/views/config-quick.ts | 22 +++--- 5 files changed, 115 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278d0cd2d05..18f2b01064b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Control UI: polish the quick settings dashboard grid so common cards align across desktop, tablet, and mobile layouts without wasting horizontal space. Thanks @BunsDev. - Matrix/E2EE: add `openclaw matrix encryption setup` to enable Matrix encryption, bootstrap recovery, and print verification status from one setup flow. Thanks @gumadeiras. - Agents/compaction: add an opt-in `agents.defaults.compaction.maxActiveTranscriptBytes` preflight trigger that runs normal local compaction when the active JSONL grows too large, requiring transcript rotation so successful compaction moves future turns onto a smaller successor file instead of raw byte-splitting history. Thanks @vincentkoc. diff --git a/ui/src/styles/config-quick.css b/ui/src/styles/config-quick.css index 172c9044647..2fb160ae1f8 100644 --- a/ui/src/styles/config-quick.css +++ b/ui/src/styles/config-quick.css @@ -2,9 +2,9 @@ .qs-container { width: 100%; - max-width: none; - margin: 0; - padding: 32px 0 56px; + max-width: 1520px; + margin: 0 auto; + padding: 32px 16px 56px; } .qs-header { @@ -44,14 +44,16 @@ .qs-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - align-items: start; + grid-template-columns: repeat(12, minmax(0, 1fr)); + align-items: stretch; gap: 14px; } -.qs-stack { +.qs-side-stack { display: grid; - align-content: start; + grid-column: span 4; + grid-template-rows: auto 1fr; + align-self: stretch; gap: 14px; min-width: 0; } @@ -78,8 +80,14 @@ grid-column: 1 / -1; } +.qs-card--model, +.qs-card--channels, +.qs-card--security { + grid-column: span 4; +} + .qs-card--personal { - grid-column: 1 / -1; + grid-column: span 8; } .qs-card--personal .qs-identity-grid { @@ -144,7 +152,7 @@ align-items: center; justify-content: space-between; padding: 9px 16px; - min-height: 38px; + min-height: 42px; gap: 10px; } @@ -156,6 +164,8 @@ display: flex; align-items: center; gap: 8px; + min-width: 0; + flex: 1 1 auto; font-size: 0.8125rem; font-weight: 450; color: var(--text); @@ -165,9 +175,12 @@ .qs-row__value { display: flex; align-items: center; + justify-content: flex-end; gap: 8px; + min-width: 0; font-size: 0.8125rem; color: var(--muted); + text-align: right; } .qs-row__value--action { @@ -226,8 +239,8 @@ .qs-identity-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); - gap: 10px; - padding: 14px 16px 10px; + gap: 12px; + padding: 14px 16px 16px; } .qs-identity-card { @@ -240,23 +253,13 @@ padding: 12px; border: 1px solid color-mix(in srgb, var(--border) 60%, transparent); border-radius: var(--radius-md); - background: - radial-gradient( - circle at 18% 18%, - color-mix(in srgb, var(--accent) 10%, transparent), - transparent 46% - ), - color-mix(in srgb, var(--bg-elevated) 42%, var(--card) 58%); + background: color-mix(in srgb, var(--bg-elevated) 42%, var(--card) 58%); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 42%, transparent); } .qs-identity-card--assistant { - background: - radial-gradient( - circle at 82% 12%, - color-mix(in srgb, var(--accent) 14%, transparent), - transparent 48% - ), - color-mix(in srgb, var(--bg-elevated) 52%, var(--card) 48%); + background: color-mix(in srgb, var(--bg-elevated) 50%, var(--card) 50%); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--border-strong) 70%, transparent); } .qs-identity-card__copy { @@ -414,7 +417,10 @@ .qs-segmented { display: flex; + flex-wrap: wrap; + justify-content: flex-end; gap: 2px; + max-width: 100%; background: color-mix(in srgb, var(--bg) 80%, var(--bg-elevated) 20%); border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-md); @@ -1071,6 +1077,56 @@ @media (max-width: 1100px) { .qs-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: stretch; + } + + .qs-side-stack { + display: contents; + } + + .qs-card, + .qs-card--span-all, + .qs-card--personal, + .qs-card--model, + .qs-card--channels, + .qs-card--security, + .qs-card--appearance, + .qs-card--automations { + grid-column: span 1; + } + + .qs-card--personal, + .qs-card--span-all { + grid-column: 1 / -1; + } + + .qs-card--model { + order: 1; + } + + .qs-card--channels { + order: 2; + } + + .qs-card--security { + order: 3; + } + + .qs-card--appearance { + order: 4; + } + + .qs-card--personal { + order: 5; + } + + .qs-card--automations { + grid-column: 1 / -1; + order: 6; + } + + .qs-card--span-all { + order: 7; } } diff --git a/ui/src/styles/config-quick.test.ts b/ui/src/styles/config-quick.test.ts index f4967cd9958..90300ebb48c 100644 --- a/ui/src/styles/config-quick.test.ts +++ b/ui/src/styles/config-quick.test.ts @@ -16,12 +16,23 @@ describe("config-quick styles", () => { expect(css).toContain(".qs-card--personal"); }); - it("includes the stacked quick-settings density layout", () => { - expect(css).toContain(".qs-stack"); + it("includes the dashboard quick-settings density layout", () => { + expect(css).toContain(".qs-card--model"); + expect(css).toContain(".qs-card--automations"); + expect(css).toContain(".qs-side-stack"); + expect(css).toContain("grid-template-rows: auto 1fr;"); expect(css).toContain(".qs-identity-card__actions"); - expect(css).toContain("grid-template-columns: repeat(3, minmax(0, 1fr));"); + expect(css).toContain("grid-template-columns: repeat(12, minmax(0, 1fr));"); + expect(css).toContain("grid-column: 1 / -1;"); + expect(css).toContain("grid-column: span 4;"); expect(css).toContain("grid-template-columns: repeat(2, minmax(0, 1fr));"); - expect(css).toContain("@media (max-width: 760px)"); + expect(css).toContain("align-items: stretch;"); + expect(css).toContain("display: contents;"); + expect(css).toContain(".qs-card--appearance {\n order: 4;"); + expect(css).toContain(".qs-card--appearance"); + expect(css).toContain("order: 4"); + expect(css).toContain(".qs-card--automations"); + expect(css).toContain("order: 6"); }); it("includes explicit context profile layout hooks", () => { diff --git a/ui/src/ui/views/config-quick.test.ts b/ui/src/ui/views/config-quick.test.ts index c3aa07633ff..615626ad3f5 100644 --- a/ui/src/ui/views/config-quick.test.ts +++ b/ui/src/ui/views/config-quick.test.ts @@ -62,12 +62,18 @@ function createProps(overrides: Partial = {}): QuickSettings } describe("renderQuickSettings", () => { - it("uses stacked columns for the compact settings layout", () => { + it("uses direct dashboard cards for the compact settings layout", () => { const container = document.createElement("div"); render(renderQuickSettings(createProps()), container); - expect(container.querySelectorAll(".qs-stack")).toHaveLength(2); + expect(container.querySelector(".qs-card--model")).not.toBeNull(); + expect(container.querySelector(".qs-card--channels")).not.toBeNull(); + expect(container.querySelector(".qs-card--security")).not.toBeNull(); + expect(container.querySelector(".qs-card--appearance")).not.toBeNull(); + expect(container.querySelector(".qs-card--automations")).not.toBeNull(); + expect(container.querySelector(".qs-side-stack .qs-card--appearance")).not.toBeNull(); + expect(container.querySelector(".qs-side-stack .qs-card--automations")).not.toBeNull(); expect(container.querySelector(".qs-card--personal")).not.toBeNull(); expect(container.querySelectorAll(".qs-card--span-all")).toHaveLength(1); }); diff --git a/ui/src/ui/views/config-quick.ts b/ui/src/ui/views/config-quick.ts index b307a43049f..bfa4ed941c6 100644 --- a/ui/src/ui/views/config-quick.ts +++ b/ui/src/ui/views/config-quick.ts @@ -376,7 +376,7 @@ function renderCardHeader(icon: TemplateResult, title: string, action?: Template function renderModelCard(props: QuickSettingsProps) { return html` -
+
${renderCardHeader(icons.brain, "Model & Thinking")}
@@ -426,7 +426,7 @@ function renderChannelsCard(props: QuickSettingsProps) { : undefined; return html` -
+
${renderCardHeader(icons.send, "Channels", badge)}
${props.channels.length === 0 @@ -460,7 +460,7 @@ function renderAutomationsCard(props: QuickSettingsProps) { const { cronJobCount, skillCount, mcpServerCount } = props.automation; return html` -
+
${renderCardHeader(icons.zap, "Automations")}
@@ -490,7 +490,7 @@ function renderSecurityCard(props: QuickSettingsProps) { const { gatewayAuth, execPolicy, deviceAuth } = props.security; return html` -
+
${renderCardHeader( icons.eye, "Security", @@ -525,7 +525,7 @@ function renderSecurityCard(props: QuickSettingsProps) { function renderAppearanceCard(props: QuickSettingsProps) { const themeOptions: ThemeOption[] = [...BUILTIN_THEME_OPTIONS, { id: "custom", label: "Custom" }]; return html` -
+
${renderCardHeader(icons.spark, "Appearance")}
@@ -976,10 +976,6 @@ function renderConnectionFooter(props: QuickSettingsProps) { `; } -function renderStack(...cards: TemplateResult[]) { - return html`
${cards}
`; -} - // ── Main render ── export function renderQuickSettings(props: QuickSettingsProps) { @@ -993,9 +989,11 @@ export function renderQuickSettings(props: QuickSettingsProps) {
- ${renderStack(renderModelCard(props), renderSecurityCard(props))} - ${renderChannelsCard(props)} ${renderPersonalCard(props)} - ${renderStack(renderAppearanceCard(props), renderAutomationsCard(props))} + ${renderModelCard(props)} ${renderChannelsCard(props)} ${renderSecurityCard(props)} + ${renderPersonalCard(props)} +
+ ${renderAppearanceCard(props)} ${renderAutomationsCard(props)} +
${renderPresetsCard(props)}