feat: Compress related editor fields into grid rows (#2589)

This commit is contained in:
Dermot Duffy
2026-07-21 22:31:13 -07:00
committed by GitHub
parent e29c0826cf
commit f28b16a4b5
19 changed files with 801 additions and 644 deletions
+85 -55
View File
@@ -5,6 +5,7 @@ import type {
} from '../../../ha/types'; } from '../../../ha/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import { capabilityKeys } from '../../../types'; import { capabilityKeys } from '../../../types';
import { createGrid } from './common/grid';
import { getImageFieldsSchema } from './common/image'; import { getImageFieldsSchema } from './common/image';
import { getProxySchema } from './common/proxy'; import { getProxySchema } from './common/proxy';
import { createNumberSelector, createSelectSelector } from './common/selectors'; import { createNumberSelector, createSelectSelector } from './common/selectors';
@@ -39,8 +40,10 @@ const getMediaLayoutSchema = (): HAFormExpandableSchema => ({
type: 'expandable', type: 'expandable',
title: localize('config.cameras.dimensions.layout.position.editor_label'), title: localize('config.cameras.dimensions.layout.position.editor_label'),
schema: [ schema: [
{ name: 'x', selector: percentageSelector }, createGrid([
{ name: 'y', selector: percentageSelector }, { name: 'x', selector: percentageSelector },
{ name: 'y', selector: percentageSelector },
]),
], ],
}, },
{ {
@@ -48,10 +51,12 @@ const getMediaLayoutSchema = (): HAFormExpandableSchema => ({
type: 'expandable', type: 'expandable',
title: localize('config.cameras.dimensions.layout.view_box.editor_label'), title: localize('config.cameras.dimensions.layout.view_box.editor_label'),
schema: [ schema: [
{ name: 'top', selector: percentageSelector }, createGrid([
{ name: 'bottom', selector: percentageSelector }, { name: 'top', selector: percentageSelector },
{ name: 'left', selector: percentageSelector }, { name: 'bottom', selector: percentageSelector },
{ name: 'right', selector: percentageSelector }, { name: 'left', selector: percentageSelector },
{ name: 'right', selector: percentageSelector },
]),
], ],
}, },
{ {
@@ -59,8 +64,10 @@ const getMediaLayoutSchema = (): HAFormExpandableSchema => ({
type: 'expandable', type: 'expandable',
title: localize('config.cameras.dimensions.layout.pan.editor_label'), title: localize('config.cameras.dimensions.layout.pan.editor_label'),
schema: [ schema: [
{ name: 'x', selector: percentageSelector }, createGrid([
{ name: 'y', selector: percentageSelector }, { name: 'x', selector: percentageSelector },
{ name: 'y', selector: percentageSelector },
]),
], ],
}, },
{ {
@@ -95,8 +102,10 @@ export const getCameraSimpleFields = (): HAFormSchema[] => [
}, },
]), ]),
}, },
{ name: 'title', selector: { text: {} } }, createGrid([
{ name: 'icon', selector: { icon: {} } }, { name: 'title', selector: { text: {} } },
{ name: 'icon', selector: { icon: {} } },
]),
]; ];
interface CameraSchemaOptions { interface CameraSchemaOptions {
@@ -174,8 +183,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
type: 'expandable', type: 'expandable',
title: localize('config.cameras.cast.dashboard.editor_label'), title: localize('config.cameras.cast.dashboard.editor_label'),
schema: [ schema: [
{ name: 'dashboard_path', selector: { text: {} } }, createGrid([
{ name: 'view_path', selector: { text: {} } }, { name: 'dashboard_path', selector: { text: {} } },
{ name: 'view_path', selector: { text: {} } },
]),
], ],
}, },
], ],
@@ -199,16 +210,24 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
title: localize('config.cameras.dimensions.editor_label'), title: localize('config.cameras.dimensions.editor_label'),
icon: 'mdi:aspect-ratio', icon: 'mdi:aspect-ratio',
schema: [ schema: [
{ name: 'aspect_ratio', selector: { text: {} } }, createGrid([
{ { name: 'aspect_ratio', selector: { text: {} } },
name: 'rotation', {
selector: createSelectSelector([ name: 'rotation',
{ value: 0, label: localize('config.cameras.dimensions.rotations.0') }, selector: createSelectSelector([
{ value: 90, label: localize('config.cameras.dimensions.rotations.90') }, { value: 0, label: localize('config.cameras.dimensions.rotations.0') },
{ value: 180, label: localize('config.cameras.dimensions.rotations.180') }, { value: 90, label: localize('config.cameras.dimensions.rotations.90') },
{ value: 270, label: localize('config.cameras.dimensions.rotations.270') }, {
]), value: 180,
}, label: localize('config.cameras.dimensions.rotations.180'),
},
{
value: 270,
label: localize('config.cameras.dimensions.rotations.270'),
},
]),
},
]),
getMediaLayoutSchema(), getMediaLayoutSchema(),
], ],
}, },
@@ -258,8 +277,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
type: 'expandable', type: 'expandable',
title: localize('config.cameras.motioneye.images.editor_label'), title: localize('config.cameras.motioneye.images.editor_label'),
schema: [ schema: [
{ name: 'directory_pattern', selector: { text: {} } }, createGrid([
{ name: 'file_pattern', selector: { text: {} } }, { name: 'directory_pattern', selector: { text: {} } },
{ name: 'file_pattern', selector: { text: {} } },
]),
], ],
}, },
{ {
@@ -267,8 +288,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
type: 'expandable', type: 'expandable',
title: localize('config.cameras.motioneye.movies.editor_label'), title: localize('config.cameras.motioneye.movies.editor_label'),
schema: [ schema: [
{ name: 'directory_pattern', selector: { text: {} } }, createGrid([
{ name: 'file_pattern', selector: { text: {} } }, { name: 'directory_pattern', selector: { text: {} } },
{ name: 'file_pattern', selector: { text: {} } },
]),
], ],
}, },
], ],
@@ -331,8 +354,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
{ multiple: true }, { multiple: true },
), ),
}, },
{ name: 'stream', selector: { text: {} } }, createGrid([
{ name: 'url', selector: { text: {} } }, { name: 'stream', selector: { text: {} } },
{ name: 'url', selector: { text: {} } },
]),
{ name: 'metadata_fetch_timeout_seconds', selector: createNumberSelector() }, { name: 'metadata_fetch_timeout_seconds', selector: createNumberSelector() },
], ],
}, },
@@ -361,33 +386,38 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
title: localize('config.cameras.media.editor_label'), title: localize('config.cameras.media.editor_label'),
icon: 'mdi:play-box-outline', icon: 'mdi:play-box-outline',
schema: [ schema: [
{ createGrid([
name: 'type', {
selector: createSelectSelector([ name: 'type',
{ value: 'auto', label: localize('config.common.media_types.auto') }, selector: createSelectSelector([
{ value: 'reviews', label: localize('config.common.media_types.reviews') }, { value: 'auto', label: localize('config.common.media_types.auto') },
{ value: 'events', label: localize('config.common.media_types.events') }, {
{ value: 'reviews',
value: 'recordings', label: localize('config.common.media_types.reviews'),
label: localize('config.common.media_types.recordings'), },
}, { value: 'events', label: localize('config.common.media_types.events') },
{ value: 'folder', label: localize('config.common.media_types.folder') }, {
]), value: 'recordings',
}, label: localize('config.common.media_types.recordings'),
{ },
name: 'events_type', { value: 'folder', label: localize('config.common.media_types.folder') },
selector: createSelectSelector([ ]),
{ value: 'all', label: localize('config.common.events_media_types.all') }, },
{ {
value: 'clips', name: 'events_type',
label: localize('config.common.events_media_types.clips'), selector: createSelectSelector([
}, { value: 'all', label: localize('config.common.events_media_types.all') },
{ {
value: 'snapshots', value: 'clips',
label: localize('config.common.events_media_types.snapshots'), label: localize('config.common.events_media_types.clips'),
}, },
]), {
}, value: 'snapshots',
label: localize('config.common.events_media_types.snapshots'),
},
]),
},
]),
{ {
name: 'reviewed', name: 'reviewed',
selector: createSelectSelector([ selector: createSelectSelector([
@@ -4,6 +4,7 @@ import type {
HASelectSelectorOption, HASelectSelectorOption,
} from '../../../../../ha/types'; } from '../../../../../ha/types';
import { localize } from '../../../../../localize/localize'; import { localize } from '../../../../../localize/localize';
import { createGrid } from '../grid';
import { createNumberSelector, createSelectSelector } from '../selectors'; import { createNumberSelector, createSelectSelector } from '../selectors';
/** /**
@@ -59,16 +60,18 @@ export const getNextPreviousSchema = (options?: {
title: localize('config.common.controls.next_previous.editor_label'), title: localize('config.common.controls.next_previous.editor_label'),
icon: 'mdi:arrow-right-bold-circle', icon: 'mdi:arrow-right-bold-circle',
schema: [ schema: [
{ createGrid([
name: 'style', {
label: localize('config.common.controls.next_previous.style'), name: 'style',
selector: createSelectSelector(styleOptions), label: localize('config.common.controls.next_previous.style'),
}, selector: createSelectSelector(styleOptions),
{ },
name: 'size', {
label: localize('config.common.controls.next_previous.size'), name: 'size',
selector: createNumberSelector({ min: BUTTON_SIZE_MIN }), label: localize('config.common.controls.next_previous.size'),
}, selector: createNumberSelector({ min: BUTTON_SIZE_MIN }),
},
]),
{ {
name: 'auto_hide', name: 'auto_hide',
label: localize('config.common.controls.next_previous.auto_hide'), label: localize('config.common.controls.next_previous.auto_hide'),
@@ -3,6 +3,7 @@ import type {
HASelectSelectorOption, HASelectSelectorOption,
} from '../../../../../ha/types'; } from '../../../../../ha/types';
import { localize } from '../../../../../localize/localize'; import { localize } from '../../../../../localize/localize';
import { createGrid } from '../grid';
import { createSelectSelector } from '../selectors'; import { createSelectSelector } from '../selectors';
// The PTZ controls are shared between live (physical or digital pan/tilt/zoom) // The PTZ controls are shared between live (physical or digital pan/tilt/zoom)
@@ -32,81 +33,85 @@ export const getPTZSchema = (options?: {
title: localize('config.common.controls.ptz.editor_label'), title: localize('config.common.controls.ptz.editor_label'),
icon: 'mdi:pan', icon: 'mdi:pan',
schema: [ schema: [
{ createGrid([
name: 'mode', {
label: localize('config.common.controls.ptz.mode'), name: 'mode',
selector: createSelectSelector(modeOptions), label: localize('config.common.controls.ptz.mode'),
}, selector: createSelectSelector(modeOptions),
{ },
name: 'type', {
label: localize('config.common.controls.ptz.type'), name: 'type',
selector: createSelectSelector([ label: localize('config.common.controls.ptz.type'),
{ selector: createSelectSelector([
value: 'buttons', {
label: localize('config.common.controls.ptz.types.buttons'), value: 'buttons',
}, label: localize('config.common.controls.ptz.types.buttons'),
{ },
value: 'gestures', {
label: localize('config.common.controls.ptz.types.gestures'), value: 'gestures',
}, label: localize('config.common.controls.ptz.types.gestures'),
]), },
}, ]),
{ },
name: 'position', {
label: localize('config.common.controls.ptz.position'), name: 'position',
selector: createSelectSelector([ label: localize('config.common.controls.ptz.position'),
{ selector: createSelectSelector([
value: 'top-left', {
label: localize('config.common.controls.ptz.positions.top-left'), value: 'top-left',
}, label: localize('config.common.controls.ptz.positions.top-left'),
{ },
value: 'top-right', {
label: localize('config.common.controls.ptz.positions.top-right'), value: 'top-right',
}, label: localize('config.common.controls.ptz.positions.top-right'),
{ },
value: 'bottom-left', {
label: localize('config.common.controls.ptz.positions.bottom-left'), value: 'bottom-left',
}, label: localize('config.common.controls.ptz.positions.bottom-left'),
{ },
value: 'bottom-right', {
label: localize('config.common.controls.ptz.positions.bottom-right'), value: 'bottom-right',
}, label: localize('config.common.controls.ptz.positions.bottom-right'),
]), },
}, ]),
{ },
name: 'orientation', {
label: localize('config.common.controls.ptz.orientation'), name: 'orientation',
selector: createSelectSelector([ label: localize('config.common.controls.ptz.orientation'),
{ selector: createSelectSelector([
value: 'vertical', {
label: localize('config.common.controls.ptz.orientations.vertical'), value: 'vertical',
}, label: localize('config.common.controls.ptz.orientations.vertical'),
{ },
value: 'horizontal', {
label: localize('config.common.controls.ptz.orientations.horizontal'), value: 'horizontal',
}, label: localize('config.common.controls.ptz.orientations.horizontal'),
]), },
}, ]),
{ },
name: 'hide_pan_tilt', ]),
label: localize('config.common.controls.ptz.hide_pan_tilt'), createGrid([
selector: { boolean: {} }, {
}, name: 'hide_pan_tilt',
{ label: localize('config.common.controls.ptz.hide_pan_tilt'),
name: 'hide_zoom', selector: { boolean: {} },
label: localize('config.common.controls.ptz.hide_zoom'), },
selector: { boolean: {} }, {
}, name: 'hide_zoom',
{ label: localize('config.common.controls.ptz.hide_zoom'),
name: 'hide_home', selector: { boolean: {} },
label: localize('config.common.controls.ptz.hide_home'), },
selector: { boolean: {} }, {
}, name: 'hide_home',
{ label: localize('config.common.controls.ptz.hide_home'),
name: 'hide_type', selector: { boolean: {} },
label: localize('config.common.controls.ptz.hide_type'), },
selector: { boolean: {} }, {
}, name: 'hide_type',
label: localize('config.common.controls.ptz.hide_type'),
selector: { boolean: {} },
},
]),
], ],
}; };
}; };
@@ -4,6 +4,7 @@ import {
} from '../../../../../config/schema/common/controls/thumbnails'; } from '../../../../../config/schema/common/controls/thumbnails';
import type { HAFormExpandableSchema, HAFormSchema } from '../../../../../ha/types'; import type { HAFormExpandableSchema, HAFormSchema } from '../../../../../ha/types';
import { localize } from '../../../../../localize/localize'; import { localize } from '../../../../../localize/localize';
import { createGrid } from '../grid';
import { createNumberSelector, createSelectSelector } from '../selectors'; import { createNumberSelector, createSelectSelector } from '../selectors';
/** /**
@@ -14,74 +15,86 @@ import { createNumberSelector, createSelectSelector } from '../selectors';
export const getThumbnailsSchema = (options?: { export const getThumbnailsSchema = (options?: {
includeMode?: boolean; includeMode?: boolean;
}): HAFormExpandableSchema => { }): HAFormExpandableSchema => {
const sizeField: HAFormSchema = {
name: 'size',
label: localize('config.common.controls.thumbnails.size'),
selector: createNumberSelector({
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
}),
};
const schema: HAFormSchema[] = []; const schema: HAFormSchema[] = [];
if (options?.includeMode) { if (options?.includeMode) {
schema.push({ // Where the thumbnails go and how big they are: both describe the space
name: 'mode', // they take up.
label: localize('config.common.controls.thumbnails.mode'), schema.push(
selector: createSelectSelector([ createGrid([
{ {
value: 'none', name: 'mode',
label: localize('config.common.controls.thumbnails.modes.none'), label: localize('config.common.controls.thumbnails.mode'),
}, selector: createSelectSelector([
{ {
value: 'above', value: 'none',
label: localize('config.common.controls.thumbnails.modes.above'), label: localize('config.common.controls.thumbnails.modes.none'),
}, },
{ {
value: 'below', value: 'above',
label: localize('config.common.controls.thumbnails.modes.below'), label: localize('config.common.controls.thumbnails.modes.above'),
}, },
{ {
value: 'left', value: 'below',
label: localize('config.common.controls.thumbnails.modes.left'), label: localize('config.common.controls.thumbnails.modes.below'),
}, },
{ {
value: 'right', value: 'left',
label: localize('config.common.controls.thumbnails.modes.right'), label: localize('config.common.controls.thumbnails.modes.left'),
},
{
value: 'right',
label: localize('config.common.controls.thumbnails.modes.right'),
},
]),
}, },
sizeField,
]), ]),
}); );
} else {
schema.push(sizeField);
} }
schema.push( schema.push(
{ createGrid([
name: 'size', {
label: localize('config.common.controls.thumbnails.size'), name: 'show_details',
selector: createNumberSelector({ label: localize('config.common.controls.thumbnails.show_details'),
min: THUMBNAIL_WIDTH_MIN, selector: { boolean: {} },
max: THUMBNAIL_WIDTH_MAX, },
}), {
}, name: 'show_favorite_control',
{ label: localize('config.common.controls.thumbnails.show_favorite_control'),
name: 'show_details', selector: { boolean: {} },
label: localize('config.common.controls.thumbnails.show_details'), },
selector: { boolean: {} }, {
}, name: 'show_timeline_control',
{ label: localize('config.common.controls.thumbnails.show_timeline_control'),
name: 'show_favorite_control', selector: { boolean: {} },
label: localize('config.common.controls.thumbnails.show_favorite_control'), },
selector: { boolean: {} }, {
}, name: 'show_download_control',
{ label: localize('config.common.controls.thumbnails.show_download_control'),
name: 'show_timeline_control', selector: { boolean: {} },
label: localize('config.common.controls.thumbnails.show_timeline_control'), },
selector: { boolean: {} }, {
}, name: 'show_review_control',
{ label: localize('config.common.controls.thumbnails.show_review_control'),
name: 'show_download_control', selector: { boolean: {} },
label: localize('config.common.controls.thumbnails.show_download_control'), },
selector: { boolean: {} }, {
}, name: 'show_info_control',
{ label: localize('config.common.controls.thumbnails.show_info_control'),
name: 'show_review_control', selector: { boolean: {} },
label: localize('config.common.controls.thumbnails.show_review_control'), },
selector: { boolean: {} }, ]),
},
{
name: 'show_info_control',
label: localize('config.common.controls.thumbnails.show_info_control'),
selector: { boolean: {} },
},
); );
return { return {
name: 'thumbnails', name: 'thumbnails',
@@ -1,5 +1,6 @@
import type { HAFormExpandableSchema } from '../../../../ha/types'; import type { HAFormExpandableSchema } from '../../../../ha/types';
import { localize } from '../../../../localize/localize'; import { localize } from '../../../../localize/localize';
import { createGrid } from './grid';
import { createNumberSelector, createSelectSelector } from './selectors'; import { createNumberSelector, createSelectSelector } from './selectors';
/** /**
@@ -43,15 +44,17 @@ export const getDisplaySchema = (): HAFormExpandableSchema => ({
label: localize('config.common.display.grid_selected_width_factor'), label: localize('config.common.display.grid_selected_width_factor'),
selector: createNumberSelector({ min: 0 }), selector: createNumberSelector({ min: 0 }),
}, },
{ createGrid([
name: 'grid_columns', {
label: localize('config.common.display.grid_columns'), name: 'grid_columns',
selector: createNumberSelector({ min: 0 }), label: localize('config.common.display.grid_columns'),
}, selector: createNumberSelector({ min: 0 }),
{ },
name: 'grid_max_columns', {
label: localize('config.common.display.grid_max_columns'), name: 'grid_max_columns',
selector: createNumberSelector({ min: 0 }), label: localize('config.common.display.grid_max_columns'),
}, selector: createNumberSelector({ min: 0 }),
},
]),
], ],
}); });
@@ -0,0 +1,40 @@
import {
isFormFieldSchema,
isNumberFieldSelector,
type HAFormGridSchema,
type HAFormSchema,
} from '../../../../ha/types';
// The width a column may shrink to before the fields fall back to one per row.
// Narrower than `ha-form`'s own 200px default, which a row nested inside a
// couple of groups can no longer reach: each enclosing panel insets its
// contents, so a field deep in the form has appreciably less width to divide
// than one at the top of a section.
const COLUMN_MIN_WIDTH = '150px';
/**
* Show a number as an input box rather than a slider, since a slider writes its
* label above its control while every other kind of field writes it inside, so
* a slider beside anything else leaves the two sitting at different heights and
* looks odd. A slider is also arguably a poor use of a column's width.
* @param field The field to show as a box.
* @returns The field, as a box if it is a number.
*/
const asBoxedNumber = (field: HAFormSchema): HAFormSchema =>
isFormFieldSchema(field) && isNumberFieldSelector(field.selector)
? {
...field,
selector: { number: { ...field.selector.number, mode: 'box' } },
}
: field;
/**
* Lay fields out in columns rather than one per row.
* @param schema The fields to lay out.
* @returns A grid of the fields.
*/
export const createGrid = (schema: HAFormSchema[]): HAFormGridSchema => ({
type: 'grid',
column_min_width: COLUMN_MIN_WIDTH,
schema: schema.map(asBoxedNumber),
});
@@ -4,6 +4,7 @@ import type {
HASelectSelectorOption, HASelectSelectorOption,
} from '../../../../ha/types'; } from '../../../../ha/types';
import { localize } from '../../../../localize/localize'; import { localize } from '../../../../localize/localize';
import { createGrid } from './grid';
import { createSelectSelector } from './selectors'; import { createSelectSelector } from './selectors';
/** /**
@@ -31,67 +32,78 @@ export const getProxySchema = (options: {
selector: { boolean: {} }, selector: { boolean: {} },
}); });
} }
// What is proxied, laid out together: a proxy may cover the live view, the
// media, or both.
const proxiedFields: HAFormSchema[] = [];
if (options.includeLive) { if (options.includeLive) {
schema.push({ proxiedFields.push({
name: 'live', name: 'live',
label: localize('config.cameras.proxy.live'), label: localize('config.cameras.proxy.live'),
selector: createSelectSelector(proxyModeOptions), selector: createSelectSelector(proxyModeOptions),
}); });
} }
if (options.includeMedia) { if (options.includeMedia) {
schema.push({ proxiedFields.push({
name: 'media', name: 'media',
label: localize('config.cameras.proxy.media'), label: localize('config.cameras.proxy.media'),
selector: createSelectSelector(proxyModeOptions), selector: createSelectSelector(proxyModeOptions),
}); });
} }
if (proxiedFields.length > 1) {
schema.push(createGrid(proxiedFields));
} else {
schema.push(...proxiedFields);
}
schema.push( schema.push(
{ {
name: 'dynamic', name: 'dynamic',
label: localize('config.common.proxy.dynamic'), label: localize('config.common.proxy.dynamic'),
selector: { boolean: {} }, selector: { boolean: {} },
}, },
{ createGrid([
name: 'ssl_verification', {
label: localize('config.common.proxy.ssl_verification.editor_label'), name: 'ssl_verification',
selector: createSelectSelector([ label: localize('config.common.proxy.ssl_verification.editor_label'),
{ selector: createSelectSelector([
value: 'auto', {
label: localize('config.common.proxy.ssl_verification.auto'), value: 'auto',
}, label: localize('config.common.proxy.ssl_verification.auto'),
{ },
value: true, {
label: localize('config.common.proxy.ssl_verification.true'), value: true,
}, label: localize('config.common.proxy.ssl_verification.true'),
{ },
value: false, {
label: localize('config.common.proxy.ssl_verification.false'), value: false,
}, label: localize('config.common.proxy.ssl_verification.false'),
]), },
}, ]),
{ },
name: 'ssl_ciphers', {
label: localize('config.common.proxy.ssl_ciphers.editor_label'), name: 'ssl_ciphers',
selector: createSelectSelector([ label: localize('config.common.proxy.ssl_ciphers.editor_label'),
{ value: 'auto', label: localize('config.common.proxy.ssl_ciphers.auto') }, selector: createSelectSelector([
{ { value: 'auto', label: localize('config.common.proxy.ssl_ciphers.auto') },
value: 'default', {
label: localize('config.common.proxy.ssl_ciphers.default'), value: 'default',
}, label: localize('config.common.proxy.ssl_ciphers.default'),
{ },
value: 'insecure', {
label: localize('config.common.proxy.ssl_ciphers.insecure'), value: 'insecure',
}, label: localize('config.common.proxy.ssl_ciphers.insecure'),
{ },
value: 'intermediate', {
label: localize('config.common.proxy.ssl_ciphers.intermediate'), value: 'intermediate',
}, label: localize('config.common.proxy.ssl_ciphers.intermediate'),
{ },
value: 'modern', {
label: localize('config.common.proxy.ssl_ciphers.modern'), value: 'modern',
}, label: localize('config.common.proxy.ssl_ciphers.modern'),
]), },
}, ]),
},
]),
); );
return { return {
+11 -8
View File
@@ -1,6 +1,7 @@
import type { HASelectSelectorOption } from '../../../ha/types'; import type { HASelectSelectorOption } from '../../../ha/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import type { EditorForm } from '../types'; import type { EditorForm } from '../types';
import { createGrid } from './common/grid';
import { createSelectSelector } from './common/selectors'; import { createSelectSelector } from './common/selectors';
/** /**
@@ -30,14 +31,16 @@ export const getDimensionsSectionForms = (): EditorForm[] => [
{ {
basePath: ['dimensions'], basePath: ['dimensions'],
schema: [ schema: [
{ createGrid([
name: 'aspect_ratio_mode', {
selector: createSelectSelector(getAspectRatioModeOptions()), name: 'aspect_ratio_mode',
}, selector: createSelectSelector(getAspectRatioModeOptions()),
{ },
name: 'aspect_ratio', {
selector: { text: {} }, name: 'aspect_ratio',
}, selector: { text: {} },
},
]),
{ {
name: 'height', name: 'height',
selector: { text: {} }, selector: { text: {} },
+23 -18
View File
@@ -1,5 +1,6 @@
import type { HAFormSchema } from '../../../ha/types'; import type { HAFormSchema } from '../../../ha/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import { createGrid } from './common/grid';
import { createSelectSelector } from './common/selectors'; import { createSelectSelector } from './common/selectors';
/** /**
@@ -8,24 +9,28 @@ import { createSelectSelector } from './common/selectors';
* @returns The form schema for one folder. * @returns The form schema for one folder.
*/ */
export const getFolderSchema = (): HAFormSchema[] => [ export const getFolderSchema = (): HAFormSchema[] => [
{ createGrid([
name: 'type', {
selector: createSelectSelector([ name: 'type',
{ value: 'ha', label: localize('config.folders.types.ha') }, selector: createSelectSelector([
]), { value: 'ha', label: localize('config.folders.types.ha') },
}, ]),
{ },
name: 'title', {
selector: { text: {} }, name: 'id',
}, selector: { text: {} },
{ },
name: 'icon', ]),
selector: { icon: {} }, createGrid([
}, {
{ name: 'title',
name: 'id', selector: { text: {} },
selector: { text: {} }, },
}, {
name: 'icon',
selector: { icon: {} },
},
]),
{ {
name: 'ha', name: 'ha',
type: 'expandable', type: 'expandable',
+37 -32
View File
@@ -7,6 +7,7 @@ import { getPTZSchema } from './common/controls/ptz';
import { getThumbnailsSchema } from './common/controls/thumbnails'; import { getThumbnailsSchema } from './common/controls/thumbnails';
import { getMiniTimelineSchema } from './common/controls/timeline'; import { getMiniTimelineSchema } from './common/controls/timeline';
import { getDisplaySchema } from './common/display'; import { getDisplaySchema } from './common/display';
import { createGrid } from './common/grid';
import { import {
getLiveAutoMuteOptions, getLiveAutoMuteOptions,
getLiveAutoUnmuteOptions, getLiveAutoUnmuteOptions,
@@ -31,36 +32,38 @@ const getCallSchema = (): HAFormExpandableSchema => ({
title: localize('config.live.controls.call.ringtone.editor_label'), title: localize('config.live.controls.call.ringtone.editor_label'),
icon: 'mdi:music-note', icon: 'mdi:music-note',
schema: [ schema: [
{ createGrid([
name: 'type', {
selector: createSelectSelector([ name: 'type',
{ selector: createSelectSelector([
value: 'none', {
label: localize('config.live.controls.call.ringtone.types.none'), value: 'none',
}, label: localize('config.live.controls.call.ringtone.types.none'),
{ },
value: 'chime', {
label: localize('config.live.controls.call.ringtone.types.chime'), value: 'chime',
}, label: localize('config.live.controls.call.ringtone.types.chime'),
{ },
value: 'westminster', {
label: localize('config.live.controls.call.ringtone.types.westminster'), value: 'westminster',
}, label: localize('config.live.controls.call.ringtone.types.westminster'),
{ },
value: 'arpeggio', {
label: localize('config.live.controls.call.ringtone.types.arpeggio'), value: 'arpeggio',
}, label: localize('config.live.controls.call.ringtone.types.arpeggio'),
{ },
value: 'melody', {
label: localize('config.live.controls.call.ringtone.types.melody'), value: 'melody',
}, label: localize('config.live.controls.call.ringtone.types.melody'),
{ },
value: 'custom', {
label: localize('config.live.controls.call.ringtone.types.custom'), value: 'custom',
}, label: localize('config.live.controls.call.ringtone.types.custom'),
]), },
}, ]),
{ name: 'url', selector: { text: {} } }, },
{ name: 'url', selector: { text: {} } },
]),
{ name: 'repeat', selector: createNumberSelector({ min: 0 }) }, { name: 'repeat', selector: createNumberSelector({ min: 0 }) },
], ],
}, },
@@ -125,8 +128,10 @@ export const getLiveSectionForms = (): EditorForm[] => [
basePath: ['live'], basePath: ['live'],
schema: [ schema: [
{ name: 'preload', selector: { boolean: {} } }, { name: 'preload', selector: { boolean: {} } },
{ name: 'draggable', selector: { boolean: {} } }, createGrid([
{ name: 'zoomable', selector: { boolean: {} } }, { name: 'draggable', selector: { boolean: {} } },
{ name: 'zoomable', selector: { boolean: {} } },
]),
{ name: 'lazy_load', selector: { boolean: {} } }, { name: 'lazy_load', selector: { boolean: {} } },
{ {
name: 'lazy_unload', name: 'lazy_unload',
@@ -6,6 +6,7 @@ import { getPTZSchema } from './common/controls/ptz';
import { getThumbnailsSchema } from './common/controls/thumbnails'; import { getThumbnailsSchema } from './common/controls/thumbnails';
import { getMiniTimelineSchema } from './common/controls/timeline'; import { getMiniTimelineSchema } from './common/controls/timeline';
import { getDisplaySchema } from './common/display'; import { getDisplaySchema } from './common/display';
import { createGrid } from './common/grid';
import { import {
getMediaActionNegativeOptions, getMediaActionNegativeOptions,
getMediaActionPositiveOptions, getMediaActionPositiveOptions,
@@ -68,8 +69,10 @@ export const getMediaViewerSectionForms = (): EditorForm[] => [
multiple: true, multiple: true,
}), }),
}, },
{ name: 'draggable', selector: { boolean: {} } }, createGrid([
{ name: 'zoomable', selector: { boolean: {} } }, { name: 'draggable', selector: { boolean: {} } },
{ name: 'zoomable', selector: { boolean: {} } },
]),
{ name: 'lazy_load', selector: { boolean: {} } }, { name: 'lazy_load', selector: { boolean: {} } },
getTransitionEffectSchema(), getTransitionEffectSchema(),
{ name: 'snapshot_click_plays_clip', selector: { boolean: {} } }, { name: 'snapshot_click_plays_clip', selector: { boolean: {} } },
+65 -56
View File
@@ -6,6 +6,7 @@ import type {
} from '../../../ha/types'; } from '../../../ha/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import type { EditorForm } from '../types'; import type { EditorForm } from '../types';
import { createGrid } from './common/grid';
import { createNumberSelector, createSelectSelector } from './common/selectors'; import { createNumberSelector, createSelectSelector } from './common/selectors';
// The menu buttons in their display order. // The menu buttons in their display order.
@@ -96,50 +97,56 @@ const getMenuButtonSchema = (
title: localize(`config.menu.buttons.${button}`), title: localize(`config.menu.buttons.${button}`),
icon: 'mdi:gesture-tap-button', icon: 'mdi:gesture-tap-button',
schema: [ schema: [
{ createGrid([
name: 'enabled', {
label: localize('config.menu.buttons.enabled'), name: 'enabled',
selector: { boolean: {} }, label: localize('config.menu.buttons.enabled'),
}, selector: { boolean: {} },
{ },
name: 'alignment', {
label: localize('config.menu.buttons.alignment'), name: 'permanent',
selector: createSelectSelector([ label: localize('config.menu.buttons.permanent'),
{ selector: { boolean: {} },
value: 'matching', },
label: localize('config.menu.buttons.alignments.matching'), ]),
}, createGrid([
{ {
value: 'opposing', name: 'alignment',
label: localize('config.menu.buttons.alignments.opposing'), label: localize('config.menu.buttons.alignment'),
}, selector: createSelectSelector([
]), {
}, value: 'matching',
{ label: localize('config.menu.buttons.alignments.matching'),
name: 'permanent', },
label: localize('config.menu.buttons.permanent'), {
selector: { boolean: {} }, value: 'opposing',
}, label: localize('config.menu.buttons.alignments.opposing'),
{ },
name: 'priority', ]),
label: localize('config.menu.buttons.priority'), },
selector: createNumberSelector({ max: MENU_PRIORITY_MAX }), {
}, name: 'priority',
label: localize('config.menu.buttons.priority'),
selector: createNumberSelector({ max: MENU_PRIORITY_MAX }),
},
]),
{ {
name: 'icon', name: 'icon',
label: localize('config.menu.buttons.icon'), label: localize('config.menu.buttons.icon'),
selector: { icon: {} }, selector: { icon: {} },
}, },
{ createGrid([
name: 'state_color', {
label: localize('config.menu.buttons.state_color'), name: 'state_color',
selector: { boolean: {} }, label: localize('config.menu.buttons.state_color'),
}, selector: { boolean: {} },
{ },
name: 'inert', {
label: localize('config.menu.buttons.inert'), name: 'inert',
selector: { boolean: {} }, label: localize('config.menu.buttons.inert'),
}, selector: { boolean: {} },
},
]),
...extraSchema, ...extraSchema,
], ],
}); });
@@ -152,23 +159,25 @@ export const getMenuSectionForms = (): EditorForm[] => [
{ {
basePath: ['menu'], basePath: ['menu'],
schema: [ schema: [
{ createGrid([
name: 'style', {
selector: createSelectSelector(getMenuStyleOptions()), name: 'style',
}, selector: createSelectSelector(getMenuStyleOptions()),
{ },
name: 'position', {
selector: createSelectSelector(getMenuPositionOptions()), name: 'position',
}, selector: createSelectSelector(getMenuPositionOptions()),
{ },
name: 'alignment', {
selector: createSelectSelector([ name: 'alignment',
{ value: 'left', label: localize('config.menu.alignments.left') }, selector: createSelectSelector([
{ value: 'right', label: localize('config.menu.alignments.right') }, { value: 'left', label: localize('config.menu.alignments.left') },
{ value: 'top', label: localize('config.menu.alignments.top') }, { value: 'right', label: localize('config.menu.alignments.right') },
{ value: 'bottom', label: localize('config.menu.alignments.bottom') }, { value: 'top', label: localize('config.menu.alignments.top') },
]), { value: 'bottom', label: localize('config.menu.alignments.bottom') },
}, ]),
},
]),
{ {
name: 'auto_hide', name: 'auto_hide',
selector: createSelectSelector( selector: createSelectSelector(
+20 -13
View File
@@ -1,6 +1,7 @@
import { MEDIA_CHUNK_SIZE_MAX } from '../../../config/schema/performance'; import { MEDIA_CHUNK_SIZE_MAX } from '../../../config/schema/performance';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import type { EditorForm } from '../types'; import type { EditorForm } from '../types';
import { createGrid } from './common/grid';
import { createNumberSelector } from './common/selectors'; import { createNumberSelector } from './common/selectors';
/** /**
@@ -18,17 +19,21 @@ export const getPerformanceSectionForms = (): EditorForm[] => [
title: localize('config.performance.features.editor_label'), title: localize('config.performance.features.editor_label'),
icon: 'mdi:feature-search', icon: 'mdi:feature-search',
schema: [ schema: [
{ name: 'card_loading_indicator', selector: { boolean: {} } }, createGrid([
{ name: 'card_loading_effects', selector: { boolean: {} } }, { name: 'card_loading_indicator', selector: { boolean: {} } },
{ name: 'animated_progress_indicator', selector: { boolean: {} } }, { name: 'card_loading_effects', selector: { boolean: {} } },
{ { name: 'animated_progress_indicator', selector: { boolean: {} } },
name: 'media_chunk_size', ]),
selector: createNumberSelector({ max: MEDIA_CHUNK_SIZE_MAX }), createGrid([
}, {
{ name: 'media_chunk_size',
name: 'max_simultaneous_engine_requests', selector: createNumberSelector({ max: MEDIA_CHUNK_SIZE_MAX }),
selector: createNumberSelector({ min: 1 }), },
}, {
name: 'max_simultaneous_engine_requests',
selector: createNumberSelector({ min: 1 }),
},
]),
], ],
}, },
{ {
@@ -37,8 +42,10 @@ export const getPerformanceSectionForms = (): EditorForm[] => [
title: localize('config.performance.style.editor_label'), title: localize('config.performance.style.editor_label'),
icon: 'mdi:palette-swatch-variant', icon: 'mdi:palette-swatch-variant',
schema: [ schema: [
{ name: 'border_radius', selector: { boolean: {} } }, createGrid([
{ name: 'box_shadow', selector: { boolean: {} } }, { name: 'border_radius', selector: { boolean: {} } },
{ name: 'box_shadow', selector: { boolean: {} } },
]),
], ],
}, },
], ],
+21 -24
View File
@@ -4,6 +4,7 @@ import type { RawAdvancedCameraCardConfig } from '../../../config/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import type { ConfigChange, ConfigPath, EditorForm, FieldBinding } from '../types'; import type { ConfigChange, ConfigPath, EditorForm, FieldBinding } from '../types';
import { getCameraSimpleFields } from './cameras'; import { getCameraSimpleFields } from './cameras';
import { createGrid } from './common/grid';
import { createSelectSelector } from './common/selectors'; import { createSelectSelector } from './common/selectors';
import { getAspectRatioModeOptions } from './dimensions'; import { getAspectRatioModeOptions } from './dimensions';
import { import {
@@ -74,14 +75,16 @@ export const getSimpleMenuForms = (): EditorForm[] => [
{ {
basePath: [], basePath: [],
schema: [ schema: [
{ createGrid([
name: 'menu_style', {
selector: createSelectSelector(getMenuStyleOptions()), name: 'menu_style',
}, selector: createSelectSelector(getMenuStyleOptions()),
{ },
name: 'menu_position', {
selector: createSelectSelector(getMenuPositionOptions()), name: 'menu_position',
}, selector: createSelectSelector(getMenuPositionOptions()),
},
]),
{ {
name: 'menu_buttons', name: 'menu_buttons',
label: localize('config.menu.buttons.editor_label'), label: localize('config.menu.buttons.editor_label'),
@@ -110,22 +113,16 @@ export const getSimpleTopLevelForms = (): EditorForm[] => [
name: 'default_view', name: 'default_view',
selector: createSelectSelector(getViewModeOptions()), selector: createSelectSelector(getViewModeOptions()),
}, },
createGrid([
// The ratio only means anything alongside the mode that uses it, so the {
// two are shown together. name: 'aspect_ratio_mode',
{ selector: createSelectSelector(getAspectRatioModeOptions()),
type: 'grid', },
schema: [ {
{ name: 'aspect_ratio',
name: 'aspect_ratio_mode', selector: { text: {} },
selector: createSelectSelector(getAspectRatioModeOptions()), },
}, ]),
{
name: 'aspect_ratio',
selector: { text: {} },
},
],
},
getProfilesField(), getProfilesField(),
], ],
bindings: [ bindings: [
+51 -44
View File
@@ -3,6 +3,7 @@ import { STATUS_BAR_HEIGHT_MIN } from '../../../config/schema/status-bar';
import type { HAFormExpandableSchema } from '../../../ha/types'; import type { HAFormExpandableSchema } from '../../../ha/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import type { EditorForm } from '../types'; import type { EditorForm } from '../types';
import { createGrid } from './common/grid';
import { createNumberSelector, createSelectSelector } from './common/selectors'; import { createNumberSelector, createSelectSelector } from './common/selectors';
const STATUS_BAR_ITEMS = [ const STATUS_BAR_ITEMS = [
@@ -20,21 +21,23 @@ const getStatusBarItemSchema = (item: string): HAFormExpandableSchema => ({
title: localize(`config.status_bar.items.${item}`), title: localize(`config.status_bar.items.${item}`),
icon: 'mdi:feature-search', icon: 'mdi:feature-search',
schema: [ schema: [
{ createGrid([
name: 'enabled', {
label: localize('config.status_bar.items.enabled'), name: 'enabled',
selector: { boolean: {} }, label: localize('config.status_bar.items.enabled'),
}, selector: { boolean: {} },
{ },
name: 'permanent', {
label: localize('config.status_bar.items.permanent'), name: 'permanent',
selector: { boolean: {} }, label: localize('config.status_bar.items.permanent'),
}, selector: { boolean: {} },
{ },
name: 'priority', {
label: localize('config.status_bar.items.priority'), name: 'priority',
selector: createNumberSelector({ max: STATUS_BAR_PRIORITY_MAX }), label: localize('config.status_bar.items.priority'),
}, selector: createNumberSelector({ max: STATUS_BAR_PRIORITY_MAX }),
},
]),
], ],
}); });
@@ -46,27 +49,29 @@ export const getStatusBarSectionForms = (): EditorForm[] => [
{ {
basePath: ['status_bar'], basePath: ['status_bar'],
schema: [ schema: [
{ createGrid([
name: 'style', {
selector: createSelectSelector([ name: 'style',
{ value: 'hover', label: localize('config.status_bar.styles.hover') }, selector: createSelectSelector([
{ { value: 'hover', label: localize('config.status_bar.styles.hover') },
value: 'hover-card', {
label: localize('config.status_bar.styles.hover-card'), value: 'hover-card',
}, label: localize('config.status_bar.styles.hover-card'),
{ value: 'none', label: localize('config.status_bar.styles.none') }, },
{ value: 'outside', label: localize('config.status_bar.styles.outside') }, { value: 'none', label: localize('config.status_bar.styles.none') },
{ value: 'overlay', label: localize('config.status_bar.styles.overlay') }, { value: 'outside', label: localize('config.status_bar.styles.outside') },
{ value: 'popup', label: localize('config.status_bar.styles.popup') }, { value: 'overlay', label: localize('config.status_bar.styles.overlay') },
]), { value: 'popup', label: localize('config.status_bar.styles.popup') },
}, ]),
{ },
name: 'position', {
selector: createSelectSelector([ name: 'position',
{ value: 'top', label: localize('config.status_bar.positions.top') }, selector: createSelectSelector([
{ value: 'bottom', label: localize('config.status_bar.positions.bottom') }, { value: 'top', label: localize('config.status_bar.positions.top') },
]), { value: 'bottom', label: localize('config.status_bar.positions.bottom') },
}, ]),
},
]),
{ {
name: 'auto_hide', name: 'auto_hide',
selector: createSelectSelector( selector: createSelectSelector(
@@ -83,14 +88,16 @@ export const getStatusBarSectionForms = (): EditorForm[] => [
{ multiple: true }, { multiple: true },
), ),
}, },
{ createGrid([
name: 'height', {
selector: createNumberSelector({ min: STATUS_BAR_HEIGHT_MIN }), name: 'height',
}, selector: createNumberSelector({ min: STATUS_BAR_HEIGHT_MIN }),
{ },
name: 'popup_seconds', {
selector: createNumberSelector({ min: 0, max: 60 }), name: 'popup_seconds',
}, selector: createNumberSelector({ min: 0, max: 60 }),
},
]),
], ],
}, },
{ {
+64 -59
View File
@@ -1,6 +1,7 @@
import type { HASelectSelectorOption } from '../../../ha/types'; import type { HASelectSelectorOption } from '../../../ha/types';
import { localize } from '../../../localize/localize'; import { localize } from '../../../localize/localize';
import type { EditorForm } from '../types'; import type { EditorForm } from '../types';
import { createGrid } from './common/grid';
import { getInteractionModeOptions } from './common/interaction-mode'; import { getInteractionModeOptions } from './common/interaction-mode';
import { createNumberSelector, createSelectSelector } from './common/selectors'; import { createNumberSelector, createSelectSelector } from './common/selectors';
@@ -38,17 +39,19 @@ export const getViewSectionForms = (): EditorForm[] => [
{ {
basePath: ['view'], basePath: ['view'],
schema: [ schema: [
{ createGrid([
name: 'default', {
selector: createSelectSelector(getViewModeOptions()), name: 'default',
}, selector: createSelectSelector(getViewModeOptions()),
{ },
name: 'camera_select', {
selector: createSelectSelector([ name: 'camera_select',
...getViewModeOptions(), selector: createSelectSelector([
{ value: 'current', label: localize('config.view.views.current') }, ...getViewModeOptions(),
]), { value: 'current', label: localize('config.view.views.current') },
}, ]),
},
]),
{ {
name: 'dim', name: 'dim',
selector: { boolean: {} }, selector: { boolean: {} },
@@ -135,54 +138,56 @@ export const getViewSectionForms = (): EditorForm[] => [
title: localize('config.view.triggers.actions.editor_label'), title: localize('config.view.triggers.actions.editor_label'),
icon: 'mdi:cogs', icon: 'mdi:cogs',
schema: [ schema: [
{ createGrid([
name: 'trigger', {
label: localize('config.view.triggers.actions.trigger'), name: 'trigger',
selector: createSelectSelector([ label: localize('config.view.triggers.actions.trigger'),
{ selector: createSelectSelector([
value: 'call', {
label: localize('config.view.triggers.actions.triggers.call'), value: 'call',
}, label: localize('config.view.triggers.actions.triggers.call'),
{ },
value: 'default', {
label: localize('config.view.triggers.actions.triggers.default'), value: 'default',
}, label: localize('config.view.triggers.actions.triggers.default'),
{ },
value: 'live', {
label: localize('config.view.triggers.actions.triggers.live'), value: 'live',
}, label: localize('config.view.triggers.actions.triggers.live'),
{ },
value: 'media', {
label: localize('config.view.triggers.actions.triggers.media'), value: 'media',
}, label: localize('config.view.triggers.actions.triggers.media'),
{ },
value: 'none', {
label: localize('config.view.triggers.actions.triggers.none'), value: 'none',
}, label: localize('config.view.triggers.actions.triggers.none'),
{ },
value: 'update', {
label: localize('config.view.triggers.actions.triggers.update'), value: 'update',
}, label: localize('config.view.triggers.actions.triggers.update'),
]), },
}, ]),
{ },
name: 'untrigger', {
label: localize('config.view.triggers.actions.untrigger'), name: 'untrigger',
selector: createSelectSelector([ label: localize('config.view.triggers.actions.untrigger'),
{ selector: createSelectSelector([
value: 'call', {
label: localize('config.view.triggers.actions.untriggers.call'), value: 'call',
}, label: localize('config.view.triggers.actions.untriggers.call'),
{ },
value: 'default', {
label: localize('config.view.triggers.actions.untriggers.default'), value: 'default',
}, label: localize('config.view.triggers.actions.untriggers.default'),
{ },
value: 'none', {
label: localize('config.view.triggers.actions.untriggers.none'), value: 'none',
}, label: localize('config.view.triggers.actions.untriggers.none'),
]), },
}, ]),
},
]),
{ {
name: 'interaction_mode', name: 'interaction_mode',
label: localize('config.view.triggers.actions.interaction_mode'), label: localize('config.view.triggers.actions.interaction_mode'),
+11 -1
View File
@@ -373,7 +373,7 @@ export interface HAFormExpandableSchema
// Fields laid out in columns rather than one per row. A grid is nameless like // Fields laid out in columns rather than one per row. A grid is nameless like
// any other visual-only grouping, so the fields within it are stored where they // any other visual-only grouping, so the fields within it are stored where they
// would be without it. // would be without it.
interface HAFormGridSchema export interface HAFormGridSchema
extends Omit<StockHAFormGridSchema, 'name'>, extends Omit<StockHAFormGridSchema, 'name'>,
CardHAFormSchemaExtensions { CardHAFormSchemaExtensions {
name?: string; name?: string;
@@ -394,3 +394,13 @@ export type HAFormSchema =
export const isFormFieldSchema = ( export const isFormFieldSchema = (
schema: HAFormSchema, schema: HAFormSchema,
): schema is HAFormSelectorSchema => 'selector' in schema; ): schema is HAFormSelectorSchema => 'selector' in schema;
/**
* Whether a field takes a number. Recognized by the key naming the kind of
* selector, which is all a selector carries to tell it apart by.
* @param selector The field's selector.
* @returns `true` for a number.
*/
export const isNumberFieldSelector = (
selector: HASelector,
): selector is HANumberSelector => 'number' in selector;
+131 -131
View File
@@ -80,29 +80,29 @@
}, },
"pan": { "pan": {
"editor_label": "Layout Pan", "editor_label": "Layout Pan",
"x": "Pan horizontal percentage", "x": "Horizontal %",
"y": "Pan vertical percentage" "y": "Vertical %"
}, },
"position": { "position": {
"editor_label": "Layout Cover Position", "editor_label": "Layout Cover Position",
"x": "Horizontal placement percentage", "x": "Horizontal %",
"y": "Vertical placement percentage" "y": "Vertical %"
}, },
"view_box": { "view_box": {
"bottom": "Bottom inset percentage", "bottom": "Bottom inset %",
"editor_label": "Layout View Box", "editor_label": "Layout View Box",
"left": "Left inset percentage", "left": "Left inset %",
"right": "Right inset percentage", "right": "Right inset %",
"top": "Top inset percentage" "top": "Top inset %"
}, },
"zoom": "Zoom factor" "zoom": "Zoom factor"
}, },
"rotation": "Rotation", "rotation": "Rotation",
"rotations": { "rotations": {
"0": "No rotation", "0": "None",
"180": "180 degrees clockwise", "90": "90° clockwise",
"270": "270 degrees clockwise", "180": "180° clockwise",
"90": "90 degrees clockwise" "270": "270° clockwise"
} }
}, },
"engine": "Camera engine", "engine": "Camera engine",
@@ -133,10 +133,10 @@
"mse": "Media Source Extensions (MSE)", "mse": "Media Source Extensions (MSE)",
"webrtc": "Web Real-Time Communication (WebRTC)" "webrtc": "Web Real-Time Communication (WebRTC)"
}, },
"stream": "go2rtc stream name", "stream": "Stream name",
"url": "go2rtc URL" "url": "URL"
}, },
"icon": "Icon for this camera (autodetected from entity)", "icon": "Icon (autodetected)",
"id": "Unique ID for this camera in this card", "id": "Unique ID for this camera in this card",
"image": { "image": {
"editor_label": "Image", "editor_label": "Image",
@@ -158,7 +158,7 @@
}, },
"media": { "media": {
"editor_label": "Media", "editor_label": "Media",
"events_type": "Events media subtype", "events_type": "Events subtype",
"folders": "Folder IDs", "folders": "Folder IDs",
"reviewed": "Review status filter", "reviewed": "Review status filter",
"revieweds": { "revieweds": {
@@ -166,26 +166,26 @@
"reviewed": "Only reviewed", "reviewed": "Only reviewed",
"unreviewed": "Only unreviewed" "unreviewed": "Only unreviewed"
}, },
"type": "Default media type" "type": "Default type"
}, },
"motioneye": { "motioneye": {
"editor_label": "MotionEye", "editor_label": "MotionEye",
"images": { "images": {
"directory_pattern": "Images directory pattern", "directory_pattern": "Directory pattern",
"editor_label": "Images", "editor_label": "Images",
"file_pattern": "Images file pattern" "file_pattern": "File pattern"
}, },
"movies": { "movies": {
"directory_pattern": "Movies directory pattern", "directory_pattern": "Directory pattern",
"editor_label": "Movies", "editor_label": "Movies",
"file_pattern": "Movies file pattern" "file_pattern": "File pattern"
}, },
"url": "MotionEye UI URL" "url": "MotionEye UI URL"
}, },
"proxy": { "proxy": {
"editor_label": "Proxying", "editor_label": "Proxying",
"live": "Live proxying", "live": "Live",
"media": "Media proxying" "media": "Media"
}, },
"reolink": { "reolink": {
"editor_label": "Reolink", "editor_label": "Reolink",
@@ -196,7 +196,7 @@
}, },
"url": "Reolink UI URL" "url": "Reolink UI URL"
}, },
"title": "Title for this camera (autodetected from entity)", "title": "Title (autodetected)",
"triggers": { "triggers": {
"doorbell": "Trigger by auto-detecting doorbell event entities", "doorbell": "Trigger by auto-detecting doorbell event entities",
"editor_label": "Triggers", "editor_label": "Triggers",
@@ -243,10 +243,10 @@
} }
}, },
"next_previous": { "next_previous": {
"auto_hide": "Automatically hide next & previous controls", "auto_hide": "Automatically hide",
"editor_label": "Next & Previous", "editor_label": "Next & Previous",
"size": "Next & previous control size in pixels", "size": "Size in pixels",
"style": "Next & previous control style", "style": "Style",
"styles": { "styles": {
"chevrons": "Chevrons", "chevrons": "Chevrons",
"icons": "Icons", "icons": "Icons",
@@ -256,10 +256,10 @@
}, },
"ptz": { "ptz": {
"editor_label": "PTZ", "editor_label": "PTZ",
"hide_home": "Hide home & preset controls", "hide_home": "Hide home & presets",
"hide_pan_tilt": "Hide pan & tilt control", "hide_pan_tilt": "Hide pan & tilt",
"hide_type": "Hide type toggle button", "hide_type": "Hide type toggle",
"hide_zoom": "Hide zoom control", "hide_zoom": "Hide zoom",
"mode": "Mode", "mode": "Mode",
"modes": { "modes": {
"auto": "Automatic", "auto": "Automatic",
@@ -286,21 +286,21 @@
}, },
"thumbnails": { "thumbnails": {
"editor_label": "Thumbnails", "editor_label": "Thumbnails",
"mode": "Thumbnails mode", "mode": "Mode",
"modes": { "modes": {
"above": "Thumbnails above", "above": "Above",
"below": "Thumbnails below", "below": "Below",
"left": "Thumbnails in a drawer to the left", "left": "Left drawer",
"none": "No thumbnails", "none": "None",
"right": "Thumbnails in a drawer to the right" "right": "Right drawer"
}, },
"show_details": "Show details with thumbnails", "show_details": "Show details",
"show_download_control": "Show download control on thumbnails", "show_download_control": "Show download control",
"show_favorite_control": "Show favorite control on thumbnails", "show_favorite_control": "Show favorite control",
"show_info_control": "Show info control on thumbnails", "show_info_control": "Show info control",
"show_review_control": "Show review control on thumbnails", "show_review_control": "Show review control",
"show_timeline_control": "Show timeline control on thumbnails", "show_timeline_control": "Show timeline control",
"size": "Thumbnails size in pixels" "size": "Size in pixels"
}, },
"timeline": { "timeline": {
"editor_label": "Mini Timeline", "editor_label": "Mini Timeline",
@@ -326,8 +326,8 @@
}, },
"display": { "display": {
"editor_label": "Display", "editor_label": "Display",
"grid_columns": "Exact number of grid columns", "grid_columns": "Exact grid columns",
"grid_max_columns": "Maximum number of grid columns", "grid_max_columns": "Maximum grid columns",
"grid_selected_position": "Position of selected media in grid", "grid_selected_position": "Position of selected media in grid",
"grid_selected_positions": { "grid_selected_positions": {
"default": "Selected item in default position", "default": "Selected item in default position",
@@ -383,25 +383,25 @@
"reviews": "Reviews" "reviews": "Reviews"
}, },
"proxy": { "proxy": {
"dynamic": "Dynamic proxying", "dynamic": "Dynamic",
"modes": { "modes": {
"auto": "Proxying automatically configured", "auto": "Automatic",
"false": "Proxying disabled", "false": "Disabled",
"true": "Proxying enabled" "true": "Enabled"
}, },
"ssl_ciphers": { "ssl_ciphers": {
"auto": "SSL ciphers automatically configured", "auto": "Automatic",
"default": "Default SSL ciphers", "default": "Default",
"editor_label": "SSL ciphers", "editor_label": "SSL ciphers",
"insecure": "Insecure SSL ciphers", "insecure": "Insecure",
"intermediate": "Intermediate SSL ciphers", "intermediate": "Intermediate",
"modern": "Modern SSL ciphers" "modern": "Modern"
}, },
"ssl_verification": { "ssl_verification": {
"auto": "SSL verification automatically configured", "auto": "Automatic",
"editor_label": "SSL verification", "editor_label": "SSL verification",
"false": "SSL verification disabled", "false": "Disabled",
"true": "SSL verification enabled" "true": "Enabled"
} }
}, },
"timeline": { "timeline": {
@@ -420,12 +420,12 @@
} }
}, },
"dimensions": { "dimensions": {
"aspect_ratio": "Default aspect ratio (e.g. '16:9')", "aspect_ratio": "Ratio (e.g. '16:9')",
"aspect_ratio_mode": "Aspect ratio mode", "aspect_ratio_mode": "Aspect ratio mode",
"aspect_ratio_modes": { "aspect_ratio_modes": {
"dynamic": "Aspect ratio adjusts to media", "dynamic": "Dynamic",
"static": "Static aspect ratio", "static": "Static",
"unconstrained": "Unconstrained aspect ratio" "unconstrained": "Unconstrained"
}, },
"height": "Card height in CSS units (e.g. '500px')" "height": "Card height in CSS units (e.g. '500px')"
}, },
@@ -438,16 +438,16 @@
"path_info": "Additional path matching functionality is available in the text editor", "path_info": "Additional path matching functionality is available in the text editor",
"url": "URL of media folder" "url": "URL of media folder"
}, },
"icon": "Icon for this folder", "icon": "Icon",
"id": "Unique ID for this folder in this card", "id": "Unique ID",
"title": "Title for this folder", "title": "Title",
"type": "Folder type", "type": "Type",
"types": { "types": {
"ha": "Home Assistant Media Folder" "ha": "Home Assistant Media Folder"
} }
}, },
"image": { "image": {
"zoomable": "Whether the image can be zoomed and panned" "zoomable": "Zoomable/Pannable"
}, },
"live": { "live": {
"auto_mute": "Automatically mute live cameras", "auto_mute": "Automatically mute live cameras",
@@ -467,7 +467,7 @@
"ringtone": { "ringtone": {
"editor_label": "Ringtone", "editor_label": "Ringtone",
"repeat": "Ringtone repeats per inbound call (0=indefinite)", "repeat": "Ringtone repeats per inbound call (0=indefinite)",
"type": "Ringtone for inbound calls", "type": "Type",
"types": { "types": {
"arpeggio": "Arpeggio", "arpeggio": "Arpeggio",
"chime": "Chime", "chime": "Chime",
@@ -476,7 +476,7 @@
"none": "None", "none": "None",
"westminster": "Westminster" "westminster": "Westminster"
}, },
"url": "Custom ringtone URL" "url": "Custom URL"
}, },
"start": "Start 2-way audio call", "start": "Start 2-way audio call",
"unanswered_timeout_seconds": "Seconds before ending unanswered inbound calls (0=never)", "unanswered_timeout_seconds": "Seconds before ending unanswered inbound calls (0=never)",
@@ -485,7 +485,7 @@
}, },
"editor_label": "Live Controls" "editor_label": "Live Controls"
}, },
"draggable": "Live cameras view can be dragged/swiped", "draggable": "Draggable",
"lazy_load": "Live cameras are lazily loaded", "lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded", "lazy_unload": "Live cameras are lazily unloaded",
"microphone": { "microphone": {
@@ -499,7 +499,7 @@
"preload": "Preload live view in the background", "preload": "Preload live view in the background",
"show_image_during_load": "Show still image while the live stream is loading", "show_image_during_load": "Show still image while the live stream is loading",
"transition_effect": "Live camera transition effect", "transition_effect": "Live camera transition effect",
"zoomable": "Live cameras can be zoomed/panned" "zoomable": "Zoomable/Pannable"
}, },
"media_viewer": { "media_viewer": {
"auto_mute": "Automatically mute media", "auto_mute": "Automatically mute media",
@@ -509,27 +509,27 @@
"controls": { "controls": {
"editor_label": "Media Viewer Controls" "editor_label": "Media Viewer Controls"
}, },
"draggable": "Media Viewer can be dragged/swiped", "draggable": "Draggable",
"lazy_load": "Media Viewer media is lazily loaded in carousel", "lazy_load": "Media Viewer media is lazily loaded in carousel",
"snapshot_click_plays_clip": "Clicking on a snapshot plays a related clip", "snapshot_click_plays_clip": "Clicking on a snapshot plays a related clip",
"transition_effect": "Media Viewer transition effect", "transition_effect": "Media Viewer transition effect",
"zoomable": "Media Viewer can be zoomed/panned" "zoomable": "Zoomable/Pannable"
}, },
"menu": { "menu": {
"alignment": "Menu alignment", "alignment": "Alignment",
"alignments": { "alignments": {
"bottom": "Aligned to the bottom", "bottom": "Bottom",
"left": "Aligned to the left", "left": "Left",
"right": "Aligned to the right", "right": "Right",
"top": "Aligned to the top" "top": "Top"
}, },
"auto_hide": "Conditions under which the menu auto-hides", "auto_hide": "Auto-hide conditions",
"button_size": "Menu button size in pixels", "button_size": "Button size in pixels",
"buttons": { "buttons": {
"alignment": "Button alignment", "alignment": "Alignment",
"alignments": { "alignments": {
"matching": "Matching the menu alignment", "matching": "Matching",
"opposing": "Opposing the menu alignment" "opposing": "Opposing"
}, },
"call": "Call / Two-way audio", "call": "Call / Two-way audio",
"camera_ui": "Camera user interface", "camera_ui": "Camera user interface",
@@ -538,14 +538,14 @@
"display_mode": "Display mode", "display_mode": "Display mode",
"download": "Download", "download": "Download",
"editor_label": "Menu buttons", "editor_label": "Menu buttons",
"enabled": "Button enabled", "enabled": "Enabled",
"expand": "Expand", "expand": "Expand",
"folders": "Folders", "folders": "Folders",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"gallery": "Gallery", "gallery": "Gallery",
"icon": "Icon", "icon": "Icon",
"image": "Image", "image": "Image",
"inert": "Button non-interactive", "inert": "Non-interactive",
"info": "Info", "info": "Info",
"iris": "Iris / Default View / Unhide menu", "iris": "Iris / Default View / Unhide menu",
"live": "Live", "live": "Live",
@@ -553,7 +553,7 @@
"media_player": "Send to media player", "media_player": "Send to media player",
"microphone": "Microphone", "microphone": "Microphone",
"mute": "Mute / Unmute", "mute": "Mute / Unmute",
"permanent": "Show when the menu is hidden", "permanent": "Show when menu hidden",
"pip": "Picture in Picture", "pip": "Picture in Picture",
"play": "Play / Pause", "play": "Play / Pause",
"priority": "Priority", "priority": "Priority",
@@ -564,7 +564,7 @@
"screenshot": "Screenshot", "screenshot": "Screenshot",
"set_review": "Set review status", "set_review": "Set review status",
"snapshots": "Snapshots", "snapshots": "Snapshots",
"state_color": "Color button by entity state", "state_color": "Color by entity state",
"substreams": "Substream(s)", "substreams": "Substream(s)",
"timeline": "Timeline", "timeline": "Timeline",
"type": "Button type", "type": "Button type",
@@ -573,21 +573,21 @@
"toggle": "Toggle" "toggle": "Toggle"
} }
}, },
"position": "Menu position", "position": "Position",
"positions": { "positions": {
"bottom": "Positioned on the bottom", "bottom": "Bottom",
"left": "Positioned on the left", "left": "Left",
"right": "Positioned on the right", "right": "Right",
"top": "Positioned on the top" "top": "Top"
}, },
"style": "Menu style", "style": "Style",
"styles": { "styles": {
"hidden": "Hidden menu", "hidden": "Hidden",
"hover": "Hover menu", "hover": "Hover",
"hover-card": "Hover menu (card-wide)", "hover-card": "Hover (card-wide)",
"none": "No menu", "none": "None",
"outside": "Outside menu", "outside": "Outside",
"overlay": "Overlay menu" "overlay": "Overlay"
} }
}, },
"overrides": { "overrides": {
@@ -595,11 +595,11 @@
}, },
"performance": { "performance": {
"features": { "features": {
"animated_progress_indicator": "Animated Progress Indicator", "animated_progress_indicator": "Animated progress",
"card_loading_effects": "Card Loading Effects", "card_loading_effects": "Loading effects",
"card_loading_indicator": "Card Loading Indicator", "card_loading_indicator": "Loading indicator",
"editor_label": "Feature Options", "editor_label": "Feature Options",
"max_simultaneous_engine_requests": "Max simultaneous camera engine requests", "max_simultaneous_engine_requests": "Parallel engine requests",
"media_chunk_size": "Media chunk size" "media_chunk_size": "Media chunk size"
}, },
"style": { "style": {
@@ -628,37 +628,37 @@
} }
}, },
"status_bar": { "status_bar": {
"auto_hide": "Conditions under which the status bar auto-hides", "auto_hide": "Auto-hide conditions",
"height": "Status bar height in pixels", "height": "Height in pixels",
"items": { "items": {
"enabled": "Item enabled", "enabled": "Enabled",
"engine": "Camera engine", "engine": "Camera engine",
"issues": "Issues", "issues": "Issues",
"permanent": "Item keeps status bar shown", "permanent": "Keeps bar shown",
"priority": "Item priority", "priority": "Priority",
"resolution": "Resolution", "resolution": "Resolution",
"severity": "Severity", "severity": "Severity",
"technology": "Technology", "technology": "Technology",
"title": "Title" "title": "Title"
}, },
"popup_seconds": "Status bar popup seconds", "popup_seconds": "Popup seconds",
"position": "Status bar position", "position": "Position",
"positions": { "positions": {
"bottom": "Positioned on the bottom", "bottom": "Bottom",
"top": "Positioned on the top" "top": "Top"
}, },
"style": "Status bar style", "style": "Style",
"styles": { "styles": {
"hover": "Hover status bar", "hover": "Hover",
"hover-card": "Hover status bar (card-wide)", "hover-card": "Hover (card-wide)",
"none": "No status bar", "none": "None",
"outside": "Outside status bar", "outside": "Outside",
"overlay": "Overlay status bar", "overlay": "Overlay",
"popup": "Popup status bar" "popup": "Popup"
} }
}, },
"view": { "view": {
"camera_select": "View for newly selected cameras", "camera_select": "View on camera select",
"default": "Default view", "default": "Default view",
"default_cycle_camera": "Cycle through cameras when default view updates", "default_cycle_camera": "Cycle through cameras when default view updates",
"default_reset": { "default_reset": {
@@ -699,20 +699,20 @@
"actions": { "actions": {
"editor_label": "Trigger actions", "editor_label": "Trigger actions",
"interaction_mode": "How to handle actions when the card has human interaction", "interaction_mode": "How to handle actions when the card has human interaction",
"trigger": "Trigger action", "trigger": "Trigger",
"triggers": { "triggers": {
"call": "Start a 2-way audio call", "call": "Start call",
"default": "Change to or update default view", "default": "Default view",
"live": "Change to or update live view", "live": "Live view",
"media": "Change to the relevant media view for new media", "media": "Media view",
"none": "No action", "none": "None",
"update": "Update the current view" "update": "Update view"
}, },
"untrigger": "Untrigger action", "untrigger": "Untrigger",
"untriggers": { "untriggers": {
"call": "End a 2-way audio call", "call": "End call",
"default": "Change to default view/camera", "default": "Default view",
"none": "No action" "none": "None"
} }
}, },
"editor_label": "Trigger behavior", "editor_label": "Trigger behavior",
@@ -53,7 +53,7 @@ describe('computeFormLabel', () => {
{ name: 'title', selector: { text: {} } }, { name: 'title', selector: { text: {} } },
{ path: [] }, { path: [] },
), ),
).toBe('Title for this camera (autodetected from entity)'); ).toBe('Title (autodetected)');
}); });
it('should order nested container paths into configuration order', () => { it('should order nested container paths into configuration order', () => {
@@ -72,7 +72,7 @@ describe('computeFormLabel', () => {
createForm([], [{ formPath: ['menu_style'], configPath: ['menu', 'style'] }]), createForm([], [{ formPath: ['menu_style'], configPath: ['menu', 'style'] }]),
{ name: 'menu_style', selector: { text: {} } }, { name: 'menu_style', selector: { text: {} } },
), ),
).toBe('Menu style'); ).toBe('Style');
}); });
it('should localize a field that reads and writes itself where it sits', () => { it('should localize a field that reads and writes itself where it sits', () => {