From 716546824ff36cf3b3d1c54f6d0eecf5c1f9e565 Mon Sep 17 00:00:00 2001 From: Maude Bot Date: Tue, 20 Jan 2026 10:56:44 -0500 Subject: [PATCH] feat(ui): improve config page with collapsible sections - Group config settings into logical sections (Core, Agents, Communication, etc.) - Add collapsible accordion UI for each section group - Add icons and labels for each config category - Improve mobile responsiveness with better button layout - Style improvements for nested fieldsets and arrays --- ui/src/styles/components.css | 250 ++++++++++++++++++++++++++ ui/src/ui/views/config-form.render.ts | 128 ++++++++++--- ui/src/ui/views/config.ts | 35 ++-- 3 files changed, 375 insertions(+), 38 deletions(-) diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 435fd59bc..18055eadc 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1274,3 +1274,253 @@ flex-wrap: wrap; gap: 10px; } + +/* Config Form Sections */ +.config-form--sectioned { + display: grid; + gap: 12px; +} + +.config-section { + border: 1px solid var(--border); + border-radius: 14px; + background: linear-gradient(160deg, rgba(255, 255, 255, 0.03), transparent 70%), + rgba(0, 0, 0, 0.15); + overflow: hidden; +} + +:root[data-theme="light"] .config-section { + background: linear-gradient(160deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.5) 70%); +} + +.config-section__header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + cursor: pointer; + user-select: none; + border-bottom: 1px solid transparent; + transition: background 150ms ease, border-color 150ms ease; +} + +.config-section__header:hover { + background: rgba(255, 255, 255, 0.04); +} + +:root[data-theme="light"] .config-section__header:hover { + background: rgba(0, 0, 0, 0.03); +} + +.config-section[open] .config-section__header { + border-bottom-color: var(--border); + background: rgba(255, 255, 255, 0.02); +} + +:root[data-theme="light"] .config-section[open] .config-section__header { + background: rgba(0, 0, 0, 0.02); +} + +.config-section__title { + font-family: var(--font-display); + font-size: 13px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + flex: 1; +} + +.config-section__count { + font-size: 11px; + color: var(--muted); + padding: 3px 8px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border); +} + +:root[data-theme="light"] .config-section__count { + background: rgba(0, 0, 0, 0.05); +} + +.config-section__chevron { + width: 18px; + height: 18px; + color: var(--muted); + transition: transform 200ms ease; + flex-shrink: 0; +} + +.config-section[open] .config-section__chevron { + transform: rotate(180deg); +} + +.config-section__content { + padding: 12px 16px 16px; + display: grid; + gap: 16px; +} + +.config-field-group { + border: 1px solid var(--border); + border-radius: 12px; + background: rgba(0, 0, 0, 0.12); + overflow: hidden; +} + +:root[data-theme="light"] .config-field-group { + background: rgba(255, 255, 255, 0.6); +} + +.config-field-group__header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border); +} + +:root[data-theme="light"] .config-field-group__header { + background: rgba(0, 0, 0, 0.03); +} + +.config-field-group__icon { + font-size: 16px; + line-height: 1; +} + +.config-field-group__label { + font-weight: 600; + font-size: 13px; + letter-spacing: 0.3px; +} + +.config-field-group__content { + padding: 14px; +} + +.config-field-group__content .fieldset { + border: none; + padding: 0; + margin: 0; +} + +.config-field-group__content .fieldset > .legend { + display: none; +} + +/* Config form field improvements */ +.config-form .fieldset { + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px; + margin-top: 8px; + background: rgba(0, 0, 0, 0.08); +} + +:root[data-theme="light"] .config-form .fieldset { + background: rgba(255, 255, 255, 0.5); +} + +.config-form .fieldset .legend { + font-weight: 600; + font-size: 12px; + letter-spacing: 0.3px; + color: var(--text); + margin-bottom: 10px; + padding-bottom: 8px; + border-bottom: 1px dashed var(--border); +} + +.config-form .fieldset .fieldset { + margin-top: 12px; +} + +.config-form .array { + display: grid; + gap: 8px; +} + +.config-form .array-item { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 10px; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(0, 0, 0, 0.1); +} + +:root[data-theme="light"] .config-form .array-item { + background: rgba(255, 255, 255, 0.6); +} + +/* Mobile responsiveness */ +@media (max-width: 640px) { + .config-section__header { + padding: 12px 14px; + } + + .config-section__content { + padding: 10px 12px 14px; + } + + .config-field-group__content { + padding: 12px; + } + + .config-form .array-item { + flex-direction: column; + } + + .config-form .array-item > div:first-child { + width: 100%; + } + + .config-form .array-item .btn { + align-self: flex-end; + } +} + +/* Config Header Layout */ +.config-header { + display: flex; + flex-direction: column; + gap: 12px; +} + +.config-header__top { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.config-header__actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +@media (max-width: 640px) { + .config-header__top { + flex-direction: column; + align-items: stretch; + } + + .config-header__top .row { + justify-content: space-between; + } + + .config-header__actions { + justify-content: flex-end; + } + + .config-header__actions .btn { + flex: 1; + min-width: 70px; + text-align: center; + } +} diff --git a/ui/src/ui/views/config-form.render.ts b/ui/src/ui/views/config-form.render.ts index c21c276fc..bcb7e53bf 100644 --- a/ui/src/ui/views/config-form.render.ts +++ b/ui/src/ui/views/config-form.render.ts @@ -1,4 +1,4 @@ -import { html } from "lit"; +import { html, nothing } from "lit"; import type { ConfigUiHints } from "../types"; import { hintForPath, schemaType, type JsonSchema } from "./config-form.shared"; import { renderNode } from "./config-form.node"; @@ -12,6 +12,43 @@ export type ConfigFormProps = { onPatch: (path: Array, value: unknown) => void; }; +// Define logical section groupings +const SECTION_CONFIG: Record = { + // Core + env: { label: "Environment", icon: "🔧", order: 0 }, + update: { label: "Updates", icon: "📦", order: 1 }, + + // Identity & Agents + agents: { label: "Agents", icon: "🤖", order: 10 }, + auth: { label: "Authentication", icon: "🔐", order: 11 }, + + // Communication + channels: { label: "Channels", icon: "💬", order: 20 }, + messages: { label: "Messages", icon: "📨", order: 21 }, + + // Automation + commands: { label: "Commands", icon: "⌨️", order: 30 }, + hooks: { label: "Hooks", icon: "🪝", order: 31 }, + skills: { label: "Skills", icon: "✨", order: 32 }, + + // Tools & Gateway + tools: { label: "Tools", icon: "🛠️", order: 40 }, + gateway: { label: "Gateway", icon: "🌐", order: 41 }, + + // System + wizard: { label: "Setup Wizard", icon: "🧙", order: 50 }, +}; + +// Logical groupings for the accordion layout +const SECTION_GROUPS: Array<{ title: string; keys: string[] }> = [ + { title: "Core Settings", keys: ["env", "update"] }, + { title: "Identity & Agents", keys: ["agents", "auth"] }, + { title: "Communication", keys: ["channels", "messages"] }, + { title: "Automation", keys: ["commands", "hooks", "skills"] }, + { title: "Tools & Gateway", keys: ["tools", "gateway"] }, + { title: "System", keys: ["wizard"] }, +]; + export function renderConfigForm(props: ConfigFormProps) { if (!props.schema) { return html`
Schema unavailable.
`; @@ -22,28 +59,77 @@ export function renderConfigForm(props: ConfigFormProps) { return html`
Unsupported schema. Use Raw.
`; } const unsupported = new Set(props.unsupportedPaths ?? []); - const entries = Object.entries(schema.properties); - const sorted = entries.sort((a, b) => { - const orderA = hintForPath([a[0]], props.uiHints)?.order ?? 0; - const orderB = hintForPath([b[0]], props.uiHints)?.order ?? 0; - if (orderA !== orderB) return orderA - orderB; - return a[0].localeCompare(b[0]); - }); + const properties = schema.properties; + const allKeys = new Set(Object.keys(properties)); + + // Collect any keys not in our defined groups + const groupedKeys = new Set(SECTION_GROUPS.flatMap(g => g.keys)); + const ungroupedKeys = [...allKeys].filter(k => !groupedKeys.has(k)); + + // Build the groups with their entries + const groups = SECTION_GROUPS.map(group => { + const entries = group.keys + .filter(key => allKeys.has(key)) + .map(key => ({ key, node: properties[key] })); + return { ...group, entries }; + }).filter(group => group.entries.length > 0); + + // Add ungrouped keys as "Other" if any exist + if (ungroupedKeys.length > 0) { + const sortedUngrouped = ungroupedKeys.sort((a, b) => { + const orderA = hintForPath([a], props.uiHints)?.order ?? 100; + const orderB = hintForPath([b], props.uiHints)?.order ?? 100; + if (orderA !== orderB) return orderA - orderB; + return a.localeCompare(b); + }); + groups.push({ + title: "Other", + keys: sortedUngrouped, + entries: sortedUngrouped.map(key => ({ key, node: properties[key] })), + }); + } return html` -
- ${sorted.map(([key, node]) => - renderNode({ - schema: node, - value: (value as Record)[key], - path: [key], - hints: props.uiHints, - unsupported, - disabled: props.disabled ?? false, - onPatch: props.onPatch, - }), - )} +
+ ${groups.map((group, groupIndex) => html` +
+ + ${group.title} + ${group.entries.length} ${group.entries.length === 1 ? 'setting' : 'settings'} + + + + +
+ ${group.entries.map(({ key, node }) => { + const sectionInfo = SECTION_CONFIG[key]; + const icon = sectionInfo?.icon ?? "📄"; + const label = sectionInfo?.label ?? key; + + return html` +
+
+ ${icon} + ${label} +
+
+ ${renderNode({ + schema: node, + value: (value as Record)[key], + path: [key], + hints: props.uiHints, + unsupported, + disabled: props.disabled ?? false, + showLabel: false, + onPatch: props.onPatch, + })} +
+
+ `; + })} +
+
+ `)}
`; } - diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 0930e9920..a87c01b91 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -47,58 +47,59 @@ export function renderConfig(props: ConfigProps) { return html`
-
-
-
Config
- ${validity} -
-
+
+
+
+
Config
+ ${validity} +
-
+
+
-
+
Writes to ~/.clawdbot/clawdbot.json. Apply & - Update restart the gateway and will ping the last active session when it - comes back. + Update restart the gateway.