Compare commits

...

6 Commits

Author SHA1 Message Date
Aidan Timson
5f09129533 Fill 2026-04-24 12:15:31 +01:00
Aidan Timson
8215a124b9 Update UI 2026-04-24 12:09:03 +01:00
Aidan Timson
94187923c7 More options 2026-04-24 12:07:18 +01:00
Aidan Timson
6a8a3f3628 Categorise 2026-04-24 12:00:17 +01:00
Aidan Timson
4a9e20c731 Config used 2026-04-24 11:53:04 +01:00
Aidan Timson
595a45202e Selector testing in devtools overflow menu 2026-04-24 11:40:00 +01:00
6 changed files with 1182 additions and 2 deletions

View File

@@ -50,6 +50,10 @@ class DeveloperToolsRouter extends HassRouterPage {
tag: "developer-tools-assist",
load: () => import("./assist/developer-tools-assist"),
},
selectors: {
tag: "developer-tools-selectors",
load: () => import("./selectors/developer-tools-selectors"),
},
debug: {
tag: "developer-tools-debug",
load: () => import("./debug/developer-tools-debug"),

View File

@@ -55,6 +55,11 @@ class PanelDeveloperTools extends LitElement {
"ui.panel.config.developer-tools.tabs.debug.title"
)}
</ha-dropdown-item>
<ha-dropdown-item value="selectors">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.title"
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
<ha-tab-group @wa-tab-show=${this._handlePageSelected}>
@@ -138,8 +143,8 @@ class PanelDeveloperTools extends LitElement {
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
if (action === "debug") {
navigate(`/config/developer-tools/debug`);
if (action === "debug" || action === "selectors") {
navigate(`/config/developer-tools/${action}`);
}
}

View File

@@ -0,0 +1,596 @@
import { mdiContentCopy } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dump } from "js-yaml";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-code-editor";
import "../../../../components/ha-generic-picker";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
import "../../../../components/ha-selector/ha-selector";
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
import type { Selector } from "../../../../data/selector";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import type { SelectorKey } from "./presets";
import {
SELECTOR_OPTIONS,
SELECTOR_PRESETS,
formatSelectorName,
getInitialConfig,
getVariantGroups,
getVariants,
} from "./presets";
const formatYaml = (value: unknown): string => {
if (value === undefined) {
return "";
}
try {
return dump(value, { quotingType: '"', noRefs: true, lineWidth: 100 });
} catch (_err) {
return String(value);
}
};
@customElement("developer-tools-selectors")
class DeveloperToolsSelectors extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _type: SelectorKey = "select";
@state() private _variantId: string =
getVariants("select")[0]?.id ?? "default";
@state() private _config: Record<string, unknown> =
getInitialConfig("select");
@state() private _configValid = true;
@state() private _value: unknown;
@state() private _label = true;
@state() private _helper = false;
@state() private _required = false;
@state() private _disabled = false;
protected render(): TemplateResult {
const selector = { [this._type]: this._config } as unknown as Selector;
const configYaml = formatYaml(this._buildFullConfig(selector)).trimEnd();
const variants = getVariants(this._type);
const groups = getVariantGroups(this._type);
const showVariants = variants.length > 1;
const variantSections = groups.map((g) => ({
id: g.id,
label: g.label,
}));
return html`
<div class="content">
<ha-card header=${this._localizeTitle()}>
<div class="card-content">
<p class="description">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.description"
)}
</p>
<div class="grid">
<div class="config-pane">
<ha-generic-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.selector_type"
)}
.value=${this._type}
.getItems=${this._getTypeItems}
.valueRenderer=${this._typeValueRenderer}
@value-changed=${this._typePicked}
></ha-generic-picker>
${showVariants
? html`
<ha-generic-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.preset"
)}
.value=${this._variantId}
.getItems=${this._getVariantItems}
.valueRenderer=${this._variantValueRenderer}
.sections=${variantSections.length
? variantSections
: undefined}
@value-changed=${this._variantPicked}
></ha-generic-picker>
`
: nothing}
<ha-md-list class="toggles">
<ha-md-list-item>
<span slot="headline">Label</span>
<span slot="supporting-text"
>Pass the selector type as the field label.</span
>
<ha-switch
slot="end"
.name=${"label"}
.checked=${this._label}
@change=${this._toggleChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Helper</span>
<span slot="supporting-text"
>Show a helper text below the field.</span
>
<ha-switch
slot="end"
.name=${"helper"}
.checked=${this._helper}
@change=${this._toggleChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Required</span>
<span slot="supporting-text"
>Mark the selector as required.</span
>
<ha-switch
slot="end"
.name=${"required"}
.checked=${this._required}
@change=${this._toggleChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Disabled</span>
<span slot="supporting-text"
>Render the selector in its disabled state.</span
>
<ha-switch
slot="end"
.name=${"disabled"}
.checked=${this._disabled}
@change=${this._toggleChanged}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
<div class="field">
<div class="field-label">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.configuration"
)}
</div>
<ha-yaml-editor
.hass=${this.hass}
.value=${this._config}
@value-changed=${this._configChanged}
></ha-yaml-editor>
</div>
<div class="actions">
<ha-button @click=${this._resetConfig} appearance="plain">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.reset_config"
)}
</ha-button>
<ha-button @click=${this._resetValue} appearance="plain">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.reset_value"
)}
</ha-button>
</div>
</div>
<div class="preview-pane">
<div class="field">
<div class="field-label">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.preview"
)}
</div>
<div class="preview">
${this._configValid
? html`
<ha-selector
.hass=${this.hass}
.narrow=${this.narrow}
.selector=${selector}
.value=${this._value}
.label=${this._label
? formatSelectorName(this._type)
: undefined}
.helper=${this._helper
? "Example helper text"
: undefined}
.required=${this._required}
.disabled=${this._disabled}
.localizeValue=${this._localizeValue}
@value-changed=${this._valueChanged}
></ha-selector>
`
: html`
<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.invalid_config"
)}
</ha-alert>
`}
</div>
</div>
<div class="field">
<div class="field-header">
<div class="field-label">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.configuration_used"
)}
</div>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.copy_configuration"
)}
.path=${mdiContentCopy}
.disabled=${!this._configValid}
@click=${this._copyConfig}
></ha-icon-button>
</div>
<ha-code-editor
class="readonly-editor"
mode="yaml"
.hass=${this.hass}
.value=${configYaml}
read-only
dir="ltr"
></ha-code-editor>
</div>
<div class="field">
<div class="field-label">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.value"
)}
</div>
${this._value === undefined || this._value === null
? html`<div class="empty-value">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.no_value"
)}
</div>`
: html`<ha-code-editor
class="readonly-editor"
mode="yaml"
.hass=${this.hass}
.value=${formatYaml(this._value).trimEnd()}
read-only
dir="ltr"
></ha-code-editor>`}
</div>
</div>
</div>
</div>
</ha-card>
</div>
`;
}
private async _copyConfig() {
const selector = { [this._type]: this._config } as unknown as Selector;
const yaml = formatYaml(this._buildFullConfig(selector)).trimEnd();
if (!yaml) {
return;
}
await copyToClipboard(yaml);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
// Mirrors the props flipped by the switches so the YAML output matches what
// the preview is rendering. Ordered so the selector body stays at the bottom.
private _buildFullConfig(selector: Selector): Record<string, unknown> {
const config: Record<string, unknown> = {};
if (this._label) {
config.label = formatSelectorName(this._type);
}
if (this._helper) {
config.helper = "Example helper text";
}
if (this._required) {
config.required = true;
}
if (this._disabled) {
config.disabled = true;
}
config.selector = selector;
return config;
}
private _localizeTitle() {
return this.hass.localize(
"ui.panel.config.developer-tools.tabs.selectors.title"
);
}
private _typePicked(ev: CustomEvent<{ value: string | undefined }>) {
ev.stopPropagation();
const newType = ev.detail?.value as SelectorKey | undefined;
if (!newType || newType === this._type) {
return;
}
this._type = newType;
const variants = getVariants(newType);
this._variantId = variants[0]?.id ?? "default";
this._config = { ...(variants[0]?.config ?? {}) };
this._configValid = true;
this._value = undefined;
}
private _getTypeItems = (searchString?: string): PickerComboBoxItem[] => {
const lowerSearch = searchString?.toLowerCase();
const items = lowerSearch
? SELECTOR_OPTIONS.filter(
(o) =>
o.value.toLowerCase().includes(lowerSearch) ||
o.label.toLowerCase().includes(lowerSearch)
)
: SELECTOR_OPTIONS;
return items.map((o) => ({
id: o.value,
primary: o.label,
secondary: o.value,
sorting_label: o.label,
}));
};
private _typeValueRenderer: PickerValueRenderer = (value) => {
const option = SELECTOR_OPTIONS.find((o) => o.value === value);
return html`<span slot="headline">${option?.label ?? value}</span>`;
};
private _variantPicked(ev: CustomEvent<{ value: string | undefined }>) {
ev.stopPropagation();
const newVariantId = ev.detail?.value;
if (!newVariantId || newVariantId === this._variantId) {
return;
}
const variant = getVariants(this._type).find((v) => v.id === newVariantId);
if (!variant) {
return;
}
this._variantId = newVariantId;
this._config = { ...variant.config };
this._configValid = true;
this._value = undefined;
}
// Feeds items (with string section headers) into ha-generic-picker's list.
// When `section` is set the user has clicked a section filter button and we
// emit only that group's variants with no header.
private _getVariantItems = (
searchString?: string,
section?: string
): (PickerComboBoxItem | string)[] => {
const groups = getVariantGroups(this._type);
const lowerSearch = searchString?.toLowerCase();
const items: (PickerComboBoxItem | string)[] = [];
if (!groups.length) {
return getVariants(this._type).map((v) => ({
id: v.id,
primary: v.name,
sorting_label: v.name,
}));
}
for (const group of groups) {
if (section && section !== group.id) {
continue;
}
const matching = lowerSearch
? group.variants.filter((v) =>
v.name.toLowerCase().includes(lowerSearch)
)
: group.variants;
if (!matching.length) {
continue;
}
if (!section) {
items.push(group.label);
}
for (const v of matching) {
items.push({
id: v.id,
primary: v.name,
sorting_label: v.name,
});
}
}
return items;
};
private _variantValueRenderer: PickerValueRenderer = (value) => {
const variant = getVariants(this._type).find((v) => v.id === value);
return html`<span slot="headline">${variant?.name ?? value}</span>`;
};
private _configChanged(ev: CustomEvent) {
ev.stopPropagation();
const { value, isValid } = ev.detail as {
value: Record<string, unknown>;
isValid: boolean;
};
this._configValid = isValid;
if (!isValid) {
return;
}
this._config = value ?? {};
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this._value = ev.detail?.value;
}
// Fake localizer so presets that set `translation_key` show a visible effect
// in the preview. `ha-selector-select` calls this with
// `<translation_key>.options.<value>` and swaps the option label with the
// returned string when truthy.
private _localizeValue = (key: string): string => {
const match = key.match(/^demo_select\.options\.(.+)$/);
if (!match) {
return "";
}
const value = match[1];
const labels: Record<string, string> = {
option_1: "Living room",
option_2: "Kitchen",
option_3: "Bedroom",
option_4: "Office",
};
return labels[value] ?? `Localized: ${value}`;
};
private _toggleChanged(ev: Event) {
const target = ev.target as HTMLInputElement & { name: string };
const key = `_${target.name}` as
| "_label"
| "_helper"
| "_required"
| "_disabled";
this[key] = target.checked;
}
private _resetConfig() {
const variant = getVariants(this._type).find(
(v) => v.id === this._variantId
);
this._config = {
...(variant?.config ?? SELECTOR_PRESETS[this._type] ?? {}),
};
this._configValid = true;
}
private _resetValue() {
this._value = undefined;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
.content {
padding: var(--ha-space-4);
max-width: 1400px;
margin: 0 auto;
}
.description {
margin-top: 0;
color: var(--secondary-text-color);
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--ha-space-5);
}
@media (max-width: 870px) {
.grid {
grid-template-columns: 1fr;
}
}
.config-pane,
.preview-pane {
display: flex;
flex-direction: column;
gap: var(--ha-space-4);
min-width: 0;
}
ha-generic-picker {
display: block;
}
.toggles {
padding: 0;
background: none;
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-md, 8px);
overflow: hidden;
}
.field {
display: flex;
flex-direction: column;
gap: var(--ha-space-2);
min-width: 0;
}
.field-label {
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.field-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--ha-space-2);
}
.field-header .field-label {
flex: 1;
}
.field-header ha-icon-button {
--mdc-icon-button-size: 32px;
--mdc-icon-size: 18px;
color: var(--secondary-text-color);
}
.preview {
padding: var(--ha-space-3);
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-md, 8px);
background: var(--card-background-color);
min-height: 56px;
}
.readonly-editor {
display: block;
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-md, 8px);
overflow: hidden;
}
.empty-value {
padding: var(--ha-space-3);
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-md, 8px);
color: var(--secondary-text-color);
font-style: italic;
}
.actions {
display: flex;
gap: var(--ha-space-2);
flex-wrap: wrap;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"developer-tools-selectors": DeveloperToolsSelectors;
}
}

