fix(config-builder): align explorer visuals with web UI

This commit is contained in:
Sebastian
2026-02-09 20:46:58 -05:00
parent d3bac0b26d
commit 58b54b24f0
2 changed files with 274 additions and 392 deletions

View File

@@ -1,21 +1,112 @@
:root {
color-scheme: dark;
font-family: Inter, "Segoe UI", Roboto, sans-serif;
background: #09090b;
color: #fafafa;
}
@import "../../../ui/src/styles/base.css";
@import "../../../ui/src/styles/components.css";
@import "../../../ui/src/styles/config.css";
* {
box-sizing: border-box;
}
/* Config-builder specific wrappers (reuse OpenClaw UI tokens/classes above). */
html,
body {
margin: 0;
min-height: 100%;
background: #09090b;
}
body {
config-builder-app {
display: block;
min-height: 100vh;
}
.builder-screen {
min-height: 100vh;
padding: 0;
background: var(--bg);
}
.builder-layout.config-layout {
margin: 0;
height: 100vh;
border: 0;
border-radius: 0;
}
@supports (height: 100dvh) {
.builder-layout.config-layout {
height: 100dvh;
}
}
.builder-subtitle {
margin-top: 4px;
font-size: 12px;
color: var(--muted);
}
.builder-count {
margin-left: auto;
font-size: 11px;
color: var(--muted);
}
.builder-icon {
width: 18px;
height: 18px;
border-radius: var(--radius-full);
border: 1px solid var(--border);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
}
.builder-footer-note {
font-size: 12px;
color: var(--muted);
line-height: 1.45;
}
.builder-search-state {
margin-bottom: 14px;
font-size: 13px;
color: var(--muted);
}
.builder-section-glyph {
width: 34px;
height: 34px;
border-radius: var(--radius-full);
border: 1px solid var(--border);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: var(--accent);
}
.builder-field {
padding: 12px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-accent);
}
.builder-field__head {
display: flex;
align-items: flex-start;
gap: 8px;
justify-content: space-between;
}
.builder-field__badges {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
}
.builder-field__path {
margin-top: 6px;
font-size: 12px;
color: var(--muted);
word-break: break-word;
}
@media (max-width: 980px) {
.builder-layout.config-layout {
height: auto;
min-height: 100vh;
}
}

View File

