feat: Add a simple mode to the visual card editor (#2588)

This commit is contained in:
Dermot Duffy
2026-07-21 19:13:29 -07:00
committed by GitHub
parent b6e1de5999
commit e29c0826cf
56 changed files with 1722 additions and 353 deletions
+104 -8
View File
@@ -16,9 +16,16 @@ import './components/editor/section.js';
import './components/icon.js';
import { EditorController } from './components-lib/editor/controller.js';
import type { FormsInput } from './components-lib/editor/forms-controller.js';
import {
FormsController,
type FormsInput,
} from './components-lib/editor/forms-controller.js';
import type { EditorIntent } from './components-lib/editor/intents.js';
import type { FormRequest } from './components-lib/editor/schema/registry.js';
import type { ConfigChange, ConfigPath } from './components-lib/editor/types.js';
import { renderDocumentation } from './components/editor/doc-link.js';
import { renderForms } from './components/editor/form.js';
import { CONF_CAMERAS } from './config/const.js';
import type { RawAdvancedCameraCardConfig } from './config/types.js';
import type { HAFormSchema, HomeAssistant, LovelaceCardEditor } from './ha/types.js';
import { localize } from './localize/localize.js';
@@ -30,6 +37,12 @@ interface EditorSection {
name: string;
description: string;
// The forms the section shows, absent for a section that has none of its own.
request?: FormRequest;
// The path whose documentation the section links to.
docPath?: ConfigPath;
// The part of the section a schema cannot express: a list the user adds to
// and reorders, or a key assigned by pressing it. A section whose whole
// configuration is such a list has no schema forms, and shows only this
@@ -133,6 +146,33 @@ const SECTIONS: Record<string, EditorSection> = {
},
};
// The two parts of the simple editor with more behind them than a handful of
// controls: a list of cameras to add to and drill into, and a menu whose
// buttons are a longer list than everything else in the editor put together.
// The rest of its settings are shown as they are, below.
const SIMPLE_SECTIONS: EditorSection[] = [
{
icon: 'video',
name: localize('editor.cameras'),
description: localize('editor.cameras_secondary'),
docPath: [CONF_CAMERAS],
renderCustomContent: (hass, input) => html`
<advanced-camera-card-editor-cameras
mode="simple"
.hass=${hass}
.input=${input}
></advanced-camera-card-editor-cameras>
`,
},
{
icon: 'menu',
name: localize('editor.menu'),
description: localize('editor.menu_secondary'),
request: { kind: 'simple-menu' },
docPath: ['menu'],
},
];
/**
* Used to side-load lazily-loaded selectors the editor will use.
* See {@link AdvancedCameraCardEditor._renderSelectorSideload}.
@@ -146,9 +186,13 @@ const SELECTOR_SIDELOAD_SCHEMA: HAFormSchema[] = [
{ name: 'entity', selector: { entity: {} } },
{ name: 'object', selector: { object: {} } },
{ name: 'sideload', type: 'expandable', title: '', schema: [] },
{ name: 'sideload-grid', type: 'grid', schema: [] },
];
const SELECTOR_SIDELOAD_DATA = {};
const MODE_REQUEST: FormRequest = { kind: 'editor-mode' };
const SIMPLE_TOP_LEVEL_REQUEST: FormRequest = { kind: 'simple-top-level' };
@customElement('advanced-camera-card-editor')
export class AdvancedCameraCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false })
@@ -156,6 +200,20 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
private _controller = new EditorController(this);
// The forms the editor renders itself rather than within a full editor
// section: the switch between the editors, which belongs to neither of them,
// and the simple editor's remaining settings, which are shown as they are.
private _modeFormsController = this._createFormsController();
private _simpleFormsController = this._createFormsController();
private _createFormsController(): FormsController {
return new FormsController(
(changes: ConfigChange[]) =>
this._controller.applyIntent({ type: 'changes', changes }),
(path) => renderDocumentation(path),
);
}
public setConfig(config: RawAdvancedCameraCardConfig): void {
this._controller.setConfig(config);
}
@@ -186,32 +244,70 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
@advanced-camera-card:editor:intent=${(ev: CustomEvent<EditorIntent>) =>
this._controller.applyIntent(ev.detail)}
>
${this._renderNotices()}
${Object.keys(SECTIONS).map((name) => this._renderSection(name, hass, input))}
${[this._renderNotices(), this._renderModeSwitch(hass, input)]}
${this._controller.getEditorMode() === 'simple'
? this._renderSimple(hass, input)
: this._renderFull(hass, input)}
${this._renderActionButtons()}
</div>
`;
}
private _renderModeSwitch(hass: HomeAssistant, input: FormsInput): TemplateResult {
this._modeFormsController.setInput(MODE_REQUEST, input);
return html`
<div class="mode">
<advanced-camera-card-icon
.icon=${{ icon: 'mdi:tune' }}
></advanced-camera-card-icon>
${renderForms(hass, this._modeFormsController.getContexts())}
</div>
`;
}
private _renderSimple(hass: HomeAssistant, input: FormsInput): TemplateResult {
this._simpleFormsController.setInput(SIMPLE_TOP_LEVEL_REQUEST, input);
return html`
${SIMPLE_SECTIONS.map((section) => this._renderSection(section, hass, input))}
<div class="settings">
${renderForms(hass, this._simpleFormsController.getContexts())}
</div>
`;
}
private _renderFull(hass: HomeAssistant, input: FormsInput): TemplateResult {
return html`
${Object.entries(SECTIONS).map(([name, section]) =>
this._renderSection(
{
...section,
request: { kind: 'full-section', name },
docPath: [name],
},
hass,
input,
),
)}
`;
}
private _renderSection(
name: string,
section: EditorSection,
hass: HomeAssistant,
input: FormsInput,
): TemplateResult {
const section = SECTIONS[name];
const request: FormRequest = { kind: 'section', name };
const renderCustomContent = section.renderCustomContent;
return html`
<advanced-camera-card-editor-section
class="section"
.hass=${hass}
.request=${request}
.request=${section.request}
.input=${input}
.icon=${`mdi:${section.icon}`}
.heading=${section.name}
.description=${section.description}
.documentationPath=${[name]}
.docPath=${section.docPath}
.renderCustomContent=${renderCustomContent
? () => renderCustomContent(hass, input)
: undefined}