View File

@@ -0,0 +1,200 @@
// Shared preset data for the developer tools Selectors test page.
//
// Each selector type has a default config in SELECTOR_PRESETS. Types that have
// multiple meaningful configurations worth side-by-side testing expose named
// variants via SELECTOR_VARIANT_GROUPS; groups become section filter buttons
// and section headers in the `ha-generic-picker` preset picker. Types without
// an entry there fall back to a single "Default" variant built from
// SELECTOR_PRESETS.
//
// Per-type variant groups live in sibling files (e.g. ./select.ts) and are
// aggregated into SELECTOR_VARIANT_GROUPS below.
import { SELECT_DEFAULT_CONFIG, SELECT_VARIANT_GROUPS } from "./select";
export type SelectorKey =
| "action"
| "addon"
| "app"
| "area"
| "areas_display"
| "assist_pipeline"
| "attribute"
| "backup_location"
| "boolean"
| "button_toggle"
| "choose"
| "color_rgb"
| "color_temp"
| "condition"
| "config_entry"
| "constant"
| "conversation_agent"
| "country"
| "date"
| "datetime"
| "device"
| "duration"
| "entity"
| "entity_name"
| "file"
| "floor"
| "icon"
| "label"
| "language"
| "location"
| "media"
| "navigation"
| "number"
| "numeric_threshold"
| "object"
| "period"
| "qr_code"
| "select"
| "selector"
| "serial_port"
| "state"
| "statistic"
| "stt"
| "target"
| "template"
| "text"
| "theme"
| "time"
| "timezone"
| "trigger"
| "tts"
| "tts_voice"
| "ui_action"
| "ui_color"
| "ui_state_content";
export interface SelectorVariant {
id: string;
name: string;
config: Record<string, unknown>;
}
export interface SelectorVariantGroup {
id: string;
label: string;
variants: SelectorVariant[];
}
export const SELECTOR_PRESETS: Record<SelectorKey, Record<string, unknown>> = {
action: {},
addon: {},
app: {},
area: {},
areas_display: {},
assist_pipeline: {},
attribute: { entity_id: "" },
backup_location: {},
boolean: {},
button_toggle: {
options: [
{ value: "on", label: "On" },
{ value: "off", label: "Off" },
],
},
choose: {
choices: {
number: { selector: { number: { min: 0, max: 100 } } },
text: { selector: { text: {} } },
},
},
color_rgb: {},
color_temp: {},
condition: {},
config_entry: {},
constant: { value: true, label: "Enabled" },
conversation_agent: {},
country: {},
date: {},
datetime: {},
device: {},
duration: {},
entity: {},
entity_name: {},
file: { accept: "*" },
floor: {},
icon: {},
label: {},
language: {},
location: {},
media: {},
navigation: {},
number: { min: 0, max: 100, mode: "slider", step: 1 },
numeric_threshold: {},
object: {},
period: {},
qr_code: { data: "https://www.home-assistant.io" },
select: SELECT_DEFAULT_CONFIG,
selector: {},
serial_port: {},
state: { entity_id: "" },
statistic: {},
stt: {},
target: {},
template: {},
text: {},
theme: {},
time: {},
timezone: {},
trigger: {},
tts: {},
tts_voice: {},
ui_action: {},
ui_color: {},
ui_state_content: {},
};
export const SELECTOR_TYPES = (
Object.keys(SELECTOR_PRESETS) as SelectorKey[]
).sort();
export const SELECTOR_VARIANT_GROUPS: Partial<
Record<SelectorKey, SelectorVariantGroup[]>
> = {
select: SELECT_VARIANT_GROUPS,
};
export const getVariantGroups = (type: SelectorKey): SelectorVariantGroup[] =>
SELECTOR_VARIANT_GROUPS[type] ?? [];
export const getVariants = (type: SelectorKey): SelectorVariant[] => {
const groups = getVariantGroups(type);
if (groups.length) {
return groups.flatMap((g) => g.variants);
}
return [
{
id: "default",
name: "Default",
config: SELECTOR_PRESETS[type] ?? {},
},
];
};
export const getInitialConfig = (
type: SelectorKey
): Record<string, unknown> => ({
...(getVariants(type)[0]?.config ?? {}),
});
const ACRONYMS = new Set(["qr", "rgb", "stt", "tts", "ui"]);
export const formatSelectorName = (type: string): string =>
type
.split("_")
.map((word) =>
ACRONYMS.has(word)
? word.toUpperCase()
: word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ");
export const SELECTOR_OPTIONS = SELECTOR_TYPES.map((type) => ({
value: type,
label: formatSelectorName(type),
}));

View File

@@ -0,0 +1,360 @@
// Preset variants for the `select` selector, grouped by the UI branch of
// `ha-selector-select` that they exercise:
//
// - Dropdown : <ha-select> (single, mode=dropdown or auto >= 6)
// - List : <ha-radio> / <ha-checkbox>
// (mode=list, no reorder/custom_value)
// - Box : <ha-select-box> (single, mode=box)
// - Chips : <ha-chip-set> + <ha-generic-picker>
// (multiple)
// - Picker : <ha-generic-picker> (single + custom_value)
//
// Configs mirror real usage from the codebase where applicable, e.g.:
// - Box multi-column: frontend/src/panels/lovelace/editor/view-header/
// hui-view-header-settings-editor.ts
// - Box with images/descriptions: frontend/src/panels/config/integrations/
// integration-panels/zwave_js/add-node/zwave-js-add-node-configure-device.ts
// - Single-column box with descriptions: .../zwave-js-add-node-select-
// security-strategy.ts
// - Chips with reorder + custom value: frontend/src/panels/lovelace/editor/
// config-elements/hui-area-card-editor.ts
import type { SelectorVariantGroup } from ".";
const SELECT_OPTIONS_SIMPLE = [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
];
const SELECT_OPTIONS_SHORT = SELECT_OPTIONS_SIMPLE.slice(0, 4);
const DISABLED_OPTIONS = [
{ label: "Option 1", value: "option_1" },
{ label: "Option 2", value: "option_2" },
{ label: "Option 3 (disabled)", value: "option_3", disabled: true },
{ label: "Option 4", value: "option_4" },
];
// Options whose values line up with the `demo_select` localizer in
// developer-tools-selectors.ts so the `translation_key` preset shows visible
// relabeling in the preview.
const TRANSLATION_KEY_OPTIONS = [
{ value: "option_1", label: "Option 1 (raw)" },
{ value: "option_2", label: "Option 2 (raw)" },
{ value: "option_3", label: "Option 3 (raw)" },
{ value: "option_4", label: "Option 4 (raw)" },
];
export const SELECT_DEFAULT_CONFIG: Record<string, unknown> = {
options: SELECT_OPTIONS_SIMPLE,
};
export const SELECT_VARIANT_GROUPS: SelectorVariantGroup[] = [
{
id: "dropdown",
label: "Dropdown",
variants: [
{
id: "dropdown",
name: "Basic",
config: { mode: "dropdown", options: SELECT_OPTIONS_SIMPLE },
},
{
id: "dropdown_sorted",
name: "Sorted",
config: {
mode: "dropdown",
sort: true,
options: ["Charlie", "Alpha", "Echo", "Bravo", "Delta"],
},
},
{
id: "dropdown_disabled_options",
name: "With disabled option",
config: { mode: "dropdown", options: DISABLED_OPTIONS },
},
{
id: "dropdown_auto_from_count",
name: "Auto-selected for 6+ options",
config: { options: SELECT_OPTIONS_SIMPLE },
},
{
id: "dropdown_translation_key",
name: "translation_key (relabels via localizeValue)",
config: {
mode: "dropdown",
translation_key: "demo_select",
options: TRANSLATION_KEY_OPTIONS,
},
},
],
},
{
id: "list",
label: "List",
variants: [
{
id: "list_radios",
name: "Radios",
config: { mode: "list", options: SELECT_OPTIONS_SHORT },
},
{
id: "list_radios_disabled",
name: "Radios with disabled option",
config: { mode: "list", options: DISABLED_OPTIONS },
},
{
id: "list_checkboxes",
name: "Checkboxes (multiple)",
config: {
mode: "list",
multiple: true,
options: SELECT_OPTIONS_SHORT,
},
},
{
id: "list_auto_from_count",
name: "Auto-selected for <6 options",
config: { options: SELECT_OPTIONS_SHORT },
},
],
},
{
id: "box",
label: "Box",
variants: [
{
id: "box_basic",
name: "Basic",
config: { mode: "box", options: SELECT_OPTIONS_SHORT },
},
{
id: "box_multi_column",
name: "Multi-column",
config: {
mode: "box",
box_max_columns: 3,
options: [
{
value: "responsive",
label: "Responsive",
description: "Fill the available width responsively.",
},
{
value: "start",
label: "Start",
description: "Align content to the start of the container.",
},
{
value: "center",
label: "Center",
description: "Center content in the container.",
},
],
},
},
{
id: "box_single_column_descriptions",
name: "Single column with descriptions",
config: {
mode: "box",
box_max_columns: 1,
options: [
{
value: "default",
label: "Default",
description:
"Use the highest security protocol advertised by the device.",
},
{
value: "s2",
label: "Security S2",
description:
"Use Security S2 for this inclusion (requires compatible hardware).",
},
{
value: "s0",
label: "Security S0",
description: "Use Security S0 for this inclusion (legacy).",
},
{
value: "insecure",
label: "Insecure",
description: "Skip encryption entirely. Not recommended.",
},
],
},
},
{
id: "box_with_images",
name: "With images (object form)",
config: {
mode: "box",
box_max_columns: 1,
options: [
{
value: "long_range",
label: "Long Range",
description:
"High-bandwidth, star-topology network with per-device ranges of several kilometers.",
image: {
src: "/static/images/z-wave-add-node/long-range.svg",
src_dark: "/static/images/z-wave-add-node/long-range_dark.svg",
flip_rtl: true,
},
},
{
value: "mesh",
label: "Mesh",
description:
"Traditional mesh network where devices route traffic for each other.",
image: {
src: "/static/images/z-wave-add-node/mesh.svg",
src_dark: "/static/images/z-wave-add-node/mesh_dark.svg",
flip_rtl: true,
},
},
],
},
},
{
id: "box_image_string",
name: "With image (string form)",
config: {
mode: "box",
box_max_columns: 1,
options: [
{
value: "ohf",
label: "Open Home Foundation",
description: "image is a plain URL string, not an object.",
image: "/static/images/ohf-badge.svg",
},
{
value: "none",
label: "None",
description: "An option without an image for contrast.",
},
],
},
},
{
id: "box_two_columns",
name: "Two columns",
config: {
mode: "box",
box_max_columns: 2,
options: SELECT_OPTIONS_SHORT,
},
},
{
id: "box_disabled_option",
name: "With disabled option",
config: {
mode: "box",
box_max_columns: 2,
options: DISABLED_OPTIONS,
},
},
],
},
{
id: "chips",
label: "Chips",
variants: [
{
id: "chips_basic",
name: "Basic (multiple)",
config: { multiple: true, options: SELECT_OPTIONS_SIMPLE },
},
{
id: "chips_reorder",
name: "Reorder",
config: {
multiple: true,
reorder: true,
options: SELECT_OPTIONS_SIMPLE,
},
},
{
id: "chips_custom_value",
name: "Custom value",
config: {
multiple: true,
custom_value: true,
options: SELECT_OPTIONS_SIMPLE,
},
},
{
id: "chips_reorder_custom_value",
name: "Reorder + custom value",
config: {
multiple: true,
reorder: true,
custom_value: true,
options: SELECT_OPTIONS_SIMPLE,
},
},
{
id: "chips_sorted",
name: "Sorted",
config: {
multiple: true,
sort: true,
options: ["Charlie", "Alpha", "Echo", "Bravo", "Delta"],
},
},
{
id: "chips_disabled_option",
name: "With disabled option",
config: {
multiple: true,
options: DISABLED_OPTIONS,
},
},
{
id: "chips_list_mode_reorder",
name: "mode:list + reorder (falls through to chips)",
config: {
mode: "list",
multiple: true,
reorder: true,
options: SELECT_OPTIONS_SIMPLE,
},
},
{
id: "chips_box_mode",
name: "mode:box + multiple (falls through to chips)",
config: {
mode: "box",
multiple: true,
options: SELECT_OPTIONS_SIMPLE,
},
},
],
},
{
id: "picker",
label: "Picker",
variants: [
{
id: "picker_custom_value",
name: "Custom value (searchable)",
config: { custom_value: true, options: SELECT_OPTIONS_SIMPLE },
},
{
id: "picker_list_mode",
name: "mode:list + custom_value (falls through to picker)",
config: {
mode: "list",
custom_value: true,
options: SELECT_OPTIONS_SIMPLE,
},
},
],
},
];

View File

@@ -3719,6 +3719,21 @@
"no_match": "No intent matched",
"language": "[%key:ui::components::language-picker::language%]"
},
"selectors": {
"title": "Selectors",
"description": "Preview any selector with a custom configuration. Changes to the configuration update the preview live.",
"selector_type": "Selector type",
"preset": "Preset",
"configuration": "Configuration",
"configuration_used": "Configuration used",
"copy_configuration": "Copy configuration",
"preview": "Preview",
"value": "Value",
"no_value": "No value set",
"invalid_config": "Configuration is not valid YAML.",
"reset_config": "Reset configuration",
"reset_value": "Reset value"
},
"debug": {
"title": "Debug tools",
"debug_connection": {