@@ -1,4 +1,4 @@
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import {
buildExplorerSnapshot,
type ExplorerField,
@@ -37,279 +37,20 @@ function matchesSection(section: ExplorerSection, query: string): boolean {
);
}
function sectionGlyph(label: string): string {
return label.trim().charAt(0).toUpperCase() || "•";
}
class ConfigBuilderApp extends LitElement {
static override styles = css`
:host {
display: block;
min-height: 100vh;
background: #09090b;
color: #fafafa;
font-family: Inter, "Segoe UI", Roboto, sans-serif;
}
.shell {
min-height: 100vh;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
}
.sidebar {
border-right: 1px solid #27272a;
background: #09090b;
padding: 16px;
position: sticky;
top: 0;
max-height: 100vh;
overflow-y: auto;
}
.brand {
font-size: 0.98rem;
font-weight: 700;
margin: 0;
}
.sub {
margin-top: 6px;
color: #a1a1aa;
font-size: 0.82rem;
line-height: 1.4;
}
.search {
margin-top: 16px;
width: 100%;
background: #18181b;
border: 1px solid #27272a;
border-radius: 8px;
color: #fafafa;
font-size: 0.86rem;
padding: 10px 12px;
}
.search:focus-visible {
outline: 2px solid #ff5a36;
outline-offset: 1px;
border-color: #ff5a36;
}
.nav {
margin-top: 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.nav-btn {
width: 100%;
border: 1px solid #27272a;
background: #18181b;
color: #a1a1aa;
border-radius: 8px;
text-align: left;
font-size: 0.82rem;
padding: 8px 10px;
display: flex;
justify-content: space-between;
gap: 8px;
cursor: pointer;
}
.nav-btn:hover {
color: #fafafa;
border-color: #3f3f46;
}
.nav-btn.active {
color: #ff5a36;
border-color: #ff5a36;
background: rgba(255, 90, 54, 0.08);
}
.count {
color: #71717a;
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
font-size: 0.75rem;
}
.main {
padding: 20px;
}
.hero {
border: 1px solid #27272a;
background: #18181b;
border-radius: 12px;
padding: 16px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.hero-item {
border: 1px solid #27272a;
border-radius: 10px;
padding: 10px;
background: #0f0f12;
}
.hero-label {
color: #71717a;
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.hero-value {
margin-top: 6px;
font-size: 0.9rem;
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
word-break: break-word;
}
.sections {
margin-top: 18px;
display: grid;
gap: 14px;
}
.section-card {
border: 1px solid #27272a;
border-radius: 12px;
background: #18181b;
overflow: hidden;
}
.section-header {
border-bottom: 1px solid #27272a;
padding: 14px;
background: #111114;
}
.section-title {
margin: 0;
font-size: 1rem;
font-weight: 700;
}
.section-meta {
margin-top: 6px;
color: #a1a1aa;
font-size: 0.82rem;
line-height: 1.4;
}
.field-list {
padding: 6px 10px 10px;
display: grid;
gap: 8px;
}
.field {
border: 1px solid #27272a;
border-radius: 10px;
padding: 10px;
background: #121216;
}
.field-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.field-label {
font-size: 0.86rem;
font-weight: 600;
color: #fafafa;
}
.badges {
display: inline-flex;
gap: 6px;
flex-wrap: wrap;
}
.badge {
border: 1px solid #3f3f46;
border-radius: 999px;
padding: 2px 7px;
font-size: 0.68rem;
color: #a1a1aa;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge.sensitive {
color: #fb7185;
border-color: #fb7185;
}
.badge.advanced {
color: #fbbf24;
border-color: #fbbf24;
}
.field-path {
margin-top: 6px;
font-size: 0.75rem;
color: #71717a;
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
word-break: break-word;
}
.field-help {
margin-top: 6px;
color: #a1a1aa;
font-size: 0.78rem;
line-height: 1.4;
}
.empty,
.error,
.loading {
border: 1px solid #27272a;
border-radius: 12px;
padding: 16px;
background: #18181b;
margin-top: 18px;
color: #a1a1aa;
}
.error {
color: #f87171;
white-space: pre-wrap;
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
font-size: 0.8rem;
}
@media (max-width: 980px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
max-height: none;
border-right: none;
border-bottom: 1px solid #27272a;
}
.hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
`;
private state: AppState = { status: "loading" };
private selectedSectionId: string | null = null;
private searchQuery = "";
override createRenderRoot() {
// Match the existing OpenClaw web UI approach (global CSS classes/tokens).
return this;
}
override connectedCallback(): void {
super.connectedCallback();
this.bootstrap();
@@ -362,127 +103,177 @@ class ConfigBuilderApp extends LitElement {
return visible;
}
private renderSearch() {
return html`
<div class="config-search">
<svg
class="config-search__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.35-4.35"></path>
</svg>
<input
class="config-search__input"
type="text"
placeholder="Search fields, labels, help…"
@input=${(event: Event) => this.setSearchQuery((event.target as HTMLInputElement).value)}
/>
${this.searchQuery
? html`
<button
class="config-search__clear"
title="Clear search"
@click=${() => this.setSearchQuery("")}
>
×
</button>
`
: nothing}
</div>
`;
}
private renderSidebar(snapshot: ExplorerSnapshot) {
return html`
<aside class="config-sidebar">
<div class="config-sidebar__header">
<div>
<div class="config-sidebar__title">Config Builder</div>
<div class="builder-subtitle">Explorer scaffold</div>
</div>
<span class="pill pill--sm pill--ok">ready</span>
</div>
${this.renderSearch()}
<nav class="config-nav">
<button
class="config-nav__item ${this.selectedSectionId === null ? "active" : ""}"
@click=${() => this.setSection(null)}
>
<span class="config-nav__icon builder-icon" aria-hidden="true">A</span>
<span class="config-nav__label">All sections</span>
<span class="builder-count mono">${snapshot.fieldCount}</span>
</button>
${snapshot.sections.map(
(section) => html`
<button
class="config-nav__item ${this.selectedSectionId === section.id ? "active" : ""}"
@click=${() => this.setSection(section.id)}
>
<span class="config-nav__icon builder-icon" aria-hidden="true"
>${sectionGlyph(section.label)}</span
>
<span class="config-nav__label">${section.label}</span>
<span class="builder-count mono">${section.fields.length}</span>
</button>
`,
)}
</nav>
<div class="config-sidebar__footer">
<div class="builder-footer-note">
Read-only schema explorer using OpenClaw config hints.
</div>
</div>
</aside>
`;
}
private renderField(field: ExplorerField) {
return html`
<div class="cfg-field builder-field">
<div class="builder-field__head">
<div class="cfg-field__label">${field.label}</div>
<div class="builder-field__badges">
${field.sensitive ? html`<span class="pill pill--sm pill--danger">sensitive</span>` : nothing}
${field.advanced ? html`<span class="pill pill--sm">advanced</span>` : nothing}
</div>
</div>
<div class="builder-field__path mono">${field.path}</div>
${field.help ? html`<div class="cfg-field__help">${field.help}</div>` : nothing}
</div>
`;
}
private renderSections(visibleSections: ExplorerSection[]) {
if (visibleSections.length === 0) {
return html`<div class="config-empty"><div class="config-empty__text">No matching sections/fields for this filter.</div></div>`;
}
return html`
<div class="config-form config-form--modern">
${visibleSections.map(
(section) => html`
<section class="config-section-card" id=${`section-${section.id}`}>
<div class="config-section-card__header">
<div class="config-section-card__icon builder-section-glyph" aria-hidden="true">
${sectionGlyph(section.label)}
</div>
<div class="config-section-card__titles">
<h2 class="config-section-card__title">${section.label}</h2>
<div class="config-section-card__desc">
<span class="mono">${section.id}</span>
· ${section.fields.length} field hint${section.fields.length === 1 ? "" : "s"}
${section.description ? html`<br />${section.description}` : nothing}
</div>
</div>
</div>
<div class="config-section-card__content">
<div class="cfg-fields">${section.fields.map((field) => this.renderField(field))}</div>
</div>
</section>
`,
)}
</div>
`;
}
override render() {
if (this.state.status === "loading") {
return html`<main class="main"><div class="loading">Loading schema explorer…</div></main>`;
return html`<div class="builder-screen"><div class="card">Loading schema explorer…</div></div>`;
}
if (this.state.status === "error") {
return html`<main class="main"><pre class="error">${this.state.message}</pre></main>`;
return html`<div class="builder-screen"><pre class="callout danger">${this.state.message}</pre></div>`;
}
const { snapshot } = this.state;
const visibleSections = this.getVisibleSections(snapshot);
return html`
<div class="shell">
<aside class="sidebar">
<h1 class="brand">OpenClaw Config Builder</h1>
<div class="sub">Explorer read-only scaffold (Phase 1)</div>
<div class="builder-screen">
<div class="config-layout builder-layout">
${this.renderSidebar(snapshot)}
<input
class="search"
type="text"
placeholder="Search fields, labels, help…"
@input=${(event: Event) =>
this.setSearchQuery((event.target as HTMLInputElement).value)}
/>
<nav class="nav">
<button
class="nav-btn ${this.selectedSectionId === null ? "active" : ""}"
@click=${() => this.setSection(null)}
>
<span>All sections</span>
<span class="count">${snapshot.fieldCount}</span>
</button>
${snapshot.sections.map(
(section) => html`
<button
class="nav-btn ${this.selectedSectionId === section.id ? "active" : ""}"
@click=${() => this.setSection(section.id)}
>
<span>${section.label}</span>
<span class="count">${section.fields.length}</span>
</button>
`,
)}
</nav>
</aside>
<main class="main">
<section class="hero">
<div class="hero-item">
<div class="hero-label">Schema status</div>
<div class="hero-value">ready</div>
<main class="config-main">
<div class="config-actions">
<div class="config-actions__left">
<span class="config-status">Schema explorer (read-only)</span>
</div>
<div class="config-actions__right">
<span class="pill pill--sm">sections: ${snapshot.sectionCount}</span>
<span class="pill pill--sm">fields: ${snapshot.fieldCount}</span>
<span class="pill pill--sm mono">v${snapshot.version}</span>
</div>
</div>
<div class="hero-item">
<div class="hero-label">Sections</div>
<div class="hero-value">${snapshot.sectionCount}</div>
</div>
<div class="hero-item">
<div class="hero-label">Field hints</div>
<div class="hero-value">${snapshot.fieldCount}</div>
</div>
<div class="hero-item">
<div class="hero-label">Version</div>
<div class="hero-value">${snapshot.version}</div>
</div>
</section>
${this.searchQuery
? html`<div class="sub" style="margin-top: 12px;">
Search: <code>${this.searchQuery}</code>
</div>`
: nothing}
<div class="config-content">
${this.searchQuery
? html`<div class="builder-search-state">Search: <span class="mono">${this.searchQuery}</span></div>`
: nothing}
${visibleSections.length === 0
? html`<div class="empty">No matching sections/fields for this filter.</div>`
: html`
<div class="sections">
${visibleSections.map(
(section) => html`
<section class="section-card">
<header class="section-header">
<h2 class="section-title">${section.label}</h2>
<div class="section-meta">
<strong>${section.id}</strong>
· ${section.fields.length} field hint${
section.fields.length === 1 ? "" : "s"
}
${section.description ? html`<br />${section.description}` : nothing}
</div>
</header>
<div class="field-list">
${section.fields.map(
(field) => html`
<article class="field">
<div class="field-head">
<div class="field-label">${field.label}</div>
<div class="badges">
${field.sensitive
? html`<span class="badge sensitive">sensitive</span>`
: nothing}
${field.advanced
? html`<span class="badge advanced">advanced</span>`
: nothing}
</div>
</div>
<div class="field-path">${field.path}</div>
${field.help
? html`<div class="field-help">${field.help}</div>`
: nothing}
</article>
`,
)}
</div>
</section>
`,
)}
</div>
`}
</main>
${this.renderSections(visibleSections)}
</div>
</main>
</div>
</div>
`;
}