feat: Modernize the visual card editor. (#2586)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// Registers the card's custom icons as a Home Assistant custom iconset, so
|
||||
// any `ha-icon` (and the icon picker) can render `advanced-camera-card:<name>`
|
||||
// icons -- inside or outside the card. The iconset format supports a single
|
||||
// monochrome SVG path per icon, so each asset is a single-path silhouette,
|
||||
// with its path data extracted at build time.
|
||||
|
||||
import frigateIcon from '../images/icons/frigate.svg';
|
||||
import irisIcon from '../images/icons/iris.svg';
|
||||
import motioneyeIcon from '../images/icons/motioneye.svg';
|
||||
import reolinkIcon from '../images/icons/reolink.svg';
|
||||
import tplinkIcon from '../images/icons/tplink.svg';
|
||||
|
||||
interface CustomIconsetIcon {
|
||||
path: string;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
interface CustomIconsetListItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CustomIconsetHelpers {
|
||||
getIcon: (name: string) => Promise<CustomIconsetIcon>;
|
||||
getIconList: () => Promise<CustomIconsetListItem[]>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
customIcons?: Record<string, CustomIconsetHelpers>;
|
||||
}
|
||||
}
|
||||
|
||||
export const CUSTOM_ICONSET_PREFIX = 'advanced-camera-card';
|
||||
|
||||
const ICONS: Record<string, CustomIconsetIcon> = {
|
||||
frigate: frigateIcon,
|
||||
iris: irisIcon,
|
||||
motioneye: motioneyeIcon,
|
||||
reolink: reolinkIcon,
|
||||
tplink: tplinkIcon,
|
||||
};
|
||||
|
||||
export const CUSTOM_ICON_NAMES = Object.keys(ICONS);
|
||||
|
||||
const getIcon = async (name: string): Promise<CustomIconsetIcon> => {
|
||||
const icon: CustomIconsetIcon | undefined = ICONS[name];
|
||||
if (!icon) {
|
||||
throw new Error(`Unknown icon: ${CUSTOM_ICONSET_PREFIX}:${name}`);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
const getIconList = async (): Promise<CustomIconsetListItem[]> =>
|
||||
CUSTOM_ICON_NAMES.map((name) => ({ name }));
|
||||
|
||||
/**
|
||||
* Register the card's custom iconset with Home Assistant. Safe to call more
|
||||
* than once; an existing registration (e.g. from a second loaded copy of the
|
||||
* card) is left in place.
|
||||
*/
|
||||
export const registerCustomIconset = (): void => {
|
||||
window.customIcons ??= {};
|
||||
window.customIcons[CUSTOM_ICONSET_PREFIX] ??= { getIcon, getIconList };
|
||||
};
|
||||
@@ -18,17 +18,24 @@ class HomeAssistantElementsLoadError extends AdvancedCameraCardError {
|
||||
*/
|
||||
export const sideLoadHomeAssistantElements = async (): Promise<void> => {
|
||||
const neededElements = [
|
||||
'ha-alert',
|
||||
'ha-button',
|
||||
'ha-camera-stream',
|
||||
'ha-card',
|
||||
'ha-combo-box',
|
||||
'ha-dropdown-item',
|
||||
'ha-dropdown',
|
||||
'ha-expansion-panel',
|
||||
'ha-form',
|
||||
'ha-hls-player',
|
||||
'ha-icon-button-prev',
|
||||
'ha-icon-button',
|
||||
'ha-icon',
|
||||
'ha-md-list-item',
|
||||
'ha-md-list',
|
||||
'ha-menu-button',
|
||||
'ha-selector',
|
||||
'ha-sortable',
|
||||
'ha-spinner',
|
||||
'ha-state-icon',
|
||||
'ha-web-rtc-player',
|
||||
|
||||
+108
@@ -254,3 +254,111 @@ export const resolvedMediaSchema = z.object({
|
||||
mime_type: z.string(),
|
||||
});
|
||||
export type ResolvedMedia = z.infer<typeof resolvedMediaSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Home Assistant form types.
|
||||
// *************************************************************************
|
||||
// Home Assistant does not publish frontend types as a package, so the subset
|
||||
// of the `ha-form` schema and `ha-selector` configuration shapes used by this
|
||||
// card are declared here. They must remain structurally compatible with:
|
||||
// https://github.com/home-assistant/frontend/blob/dev/src/components/ha-form/types.ts
|
||||
// https://github.com/home-assistant/frontend/blob/dev/src/data/selector.ts
|
||||
|
||||
interface HAEntitySelector {
|
||||
entity: {
|
||||
domain?: string;
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HASelectSelectorOption {
|
||||
value: unknown;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface HASelectSelector {
|
||||
select: {
|
||||
mode?: 'dropdown' | 'list';
|
||||
multiple?: boolean;
|
||||
custom_value?: boolean;
|
||||
options: string[] | HASelectSelectorOption[];
|
||||
};
|
||||
}
|
||||
|
||||
interface HAStringSelector {
|
||||
text: {
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HANumberSelector {
|
||||
number: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
mode?: 'box' | 'slider';
|
||||
step?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface HABooleanSelector {
|
||||
boolean: Record<string, never>;
|
||||
}
|
||||
|
||||
interface HAIconSelector {
|
||||
icon: Record<string, never>;
|
||||
}
|
||||
|
||||
interface HAObjectSelector {
|
||||
object: Record<string, never>;
|
||||
}
|
||||
|
||||
type HASelector =
|
||||
| HABooleanSelector
|
||||
| HAEntitySelector
|
||||
| HAIconSelector
|
||||
| HANumberSelector
|
||||
| HAObjectSelector
|
||||
| HASelectSelector
|
||||
| HAStringSelector;
|
||||
|
||||
interface StockHAFormBaseSchema {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface StockHAFormSelectorSchema extends StockHAFormBaseSchema {
|
||||
selector: HASelector;
|
||||
}
|
||||
|
||||
interface StockHAFormExpandableSchema extends StockHAFormBaseSchema {
|
||||
type: 'expandable';
|
||||
title?: string;
|
||||
icon?: string;
|
||||
schema: readonly HAFormSchema[];
|
||||
}
|
||||
|
||||
// Extensions layered onto the stock shapes; not part of `ha-form` itself.
|
||||
interface CardHAFormSchemaExtensions {
|
||||
// Override the standard path-derived label, e.g. to point shared fields at
|
||||
// `config.common.*` rather than duplicating keys under each section.
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface HAFormSelectorSchema
|
||||
extends StockHAFormSelectorSchema,
|
||||
CardHAFormSchemaExtensions {}
|
||||
|
||||
export interface HAFormExpandableSchema
|
||||
extends Omit<StockHAFormExpandableSchema, 'name'>,
|
||||
CardHAFormSchemaExtensions {
|
||||
// hass-frontend requires `name`, but `ha-form`'s runtime treats a missing name
|
||||
// as a flattened, visual-only group (the `!item.name` paths in ha-form.ts).
|
||||
// Relaxed to optional here for nameless grouping, e.g. an "Engine" group over
|
||||
// the top-level `frigate`/`motioneye` camera keys.
|
||||
name?: string;
|
||||
|
||||
// Explicit documentation-link key, for a nameless group whose link cannot be
|
||||
// derived from a configuration path.
|
||||
docPath?: string;
|
||||
}
|
||||
|
||||
export type HAFormSchema = HAFormSelectorSchema | HAFormExpandableSchema;
|
||||
|
||||
Reference in New Issue
Block a user