From f28b16a4b5121aa056477eb536015aa2731d1ceb Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 21 Jul 2026 22:31:13 -0700 Subject: [PATCH] feat: Compress related editor fields into grid rows (#2589) --- src/components-lib/editor/schema/cameras.ts | 140 ++++++---- .../schema/common/controls/next-previous.ts | 23 +- .../editor/schema/common/controls/ptz.ts | 155 ++++++----- .../schema/common/controls/thumbnails.ts | 135 +++++---- .../editor/schema/common/display.ts | 23 +- .../editor/schema/common/grid.ts | 40 +++ .../editor/schema/common/proxy.ts | 98 ++++--- .../editor/schema/dimensions.ts | 19 +- src/components-lib/editor/schema/folders.ts | 41 +-- src/components-lib/editor/schema/live.ts | 69 ++--- .../editor/schema/media-viewer.ts | 7 +- src/components-lib/editor/schema/menu.ts | 121 ++++---- .../editor/schema/performance.ts | 33 ++- src/components-lib/editor/schema/simple.ts | 45 ++- .../editor/schema/status-bar.ts | 95 ++++--- src/components-lib/editor/schema/view.ts | 123 ++++---- src/ha/types.ts | 12 +- src/localize/languages/en.json | 262 +++++++++--------- .../components-lib/editor/form-labels.test.ts | 4 +- 19 files changed, 801 insertions(+), 644 deletions(-) create mode 100644 src/components-lib/editor/schema/common/grid.ts diff --git a/src/components-lib/editor/schema/cameras.ts b/src/components-lib/editor/schema/cameras.ts index eccbf3bd..a60515ed 100644 --- a/src/components-lib/editor/schema/cameras.ts +++ b/src/components-lib/editor/schema/cameras.ts @@ -5,6 +5,7 @@ import type { } from '../../../ha/types'; import { localize } from '../../../localize/localize'; import { capabilityKeys } from '../../../types'; +import { createGrid } from './common/grid'; import { getImageFieldsSchema } from './common/image'; import { getProxySchema } from './common/proxy'; import { createNumberSelector, createSelectSelector } from './common/selectors'; @@ -39,8 +40,10 @@ const getMediaLayoutSchema = (): HAFormExpandableSchema => ({ type: 'expandable', title: localize('config.cameras.dimensions.layout.position.editor_label'), schema: [ - { name: 'x', selector: percentageSelector }, - { name: 'y', selector: percentageSelector }, + createGrid([ + { name: 'x', selector: percentageSelector }, + { name: 'y', selector: percentageSelector }, + ]), ], }, { @@ -48,10 +51,12 @@ const getMediaLayoutSchema = (): HAFormExpandableSchema => ({ type: 'expandable', title: localize('config.cameras.dimensions.layout.view_box.editor_label'), schema: [ - { name: 'top', selector: percentageSelector }, - { name: 'bottom', selector: percentageSelector }, - { name: 'left', selector: percentageSelector }, - { name: 'right', selector: percentageSelector }, + createGrid([ + { name: 'top', selector: percentageSelector }, + { name: 'bottom', selector: percentageSelector }, + { name: 'left', selector: percentageSelector }, + { name: 'right', selector: percentageSelector }, + ]), ], }, { @@ -59,8 +64,10 @@ const getMediaLayoutSchema = (): HAFormExpandableSchema => ({ type: 'expandable', title: localize('config.cameras.dimensions.layout.pan.editor_label'), schema: [ - { name: 'x', selector: percentageSelector }, - { name: 'y', selector: percentageSelector }, + createGrid([ + { name: 'x', selector: percentageSelector }, + { name: 'y', selector: percentageSelector }, + ]), ], }, { @@ -95,8 +102,10 @@ export const getCameraSimpleFields = (): HAFormSchema[] => [ }, ]), }, - { name: 'title', selector: { text: {} } }, - { name: 'icon', selector: { icon: {} } }, + createGrid([ + { name: 'title', selector: { text: {} } }, + { name: 'icon', selector: { icon: {} } }, + ]), ]; interface CameraSchemaOptions { @@ -174,8 +183,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] => type: 'expandable', title: localize('config.cameras.cast.dashboard.editor_label'), schema: [ - { name: 'dashboard_path', selector: { text: {} } }, - { name: 'view_path', selector: { text: {} } }, + createGrid([ + { 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'), icon: 'mdi:aspect-ratio', schema: [ - { name: 'aspect_ratio', selector: { text: {} } }, - { - name: 'rotation', - selector: createSelectSelector([ - { value: 0, label: localize('config.cameras.dimensions.rotations.0') }, - { 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') }, - ]), - }, + createGrid([ + { name: 'aspect_ratio', selector: { text: {} } }, + { + name: 'rotation', + selector: createSelectSelector([ + { value: 0, label: localize('config.cameras.dimensions.rotations.0') }, + { 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'), + }, + ]), + }, + ]), getMediaLayoutSchema(), ], }, @@ -258,8 +277,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] => type: 'expandable', title: localize('config.cameras.motioneye.images.editor_label'), schema: [ - { name: 'directory_pattern', selector: { text: {} } }, - { name: 'file_pattern', selector: { text: {} } }, + createGrid([ + { name: 'directory_pattern', selector: { text: {} } }, + { name: 'file_pattern', selector: { text: {} } }, + ]), ], }, { @@ -267,8 +288,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] => type: 'expandable', title: localize('config.cameras.motioneye.movies.editor_label'), schema: [ - { name: 'directory_pattern', selector: { text: {} } }, - { name: 'file_pattern', selector: { text: {} } }, + createGrid([ + { name: 'directory_pattern', selector: { text: {} } }, + { name: 'file_pattern', selector: { text: {} } }, + ]), ], }, ], @@ -331,8 +354,10 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] => { multiple: true }, ), }, - { name: 'stream', selector: { text: {} } }, - { name: 'url', selector: { text: {} } }, + createGrid([ + { name: 'stream', selector: { text: {} } }, + { name: 'url', selector: { text: {} } }, + ]), { name: 'metadata_fetch_timeout_seconds', selector: createNumberSelector() }, ], }, @@ -361,33 +386,38 @@ export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] => title: localize('config.cameras.media.editor_label'), icon: 'mdi:play-box-outline', schema: [ - { - name: 'type', - selector: createSelectSelector([ - { value: 'auto', label: localize('config.common.media_types.auto') }, - { value: 'reviews', label: localize('config.common.media_types.reviews') }, - { value: 'events', label: localize('config.common.media_types.events') }, - { - value: 'recordings', - label: localize('config.common.media_types.recordings'), - }, - { value: 'folder', label: localize('config.common.media_types.folder') }, - ]), - }, - { - name: 'events_type', - selector: createSelectSelector([ - { value: 'all', label: localize('config.common.events_media_types.all') }, - { - value: 'clips', - label: localize('config.common.events_media_types.clips'), - }, - { - value: 'snapshots', - label: localize('config.common.events_media_types.snapshots'), - }, - ]), - }, + createGrid([ + { + name: 'type', + selector: createSelectSelector([ + { value: 'auto', label: localize('config.common.media_types.auto') }, + { + value: 'reviews', + label: localize('config.common.media_types.reviews'), + }, + { value: 'events', label: localize('config.common.media_types.events') }, + { + value: 'recordings', + label: localize('config.common.media_types.recordings'), + }, + { value: 'folder', label: localize('config.common.media_types.folder') }, + ]), + }, + { + name: 'events_type', + selector: createSelectSelector([ + { value: 'all', label: localize('config.common.events_media_types.all') }, + { + value: 'clips', + label: localize('config.common.events_media_types.clips'), + }, + { + value: 'snapshots', + label: localize('config.common.events_media_types.snapshots'), + }, + ]), + }, + ]), { name: 'reviewed', selector: createSelectSelector([ diff --git a/src/components-lib/editor/schema/common/controls/next-previous.ts b/src/components-lib/editor/schema/common/controls/next-previous.ts index 974444d8..6eb15222 100644 --- a/src/components-lib/editor/schema/common/controls/next-previous.ts +++ b/src/components-lib/editor/schema/common/controls/next-previous.ts @@ -4,6 +4,7 @@ import type { HASelectSelectorOption, } from '../../../../../ha/types'; import { localize } from '../../../../../localize/localize'; +import { createGrid } from '../grid'; import { createNumberSelector, createSelectSelector } from '../selectors'; /** @@ -59,16 +60,18 @@ export const getNextPreviousSchema = (options?: { title: localize('config.common.controls.next_previous.editor_label'), icon: 'mdi:arrow-right-bold-circle', schema: [ - { - name: 'style', - label: localize('config.common.controls.next_previous.style'), - selector: createSelectSelector(styleOptions), - }, - { - name: 'size', - label: localize('config.common.controls.next_previous.size'), - selector: createNumberSelector({ min: BUTTON_SIZE_MIN }), - }, + createGrid([ + { + name: 'style', + label: localize('config.common.controls.next_previous.style'), + selector: createSelectSelector(styleOptions), + }, + { + name: 'size', + label: localize('config.common.controls.next_previous.size'), + selector: createNumberSelector({ min: BUTTON_SIZE_MIN }), + }, + ]), { name: 'auto_hide', label: localize('config.common.controls.next_previous.auto_hide'), diff --git a/src/components-lib/editor/schema/common/controls/ptz.ts b/src/components-lib/editor/schema/common/controls/ptz.ts index b9ba38a0..09412807 100644 --- a/src/components-lib/editor/schema/common/controls/ptz.ts +++ b/src/components-lib/editor/schema/common/controls/ptz.ts @@ -3,6 +3,7 @@ import type { HASelectSelectorOption, } from '../../../../../ha/types'; import { localize } from '../../../../../localize/localize'; +import { createGrid } from '../grid'; import { createSelectSelector } from '../selectors'; // 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'), icon: 'mdi:pan', schema: [ - { - name: 'mode', - label: localize('config.common.controls.ptz.mode'), - selector: createSelectSelector(modeOptions), - }, - { - name: 'type', - label: localize('config.common.controls.ptz.type'), - selector: createSelectSelector([ - { - value: 'buttons', - label: localize('config.common.controls.ptz.types.buttons'), - }, - { - value: 'gestures', - label: localize('config.common.controls.ptz.types.gestures'), - }, - ]), - }, - { - name: 'position', - label: localize('config.common.controls.ptz.position'), - selector: createSelectSelector([ - { - 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: 'bottom-left', - label: localize('config.common.controls.ptz.positions.bottom-left'), - }, - { - value: 'bottom-right', - label: localize('config.common.controls.ptz.positions.bottom-right'), - }, - ]), - }, - { - name: 'orientation', - label: localize('config.common.controls.ptz.orientation'), - selector: createSelectSelector([ - { - value: 'vertical', - label: localize('config.common.controls.ptz.orientations.vertical'), - }, - { - value: 'horizontal', - label: localize('config.common.controls.ptz.orientations.horizontal'), - }, - ]), - }, - { - name: 'hide_pan_tilt', - label: localize('config.common.controls.ptz.hide_pan_tilt'), - selector: { boolean: {} }, - }, - { - name: 'hide_zoom', - label: localize('config.common.controls.ptz.hide_zoom'), - selector: { boolean: {} }, - }, - { - name: 'hide_home', - label: localize('config.common.controls.ptz.hide_home'), - selector: { boolean: {} }, - }, - { - name: 'hide_type', - label: localize('config.common.controls.ptz.hide_type'), - selector: { boolean: {} }, - }, + createGrid([ + { + name: 'mode', + label: localize('config.common.controls.ptz.mode'), + selector: createSelectSelector(modeOptions), + }, + { + name: 'type', + label: localize('config.common.controls.ptz.type'), + selector: createSelectSelector([ + { + value: 'buttons', + label: localize('config.common.controls.ptz.types.buttons'), + }, + { + value: 'gestures', + label: localize('config.common.controls.ptz.types.gestures'), + }, + ]), + }, + { + name: 'position', + label: localize('config.common.controls.ptz.position'), + selector: createSelectSelector([ + { + 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: 'bottom-left', + label: localize('config.common.controls.ptz.positions.bottom-left'), + }, + { + value: 'bottom-right', + label: localize('config.common.controls.ptz.positions.bottom-right'), + }, + ]), + }, + { + name: 'orientation', + label: localize('config.common.controls.ptz.orientation'), + selector: createSelectSelector([ + { + value: 'vertical', + label: localize('config.common.controls.ptz.orientations.vertical'), + }, + { + value: 'horizontal', + label: localize('config.common.controls.ptz.orientations.horizontal'), + }, + ]), + }, + ]), + createGrid([ + { + name: 'hide_pan_tilt', + label: localize('config.common.controls.ptz.hide_pan_tilt'), + selector: { boolean: {} }, + }, + { + name: 'hide_zoom', + label: localize('config.common.controls.ptz.hide_zoom'), + selector: { boolean: {} }, + }, + { + name: 'hide_home', + label: localize('config.common.controls.ptz.hide_home'), + selector: { boolean: {} }, + }, + { + name: 'hide_type', + label: localize('config.common.controls.ptz.hide_type'), + selector: { boolean: {} }, + }, + ]), ], }; }; diff --git a/src/components-lib/editor/schema/common/controls/thumbnails.ts b/src/components-lib/editor/schema/common/controls/thumbnails.ts index 3a7aa10f..c08a9ddc 100644 --- a/src/components-lib/editor/schema/common/controls/thumbnails.ts +++ b/src/components-lib/editor/schema/common/controls/thumbnails.ts @@ -4,6 +4,7 @@ import { } from '../../../../../config/schema/common/controls/thumbnails'; import type { HAFormExpandableSchema, HAFormSchema } from '../../../../../ha/types'; import { localize } from '../../../../../localize/localize'; +import { createGrid } from '../grid'; import { createNumberSelector, createSelectSelector } from '../selectors'; /** @@ -14,74 +15,86 @@ import { createNumberSelector, createSelectSelector } from '../selectors'; export const getThumbnailsSchema = (options?: { includeMode?: boolean; }): 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[] = []; if (options?.includeMode) { - schema.push({ - name: 'mode', - label: localize('config.common.controls.thumbnails.mode'), - selector: createSelectSelector([ + // Where the thumbnails go and how big they are: both describe the space + // they take up. + schema.push( + createGrid([ { - value: 'none', - label: localize('config.common.controls.thumbnails.modes.none'), - }, - { - value: 'above', - label: localize('config.common.controls.thumbnails.modes.above'), - }, - { - value: 'below', - label: localize('config.common.controls.thumbnails.modes.below'), - }, - { - value: 'left', - label: localize('config.common.controls.thumbnails.modes.left'), - }, - { - value: 'right', - label: localize('config.common.controls.thumbnails.modes.right'), + name: 'mode', + label: localize('config.common.controls.thumbnails.mode'), + selector: createSelectSelector([ + { + value: 'none', + label: localize('config.common.controls.thumbnails.modes.none'), + }, + { + value: 'above', + label: localize('config.common.controls.thumbnails.modes.above'), + }, + { + value: 'below', + label: localize('config.common.controls.thumbnails.modes.below'), + }, + { + value: 'left', + 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( - { - name: 'size', - label: localize('config.common.controls.thumbnails.size'), - selector: createNumberSelector({ - min: THUMBNAIL_WIDTH_MIN, - max: THUMBNAIL_WIDTH_MAX, - }), - }, - { - name: 'show_details', - label: localize('config.common.controls.thumbnails.show_details'), - selector: { boolean: {} }, - }, - { - name: 'show_favorite_control', - label: localize('config.common.controls.thumbnails.show_favorite_control'), - selector: { boolean: {} }, - }, - { - name: 'show_timeline_control', - label: localize('config.common.controls.thumbnails.show_timeline_control'), - selector: { boolean: {} }, - }, - { - name: 'show_download_control', - label: localize('config.common.controls.thumbnails.show_download_control'), - selector: { boolean: {} }, - }, - { - name: 'show_review_control', - 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: {} }, - }, + createGrid([ + { + name: 'show_details', + label: localize('config.common.controls.thumbnails.show_details'), + selector: { boolean: {} }, + }, + { + name: 'show_favorite_control', + label: localize('config.common.controls.thumbnails.show_favorite_control'), + selector: { boolean: {} }, + }, + { + name: 'show_timeline_control', + label: localize('config.common.controls.thumbnails.show_timeline_control'), + selector: { boolean: {} }, + }, + { + name: 'show_download_control', + label: localize('config.common.controls.thumbnails.show_download_control'), + selector: { boolean: {} }, + }, + { + name: 'show_review_control', + 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 { name: 'thumbnails', diff --git a/src/components-lib/editor/schema/common/display.ts b/src/components-lib/editor/schema/common/display.ts index 4cb80402..f0f12155 100644 --- a/src/components-lib/editor/schema/common/display.ts +++ b/src/components-lib/editor/schema/common/display.ts @@ -1,5 +1,6 @@ import type { HAFormExpandableSchema } from '../../../../ha/types'; import { localize } from '../../../../localize/localize'; +import { createGrid } from './grid'; import { createNumberSelector, createSelectSelector } from './selectors'; /** @@ -43,15 +44,17 @@ export const getDisplaySchema = (): HAFormExpandableSchema => ({ label: localize('config.common.display.grid_selected_width_factor'), selector: createNumberSelector({ min: 0 }), }, - { - name: 'grid_columns', - label: localize('config.common.display.grid_columns'), - selector: createNumberSelector({ min: 0 }), - }, - { - name: 'grid_max_columns', - label: localize('config.common.display.grid_max_columns'), - selector: createNumberSelector({ min: 0 }), - }, + createGrid([ + { + name: 'grid_columns', + label: localize('config.common.display.grid_columns'), + selector: createNumberSelector({ min: 0 }), + }, + { + name: 'grid_max_columns', + label: localize('config.common.display.grid_max_columns'), + selector: createNumberSelector({ min: 0 }), + }, + ]), ], }); diff --git a/src/components-lib/editor/schema/common/grid.ts b/src/components-lib/editor/schema/common/grid.ts new file mode 100644 index 00000000..eeb4ce49 --- /dev/null +++ b/src/components-lib/editor/schema/common/grid.ts @@ -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), +}); diff --git a/src/components-lib/editor/schema/common/proxy.ts b/src/components-lib/editor/schema/common/proxy.ts index c0602f54..49e95b9a 100644 --- a/src/components-lib/editor/schema/common/proxy.ts +++ b/src/components-lib/editor/schema/common/proxy.ts @@ -4,6 +4,7 @@ import type { HASelectSelectorOption, } from '../../../../ha/types'; import { localize } from '../../../../localize/localize'; +import { createGrid } from './grid'; import { createSelectSelector } from './selectors'; /** @@ -31,67 +32,78 @@ export const getProxySchema = (options: { 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) { - schema.push({ + proxiedFields.push({ name: 'live', label: localize('config.cameras.proxy.live'), selector: createSelectSelector(proxyModeOptions), }); } if (options.includeMedia) { - schema.push({ + proxiedFields.push({ name: 'media', label: localize('config.cameras.proxy.media'), selector: createSelectSelector(proxyModeOptions), }); } + if (proxiedFields.length > 1) { + schema.push(createGrid(proxiedFields)); + } else { + schema.push(...proxiedFields); + } + schema.push( { name: 'dynamic', label: localize('config.common.proxy.dynamic'), selector: { boolean: {} }, }, - { - name: 'ssl_verification', - label: localize('config.common.proxy.ssl_verification.editor_label'), - selector: createSelectSelector([ - { - value: 'auto', - label: localize('config.common.proxy.ssl_verification.auto'), - }, - { - value: true, - label: localize('config.common.proxy.ssl_verification.true'), - }, - { - value: false, - label: localize('config.common.proxy.ssl_verification.false'), - }, - ]), - }, - { - name: 'ssl_ciphers', - label: localize('config.common.proxy.ssl_ciphers.editor_label'), - selector: createSelectSelector([ - { value: 'auto', label: localize('config.common.proxy.ssl_ciphers.auto') }, - { - value: 'default', - label: localize('config.common.proxy.ssl_ciphers.default'), - }, - { - value: 'insecure', - label: localize('config.common.proxy.ssl_ciphers.insecure'), - }, - { - value: 'intermediate', - label: localize('config.common.proxy.ssl_ciphers.intermediate'), - }, - { - value: 'modern', - label: localize('config.common.proxy.ssl_ciphers.modern'), - }, - ]), - }, + createGrid([ + { + name: 'ssl_verification', + label: localize('config.common.proxy.ssl_verification.editor_label'), + selector: createSelectSelector([ + { + value: 'auto', + label: localize('config.common.proxy.ssl_verification.auto'), + }, + { + value: true, + label: localize('config.common.proxy.ssl_verification.true'), + }, + { + value: false, + label: localize('config.common.proxy.ssl_verification.false'), + }, + ]), + }, + { + name: 'ssl_ciphers', + label: localize('config.common.proxy.ssl_ciphers.editor_label'), + selector: createSelectSelector([ + { value: 'auto', label: localize('config.common.proxy.ssl_ciphers.auto') }, + { + value: 'default', + label: localize('config.common.proxy.ssl_ciphers.default'), + }, + { + value: 'insecure', + label: localize('config.common.proxy.ssl_ciphers.insecure'), + }, + { + value: 'intermediate', + label: localize('config.common.proxy.ssl_ciphers.intermediate'), + }, + { + value: 'modern', + label: localize('config.common.proxy.ssl_ciphers.modern'), + }, + ]), + }, + ]), ); return { diff --git a/src/components-lib/editor/schema/dimensions.ts b/src/components-lib/editor/schema/dimensions.ts index 88e2e081..69e432b7 100644 --- a/src/components-lib/editor/schema/dimensions.ts +++ b/src/components-lib/editor/schema/dimensions.ts @@ -1,6 +1,7 @@ import type { HASelectSelectorOption } from '../../../ha/types'; import { localize } from '../../../localize/localize'; import type { EditorForm } from '../types'; +import { createGrid } from './common/grid'; import { createSelectSelector } from './common/selectors'; /** @@ -30,14 +31,16 @@ export const getDimensionsSectionForms = (): EditorForm[] => [ { basePath: ['dimensions'], schema: [ - { - name: 'aspect_ratio_mode', - selector: createSelectSelector(getAspectRatioModeOptions()), - }, - { - name: 'aspect_ratio', - selector: { text: {} }, - }, + createGrid([ + { + name: 'aspect_ratio_mode', + selector: createSelectSelector(getAspectRatioModeOptions()), + }, + { + name: 'aspect_ratio', + selector: { text: {} }, + }, + ]), { name: 'height', selector: { text: {} }, diff --git a/src/components-lib/editor/schema/folders.ts b/src/components-lib/editor/schema/folders.ts index ae9ba3ed..51a7f4be 100644 --- a/src/components-lib/editor/schema/folders.ts +++ b/src/components-lib/editor/schema/folders.ts @@ -1,5 +1,6 @@ import type { HAFormSchema } from '../../../ha/types'; import { localize } from '../../../localize/localize'; +import { createGrid } from './common/grid'; import { createSelectSelector } from './common/selectors'; /** @@ -8,24 +9,28 @@ import { createSelectSelector } from './common/selectors'; * @returns The form schema for one folder. */ export const getFolderSchema = (): HAFormSchema[] => [ - { - name: 'type', - selector: createSelectSelector([ - { value: 'ha', label: localize('config.folders.types.ha') }, - ]), - }, - { - name: 'title', - selector: { text: {} }, - }, - { - name: 'icon', - selector: { icon: {} }, - }, - { - name: 'id', - selector: { text: {} }, - }, + createGrid([ + { + name: 'type', + selector: createSelectSelector([ + { value: 'ha', label: localize('config.folders.types.ha') }, + ]), + }, + { + name: 'id', + selector: { text: {} }, + }, + ]), + createGrid([ + { + name: 'title', + selector: { text: {} }, + }, + { + name: 'icon', + selector: { icon: {} }, + }, + ]), { name: 'ha', type: 'expandable', diff --git a/src/components-lib/editor/schema/live.ts b/src/components-lib/editor/schema/live.ts index 28a2c284..ec482167 100644 --- a/src/components-lib/editor/schema/live.ts +++ b/src/components-lib/editor/schema/live.ts @@ -7,6 +7,7 @@ import { getPTZSchema } from './common/controls/ptz'; import { getThumbnailsSchema } from './common/controls/thumbnails'; import { getMiniTimelineSchema } from './common/controls/timeline'; import { getDisplaySchema } from './common/display'; +import { createGrid } from './common/grid'; import { getLiveAutoMuteOptions, getLiveAutoUnmuteOptions, @@ -31,36 +32,38 @@ const getCallSchema = (): HAFormExpandableSchema => ({ title: localize('config.live.controls.call.ringtone.editor_label'), icon: 'mdi:music-note', schema: [ - { - name: 'type', - selector: createSelectSelector([ - { - value: 'none', - label: localize('config.live.controls.call.ringtone.types.none'), - }, - { - value: 'chime', - label: localize('config.live.controls.call.ringtone.types.chime'), - }, - { - value: 'westminster', - label: localize('config.live.controls.call.ringtone.types.westminster'), - }, - { - value: 'arpeggio', - label: localize('config.live.controls.call.ringtone.types.arpeggio'), - }, - { - value: 'melody', - label: localize('config.live.controls.call.ringtone.types.melody'), - }, - { - value: 'custom', - label: localize('config.live.controls.call.ringtone.types.custom'), - }, - ]), - }, - { name: 'url', selector: { text: {} } }, + createGrid([ + { + name: 'type', + selector: createSelectSelector([ + { + value: 'none', + label: localize('config.live.controls.call.ringtone.types.none'), + }, + { + value: 'chime', + label: localize('config.live.controls.call.ringtone.types.chime'), + }, + { + value: 'westminster', + label: localize('config.live.controls.call.ringtone.types.westminster'), + }, + { + value: 'arpeggio', + label: localize('config.live.controls.call.ringtone.types.arpeggio'), + }, + { + value: 'melody', + label: localize('config.live.controls.call.ringtone.types.melody'), + }, + { + value: 'custom', + label: localize('config.live.controls.call.ringtone.types.custom'), + }, + ]), + }, + { name: 'url', selector: { text: {} } }, + ]), { name: 'repeat', selector: createNumberSelector({ min: 0 }) }, ], }, @@ -125,8 +128,10 @@ export const getLiveSectionForms = (): EditorForm[] => [ basePath: ['live'], schema: [ { name: 'preload', selector: { boolean: {} } }, - { name: 'draggable', selector: { boolean: {} } }, - { name: 'zoomable', selector: { boolean: {} } }, + createGrid([ + { name: 'draggable', selector: { boolean: {} } }, + { name: 'zoomable', selector: { boolean: {} } }, + ]), { name: 'lazy_load', selector: { boolean: {} } }, { name: 'lazy_unload', diff --git a/src/components-lib/editor/schema/media-viewer.ts b/src/components-lib/editor/schema/media-viewer.ts index d63ff40b..a3229d75 100644 --- a/src/components-lib/editor/schema/media-viewer.ts +++ b/src/components-lib/editor/schema/media-viewer.ts @@ -6,6 +6,7 @@ import { getPTZSchema } from './common/controls/ptz'; import { getThumbnailsSchema } from './common/controls/thumbnails'; import { getMiniTimelineSchema } from './common/controls/timeline'; import { getDisplaySchema } from './common/display'; +import { createGrid } from './common/grid'; import { getMediaActionNegativeOptions, getMediaActionPositiveOptions, @@ -68,8 +69,10 @@ export const getMediaViewerSectionForms = (): EditorForm[] => [ multiple: true, }), }, - { name: 'draggable', selector: { boolean: {} } }, - { name: 'zoomable', selector: { boolean: {} } }, + createGrid([ + { name: 'draggable', selector: { boolean: {} } }, + { name: 'zoomable', selector: { boolean: {} } }, + ]), { name: 'lazy_load', selector: { boolean: {} } }, getTransitionEffectSchema(), { name: 'snapshot_click_plays_clip', selector: { boolean: {} } }, diff --git a/src/components-lib/editor/schema/menu.ts b/src/components-lib/editor/schema/menu.ts index ab310e2b..80cc95e9 100644 --- a/src/components-lib/editor/schema/menu.ts +++ b/src/components-lib/editor/schema/menu.ts @@ -6,6 +6,7 @@ import type { } from '../../../ha/types'; import { localize } from '../../../localize/localize'; import type { EditorForm } from '../types'; +import { createGrid } from './common/grid'; import { createNumberSelector, createSelectSelector } from './common/selectors'; // The menu buttons in their display order. @@ -96,50 +97,56 @@ const getMenuButtonSchema = ( title: localize(`config.menu.buttons.${button}`), icon: 'mdi:gesture-tap-button', schema: [ - { - name: 'enabled', - label: localize('config.menu.buttons.enabled'), - selector: { boolean: {} }, - }, - { - name: 'alignment', - label: localize('config.menu.buttons.alignment'), - selector: createSelectSelector([ - { - value: 'matching', - label: localize('config.menu.buttons.alignments.matching'), - }, - { - value: 'opposing', - label: localize('config.menu.buttons.alignments.opposing'), - }, - ]), - }, - { - name: 'permanent', - label: localize('config.menu.buttons.permanent'), - selector: { boolean: {} }, - }, - { - name: 'priority', - label: localize('config.menu.buttons.priority'), - selector: createNumberSelector({ max: MENU_PRIORITY_MAX }), - }, + createGrid([ + { + name: 'enabled', + label: localize('config.menu.buttons.enabled'), + selector: { boolean: {} }, + }, + { + name: 'permanent', + label: localize('config.menu.buttons.permanent'), + selector: { boolean: {} }, + }, + ]), + createGrid([ + { + name: 'alignment', + label: localize('config.menu.buttons.alignment'), + selector: createSelectSelector([ + { + value: 'matching', + label: localize('config.menu.buttons.alignments.matching'), + }, + { + value: 'opposing', + label: localize('config.menu.buttons.alignments.opposing'), + }, + ]), + }, + { + name: 'priority', + label: localize('config.menu.buttons.priority'), + selector: createNumberSelector({ max: MENU_PRIORITY_MAX }), + }, + ]), { name: 'icon', label: localize('config.menu.buttons.icon'), selector: { icon: {} }, }, - { - name: 'state_color', - label: localize('config.menu.buttons.state_color'), - selector: { boolean: {} }, - }, - { - name: 'inert', - label: localize('config.menu.buttons.inert'), - selector: { boolean: {} }, - }, + createGrid([ + { + name: 'state_color', + label: localize('config.menu.buttons.state_color'), + selector: { boolean: {} }, + }, + { + name: 'inert', + label: localize('config.menu.buttons.inert'), + selector: { boolean: {} }, + }, + ]), ...extraSchema, ], }); @@ -152,23 +159,25 @@ export const getMenuSectionForms = (): EditorForm[] => [ { basePath: ['menu'], schema: [ - { - name: 'style', - selector: createSelectSelector(getMenuStyleOptions()), - }, - { - name: 'position', - selector: createSelectSelector(getMenuPositionOptions()), - }, - { - name: 'alignment', - selector: createSelectSelector([ - { value: 'left', label: localize('config.menu.alignments.left') }, - { value: 'right', label: localize('config.menu.alignments.right') }, - { value: 'top', label: localize('config.menu.alignments.top') }, - { value: 'bottom', label: localize('config.menu.alignments.bottom') }, - ]), - }, + createGrid([ + { + name: 'style', + selector: createSelectSelector(getMenuStyleOptions()), + }, + { + name: 'position', + selector: createSelectSelector(getMenuPositionOptions()), + }, + { + name: 'alignment', + selector: createSelectSelector([ + { value: 'left', label: localize('config.menu.alignments.left') }, + { value: 'right', label: localize('config.menu.alignments.right') }, + { value: 'top', label: localize('config.menu.alignments.top') }, + { value: 'bottom', label: localize('config.menu.alignments.bottom') }, + ]), + }, + ]), { name: 'auto_hide', selector: createSelectSelector( diff --git a/src/components-lib/editor/schema/performance.ts b/src/components-lib/editor/schema/performance.ts index f3bcd16b..b2046d37 100644 --- a/src/components-lib/editor/schema/performance.ts +++ b/src/components-lib/editor/schema/performance.ts @@ -1,6 +1,7 @@ import { MEDIA_CHUNK_SIZE_MAX } from '../../../config/schema/performance'; import { localize } from '../../../localize/localize'; import type { EditorForm } from '../types'; +import { createGrid } from './common/grid'; import { createNumberSelector } from './common/selectors'; /** @@ -18,17 +19,21 @@ export const getPerformanceSectionForms = (): EditorForm[] => [ title: localize('config.performance.features.editor_label'), icon: 'mdi:feature-search', schema: [ - { name: 'card_loading_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 }), - }, - { - name: 'max_simultaneous_engine_requests', - selector: createNumberSelector({ min: 1 }), - }, + createGrid([ + { name: 'card_loading_indicator', selector: { boolean: {} } }, + { name: 'card_loading_effects', selector: { boolean: {} } }, + { name: 'animated_progress_indicator', selector: { boolean: {} } }, + ]), + createGrid([ + { + name: 'media_chunk_size', + selector: createNumberSelector({ max: MEDIA_CHUNK_SIZE_MAX }), + }, + { + name: 'max_simultaneous_engine_requests', + selector: createNumberSelector({ min: 1 }), + }, + ]), ], }, { @@ -37,8 +42,10 @@ export const getPerformanceSectionForms = (): EditorForm[] => [ title: localize('config.performance.style.editor_label'), icon: 'mdi:palette-swatch-variant', schema: [ - { name: 'border_radius', selector: { boolean: {} } }, - { name: 'box_shadow', selector: { boolean: {} } }, + createGrid([ + { name: 'border_radius', selector: { boolean: {} } }, + { name: 'box_shadow', selector: { boolean: {} } }, + ]), ], }, ], diff --git a/src/components-lib/editor/schema/simple.ts b/src/components-lib/editor/schema/simple.ts index fd705931..86873769 100644 --- a/src/components-lib/editor/schema/simple.ts +++ b/src/components-lib/editor/schema/simple.ts @@ -4,6 +4,7 @@ import type { RawAdvancedCameraCardConfig } from '../../../config/types'; import { localize } from '../../../localize/localize'; import type { ConfigChange, ConfigPath, EditorForm, FieldBinding } from '../types'; import { getCameraSimpleFields } from './cameras'; +import { createGrid } from './common/grid'; import { createSelectSelector } from './common/selectors'; import { getAspectRatioModeOptions } from './dimensions'; import { @@ -74,14 +75,16 @@ export const getSimpleMenuForms = (): EditorForm[] => [ { basePath: [], schema: [ - { - name: 'menu_style', - selector: createSelectSelector(getMenuStyleOptions()), - }, - { - name: 'menu_position', - selector: createSelectSelector(getMenuPositionOptions()), - }, + createGrid([ + { + name: 'menu_style', + selector: createSelectSelector(getMenuStyleOptions()), + }, + { + name: 'menu_position', + selector: createSelectSelector(getMenuPositionOptions()), + }, + ]), { name: 'menu_buttons', label: localize('config.menu.buttons.editor_label'), @@ -110,22 +113,16 @@ export const getSimpleTopLevelForms = (): EditorForm[] => [ name: 'default_view', selector: createSelectSelector(getViewModeOptions()), }, - - // The ratio only means anything alongside the mode that uses it, so the - // two are shown together. - { - type: 'grid', - schema: [ - { - name: 'aspect_ratio_mode', - selector: createSelectSelector(getAspectRatioModeOptions()), - }, - { - name: 'aspect_ratio', - selector: { text: {} }, - }, - ], - }, + createGrid([ + { + name: 'aspect_ratio_mode', + selector: createSelectSelector(getAspectRatioModeOptions()), + }, + { + name: 'aspect_ratio', + selector: { text: {} }, + }, + ]), getProfilesField(), ], bindings: [ diff --git a/src/components-lib/editor/schema/status-bar.ts b/src/components-lib/editor/schema/status-bar.ts index d25205ec..37fb3181 100644 --- a/src/components-lib/editor/schema/status-bar.ts +++ b/src/components-lib/editor/schema/status-bar.ts @@ -3,6 +3,7 @@ import { STATUS_BAR_HEIGHT_MIN } from '../../../config/schema/status-bar'; import type { HAFormExpandableSchema } from '../../../ha/types'; import { localize } from '../../../localize/localize'; import type { EditorForm } from '../types'; +import { createGrid } from './common/grid'; import { createNumberSelector, createSelectSelector } from './common/selectors'; const STATUS_BAR_ITEMS = [ @@ -20,21 +21,23 @@ const getStatusBarItemSchema = (item: string): HAFormExpandableSchema => ({ title: localize(`config.status_bar.items.${item}`), icon: 'mdi:feature-search', schema: [ - { - name: 'enabled', - label: localize('config.status_bar.items.enabled'), - selector: { boolean: {} }, - }, - { - name: 'permanent', - label: localize('config.status_bar.items.permanent'), - selector: { boolean: {} }, - }, - { - name: 'priority', - label: localize('config.status_bar.items.priority'), - selector: createNumberSelector({ max: STATUS_BAR_PRIORITY_MAX }), - }, + createGrid([ + { + name: 'enabled', + label: localize('config.status_bar.items.enabled'), + selector: { boolean: {} }, + }, + { + name: 'permanent', + label: localize('config.status_bar.items.permanent'), + selector: { boolean: {} }, + }, + { + name: 'priority', + 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'], schema: [ - { - name: 'style', - selector: createSelectSelector([ - { value: 'hover', label: localize('config.status_bar.styles.hover') }, - { - 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: 'overlay', label: localize('config.status_bar.styles.overlay') }, - { value: 'popup', label: localize('config.status_bar.styles.popup') }, - ]), - }, - { - name: 'position', - selector: createSelectSelector([ - { value: 'top', label: localize('config.status_bar.positions.top') }, - { value: 'bottom', label: localize('config.status_bar.positions.bottom') }, - ]), - }, + createGrid([ + { + name: 'style', + selector: createSelectSelector([ + { value: 'hover', label: localize('config.status_bar.styles.hover') }, + { + 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: 'overlay', label: localize('config.status_bar.styles.overlay') }, + { value: 'popup', label: localize('config.status_bar.styles.popup') }, + ]), + }, + { + name: 'position', + selector: createSelectSelector([ + { value: 'top', label: localize('config.status_bar.positions.top') }, + { value: 'bottom', label: localize('config.status_bar.positions.bottom') }, + ]), + }, + ]), { name: 'auto_hide', selector: createSelectSelector( @@ -83,14 +88,16 @@ export const getStatusBarSectionForms = (): EditorForm[] => [ { multiple: true }, ), }, - { - name: 'height', - selector: createNumberSelector({ min: STATUS_BAR_HEIGHT_MIN }), - }, - { - name: 'popup_seconds', - selector: createNumberSelector({ min: 0, max: 60 }), - }, + createGrid([ + { + name: 'height', + selector: createNumberSelector({ min: STATUS_BAR_HEIGHT_MIN }), + }, + { + name: 'popup_seconds', + selector: createNumberSelector({ min: 0, max: 60 }), + }, + ]), ], }, { diff --git a/src/components-lib/editor/schema/view.ts b/src/components-lib/editor/schema/view.ts index 246f460b..28327047 100644 --- a/src/components-lib/editor/schema/view.ts +++ b/src/components-lib/editor/schema/view.ts @@ -1,6 +1,7 @@ import type { HASelectSelectorOption } from '../../../ha/types'; import { localize } from '../../../localize/localize'; import type { EditorForm } from '../types'; +import { createGrid } from './common/grid'; import { getInteractionModeOptions } from './common/interaction-mode'; import { createNumberSelector, createSelectSelector } from './common/selectors'; @@ -38,17 +39,19 @@ export const getViewSectionForms = (): EditorForm[] => [ { basePath: ['view'], schema: [ - { - name: 'default', - selector: createSelectSelector(getViewModeOptions()), - }, - { - name: 'camera_select', - selector: createSelectSelector([ - ...getViewModeOptions(), - { value: 'current', label: localize('config.view.views.current') }, - ]), - }, + createGrid([ + { + name: 'default', + selector: createSelectSelector(getViewModeOptions()), + }, + { + name: 'camera_select', + selector: createSelectSelector([ + ...getViewModeOptions(), + { value: 'current', label: localize('config.view.views.current') }, + ]), + }, + ]), { name: 'dim', selector: { boolean: {} }, @@ -135,54 +138,56 @@ export const getViewSectionForms = (): EditorForm[] => [ title: localize('config.view.triggers.actions.editor_label'), icon: 'mdi:cogs', schema: [ - { - name: 'trigger', - label: localize('config.view.triggers.actions.trigger'), - selector: createSelectSelector([ - { - value: 'call', - label: localize('config.view.triggers.actions.triggers.call'), - }, - { - value: 'default', - label: localize('config.view.triggers.actions.triggers.default'), - }, - { - value: 'live', - label: localize('config.view.triggers.actions.triggers.live'), - }, - { - value: 'media', - label: localize('config.view.triggers.actions.triggers.media'), - }, - { - value: 'none', - label: localize('config.view.triggers.actions.triggers.none'), - }, - { - value: 'update', - label: localize('config.view.triggers.actions.triggers.update'), - }, - ]), - }, - { - name: 'untrigger', - label: localize('config.view.triggers.actions.untrigger'), - selector: createSelectSelector([ - { - value: 'call', - label: localize('config.view.triggers.actions.untriggers.call'), - }, - { - value: 'default', - label: localize('config.view.triggers.actions.untriggers.default'), - }, - { - value: 'none', - label: localize('config.view.triggers.actions.untriggers.none'), - }, - ]), - }, + createGrid([ + { + name: 'trigger', + label: localize('config.view.triggers.actions.trigger'), + selector: createSelectSelector([ + { + value: 'call', + label: localize('config.view.triggers.actions.triggers.call'), + }, + { + value: 'default', + label: localize('config.view.triggers.actions.triggers.default'), + }, + { + value: 'live', + label: localize('config.view.triggers.actions.triggers.live'), + }, + { + value: 'media', + label: localize('config.view.triggers.actions.triggers.media'), + }, + { + value: 'none', + label: localize('config.view.triggers.actions.triggers.none'), + }, + { + value: 'update', + label: localize('config.view.triggers.actions.triggers.update'), + }, + ]), + }, + { + name: 'untrigger', + label: localize('config.view.triggers.actions.untrigger'), + selector: createSelectSelector([ + { + value: 'call', + label: localize('config.view.triggers.actions.untriggers.call'), + }, + { + value: 'default', + label: localize('config.view.triggers.actions.untriggers.default'), + }, + { + value: 'none', + label: localize('config.view.triggers.actions.untriggers.none'), + }, + ]), + }, + ]), { name: 'interaction_mode', label: localize('config.view.triggers.actions.interaction_mode'), diff --git a/src/ha/types.ts b/src/ha/types.ts index 940dfa47..b870b33f 100644 --- a/src/ha/types.ts +++ b/src/ha/types.ts @@ -373,7 +373,7 @@ export interface HAFormExpandableSchema // 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 // would be without it. -interface HAFormGridSchema +export interface HAFormGridSchema extends Omit, CardHAFormSchemaExtensions { name?: string; @@ -394,3 +394,13 @@ export type HAFormSchema = export const isFormFieldSchema = ( schema: HAFormSchema, ): 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; diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 0516e8b2..5595c096 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -80,29 +80,29 @@ }, "pan": { "editor_label": "Layout Pan", - "x": "Pan horizontal percentage", - "y": "Pan vertical percentage" + "x": "Horizontal %", + "y": "Vertical %" }, "position": { "editor_label": "Layout Cover Position", - "x": "Horizontal placement percentage", - "y": "Vertical placement percentage" + "x": "Horizontal %", + "y": "Vertical %" }, "view_box": { - "bottom": "Bottom inset percentage", + "bottom": "Bottom inset %", "editor_label": "Layout View Box", - "left": "Left inset percentage", - "right": "Right inset percentage", - "top": "Top inset percentage" + "left": "Left inset %", + "right": "Right inset %", + "top": "Top inset %" }, "zoom": "Zoom factor" }, "rotation": "Rotation", "rotations": { - "0": "No rotation", - "180": "180 degrees clockwise", - "270": "270 degrees clockwise", - "90": "90 degrees clockwise" + "0": "None", + "90": "90° clockwise", + "180": "180° clockwise", + "270": "270° clockwise" } }, "engine": "Camera engine", @@ -133,10 +133,10 @@ "mse": "Media Source Extensions (MSE)", "webrtc": "Web Real-Time Communication (WebRTC)" }, - "stream": "go2rtc stream name", - "url": "go2rtc URL" + "stream": "Stream name", + "url": "URL" }, - "icon": "Icon for this camera (autodetected from entity)", + "icon": "Icon (autodetected)", "id": "Unique ID for this camera in this card", "image": { "editor_label": "Image", @@ -158,7 +158,7 @@ }, "media": { "editor_label": "Media", - "events_type": "Events media subtype", + "events_type": "Events subtype", "folders": "Folder IDs", "reviewed": "Review status filter", "revieweds": { @@ -166,26 +166,26 @@ "reviewed": "Only reviewed", "unreviewed": "Only unreviewed" }, - "type": "Default media type" + "type": "Default type" }, "motioneye": { "editor_label": "MotionEye", "images": { - "directory_pattern": "Images directory pattern", + "directory_pattern": "Directory pattern", "editor_label": "Images", - "file_pattern": "Images file pattern" + "file_pattern": "File pattern" }, "movies": { - "directory_pattern": "Movies directory pattern", + "directory_pattern": "Directory pattern", "editor_label": "Movies", - "file_pattern": "Movies file pattern" + "file_pattern": "File pattern" }, "url": "MotionEye UI URL" }, "proxy": { "editor_label": "Proxying", - "live": "Live proxying", - "media": "Media proxying" + "live": "Live", + "media": "Media" }, "reolink": { "editor_label": "Reolink", @@ -196,7 +196,7 @@ }, "url": "Reolink UI URL" }, - "title": "Title for this camera (autodetected from entity)", + "title": "Title (autodetected)", "triggers": { "doorbell": "Trigger by auto-detecting doorbell event entities", "editor_label": "Triggers", @@ -243,10 +243,10 @@ } }, "next_previous": { - "auto_hide": "Automatically hide next & previous controls", + "auto_hide": "Automatically hide", "editor_label": "Next & Previous", - "size": "Next & previous control size in pixels", - "style": "Next & previous control style", + "size": "Size in pixels", + "style": "Style", "styles": { "chevrons": "Chevrons", "icons": "Icons", @@ -256,10 +256,10 @@ }, "ptz": { "editor_label": "PTZ", - "hide_home": "Hide home & preset controls", - "hide_pan_tilt": "Hide pan & tilt control", - "hide_type": "Hide type toggle button", - "hide_zoom": "Hide zoom control", + "hide_home": "Hide home & presets", + "hide_pan_tilt": "Hide pan & tilt", + "hide_type": "Hide type toggle", + "hide_zoom": "Hide zoom", "mode": "Mode", "modes": { "auto": "Automatic", @@ -286,21 +286,21 @@ }, "thumbnails": { "editor_label": "Thumbnails", - "mode": "Thumbnails mode", + "mode": "Mode", "modes": { - "above": "Thumbnails above", - "below": "Thumbnails below", - "left": "Thumbnails in a drawer to the left", - "none": "No thumbnails", - "right": "Thumbnails in a drawer to the right" + "above": "Above", + "below": "Below", + "left": "Left drawer", + "none": "None", + "right": "Right drawer" }, - "show_details": "Show details with thumbnails", - "show_download_control": "Show download control on thumbnails", - "show_favorite_control": "Show favorite control on thumbnails", - "show_info_control": "Show info control on thumbnails", - "show_review_control": "Show review control on thumbnails", - "show_timeline_control": "Show timeline control on thumbnails", - "size": "Thumbnails size in pixels" + "show_details": "Show details", + "show_download_control": "Show download control", + "show_favorite_control": "Show favorite control", + "show_info_control": "Show info control", + "show_review_control": "Show review control", + "show_timeline_control": "Show timeline control", + "size": "Size in pixels" }, "timeline": { "editor_label": "Mini Timeline", @@ -326,8 +326,8 @@ }, "display": { "editor_label": "Display", - "grid_columns": "Exact number of grid columns", - "grid_max_columns": "Maximum number of grid columns", + "grid_columns": "Exact grid columns", + "grid_max_columns": "Maximum grid columns", "grid_selected_position": "Position of selected media in grid", "grid_selected_positions": { "default": "Selected item in default position", @@ -383,25 +383,25 @@ "reviews": "Reviews" }, "proxy": { - "dynamic": "Dynamic proxying", + "dynamic": "Dynamic", "modes": { - "auto": "Proxying automatically configured", - "false": "Proxying disabled", - "true": "Proxying enabled" + "auto": "Automatic", + "false": "Disabled", + "true": "Enabled" }, "ssl_ciphers": { - "auto": "SSL ciphers automatically configured", - "default": "Default SSL ciphers", + "auto": "Automatic", + "default": "Default", "editor_label": "SSL ciphers", - "insecure": "Insecure SSL ciphers", - "intermediate": "Intermediate SSL ciphers", - "modern": "Modern SSL ciphers" + "insecure": "Insecure", + "intermediate": "Intermediate", + "modern": "Modern" }, "ssl_verification": { - "auto": "SSL verification automatically configured", + "auto": "Automatic", "editor_label": "SSL verification", - "false": "SSL verification disabled", - "true": "SSL verification enabled" + "false": "Disabled", + "true": "Enabled" } }, "timeline": { @@ -420,12 +420,12 @@ } }, "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_modes": { - "dynamic": "Aspect ratio adjusts to media", - "static": "Static aspect ratio", - "unconstrained": "Unconstrained aspect ratio" + "dynamic": "Dynamic", + "static": "Static", + "unconstrained": "Unconstrained" }, "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", "url": "URL of media folder" }, - "icon": "Icon for this folder", - "id": "Unique ID for this folder in this card", - "title": "Title for this folder", - "type": "Folder type", + "icon": "Icon", + "id": "Unique ID", + "title": "Title", + "type": "Type", "types": { "ha": "Home Assistant Media Folder" } }, "image": { - "zoomable": "Whether the image can be zoomed and panned" + "zoomable": "Zoomable/Pannable" }, "live": { "auto_mute": "Automatically mute live cameras", @@ -467,7 +467,7 @@ "ringtone": { "editor_label": "Ringtone", "repeat": "Ringtone repeats per inbound call (0=indefinite)", - "type": "Ringtone for inbound calls", + "type": "Type", "types": { "arpeggio": "Arpeggio", "chime": "Chime", @@ -476,7 +476,7 @@ "none": "None", "westminster": "Westminster" }, - "url": "Custom ringtone URL" + "url": "Custom URL" }, "start": "Start 2-way audio call", "unanswered_timeout_seconds": "Seconds before ending unanswered inbound calls (0=never)", @@ -485,7 +485,7 @@ }, "editor_label": "Live Controls" }, - "draggable": "Live cameras view can be dragged/swiped", + "draggable": "Draggable", "lazy_load": "Live cameras are lazily loaded", "lazy_unload": "Live cameras are lazily unloaded", "microphone": { @@ -499,7 +499,7 @@ "preload": "Preload live view in the background", "show_image_during_load": "Show still image while the live stream is loading", "transition_effect": "Live camera transition effect", - "zoomable": "Live cameras can be zoomed/panned" + "zoomable": "Zoomable/Pannable" }, "media_viewer": { "auto_mute": "Automatically mute media", @@ -509,27 +509,27 @@ "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", "snapshot_click_plays_clip": "Clicking on a snapshot plays a related clip", "transition_effect": "Media Viewer transition effect", - "zoomable": "Media Viewer can be zoomed/panned" + "zoomable": "Zoomable/Pannable" }, "menu": { - "alignment": "Menu alignment", + "alignment": "Alignment", "alignments": { - "bottom": "Aligned to the bottom", - "left": "Aligned to the left", - "right": "Aligned to the right", - "top": "Aligned to the top" + "bottom": "Bottom", + "left": "Left", + "right": "Right", + "top": "Top" }, - "auto_hide": "Conditions under which the menu auto-hides", - "button_size": "Menu button size in pixels", + "auto_hide": "Auto-hide conditions", + "button_size": "Button size in pixels", "buttons": { - "alignment": "Button alignment", + "alignment": "Alignment", "alignments": { - "matching": "Matching the menu alignment", - "opposing": "Opposing the menu alignment" + "matching": "Matching", + "opposing": "Opposing" }, "call": "Call / Two-way audio", "camera_ui": "Camera user interface", @@ -538,14 +538,14 @@ "display_mode": "Display mode", "download": "Download", "editor_label": "Menu buttons", - "enabled": "Button enabled", + "enabled": "Enabled", "expand": "Expand", "folders": "Folders", "fullscreen": "Fullscreen", "gallery": "Gallery", "icon": "Icon", "image": "Image", - "inert": "Button non-interactive", + "inert": "Non-interactive", "info": "Info", "iris": "Iris / Default View / Unhide menu", "live": "Live", @@ -553,7 +553,7 @@ "media_player": "Send to media player", "microphone": "Microphone", "mute": "Mute / Unmute", - "permanent": "Show when the menu is hidden", + "permanent": "Show when menu hidden", "pip": "Picture in Picture", "play": "Play / Pause", "priority": "Priority", @@ -564,7 +564,7 @@ "screenshot": "Screenshot", "set_review": "Set review status", "snapshots": "Snapshots", - "state_color": "Color button by entity state", + "state_color": "Color by entity state", "substreams": "Substream(s)", "timeline": "Timeline", "type": "Button type", @@ -573,21 +573,21 @@ "toggle": "Toggle" } }, - "position": "Menu position", + "position": "Position", "positions": { - "bottom": "Positioned on the bottom", - "left": "Positioned on the left", - "right": "Positioned on the right", - "top": "Positioned on the top" + "bottom": "Bottom", + "left": "Left", + "right": "Right", + "top": "Top" }, - "style": "Menu style", + "style": "Style", "styles": { - "hidden": "Hidden menu", - "hover": "Hover menu", - "hover-card": "Hover menu (card-wide)", - "none": "No menu", - "outside": "Outside menu", - "overlay": "Overlay menu" + "hidden": "Hidden", + "hover": "Hover", + "hover-card": "Hover (card-wide)", + "none": "None", + "outside": "Outside", + "overlay": "Overlay" } }, "overrides": { @@ -595,11 +595,11 @@ }, "performance": { "features": { - "animated_progress_indicator": "Animated Progress Indicator", - "card_loading_effects": "Card Loading Effects", - "card_loading_indicator": "Card Loading Indicator", + "animated_progress_indicator": "Animated progress", + "card_loading_effects": "Loading effects", + "card_loading_indicator": "Loading indicator", "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" }, "style": { @@ -628,37 +628,37 @@ } }, "status_bar": { - "auto_hide": "Conditions under which the status bar auto-hides", - "height": "Status bar height in pixels", + "auto_hide": "Auto-hide conditions", + "height": "Height in pixels", "items": { - "enabled": "Item enabled", + "enabled": "Enabled", "engine": "Camera engine", "issues": "Issues", - "permanent": "Item keeps status bar shown", - "priority": "Item priority", + "permanent": "Keeps bar shown", + "priority": "Priority", "resolution": "Resolution", "severity": "Severity", "technology": "Technology", "title": "Title" }, - "popup_seconds": "Status bar popup seconds", - "position": "Status bar position", + "popup_seconds": "Popup seconds", + "position": "Position", "positions": { - "bottom": "Positioned on the bottom", - "top": "Positioned on the top" + "bottom": "Bottom", + "top": "Top" }, - "style": "Status bar style", + "style": "Style", "styles": { - "hover": "Hover status bar", - "hover-card": "Hover status bar (card-wide)", - "none": "No status bar", - "outside": "Outside status bar", - "overlay": "Overlay status bar", - "popup": "Popup status bar" + "hover": "Hover", + "hover-card": "Hover (card-wide)", + "none": "None", + "outside": "Outside", + "overlay": "Overlay", + "popup": "Popup" } }, "view": { - "camera_select": "View for newly selected cameras", + "camera_select": "View on camera select", "default": "Default view", "default_cycle_camera": "Cycle through cameras when default view updates", "default_reset": { @@ -699,20 +699,20 @@ "actions": { "editor_label": "Trigger actions", "interaction_mode": "How to handle actions when the card has human interaction", - "trigger": "Trigger action", + "trigger": "Trigger", "triggers": { - "call": "Start a 2-way audio call", - "default": "Change to or update default view", - "live": "Change to or update live view", - "media": "Change to the relevant media view for new media", - "none": "No action", - "update": "Update the current view" + "call": "Start call", + "default": "Default view", + "live": "Live view", + "media": "Media view", + "none": "None", + "update": "Update view" }, - "untrigger": "Untrigger action", + "untrigger": "Untrigger", "untriggers": { - "call": "End a 2-way audio call", - "default": "Change to default view/camera", - "none": "No action" + "call": "End call", + "default": "Default view", + "none": "None" } }, "editor_label": "Trigger behavior", diff --git a/tests/components-lib/editor/form-labels.test.ts b/tests/components-lib/editor/form-labels.test.ts index 9404d858..984493fc 100644 --- a/tests/components-lib/editor/form-labels.test.ts +++ b/tests/components-lib/editor/form-labels.test.ts @@ -53,7 +53,7 @@ describe('computeFormLabel', () => { { name: 'title', selector: { text: {} } }, { path: [] }, ), - ).toBe('Title for this camera (autodetected from entity)'); + ).toBe('Title (autodetected)'); }); it('should order nested container paths into configuration order', () => { @@ -72,7 +72,7 @@ describe('computeFormLabel', () => { createForm([], [{ formPath: ['menu_style'], configPath: ['menu', 'style'] }]), { name: 'menu_style', selector: { text: {} } }, ), - ).toBe('Menu style'); + ).toBe('Style'); }); it('should localize a field that reads and writes itself where it sits', () => {