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
+33 -3
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: [
createGrid([
{ name: 'x', selector: percentageSelector }, { name: 'x', selector: percentageSelector },
{ name: 'y', 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: [
createGrid([
{ name: 'top', selector: percentageSelector }, { name: 'top', selector: percentageSelector },
{ name: 'bottom', selector: percentageSelector }, { name: 'bottom', selector: percentageSelector },
{ name: 'left', selector: percentageSelector }, { name: 'left', selector: percentageSelector },
{ name: 'right', 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: [
createGrid([
{ name: 'x', selector: percentageSelector }, { name: 'x', selector: percentageSelector },
{ name: 'y', selector: percentageSelector }, { name: 'y', selector: percentageSelector },
]),
], ],
}, },
{ {
@@ -95,8 +102,10 @@ export const getCameraSimpleFields = (): HAFormSchema[] => [
}, },
]), ]),
}, },
createGrid([
{ name: 'title', selector: { text: {} } }, { name: 'title', selector: { text: {} } },
{ name: 'icon', selector: { icon: {} } }, { 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: [
createGrid([
{ name: 'dashboard_path', selector: { text: {} } }, { name: 'dashboard_path', selector: { text: {} } },
{ name: 'view_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: [
createGrid([
{ name: 'aspect_ratio', selector: { text: {} } }, { name: 'aspect_ratio', selector: { text: {} } },
{ {
name: 'rotation', name: 'rotation',
selector: createSelectSelector([ selector: createSelectSelector([
{ value: 0, label: localize('config.cameras.dimensions.rotations.0') }, { value: 0, label: localize('config.cameras.dimensions.rotations.0') },
{ value: 90, label: localize('config.cameras.dimensions.rotations.90') }, { value: 90, label: localize('config.cameras.dimensions.rotations.90') },
{ value: 180, label: localize('config.cameras.dimensions.rotations.180') }, {
{ 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: [
createGrid([
{ name: 'directory_pattern', selector: { text: {} } }, { name: 'directory_pattern', selector: { text: {} } },
{ name: 'file_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: [
createGrid([
{ name: 'directory_pattern', selector: { text: {} } }, { name: 'directory_pattern', selector: { text: {} } },
{ name: 'file_pattern', selector: { text: {} } }, { name: 'file_pattern', selector: { text: {} } },
]),
], ],
}, },
], ],
@@ -331,8 +354,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
{ multiple: true }, { multiple: true },
), ),
}, },
createGrid([
{ name: 'stream', selector: { text: {} } }, { name: 'stream', selector: { text: {} } },
{ name: 'url', selector: { text: {} } }, { name: 'url', selector: { text: {} } },
]),
{ name: 'metadata_fetch_timeout_seconds', selector: createNumberSelector() }, { name: 'metadata_fetch_timeout_seconds', selector: createNumberSelector() },
], ],
}, },
@@ -361,11 +386,15 @@ 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', name: 'type',
selector: createSelectSelector([ selector: createSelectSelector([
{ value: 'auto', label: localize('config.common.media_types.auto') }, { value: 'auto', label: localize('config.common.media_types.auto') },
{ value: 'reviews', label: localize('config.common.media_types.reviews') }, {
value: 'reviews',
label: localize('config.common.media_types.reviews'),
},
{ value: 'events', label: localize('config.common.media_types.events') }, { value: 'events', label: localize('config.common.media_types.events') },
{ {
value: 'recordings', value: 'recordings',
@@ -388,6 +417,7 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] =>
}, },
]), ]),
}, },
]),
{ {
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,6 +60,7 @@ 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', name: 'style',
label: localize('config.common.controls.next_previous.style'), label: localize('config.common.controls.next_previous.style'),
@@ -69,6 +71,7 @@ export const getNextPreviousSchema = (options?: {
label: localize('config.common.controls.next_previous.size'), label: localize('config.common.controls.next_previous.size'),
selector: createNumberSelector({ min: BUTTON_SIZE_MIN }), 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,6 +33,7 @@ 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', name: 'mode',
label: localize('config.common.controls.ptz.mode'), label: localize('config.common.controls.ptz.mode'),
@@ -87,6 +89,8 @@ export const getPTZSchema = (options?: {
}, },
]), ]),
}, },
]),
createGrid([
{ {
name: 'hide_pan_tilt', name: 'hide_pan_tilt',
label: localize('config.common.controls.ptz.hide_pan_tilt'), label: localize('config.common.controls.ptz.hide_pan_tilt'),
@@ -107,6 +111,7 @@ export const getPTZSchema = (options?: {
label: localize('config.common.controls.ptz.hide_type'), label: localize('config.common.controls.ptz.hide_type'),
selector: { boolean: {} }, 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,9 +15,22 @@ 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
// they take up.
schema.push(
createGrid([
{
name: 'mode', name: 'mode',
label: localize('config.common.controls.thumbnails.mode'), label: localize('config.common.controls.thumbnails.mode'),
selector: createSelectSelector([ selector: createSelectSelector([
@@ -41,17 +55,15 @@ export const getThumbnailsSchema = (options?: {
label: localize('config.common.controls.thumbnails.modes.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'),
selector: createNumberSelector({
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
}),
},
{ {
name: 'show_details', name: 'show_details',
label: localize('config.common.controls.thumbnails.show_details'), label: localize('config.common.controls.thumbnails.show_details'),
@@ -82,6 +94,7 @@ export const getThumbnailsSchema = (options?: {
label: localize('config.common.controls.thumbnails.show_info_control'), label: localize('config.common.controls.thumbnails.show_info_control'),
selector: { boolean: {} }, 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,6 +44,7 @@ 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', name: 'grid_columns',
label: localize('config.common.display.grid_columns'), label: localize('config.common.display.grid_columns'),
@@ -53,5 +55,6 @@ export const getDisplaySchema = (): HAFormExpandableSchema => ({
label: localize('config.common.display.grid_max_columns'), label: localize('config.common.display.grid_max_columns'),
selector: createNumberSelector({ min: 0 }), 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,26 +32,36 @@ 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', name: 'ssl_verification',
label: localize('config.common.proxy.ssl_verification.editor_label'), label: localize('config.common.proxy.ssl_verification.editor_label'),
@@ -92,6 +103,7 @@ export const getProxySchema = (options: {
}, },
]), ]),
}, },
]),
); );
return { return {
@@ -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,6 +31,7 @@ export const getDimensionsSectionForms = (): EditorForm[] => [
{ {
basePath: ['dimensions'], basePath: ['dimensions'],
schema: [ schema: [
createGrid([
{ {
name: 'aspect_ratio_mode', name: 'aspect_ratio_mode',
selector: createSelectSelector(getAspectRatioModeOptions()), selector: createSelectSelector(getAspectRatioModeOptions()),
@@ -38,6 +40,7 @@ export const getDimensionsSectionForms = (): EditorForm[] => [
name: 'aspect_ratio', name: 'aspect_ratio',
selector: { text: {} }, selector: { text: {} },
}, },
]),
{ {
name: 'height', name: 'height',
selector: { text: {} }, selector: { text: {} },
+9 -4
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,12 +9,19 @@ 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', name: 'type',
selector: createSelectSelector([ selector: createSelectSelector([
{ value: 'ha', label: localize('config.folders.types.ha') }, { value: 'ha', label: localize('config.folders.types.ha') },
]), ]),
}, },
{
name: 'id',
selector: { text: {} },
},
]),
createGrid([
{ {
name: 'title', name: 'title',
selector: { text: {} }, selector: { text: {} },
@@ -22,10 +30,7 @@ export const getFolderSchema = (): HAFormSchema[] => [
name: 'icon', name: 'icon',
selector: { icon: {} }, selector: { icon: {} },
}, },
{ ]),
name: 'id',
selector: { text: {} },
},
{ {
name: 'ha', name: 'ha',
type: 'expandable', type: 'expandable',
+5
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,6 +32,7 @@ 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', name: 'type',
selector: createSelectSelector([ selector: createSelectSelector([
@@ -61,6 +63,7 @@ const getCallSchema = (): HAFormExpandableSchema => ({
]), ]),
}, },
{ 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: {} } },
createGrid([
{ name: 'draggable', selector: { boolean: {} } }, { name: 'draggable', selector: { boolean: {} } },
{ name: 'zoomable', 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,
}), }),
}, },
createGrid([
{ name: 'draggable', selector: { boolean: {} } }, { name: 'draggable', selector: { boolean: {} } },
{ name: 'zoomable', 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: {} } },
+14 -5
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,11 +97,19 @@ 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', name: 'enabled',
label: localize('config.menu.buttons.enabled'), label: localize('config.menu.buttons.enabled'),
selector: { boolean: {} }, selector: { boolean: {} },
}, },
{
name: 'permanent',
label: localize('config.menu.buttons.permanent'),
selector: { boolean: {} },
},
]),
createGrid([
{ {
name: 'alignment', name: 'alignment',
label: localize('config.menu.buttons.alignment'), label: localize('config.menu.buttons.alignment'),
@@ -115,21 +124,18 @@ const getMenuButtonSchema = (
}, },
]), ]),
}, },
{
name: 'permanent',
label: localize('config.menu.buttons.permanent'),
selector: { boolean: {} },
},
{ {
name: 'priority', name: 'priority',
label: localize('config.menu.buttons.priority'), label: localize('config.menu.buttons.priority'),
selector: createNumberSelector({ max: MENU_PRIORITY_MAX }), 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', name: 'state_color',
label: localize('config.menu.buttons.state_color'), label: localize('config.menu.buttons.state_color'),
@@ -140,6 +146,7 @@ const getMenuButtonSchema = (
label: localize('config.menu.buttons.inert'), label: localize('config.menu.buttons.inert'),
selector: { boolean: {} }, selector: { boolean: {} },
}, },
]),
...extraSchema, ...extraSchema,
], ],
}); });
@@ -152,6 +159,7 @@ export const getMenuSectionForms = (): EditorForm[] => [
{ {
basePath: ['menu'], basePath: ['menu'],
schema: [ schema: [
createGrid([
{ {
name: 'style', name: 'style',
selector: createSelectSelector(getMenuStyleOptions()), selector: createSelectSelector(getMenuStyleOptions()),
@@ -169,6 +177,7 @@ export const getMenuSectionForms = (): EditorForm[] => [
{ value: 'bottom', label: localize('config.menu.alignments.bottom') }, { value: 'bottom', label: localize('config.menu.alignments.bottom') },
]), ]),
}, },
]),
{ {
name: 'auto_hide', name: 'auto_hide',
selector: createSelectSelector( selector: createSelectSelector(
@@ -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,9 +19,12 @@ 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: [
createGrid([
{ name: 'card_loading_indicator', selector: { boolean: {} } }, { name: 'card_loading_indicator', selector: { boolean: {} } },
{ name: 'card_loading_effects', selector: { boolean: {} } }, { name: 'card_loading_effects', selector: { boolean: {} } },
{ name: 'animated_progress_indicator', selector: { boolean: {} } }, { name: 'animated_progress_indicator', selector: { boolean: {} } },
]),
createGrid([
{ {
name: 'media_chunk_size', name: 'media_chunk_size',
selector: createNumberSelector({ max: MEDIA_CHUNK_SIZE_MAX }), selector: createNumberSelector({ max: MEDIA_CHUNK_SIZE_MAX }),
@@ -29,6 +33,7 @@ export const getPerformanceSectionForms = (): EditorForm[] => [
name: 'max_simultaneous_engine_requests', name: 'max_simultaneous_engine_requests',
selector: createNumberSelector({ min: 1 }), 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: [
createGrid([
{ name: 'border_radius', selector: { boolean: {} } }, { name: 'border_radius', selector: { boolean: {} } },
{ name: 'box_shadow', selector: { boolean: {} } }, { name: 'box_shadow', selector: { boolean: {} } },
]),
], ],
}, },
], ],
+5 -8
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,6 +75,7 @@ export const getSimpleMenuForms = (): EditorForm[] => [
{ {
basePath: [], basePath: [],
schema: [ schema: [
createGrid([
{ {
name: 'menu_style', name: 'menu_style',
selector: createSelectSelector(getMenuStyleOptions()), selector: createSelectSelector(getMenuStyleOptions()),
@@ -82,6 +84,7 @@ export const getSimpleMenuForms = (): EditorForm[] => [
name: 'menu_position', name: 'menu_position',
selector: createSelectSelector(getMenuPositionOptions()), selector: createSelectSelector(getMenuPositionOptions()),
}, },
]),
{ {
name: 'menu_buttons', name: 'menu_buttons',
label: localize('config.menu.buttons.editor_label'), label: localize('config.menu.buttons.editor_label'),
@@ -110,12 +113,7 @@ 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.
{
type: 'grid',
schema: [
{ {
name: 'aspect_ratio_mode', name: 'aspect_ratio_mode',
selector: createSelectSelector(getAspectRatioModeOptions()), selector: createSelectSelector(getAspectRatioModeOptions()),
@@ -124,8 +122,7 @@ export const getSimpleTopLevelForms = (): EditorForm[] => [
name: 'aspect_ratio', name: 'aspect_ratio',
selector: { text: {} }, selector: { text: {} },
}, },
], ]),
},
getProfilesField(), getProfilesField(),
], ],
bindings: [ bindings: [
@@ -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,6 +21,7 @@ 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', name: 'enabled',
label: localize('config.status_bar.items.enabled'), label: localize('config.status_bar.items.enabled'),
@@ -35,6 +37,7 @@ const getStatusBarItemSchema = (item: string): HAFormExpandableSchema => ({
label: localize('config.status_bar.items.priority'), label: localize('config.status_bar.items.priority'),
selector: createNumberSelector({ max: STATUS_BAR_PRIORITY_MAX }), selector: createNumberSelector({ max: STATUS_BAR_PRIORITY_MAX }),
}, },
]),
], ],
}); });
@@ -46,6 +49,7 @@ export const getStatusBarSectionForms = (): EditorForm[] => [
{ {
basePath: ['status_bar'], basePath: ['status_bar'],
schema: [ schema: [
createGrid([
{ {
name: 'style', name: 'style',
selector: createSelectSelector([ selector: createSelectSelector([
@@ -67,6 +71,7 @@ export const getStatusBarSectionForms = (): EditorForm[] => [
{ value: 'bottom', label: localize('config.status_bar.positions.bottom') }, { value: 'bottom', label: localize('config.status_bar.positions.bottom') },
]), ]),
}, },
]),
{ {
name: 'auto_hide', name: 'auto_hide',
selector: createSelectSelector( selector: createSelectSelector(
@@ -83,6 +88,7 @@ export const getStatusBarSectionForms = (): EditorForm[] => [
{ multiple: true }, { multiple: true },
), ),
}, },
createGrid([
{ {
name: 'height', name: 'height',
selector: createNumberSelector({ min: STATUS_BAR_HEIGHT_MIN }), selector: createNumberSelector({ min: STATUS_BAR_HEIGHT_MIN }),
@@ -91,6 +97,7 @@ export const getStatusBarSectionForms = (): EditorForm[] => [
name: 'popup_seconds', name: 'popup_seconds',
selector: createNumberSelector({ min: 0, max: 60 }), selector: createNumberSelector({ min: 0, max: 60 }),
}, },
]),
], ],
}, },
{ {
+5
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,6 +39,7 @@ export const getViewSectionForms = (): EditorForm[] => [
{ {
basePath: ['view'], basePath: ['view'],
schema: [ schema: [
createGrid([
{ {
name: 'default', name: 'default',
selector: createSelectSelector(getViewModeOptions()), selector: createSelectSelector(getViewModeOptions()),
@@ -49,6 +51,7 @@ export const getViewSectionForms = (): EditorForm[] => [
{ value: 'current', label: localize('config.view.views.current') }, { value: 'current', label: localize('config.view.views.current') },
]), ]),
}, },
]),
{ {
name: 'dim', name: 'dim',
selector: { boolean: {} }, selector: { boolean: {} },
@@ -135,6 +138,7 @@ 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', name: 'trigger',
label: localize('config.view.triggers.actions.trigger'), label: localize('config.view.triggers.actions.trigger'),
@@ -183,6 +187,7 @@ export const getViewSectionForms = (): EditorForm[] => [
}, },
]), ]),
}, },
]),
{ {
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', () => {