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

This commit is contained in:
Dermot Duffy
2026-07-21 22:31:13 -07:00
committed by GitHub
parent e29c0826cf
commit f28b16a4b5
19 changed files with 801 additions and 644 deletions
+85 -55
View File
@@ -5,6 +5,7 @@ import type {
} from '../../../ha/types';
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([
@@ -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'),
@@ -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: {} },
},
]),
],
};
};
@@ -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',
@@ -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 }),
},
]),
],
});
@@ -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,
} 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 {
+11 -8
View File
@@ -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: {} },
+23 -18
View File
@@ -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',
+37 -32
View File
@@ -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',
@@ -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: {} } },
+65 -56
View File
@@ -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(
+20 -13
View File
@@ -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: {} } },
]),
],
},
],
+21 -24
View File
@@ -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: [
+51 -44
View File
@@ -3,6 +3,7 @@ import { STATUS_BAR_HEIGHT_MIN } from '../../../config/schema/status-bar';
import type { HAFormExpandableSchema } from '../../../ha/types';
import { 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 }),
},
]),
],
},
{
+64 -59
View File
@@ -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'),
+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
// 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<StockHAFormGridSchema, 'name'>,
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;
+131 -131
View File
@@ -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",
@@ -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', () => {