mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): show a simple channel settings view with a flip to advanced (#114741)
* test(ui): make channel settings renderable in the mock dev server * fix(ui): hide advanced channel settings behind the shared advanced tier * fix(ui): widen the tier renderer callback to lit's nothing * fix(ui): keep the advanced collapse control on advanced-only channel forms * docs: note the shared advanced tier on channel settings
This commit is contained in:
committed by
GitHub
parent
3a39dbcdfd
commit
fbb3525368
@@ -66,7 +66,9 @@ field map and defaults.
|
||||
Settings show common fields first. Each section keeps its advanced fields
|
||||
in a collapsed **Advanced (N)** group; use **Show advanced** to expand all
|
||||
groups. Settings search always includes both tiers and opens the matching
|
||||
advanced group when needed.
|
||||
advanced group when needed. Per-channel settings under **Settings ->
|
||||
Channels** use the same split and share the **Show advanced** preference,
|
||||
with **Hide advanced** on the divider to collapse them again.
|
||||
</Tab>
|
||||
<Tab title="Direct edit">
|
||||
Edit `~/.openclaw/openclaw.json` directly. The Gateway watches the file and applies changes automatically (see [hot reload](#config-hot-reload)).
|
||||
|
||||
@@ -6,6 +6,7 @@ import qrcode from "qrcode";
|
||||
import { createServer, type Plugin, type ViteDevServer } from "vite";
|
||||
import type { UserProfile } from "../packages/gateway-protocol/src/index.js";
|
||||
import { expectDefined } from "../packages/normalization-core/src/expect.js";
|
||||
import { applyConfigTierHints, applyResolvedConfigTierHints } from "../src/config/schema.tiers.js";
|
||||
import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../src/gateway/control-ui-contract.js";
|
||||
import {
|
||||
createControlUiMockBootstrapConfig,
|
||||
@@ -641,6 +642,15 @@ function buildConfigMocks(options: { swarmEnabled?: boolean } = {}) {
|
||||
agents: { defaults: { thinkingDefault: "medium" } },
|
||||
models: { mode: "merge" },
|
||||
...(options.swarmEnabled ? { tools: { swarm: true } } : {}),
|
||||
channels: {
|
||||
whatsapp: {
|
||||
enabled: true,
|
||||
allowFrom: ["+15551234567"],
|
||||
dmPolicy: "pairing",
|
||||
groupPolicy: "allowlist",
|
||||
selfChatMode: "off",
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
servers: {
|
||||
context7: { url: "https://mcp.context7.com/mcp", transport: "streamable-http" },
|
||||
@@ -727,6 +737,52 @@ function buildConfigMocks(options: { swarmEnabled?: boolean } = {}) {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Channel settings are the one schema surface the channels page renders,
|
||||
// so the fixture keeps both tiers represented.
|
||||
channels: {
|
||||
type: "object",
|
||||
title: "Channels",
|
||||
properties: {
|
||||
whatsapp: {
|
||||
type: "object",
|
||||
title: "WhatsApp",
|
||||
properties: {
|
||||
enabled: { type: "boolean", title: "Enabled" },
|
||||
allowFrom: { type: "array", title: "Allow from", items: { type: "string" } },
|
||||
dmPolicy: { type: "string", title: "DM policy", enum: ["pairing", "open", "off"] },
|
||||
groupPolicy: {
|
||||
type: "string",
|
||||
title: "Group policy",
|
||||
enum: ["allowlist", "open", "off"],
|
||||
},
|
||||
selfChatMode: { type: "string", title: "Self chat mode", enum: ["off", "notes"] },
|
||||
configWrites: { type: "boolean", title: "Config writes" },
|
||||
streaming: {
|
||||
type: "object",
|
||||
title: "Streaming",
|
||||
properties: {
|
||||
progress: {
|
||||
type: "object",
|
||||
properties: {
|
||||
maxLines: { type: "integer", title: "Progress max lines" },
|
||||
toolProgress: { type: "boolean", title: "Progress tool lines" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
retry: {
|
||||
type: "object",
|
||||
title: "Retry",
|
||||
properties: {
|
||||
attempts: { type: "integer", title: "Attempts" },
|
||||
minDelayMs: { type: "integer", title: "Min delay (ms)" },
|
||||
maxDelayMs: { type: "integer", title: "Max delay (ms)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
@@ -742,7 +798,12 @@ function buildConfigMocks(options: { swarmEnabled?: boolean } = {}) {
|
||||
},
|
||||
schema: {
|
||||
schema,
|
||||
uiHints: {},
|
||||
// Resolve tiers the way the gateway does so the mock reproduces the
|
||||
// real common/advanced split instead of a flat "everything advanced".
|
||||
uiHints: applyResolvedConfigTierHints(
|
||||
schema,
|
||||
applyConfigTierHints({}, { includePluginOwnedChannels: true }),
|
||||
),
|
||||
version: "mock-config-schema",
|
||||
generatedAt: new Date(0).toISOString(),
|
||||
},
|
||||
|
||||
@@ -35,6 +35,73 @@ type ConfigFormProps = {
|
||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
};
|
||||
|
||||
function renderAdvancedDivider(onHideAdvanced: (() => void) | undefined) {
|
||||
return html`<div class="config-advanced-divider">
|
||||
<span>${t("configForm.advancedDivider")}</span>
|
||||
${onHideAdvanced
|
||||
? html`<button
|
||||
type="button"
|
||||
class="config-advanced-divider__toggle"
|
||||
@click=${() => onHideAdvanced()}
|
||||
>
|
||||
${t("common.hideAdvanced")}
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Common/advanced split body shared by the config page and channel forms so
|
||||
* every schema surface hides advanced settings behind the same ghost row. */
|
||||
export function renderConfigTierGroups(params: {
|
||||
schema: JsonSchema;
|
||||
path: Array<string | number>;
|
||||
hints: ConfigUiHints;
|
||||
revealAdvanced: boolean;
|
||||
onShowAdvanced: () => void;
|
||||
/** Surfaces without a toolbar toggle pass this so the divider can collapse
|
||||
* the tier again; the config page omits it and uses its toolbar button. */
|
||||
onHideAdvanced?: () => void;
|
||||
renderTier: (node: JsonSchema) => TemplateResult | typeof nothing;
|
||||
}) {
|
||||
const split = splitConfigSchemaByTier({
|
||||
schema: params.schema,
|
||||
path: params.path.map(String),
|
||||
hints: params.hints,
|
||||
});
|
||||
// An advanced-only schema needs no separator, but a surface whose only
|
||||
// collapse control lives on the divider would otherwise strand the tier open.
|
||||
const showDivider = Boolean(split.common) || Boolean(params.onHideAdvanced);
|
||||
return html`
|
||||
${split.common
|
||||
? html`<div class="settings-group">${params.renderTier(split.common)}</div>`
|
||||
: nothing}
|
||||
${split.advanced && split.advancedLeafCount > 0
|
||||
? params.revealAdvanced
|
||||
? html`
|
||||
${showDivider ? renderAdvancedDivider(params.onHideAdvanced) : nothing}
|
||||
<div class="settings-group">${params.renderTier(split.advanced)}</div>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
type="button"
|
||||
class="config-advanced-ghost"
|
||||
@click=${() => params.onShowAdvanced()}
|
||||
>
|
||||
<span class="config-advanced-ghost__count">
|
||||
${t(
|
||||
split.advancedLeafCount === 1
|
||||
? "configForm.advancedHidden"
|
||||
: "configForm.advancedHiddenPlural",
|
||||
{ count: String(split.advancedLeafCount) },
|
||||
)}
|
||||
</span>
|
||||
<span class="config-advanced-ghost__action">${t("configForm.showAdvanced")}</span>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
function matchesSearch(params: {
|
||||
key: string;
|
||||
schema: JsonSchema;
|
||||
@@ -137,11 +204,6 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
nodeValue: unknown;
|
||||
path: Array<string | number>;
|
||||
}) => {
|
||||
const split = splitConfigSchemaByTier({
|
||||
schema: params.node,
|
||||
path: params.path.map(String),
|
||||
hints: props.uiHints,
|
||||
});
|
||||
const revealAdvanced =
|
||||
props.showAdvanced === true ||
|
||||
props.forceAdvancedSection === params.path[0] ||
|
||||
@@ -173,39 +235,14 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
${params.description
|
||||
? html`<p class="settings-section__desc">${params.description}</p>`
|
||||
: nothing}
|
||||
${split.common
|
||||
? html`<div class="settings-group">${renderTier(split.common)}</div>`
|
||||
: nothing}
|
||||
${split.advanced && split.advancedLeafCount > 0
|
||||
? revealAdvanced
|
||||
? html`
|
||||
${split.common
|
||||
? html`<div class="config-advanced-divider">
|
||||
${t("configForm.advancedDivider")}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="settings-group">${renderTier(split.advanced)}</div>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
type="button"
|
||||
class="config-advanced-ghost"
|
||||
@click=${() => props.onShowAdvanced()}
|
||||
>
|
||||
<span class="config-advanced-ghost__count">
|
||||
${t(
|
||||
split.advancedLeafCount === 1
|
||||
? "configForm.advancedHidden"
|
||||
: "configForm.advancedHiddenPlural",
|
||||
{ count: String(split.advancedLeafCount) },
|
||||
)}
|
||||
</span>
|
||||
<span class="config-advanced-ghost__action">
|
||||
${t("configForm.showAdvanced")}
|
||||
</span>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${renderConfigTierGroups({
|
||||
schema: params.node,
|
||||
path: params.path,
|
||||
hints: props.uiHints,
|
||||
revealAdvanced,
|
||||
onShowAdvanced: props.onShowAdvanced,
|
||||
renderTier,
|
||||
})}
|
||||
</section>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Control UI view renders config form screen content.
|
||||
export { renderConfigForm } from "./config-form.render.ts";
|
||||
export { renderConfigForm, renderConfigTierGroups } from "./config-form.render.ts";
|
||||
export { analyzeConfigSchema, type ConfigSchemaAnalysis } from "./config-form.analyze.ts";
|
||||
export { renderNode } from "./config-form.node.ts";
|
||||
export { schemaType, type JsonSchema } from "./config-form.shared.ts";
|
||||
|
||||
@@ -11,6 +11,7 @@ import { titleForRoute } from "../../app-navigation.ts";
|
||||
import { applicationContext, type ApplicationContext } from "../../app/context.ts";
|
||||
import { resolveControlUiAuthHeader } from "../../app/control-ui-auth.ts";
|
||||
import { hasOperatorAdminAccess, hasOperatorPairingAccess } from "../../app/operator-access.ts";
|
||||
import { loadSettings, patchSettings } from "../../app/settings.ts";
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { resolveChannelPairingAuthSignature } from "../../lib/channels/index.ts";
|
||||
@@ -71,6 +72,9 @@ class ChannelsPage extends OpenClawLightDomElement {
|
||||
@state()
|
||||
private pairingNotice: string | null = null;
|
||||
|
||||
@state()
|
||||
private showAdvancedSettings = false;
|
||||
|
||||
private readonly wizardHost = new ChannelWizardHost({
|
||||
getContext: () => this.context,
|
||||
requestUpdate: () => this.requestUpdate(),
|
||||
@@ -150,6 +154,16 @@ class ChannelsPage extends OpenClawLightDomElement {
|
||||
this.applyGatewaySnapshot(snapshot, false);
|
||||
});
|
||||
},
|
||||
)
|
||||
// The advanced tier is one global display pref; theme republishes every
|
||||
// appearance setting, so this keeps the channel forms in sync with the
|
||||
// toggle on the config pages.
|
||||
.watch(
|
||||
() => this.context?.theme,
|
||||
(theme, notify) => theme.subscribe(notify),
|
||||
() => {
|
||||
this.showAdvancedSettings = loadSettings().showAdvancedSettings === true;
|
||||
},
|
||||
);
|
||||
|
||||
private applyGatewaySnapshot(
|
||||
@@ -260,6 +274,13 @@ class ChannelsPage extends OpenClawLightDomElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private setShowAdvancedSettings(enabled: boolean) {
|
||||
patchSettings({ showAdvancedSettings: enabled });
|
||||
// Republish so the config pages and this page read the same pref without a
|
||||
// reload; patchSettings alone only writes storage and the server pref.
|
||||
this.context.theme.refresh();
|
||||
}
|
||||
|
||||
private async saveChannelConfig() {
|
||||
const context = this.context;
|
||||
if (!context) {
|
||||
@@ -662,6 +683,7 @@ class ChannelsPage extends OpenClawLightDomElement {
|
||||
configUiHints: config.configUiHints,
|
||||
configSaving: config.configSaving,
|
||||
configFormDirty: config.configFormDirty,
|
||||
showAdvancedSettings: this.showAdvancedSettings,
|
||||
nostrProfileFormState: this.nostrProfileFormState,
|
||||
nostrProfileAccountId: this.nostrProfileAccountId,
|
||||
selectedChannel: this.selectedChannel,
|
||||
@@ -696,6 +718,7 @@ class ChannelsPage extends OpenClawLightDomElement {
|
||||
void context.channels.waitWhatsApp(this.wizardHost.whatsappAccountId),
|
||||
onWhatsAppLogout: () =>
|
||||
void context.channels.logoutWhatsApp(this.wizardHost.whatsappAccountId),
|
||||
onShowAdvancedSettings: (enabled) => this.setShowAdvancedSettings(enabled),
|
||||
onConfigPatch: (path, value) => context.runtimeConfig.patchForm(path, value),
|
||||
onConfigSave: () => void this.saveChannelConfig(),
|
||||
onConfigReload: () => void this.reloadChannelConfig(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html } from "lit";
|
||||
import type { ConfigUiHints } from "../../api/types.ts";
|
||||
import {
|
||||
analyzeConfigSchema,
|
||||
renderConfigTierGroups,
|
||||
renderNode,
|
||||
schemaType,
|
||||
type JsonSchema,
|
||||
@@ -17,6 +18,8 @@ type ChannelConfigFormProps = {
|
||||
schema: unknown;
|
||||
uiHints: ConfigUiHints;
|
||||
disabled: boolean;
|
||||
showAdvanced: boolean;
|
||||
onShowAdvanced: (enabled: boolean) => void;
|
||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
};
|
||||
|
||||
@@ -100,17 +103,28 @@ function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
||||
}
|
||||
const configValue = props.configValue ?? {};
|
||||
const value = resolveChannelValue(configValue, props.channelId);
|
||||
const path = ["channels", props.channelId];
|
||||
const unsupported = new Set(analysis.unsupportedPaths);
|
||||
return html`
|
||||
<div class="config-form">
|
||||
${renderNode({
|
||||
${renderConfigTierGroups({
|
||||
schema: node,
|
||||
value,
|
||||
path: ["channels", props.channelId],
|
||||
path,
|
||||
hints: props.uiHints,
|
||||
unsupported: new Set(analysis.unsupportedPaths),
|
||||
disabled: props.disabled,
|
||||
showLabel: false,
|
||||
onPatch: props.onPatch,
|
||||
revealAdvanced: props.showAdvanced,
|
||||
onShowAdvanced: () => props.onShowAdvanced(true),
|
||||
onHideAdvanced: () => props.onShowAdvanced(false),
|
||||
renderTier: (tier) =>
|
||||
renderNode({
|
||||
schema: tier,
|
||||
value,
|
||||
path,
|
||||
hints: props.uiHints,
|
||||
unsupported,
|
||||
disabled: props.disabled,
|
||||
showLabel: false,
|
||||
onPatch: props.onPatch,
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
${renderExtraChannelFields(value)}
|
||||
@@ -130,6 +144,8 @@ export function renderChannelConfigSection(params: { channelId: string; props: C
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
showAdvanced: props.showAdvancedSettings,
|
||||
onShowAdvanced: props.onShowAdvancedSettings,
|
||||
onPatch: props.onConfigPatch,
|
||||
})}
|
||||
<div class="settings-row__control">
|
||||
|
||||
@@ -64,6 +64,7 @@ function createProps(overrides: Partial<ChannelsProps> = {}): ChannelsProps {
|
||||
configUiHints: {},
|
||||
configSaving: false,
|
||||
configFormDirty: false,
|
||||
showAdvancedSettings: false,
|
||||
nostrProfileFormState: null,
|
||||
nostrProfileAccountId: null,
|
||||
selectedChannel: null,
|
||||
@@ -88,6 +89,7 @@ function createProps(overrides: Partial<ChannelsProps> = {}): ChannelsProps {
|
||||
onWhatsAppStart: () => undefined,
|
||||
onWhatsAppWait: () => undefined,
|
||||
onWhatsAppLogout: () => undefined,
|
||||
onShowAdvancedSettings: () => undefined,
|
||||
onConfigPatch: () => undefined,
|
||||
onConfigSave: () => undefined,
|
||||
onConfigReload: () => undefined,
|
||||
|
||||
@@ -43,6 +43,7 @@ function createProps(snapshot: ChannelsProps["snapshot"]): ChannelsProps {
|
||||
configUiHints: {},
|
||||
configSaving: false,
|
||||
configFormDirty: false,
|
||||
showAdvancedSettings: false,
|
||||
nostrProfileFormState: null,
|
||||
nostrProfileAccountId: null,
|
||||
selectedChannel: null,
|
||||
@@ -67,6 +68,7 @@ function createProps(snapshot: ChannelsProps["snapshot"]): ChannelsProps {
|
||||
onWhatsAppStart: () => {},
|
||||
onWhatsAppWait: () => {},
|
||||
onWhatsAppLogout: () => {},
|
||||
onShowAdvancedSettings: () => {},
|
||||
onConfigPatch: () => {},
|
||||
onConfigSave: () => {},
|
||||
onConfigReload: () => {},
|
||||
@@ -119,6 +121,101 @@ function renderWhatsAppButtons(params: {
|
||||
};
|
||||
}
|
||||
|
||||
// Mirrors the tiers the gateway materializes on every channel schema path.
|
||||
const CHANNEL_TIER_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
channels: {
|
||||
type: "object",
|
||||
properties: {
|
||||
whatsapp: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
timeoutMs: { type: "integer" },
|
||||
retry: {
|
||||
type: "object",
|
||||
properties: { attempts: { type: "integer" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const CHANNEL_TIER_HINTS = {
|
||||
"channels.whatsapp.enabled": { advanced: false },
|
||||
"channels.whatsapp.timeoutMs": { advanced: true },
|
||||
"channels.whatsapp.retry": { advanced: true },
|
||||
"channels.whatsapp.retry.attempts": { advanced: true },
|
||||
};
|
||||
|
||||
function renderWhatsAppConfigForm(
|
||||
showAdvancedSettings: boolean,
|
||||
hints: Record<string, { advanced: boolean }> = CHANNEL_TIER_HINTS,
|
||||
) {
|
||||
const whatsapp = createWhatsAppStatus();
|
||||
const props = createProps({
|
||||
ts: Date.now(),
|
||||
channelOrder: ["whatsapp"],
|
||||
channelLabels: { whatsapp: "WhatsApp" },
|
||||
channels: { whatsapp },
|
||||
channelAccounts: {},
|
||||
channelDefaultAccountId: {},
|
||||
});
|
||||
const onShowAdvancedSettings = vi.fn();
|
||||
props.configSchema = CHANNEL_TIER_SCHEMA;
|
||||
props.configUiHints = hints;
|
||||
props.configForm = { channels: { whatsapp: { enabled: true, timeoutMs: 5000 } } };
|
||||
props.showAdvancedSettings = showAdvancedSettings;
|
||||
props.onShowAdvancedSettings = onShowAdvancedSettings;
|
||||
|
||||
const container = document.createElement("div");
|
||||
render(renderWhatsAppCard({ props, whatsapp }), container);
|
||||
return { container, onShowAdvancedSettings };
|
||||
}
|
||||
|
||||
describe("channel config advanced tier", () => {
|
||||
it("hides advanced channel settings behind the ghost row by default", () => {
|
||||
const { container, onShowAdvancedSettings } = renderWhatsAppConfigForm(false);
|
||||
|
||||
expect(container.textContent).toContain("Enabled");
|
||||
expect(container.textContent).not.toContain("Timeout Ms");
|
||||
expect(container.querySelector(".config-advanced-divider")).toBeNull();
|
||||
|
||||
const ghost = container.querySelector<HTMLButtonElement>(".config-advanced-ghost");
|
||||
expect(ghost?.textContent).toContain("2 advanced settings hidden");
|
||||
ghost!.click();
|
||||
expect(onShowAdvancedSettings).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("reveals advanced channel settings with a collapse affordance", () => {
|
||||
const { container, onShowAdvancedSettings } = renderWhatsAppConfigForm(true);
|
||||
|
||||
expect(container.textContent).toContain("Enabled");
|
||||
expect(container.textContent).toContain("Timeout Ms");
|
||||
expect(container.querySelector(".config-advanced-ghost")).toBeNull();
|
||||
|
||||
const collapse = container.querySelector<HTMLButtonElement>(".config-advanced-divider__toggle");
|
||||
expect(collapse).toBeInstanceOf(HTMLButtonElement);
|
||||
collapse!.click();
|
||||
expect(onShowAdvancedSettings).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("keeps the collapse control for channels whose settings are all advanced", () => {
|
||||
const { container, onShowAdvancedSettings } = renderWhatsAppConfigForm(true, {
|
||||
...CHANNEL_TIER_HINTS,
|
||||
"channels.whatsapp.enabled": { advanced: true },
|
||||
});
|
||||
|
||||
const collapse = container.querySelector<HTMLButtonElement>(".config-advanced-divider__toggle");
|
||||
expect(collapse).toBeInstanceOf(HTMLButtonElement);
|
||||
collapse!.click();
|
||||
expect(onShowAdvancedSettings).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("channel display selectors", () => {
|
||||
it("returns the channel summary configured flag when present", () => {
|
||||
const props = createProps({
|
||||
|
||||
@@ -54,6 +54,7 @@ export type ChannelsProps = {
|
||||
configUiHints: ConfigUiHints;
|
||||
configSaving: boolean;
|
||||
configFormDirty: boolean;
|
||||
showAdvancedSettings: boolean;
|
||||
nostrProfileFormState: NostrProfileFormState | null;
|
||||
nostrProfileAccountId: string | null;
|
||||
selectedChannel: string | null;
|
||||
@@ -80,6 +81,7 @@ export type ChannelsProps = {
|
||||
onWhatsAppStart: (force: boolean) => void;
|
||||
onWhatsAppWait: () => void;
|
||||
onWhatsAppLogout: () => void;
|
||||
onShowAdvancedSettings: (enabled: boolean) => void;
|
||||
onConfigPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
onConfigSave: () => void;
|
||||
onConfigReload: () => void;
|
||||
|
||||
@@ -86,47 +86,6 @@
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.config-advanced-divider {
|
||||
margin-top: var(--space-5);
|
||||
padding: 0 var(--space-4) var(--space-2);
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
font-weight: 550;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.config-advanced-ghost {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--secondary);
|
||||
text-align: left;
|
||||
transition: background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.config-advanced-ghost:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.config-advanced-ghost__count {
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-sm);
|
||||
}
|
||||
|
||||
.config-advanced-ghost__action {
|
||||
color: var(--accent);
|
||||
font-size: var(--control-ui-text-sm);
|
||||
font-weight: 600;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Restart affordance shown after a successful save until apply */
|
||||
.config-apply-banner {
|
||||
display: flex;
|
||||
|
||||
@@ -155,6 +155,71 @@
|
||||
border-color: color-mix(in srgb, var(--danger) 35%, var(--border) 65%);
|
||||
}
|
||||
|
||||
/* ── Advanced tier ──
|
||||
* Lives here rather than config.css because every schema-driven surface
|
||||
* (config pages and the channel detail forms) renders the same split. */
|
||||
|
||||
.config-advanced-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-5);
|
||||
padding: 0 var(--space-4) var(--space-2);
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
font-weight: 550;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.config-advanced-divider__toggle {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--accent);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-advanced-divider__toggle:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.config-advanced-ghost {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--secondary);
|
||||
text-align: left;
|
||||
transition: background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.config-advanced-ghost:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.config-advanced-ghost__count {
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-sm);
|
||||
}
|
||||
|
||||
.config-advanced-ghost__action {
|
||||
color: var(--accent);
|
||||
font-size: var(--control-ui-text-sm);
|
||||
font-weight: 600;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* ── Row ── */
|
||||
|
||||
.settings-row {
|
||||
|
||||
Reference in New Issue
Block a user