mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
fix(config-builder): align explorer visuals with web UI
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user