Add support for more advanced forms of overriding

This commit is contained in:
Dermot Duffy
2024-04-13 18:23:11 -07:00
parent 0eca5d780c
commit 323925101d
21 changed files with 855 additions and 285 deletions
+150 -23
View File
@@ -1,39 +1,166 @@
# `overrides` # `overrides`
Various parts of card configuration may [conditionally](conditions.md) be The card configuration may [conditionally](conditions.md) be overridden (e.g. to
overridden (e.g. to hide the menu in fullscreen mode). hide the menu in fullscreen mode).
```yaml ```yaml
overrides: overrides:
- conditions: - conditions:
[condition] [condition]
overrides: [...]
[override]
``` ```
Not all configuration parameters are overriddable, some because it doesn't make !> Whilst all configuration parameters are theoretically overridable, in some instances a configuration variable may only be consulted on startup or changing its value may negatively impact behavior -- override results may vary!
sense for that parameter to vary, and many because of the extra complexity of
supporting overriding given the lack of compelling usecases ([please request new
overridable parameters
here!](https://github.com/dermotduffy/frigate-hass-card/issues/new/choose)).
Each entry under the top-level `overrides` configuration block should be a list The top-level `overrides` configuration block expects a list, with each list
item, that has both of the following parameters set: item containing `conditions` and at least one of `merge`, `delete` or `set` specified.
| Option | Default | Description | | Option | Default | Description |
| - | - | - | | - | - | - |
| `conditions` | | A list of [conditions](conditions.md) that must evaluate to `true` in order for the overrides to be applied. | | `conditions` | | A list of [conditions](conditions.md) that must evaluate to `true` in order for the overrides to be applied. |
| `overrides` | | Configuration overrides to be applied. Any configuration parameter matching [Overrideable parameters](overrides.md?id=overrideable-parameters) can be overridden. | | `delete` | | An array of configuration paths to delete. See below. |
| `merge` | | A dictionary of configuration paths to merge. See below. |
| `set` | | A dictionary of configuration paths to set. See below. |
## Overrideable parameters ## Configuration Paths
| Configuration Key | Overrideable | The `delete`, `merge` and `set` parameters take configuration paths. Paths are dot-separated references to particular configuration parameters. To refer to list elements use `[n]` notation.
| - | - |
| [`cameras.*`](cameras/README.md) | :white_check_mark: | For example the path `cameras[1].dimensions.aspect_ratio` refers to the `aspect_ratio` parameter below:
| [`cameras_global.*`](cameras/README.md) | :white_check_mark: |
| [`dimensions.*`](dimensions.md) | :white_check_mark: | ```yaml
| [`image.*`](image.md) | :white_check_mark: | cameras:
| [`live.controls.*`](live.md?id=controls), [`live.display.*`](live.md?id=display), [`live.microphone.*`](live.md?id=microphone), [`live.show_image_during_load`](live.md), [`live.zoomable`](live.md) | :white_check_mark: | - camera_entity: camera.other
| [`menu.*`](menu.md) | :white_check_mark: | - camera_entity: camera.relevant
| [`view.*`](view.md) | :white_check_mark: | dimensions:
| *(Everything else)* | :heavy_multiplication_x: | aspect_ratio: '16:9'
```
## `delete`
An array of configuration paths to delete.
```yaml
overrides:
- conditions:
[condition]
delete:
- [path_1]
- [path_2]
```
### Examples
Delete the 2nd camera:
```yaml
overrides:
- conditions:
[condition]
delete:
'cameras[2]'
```
Delete the menu style parameter, thus falling back to the default:
```yaml
overrides:
- conditions:
[condition]
delete:
'menu.style'
```
## `merge`
Specifies an object to recursively merge into existing configuration.
| Option | Default | Description |
| - | - | - |
| [configuration path] | | Arbitrary configuration object to merge. Must be an object (i.e. not a literal value). |
### Examples
Hide the menu when a given condition is met:
```yaml
overrides:
- conditions:
[condition]
merge:
menu: {
style: 'hidden'
}
```
Enable thumbnails below the `live` feed:
```yaml
overrides:
- conditions:
[condition]
merge:
'live.controls.thumbnails': {
mode: 'below'
}
```
Also enables thumbnails below the `live` feed, but without using the dot-separated notation:
```yaml
overrides:
- conditions:
[condition]
merge:
live: {
controls: {
thumbnails: {
mode: 'below'
}
}
}
```
## `set`
Specifies a value to set in the configuration. This differs from `merge` in that the existing value is entirely replaced.
| Option | Default | Description |
| - | - | - |
| [configuration path] | | Arbitrary configuration value / object / list to set. |
### Examples
Set the entire menu configuration to defaults with the exception of the `style` which is set to `overlay`.
```yaml
overrides:
- conditions:
[condition]
set:
menu: {
style: 'overlay'
}
```
Set the menu style but without touching the other `menu` parameters:
```yaml
overrides:
- conditions:
[condition]
set:
'menu.style': 'overlay'
```
That is equivalent to merging the following:
```yaml
overrides:
- conditions:
[condition]
merge:
menu: {
style: 'overlay'
}
```
+25 -7
View File
@@ -424,7 +424,7 @@ overrides:
- condition: state - condition: state
entity: light.office_main_lights entity: light.office_main_lights
state: 'on' state: 'on'
overrides: merge:
menu: menu:
position: bottom position: bottom
``` ```
@@ -451,7 +451,7 @@ overrides:
- condition: state - condition: state
entity: binary_sensor.alarm_armed entity: binary_sensor.alarm_armed
state: 'off' state: 'off'
overrides: merge:
view: view:
default: image default: image
``` ```
@@ -471,7 +471,7 @@ overrides:
fullscreen: true fullscreen: true
- condition: display_mode - condition: display_mode
display_mode: grid display_mode: grid
overrides: merge:
live: live:
display: display:
grid_columns: 5 grid_columns: 5
@@ -497,7 +497,7 @@ overrides:
- conditions: - conditions:
- condition: expand - condition: expand
expand: true expand: true
overrides: merge:
menu: menu:
style: overlay style: overlay
``` ```
@@ -520,11 +520,29 @@ overrides:
- conditions: - conditions:
- condition: fullscreen - condition: fullscreen
fullscreen: true fullscreen: true
overrides: merge:
menu: menu:
style: none style: none
``` ```
### Remove a camera when an entity state changes
This example removes a camera from the card when an entity is disabled (e.g. a switch controlling power to the camera).
```yaml
type: custom:frigate-card
cameras:
- camera_entity: camera.office
- camera_entity: camera.kitchen
overrides:
- conditions:
- condition: state
entity: switch.kitchen_camera_power
state: off
delete:
- 'cameras[1]'
```
## PTZ control ## PTZ control
The card supports using PTZ controls to conveniently control pan, tilt and zoom for cameras. This example shows the PTZ controls on the `live` view. Note that if your camera engine supports it (e.g. `frigate`) this will just work out of the box with no configuration at all. The card supports using PTZ controls to conveniently control pan, tilt and zoom for cameras. This example shows the PTZ controls on the `live` view. Note that if your camera engine supports it (e.g. `frigate`) this will just work out of the box with no configuration at all.
@@ -568,7 +586,7 @@ overrides:
- conditions: - conditions:
- condition: screen - condition: screen
media_query: '(orientation: landscape)' media_query: '(orientation: landscape)'
overrides: merge:
menu: menu:
position: left position: left
``` ```
@@ -584,7 +602,7 @@ overrides:
- conditions: - conditions:
- condition: screen - condition: screen
media_query: '(max-width: 300px)' media_query: '(max-width: 300px)'
overrides: merge:
menu: menu:
style: none style: none
live: live:
+2 -2
View File
@@ -61,7 +61,7 @@
"@types/masonry-layout": "^4.2.5", "@types/masonry-layout": "^4.2.5",
"@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2", "@typescript-eslint/parser": "^5.36.2",
"@vitest/coverage-istanbul": "^1.3.1", "@vitest/coverage-istanbul": "^1.5.0",
"docsify-cli": "^4.4.4", "docsify-cli": "^4.4.4",
"eslint": "^8.23.0", "eslint": "^8.23.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
@@ -81,7 +81,7 @@
"sass": "^1.54.9", "sass": "^1.54.9",
"ts-prune": "^0.10.3", "ts-prune": "^0.10.3",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vitest": "^1.3.1", "vitest": "^1.5.0",
"vitest-mock-extended": "^1.3.1" "vitest-mock-extended": "^1.3.1"
}, },
"scripts": { "scripts": {
+1 -1
View File
@@ -145,7 +145,7 @@ export class CameraManager {
// order, to ensure that the defaults in the cameras global config do not // order, to ensure that the defaults in the cameras global config do not
// override the values specified in the per-camera config. // override the values specified in the per-camera config.
const cameras = config.cameras.map((camera) => const cameras = config.cameras.map((camera) =>
recursivelyMergeObjectsNotArrays(cloneDeep(config?.cameras_global), camera), recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
); );
try { try {
+60 -31
View File
@@ -1,14 +1,21 @@
import { CurrentUser } from '@dermotduffy/custom-card-helpers'; import { CurrentUser } from '@dermotduffy/custom-card-helpers';
import { HassEntities } from 'home-assistant-js-websocket'; import { HassEntities } from 'home-assistant-js-websocket';
import merge from 'lodash-es/merge'; import merge from 'lodash-es/merge';
import { copyConfig } from '../config/management'; import { ZodSchema } from 'zod';
import {
copyConfig,
deleteConfigValue,
getConfigValue,
setConfigValue,
} from '../config/management';
import { import {
FrigateCardCondition, FrigateCardCondition,
frigateConditionalSchema,
OverrideConfigurationKey,
RawFrigateCardConfig, RawFrigateCardConfig,
ViewDisplayMode, ViewDisplayMode,
frigateConditionalSchema,
Overrides,
} from '../config/types'; } from '../config/types';
import { desparsifyArrays } from '../utils/basic';
import { CardConditionAPI } from './types'; import { CardConditionAPI } from './types';
interface MicrophoneConditionState { interface MicrophoneConditionState {
@@ -73,44 +80,66 @@ export function evaluateConditionViaEvent(
return evaluateEvent.evaluation ?? false; return evaluateEvent.evaluation ?? false;
} }
type RawOverrides = {
conditions: FrigateCardCondition[];
overrides: RawFrigateCardConfig;
}[];
export function getOverriddenConfig( export function getOverriddenConfig(
manager: Readonly<ConditionsManager>, manager: Readonly<ConditionsManager>,
config: Readonly<RawFrigateCardConfig>, config: Readonly<RawFrigateCardConfig>,
configOverrides?: Readonly<RawOverrides>, options?: {
stateOverrides?: Partial<ConditionState>, configOverrides?: Readonly<Overrides>;
stateOverrides?: Partial<ConditionState>;
schema?: ZodSchema;
logOnParseError?: boolean;
},
): RawFrigateCardConfig { ): RawFrigateCardConfig {
const output = copyConfig(config); let output = copyConfig(config);
let overridden = false; let overridden = false;
if (configOverrides) { if (options?.configOverrides) {
for (const override of configOverrides) { for (const override of options.configOverrides) {
if (manager.evaluateConditions(override.conditions, stateOverrides)) { if (manager.evaluateConditions(override.conditions, options?.stateOverrides)) {
merge(output, override.overrides); override.delete?.forEach((deletionKey) => {
deleteConfigValue(output, deletionKey);
});
Object.keys(override.set ?? {}).forEach((setKey) => {
setConfigValue(output, setKey, override.set?.[setKey]);
});
Object.keys(override.merge ?? {}).forEach((mergeKey) => {
setConfigValue(
output,
mergeKey,
merge({}, getConfigValue(output, mergeKey), override.merge?.[mergeKey]),
);
});
overridden = true; overridden = true;
} }
} }
} }
// Attempt to return the same configuration object if it has not been
// overridden (to reduce re-renders for a configuration that has not changed).
return overridden ? output : config;
}
export function getOverridesByKey( if (!overridden) {
key: OverrideConfigurationKey, // Attempt to return the same configuration object if it has not been
overrides?: Readonly<RawOverrides>, // overridden (to reduce re-renders for a configuration that has not changed).
): RawOverrides { return config;
return ( }
overrides
?.filter((o) => key in o.overrides) if (options?.configOverrides?.some((override) => override.delete?.length)) {
.map((o) => ({ // If anything was deleted during this override, empty undefined slots may
conditions: o.conditions, // be left in arrays where values were unset. Desparsify them.
overrides: o.overrides[key] as RawFrigateCardConfig, output = desparsifyArrays(output);
})) ?? [] }
);
if (options?.schema) {
const parseResult = options.schema.safeParse(output);
if (options.logOnParseError && !parseResult.success) {
console.warn(
`Cannot parse overridden configuration`,
output,
parseResult.error.message,
);
}
return parseResult.success ? parseResult.data : config;
}
return output;
} }
// A tiny wrapper interface to allow the same manager to be passed around // A tiny wrapper interface to allow the same manager to be passed around
+6 -6
View File
@@ -4,7 +4,7 @@ import {
CardWideConfig, CardWideConfig,
FrigateCardConfig, FrigateCardConfig,
frigateCardConfigSchema, frigateCardConfigSchema,
RawFrigateCardConfig RawFrigateCardConfig,
} from '../config/types'; } from '../config/types';
import { localize } from '../localize/localize'; import { localize } from '../localize/localize';
import { setProfiles } from '../config/profiles'; import { setProfiles } from '../config/profiles';
@@ -104,11 +104,11 @@ export class ConfigManager {
if (!this._config) { if (!this._config) {
return; return;
} }
const overriddenConfig = getOverriddenConfig( const overriddenConfig = getOverriddenConfig(conditionsManager, this._config, {
conditionsManager, configOverrides: this._config.overrides,
this._config, schema: frigateCardConfigSchema,
this._config.overrides, logOnParseError: !!this.getCardWideConfig()?.debug?.logging,
) as FrigateCardConfig; }) as FrigateCardConfig;
// Save on Lit re-rendering costs by only updating the configuration if it // Save on Lit re-rendering costs by only updating the configuration if it
// actually changes. // actually changes.
+18 -13
View File
@@ -26,8 +26,9 @@ import {
CardWideConfig, CardWideConfig,
frigateCardConfigDefaults, frigateCardConfigDefaults,
LiveConfig, LiveConfig,
LiveOverrides, liveConfigAbsoluteRootSchema,
LiveProvider, LiveProvider,
Overrides,
TransitionEffect, TransitionEffect,
} from '../../config/types.js'; } from '../../config/types.js';
import { localize } from '../../localize/localize.js'; import { localize } from '../../localize/localize.js';
@@ -80,7 +81,7 @@ export class FrigateCardLive extends LitElement {
public overriddenLiveConfig?: LiveConfig; public overriddenLiveConfig?: LiveConfig;
@property({ attribute: false, hasChanged: contentsChanged }) @property({ attribute: false, hasChanged: contentsChanged })
public liveOverrides?: LiveOverrides; public overrides?: Overrides;
@property({ attribute: false }) @property({ attribute: false })
public cameraManager?: CameraManager; public cameraManager?: CameraManager;
@@ -151,7 +152,7 @@ export class FrigateCardLive extends LitElement {
.overriddenLiveConfig=${this.overriddenLiveConfig} .overriddenLiveConfig=${this.overriddenLiveConfig}
.inBackground=${this._controller.isInBackground()} .inBackground=${this._controller.isInBackground()}
.conditionsManagerEpoch=${this.conditionsManagerEpoch} .conditionsManagerEpoch=${this.conditionsManagerEpoch}
.liveOverrides=${this.liveOverrides} .overrides=${this.overrides}
.cardWideConfig=${this.cardWideConfig} .cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.microphoneManager=${this.microphoneManager} .microphoneManager=${this.microphoneManager}
@@ -182,7 +183,7 @@ export class FrigateCardLiveGrid extends LitElement {
public overriddenLiveConfig?: LiveConfig; public overriddenLiveConfig?: LiveConfig;
@property({ attribute: false, hasChanged: contentsChanged }) @property({ attribute: false, hasChanged: contentsChanged })
public liveOverrides?: LiveOverrides; public overrides?: Overrides;
@property({ attribute: false }) @property({ attribute: false })
public conditionsManagerEpoch?: ConditionsManagerEpoch; public conditionsManagerEpoch?: ConditionsManagerEpoch;
@@ -211,7 +212,7 @@ export class FrigateCardLiveGrid extends LitElement {
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig} .nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
.overriddenLiveConfig=${this.overriddenLiveConfig} .overriddenLiveConfig=${this.overriddenLiveConfig}
.conditionsManagerEpoch=${this.conditionsManagerEpoch} .conditionsManagerEpoch=${this.conditionsManagerEpoch}
.liveOverrides=${this.liveOverrides} .overrides=${this.overrides}
.cardWideConfig=${this.cardWideConfig} .cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.microphoneManager=${this.microphoneManager} .microphoneManager=${this.microphoneManager}
@@ -290,7 +291,7 @@ export class FrigateCardLiveCarousel extends LitElement {
public overriddenLiveConfig?: LiveConfig; public overriddenLiveConfig?: LiveConfig;
@property({ attribute: false, hasChanged: contentsChanged }) @property({ attribute: false, hasChanged: contentsChanged })
public liveOverrides?: LiveOverrides; public overrides?: Overrides;
@property({ attribute: false }) @property({ attribute: false })
public conditionsManagerEpoch?: ConditionsManagerEpoch; public conditionsManagerEpoch?: ConditionsManagerEpoch;
@@ -471,19 +472,23 @@ export class FrigateCardLiveCarousel extends LitElement {
// (in the carousel for example) is not necessarily the live camera *this* // (in the carousel for example) is not necessarily the live camera *this*
// <frigate-card-live-provider> is rendering right now, so we provide a // <frigate-card-live-provider> is rendering right now, so we provide a
// stateOverride to evaluate the condition in that context. // stateOverride to evaluate the condition in that context.
const config = getOverriddenConfig( const liveConfig = getOverriddenConfig(
this.conditionsManagerEpoch.manager, this.conditionsManagerEpoch.manager,
this.nonOverriddenLiveConfig, { live: this.nonOverriddenLiveConfig },
this.liveOverrides, {
{ camera: cameraID }, configOverrides: this.overrides,
) as LiveConfig; stateOverrides: { camera: cameraID },
schema: liveConfigAbsoluteRootSchema,
logOnParseError: !!this.cardWideConfig?.debug?.logging,
},
).live as LiveConfig;
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID); const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
return html` return html`
<div class="embla__slide"> <div class="embla__slide">
<frigate-card-live-provider <frigate-card-live-provider
?load=${!config.lazy_load} ?load=${!liveConfig.lazy_load}
.microphoneStream=${this.view?.camera === cameraID .microphoneStream=${this.view?.camera === cameraID
? this.microphoneManager?.getStream() ? this.microphoneManager?.getStream()
: undefined} : undefined}
@@ -493,7 +498,7 @@ export class FrigateCardLiveCarousel extends LitElement {
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined, () => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
)} )}
.label=${cameraMetadata?.title ?? ''} .label=${cameraMetadata?.title ?? ''}
.liveConfig=${config} .liveConfig=${liveConfig}
.hass=${this.hass} .hass=${this.hass}
.cardWideConfig=${this.cardWideConfig} .cardWideConfig=${this.cardWideConfig}
> >
+2 -12
View File
@@ -9,10 +9,7 @@ import {
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { CameraManager } from '../camera-manager/manager.js'; import { CameraManager } from '../camera-manager/manager.js';
import { import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js';
ConditionsManagerEpoch,
getOverridesByKey,
} from '../card-controller/conditions-manager.js';
import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js'; import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js';
import { import {
CardWideConfig, CardWideConfig,
@@ -233,10 +230,7 @@ export class FrigateCardViews extends LitElement {
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live} .nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
.overriddenLiveConfig=${this.overriddenConfig.live} .overriddenLiveConfig=${this.overriddenConfig.live}
.conditionsManagerEpoch=${this.conditionsManagerEpoch} .conditionsManagerEpoch=${this.conditionsManagerEpoch}
.liveOverrides=${getOverridesByKey( .overrides=${this.overriddenConfig.overrides}
'live',
this.overriddenConfig.overrides,
)}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig} .cardWideConfig=${this.cardWideConfig}
.microphoneManager=${this.microphoneManager} .microphoneManager=${this.microphoneManager}
@@ -248,10 +242,6 @@ export class FrigateCardViews extends LitElement {
: `` : ``
} }
</frigate-card-surround>`; </frigate-card-surround>`;
// .fetchMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.media_type : undefined}
// .fetchEventsMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.events_media_type : undefined}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
+1 -1
View File
@@ -42,7 +42,6 @@ import { arrayify } from '../utils/basic';
* @param keys The key to the property to set. * @param keys The key to the property to set.
* @param value The value to set. * @param value The value to set.
*/ */
export const setConfigValue = ( export const setConfigValue = (
obj: RawFrigateCardConfig, obj: RawFrigateCardConfig,
keys: string | (string | number)[], keys: string | (string | number)[],
@@ -689,4 +688,5 @@ const UPGRADES = [
// Delete the value if it's set to the default. // Delete the value if it's set to the default.
transform: (val) => (val === 'low' ? ['low-performance'] : null), transform: (val) => (val === 'low' ? ['low-performance'] : null),
}), }),
upgradeArrayOfObjects(CONF_OVERRIDES, upgradeMoveTo('overrides', 'merge')),
]; ];
+38 -51
View File
@@ -1008,8 +1008,24 @@ const livethumbnailsControlSchema = thumbnailsControlSchema.extend({
), ),
}); });
const liveOverridableConfigSchema = z const liveConfigSchema = z
.object({ .object({
auto_pause: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_pause),
auto_play: z
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_play),
auto_mute: z
.enum(MEDIA_MUTE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_mute),
auto_unmute: z
.enum(MEDIA_UNMUTE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_unmute),
controls: z controls: z
.object({ .object({
builtin: z.boolean().default(liveConfigDefault.controls.builtin), builtin: z.boolean().default(liveConfigDefault.controls.builtin),
@@ -1032,55 +1048,36 @@ const liveOverridableConfigSchema = z
title: titleControlConfigSchema.optional(), title: titleControlConfigSchema.optional(),
}) })
.default(liveConfigDefault.controls), .default(liveConfigDefault.controls),
show_image_during_load: z
.boolean()
.default(liveConfigDefault.show_image_during_load),
microphone: microphoneConfigSchema.default(liveConfigDefault.microphone),
zoomable: z.boolean().default(liveConfigDefault.zoomable),
display: viewDisplaySchema, display: viewDisplaySchema,
}) draggable: z.boolean().default(liveConfigDefault.draggable),
.merge(actionsSchema);
const liveConfigSchema = liveOverridableConfigSchema
.extend({
auto_play: z
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_play),
auto_pause: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_pause),
auto_mute: z
.enum(MEDIA_MUTE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_mute),
auto_unmute: z
.enum(MEDIA_UNMUTE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_unmute),
preload: z.boolean().default(liveConfigDefault.preload),
lazy_load: z.boolean().default(liveConfigDefault.lazy_load), lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
lazy_unload: z lazy_unload: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS) .enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array() .array()
.default(liveConfigDefault.lazy_unload), .default(liveConfigDefault.lazy_unload),
draggable: z.boolean().default(liveConfigDefault.draggable), microphone: microphoneConfigSchema.default(liveConfigDefault.microphone),
preload: z.boolean().default(liveConfigDefault.preload),
show_image_during_load: z
.boolean()
.default(liveConfigDefault.show_image_during_load),
transition_effect: transitionEffectConfigSchema.default( transition_effect: transitionEffectConfigSchema.default(
liveConfigDefault.transition_effect, liveConfigDefault.transition_effect,
), ),
zoomable: z.boolean().default(liveConfigDefault.zoomable),
}) })
.merge(actionsSchema)
.default(liveConfigDefault); .default(liveConfigDefault);
export type LiveConfig = z.infer<typeof liveConfigSchema>; export type LiveConfig = z.infer<typeof liveConfigSchema>;
const liveOverridesSchema = z // This schema is used when the live config needs to be overridden (see
.object({ // `live.ts`). Overrides will always be "relative" to the config root, so this
conditions: frigateCardConditionSchema.array(), // schema maintains that 'depth' from the root but without the other
overrides: liveOverridableConfigSchema, // requirements that frigateCardConfigSchema has. Without this, overrides
}) // calculated in `live.ts` would fail since cameras/type are not provided (as
.array() // these are mandatory parameters in the full config).
.optional(); export const liveConfigAbsoluteRootSchema = z.object({
export type LiveOverrides = z.infer<typeof liveOverridesSchema>; live: liveConfigSchema,
});
// ************************************************************************* // *************************************************************************
// Cast Configuration // Cast Configuration
@@ -1580,26 +1577,16 @@ export const dimensionsConfigSchema = z
// Override Configuration // Override Configuration
// ************************************************************************* // *************************************************************************
// Strip all defaults from the override schemas, to ensure values are only what
// the user has specified.
const overrideConfigurationSchema = z.object({
cameras: deepRemoveDefaults(camerasConfigSchema).optional(),
cameras_global: deepRemoveDefaults(cameraConfigSchema).optional(),
live: deepRemoveDefaults(liveOverridableConfigSchema).optional(),
menu: deepRemoveDefaults(menuConfigSchema).optional(),
image: deepRemoveDefaults(imageConfigSchema).optional(),
view: deepRemoveDefaults(viewConfigSchema).optional(),
dimensions: deepRemoveDefaults(dimensionsConfigSchema).optional(),
});
export type OverrideConfigurationKey = keyof z.infer<typeof overrideConfigurationSchema>;
const overridesSchema = z const overridesSchema = z
.object({ .object({
conditions: frigateCardConditionSchema.array(), conditions: frigateCardConditionSchema.array(),
overrides: overrideConfigurationSchema, merge: z.object({}).passthrough().optional(),
set: z.object({}).passthrough().optional(),
delete: z.string().array().optional(),
}) })
.array() .array()
.optional(); .optional();
export type Overrides = z.infer<typeof overridesSchema>;
// ************************************************************************* // *************************************************************************
// Automation Configuration // Automation Configuration
+20 -2
View File
@@ -243,8 +243,8 @@ export const getChildrenFromElement = (parent: HTMLElement): HTMLElement[] => {
return children.filter(isHTMLElement); return children.filter(isHTMLElement);
}; };
export const recursivelyMergeObjectsNotArrays = <T>(src1: T, src2: T): T => { export const recursivelyMergeObjectsNotArrays = <T>(target: T, src1: T, src2: T): T => {
return mergeWith({}, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined)); return mergeWith(target, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined));
}; };
export const aspectRatioToString = (options?: { export const aspectRatioToString = (options?: {
@@ -268,3 +268,21 @@ export const aspectRatioToStyle = (options?: {
'aspect-ratio': aspectRatioToString(options), 'aspect-ratio': aspectRatioToString(options),
}; };
}; };
/**
* Remove empty slots from nested arrays.
*/
export const desparsifyArrays = <T>(data: T): T => {
if (Array.isArray(data)) {
return <T>(
data.filter((item) => item !== undefined).map((item) => desparsifyArrays(item))
);
} else if (typeof data === 'object' && data !== null) {
const result: Record<string | number | symbol, unknown> = {};
for (const key in data) {
result[key] = desparsifyArrays(data[key]);
}
return <T>result;
}
return data;
};
+4 -1
View File
@@ -28,7 +28,10 @@ export function deepRemoveDefaults<T extends z.ZodTypeAny>(schema: T): any {
} }
if (schema instanceof z.ZodArray) { if (schema instanceof z.ZodArray) {
return z.ZodArray.create(deepRemoveDefaults(schema.element)); return z.ZodArray.create(deepRemoveDefaults(schema.element))
.min(schema._def.minLength?.value, schema._def.minLength?.message)
.max(schema._def.maxLength?.value, schema._def.maxLength?.message)
.length(schema._def.exactLength?.value, schema._def.exactLength?.message);
} }
if (schema instanceof z.ZodOptional) { if (schema instanceof z.ZodOptional) {
+2 -2
View File
@@ -457,11 +457,11 @@ describe('CameraManager', async () => {
}, },
); );
it('without cameras', async () => { it('without camera', async () => {
const api = createCardAPI(); const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, mock<CameraManagerEngine>(), []); const manager = createCameraManager(api, mock<CameraManagerEngine>());
expect(manager.generateDefaultEventQueries('not_a_camera')).toBeNull(); expect(manager.generateDefaultEventQueries('not_a_camera')).toBeNull();
}); });
+336 -63
View File
@@ -1,10 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { import {
ConditionsEvaluateRequestEvent, ConditionsEvaluateRequestEvent,
ConditionsManager, ConditionsManager,
evaluateConditionViaEvent, evaluateConditionViaEvent,
getOverriddenConfig, getOverriddenConfig,
getOverridesByKey,
} from '../../src/card-controller/conditions-manager'; } from '../../src/card-controller/conditions-manager';
import { FrigateCardCondition } from '../../src/config/types'; import { FrigateCardCondition } from '../../src/config/types';
import { import {
@@ -77,77 +77,350 @@ describe('getOverriddenConfig', () => {
style: 'none', style: 'none',
}, },
}; };
const overrides = [
{
overrides: {
menu: {
style: 'above',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
];
it('should not override config', () => { it('should not override without overrides', () => {
const manager = new ConditionsManager(createCardAPI());
expect(getOverriddenConfig(manager, config, overrides)).toBe(config);
});
it('should override config', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(getOverriddenConfig(manager, config, overrides)).toEqual({
menu: {
style: 'above',
},
});
});
it('should do nothing without overrides', () => {
const manager = new ConditionsManager(createCardAPI()); const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true }); manager.setState({ fullscreen: true });
expect(getOverriddenConfig(manager, config)).toBe(config); expect(getOverriddenConfig(manager, config)).toBe(config);
}); });
});
describe('getOverridesByKey', () => { it('should not override when condition does not match', () => {
const conditions = [ const manager = new ConditionsManager(createCardAPI());
{ expect(
condition: 'fullscreen' as const, getOverriddenConfig(manager, config, {
fullscreen: true, configOverrides: [
}, {
]; merge: {
const override = { menu: {
menu: { style: 'hidden',
style: 'above', },
}, },
}; delete: ['menu.style'],
const overrides = [ set: {
{ 'menu.style': 'overlay',
overrides: override, },
conditions: conditions, conditions: [
}, {
]; condition: 'fullscreen' as const,
fullscreen: true,
it('should get overrides', () => { },
expect(getOverridesByKey('menu', overrides)).toEqual([ ],
{ conditions: conditions, overrides: { style: 'above' } }, },
]); ],
}),
).toBe(config);
}); });
it('should get no overrides', () => { describe('should merge', () => {
expect(getOverridesByKey('live', overrides)).toEqual([]); it('with path', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: {
'live.controls.thumbnails': {
mode: 'none',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'none',
},
live: {
controls: {
thumbnails: {
mode: 'none',
},
},
},
});
});
it('without path', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: {
menu: {
style: 'hidden',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
it('with invalid merge', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: 6 as unknown as Record<string, unknown>,
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'none',
},
});
});
}); });
it('should get no overrides when undefined', () => { describe('should set', () => {
expect(getOverridesByKey('live')).toEqual([]); it('leaf node', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
set: {
'menu.style': 'hidden',
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
it('root node', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
set: {
menu: {
style: 'hidden',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
});
describe('should delete', () => {
it('leaf node', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
delete: ['menu.style' as const],
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {},
});
});
it('root node', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
delete: ['menu' as const],
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({});
});
// it('with empty value and object', () => {
// const manager = new ConditionsManager(createCardAPI());
// manager.setState({ fullscreen: true });
// expect(
// getOverriddenConfig(manager, config, {
// configOverrides: [
// {
// delete: [''],
// conditions: [
// {
// condition: 'fullscreen' as const,
// fullscreen: true,
// },
// ],
// },
// ],
// emptyKeyReplaces: true,
// }),
// ).toEqual({});
// });
});
describe('should validate schema', () => {
const testSchema = z.object({
menu: z.object({
style: z.enum(['none', 'hidden']),
}),
});
it('passing', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'hidden',
},
},
],
schema: testSchema,
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
it('failing', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'NOT_A_STYLE',
},
},
],
schema: testSchema,
}),
).toEqual(config);
});
it('failing and logging', () => {
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'NOT_A_STYLE',
},
},
],
schema: testSchema,
logOnParseError: true,
}),
).toEqual(config);
expect(consoleSpy).toBeCalledWith(
'Cannot parse overridden configuration',
expect.anything(),
expect.anything(),
);
});
}); });
}); });
@@ -194,7 +467,7 @@ describe('ConditionsManager', () => {
return createConfig({ return createConfig({
overrides: [ overrides: [
{ {
overrides: {}, merge: {},
conditions: conditions, conditions: conditions,
}, },
], ],
@@ -455,7 +728,7 @@ describe('ConditionsManager', () => {
describe('with screen condition', () => { describe('with screen condition', () => {
const mediaQueryConfig = { const mediaQueryConfig = {
type: 'custom:frigate-card', type: 'custom:frigate-card',
cameras: [], cameras: [{}],
elements: [ elements: [
{ {
type: 'custom:frigate-card-conditional', type: 'custom:frigate-card-conditional',
+50 -7
View File
@@ -420,7 +420,7 @@ describe('should handle version specific upgrades', () => {
media_loaded: true, media_loaded: true,
}, },
], ],
overrides: { merge: {
view: { view: {
default: 'clips', default: 'clips',
}, },
@@ -489,7 +489,7 @@ describe('should handle version specific upgrades', () => {
overrides: [ overrides: [
{ {
conditions: {}, conditions: {},
overrides: { merge: {
menu: { menu: {
buttons: { buttons: {
camera_ui: { camera_ui: {
@@ -1872,7 +1872,7 @@ describe('should handle version specific upgrades', () => {
views: ['clips', 'snapshots'], views: ['clips', 'snapshots'],
}, },
], ],
overrides: { merge: {
view: { view: {
default: 'clips', default: 'clips',
}, },
@@ -1987,7 +1987,7 @@ describe('should handle version specific upgrades', () => {
cameras: ['camera_1', 'camera_2'], cameras: ['camera_1', 'camera_2'],
}, },
], ],
overrides: { merge: {
view: { view: {
default: 'clips', default: 'clips',
}, },
@@ -2107,7 +2107,7 @@ describe('should handle version specific upgrades', () => {
[condition]: true, [condition]: true,
}, },
], ],
overrides: { merge: {
view: { view: {
default: 'clips', default: 'clips',
}, },
@@ -2255,7 +2255,7 @@ describe('should handle version specific upgrades', () => {
state_not: 'off', state_not: 'off',
}, },
], ],
overrides: { merge: {
view: { view: {
default: 'clips', default: 'clips',
}, },
@@ -2387,7 +2387,7 @@ describe('should handle version specific upgrades', () => {
media_query: 'query', media_query: 'query',
}, },
], ],
overrides: { merge: {
view: { view: {
default: 'clips', default: 'clips',
}, },
@@ -2497,5 +2497,48 @@ describe('should handle version specific upgrades', () => {
}); });
}); });
}); });
it('from overrides to merge', () => {
const config = {
type: 'custom:frigate-card',
cameras: [],
overrides: [
{
conditions: [
{
condition: 'view',
view: ['clips'],
},
],
overrides: {
menu: {
style: 'hidden',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:frigate-card',
cameras: [],
overrides: [
{
conditions: [
{
condition: 'view',
view: ['clips'],
},
],
merge: {
menu: {
style: 'hidden',
},
},
},
],
});
});
}); });
}); });
+1 -1
View File
@@ -25,7 +25,7 @@ describe('setProfiles', () => {
it('should handle profiles', () => { it('should handle profiles', () => {
const input = { const input = {
type: 'frigate-hass-card', type: 'frigate-hass-card',
cameras: [], cameras: [{}],
live: { live: {
controls: { controls: {
timeline: { timeline: {
+2 -2
View File
@@ -12,7 +12,7 @@ import { createConfig } from '../test-utils';
describe('config defaults', () => { describe('config defaults', () => {
it('should be as expected', () => { it('should be as expected', () => {
expect(createConfig()).toEqual({ expect(createConfig()).toEqual({
cameras: [], cameras: [{}],
cameras_global: { cameras_global: {
dependencies: { dependencies: {
all_cameras: false, all_cameras: false,
@@ -453,7 +453,7 @@ describe('should handle custom frigate elements', () => {
it('should not require title controls to specify all options', () => { it('should not require title controls to specify all options', () => {
expect( expect(
createConfig({ createConfig({
cameras: [], cameras: [{}],
live: { live: {
controls: { controls: {
title: { title: {
+1 -1
View File
@@ -67,7 +67,7 @@ export const createCondition = (
export const createConfig = (config?: RawFrigateCardConfig): FrigateCardConfig => { export const createConfig = (config?: RawFrigateCardConfig): FrigateCardConfig => {
return frigateCardConfigSchema.parse({ return frigateCardConfigSchema.parse({
type: 'frigate-hass-card', type: 'frigate-hass-card',
cameras: [], cameras: [{}],
...config, ...config,
}); });
}; };
+52 -2
View File
@@ -7,6 +7,7 @@ import {
aspectRatioToStyle, aspectRatioToStyle,
contentsChanged, contentsChanged,
dayToDate, dayToDate,
desparsifyArrays,
dispatchFrigateCardEvent, dispatchFrigateCardEvent,
errorToConsole, errorToConsole,
formatDate, formatDate,
@@ -158,6 +159,7 @@ describe('runWhenIdleIfSupported', () => {
}); });
it('should run directly when not supported', () => { it('should run directly when not supported', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).requestIdleCallback = undefined; (window as any).requestIdleCallback = undefined;
const func = vi.fn(); const func = vi.fn();
runWhenIdleIfSupported(func); runWhenIdleIfSupported(func);
@@ -166,7 +168,7 @@ describe('runWhenIdleIfSupported', () => {
it('should run idle when supported', () => { it('should run idle when supported', () => {
const requestIdle = vi.fn(); const requestIdle = vi.fn();
(window as any).requestIdleCallback = requestIdle; window.requestIdleCallback = requestIdle;
const func = vi.fn(); const func = vi.fn();
runWhenIdleIfSupported(func); runWhenIdleIfSupported(func);
expect(requestIdle).toBeCalledWith(func, {}); expect(requestIdle).toBeCalledWith(func, {});
@@ -174,7 +176,7 @@ describe('runWhenIdleIfSupported', () => {
it('should run idle with timeout when supported', () => { it('should run idle with timeout when supported', () => {
const requestIdle = vi.fn(); const requestIdle = vi.fn();
(window as any).requestIdleCallback = requestIdle; window.requestIdleCallback = requestIdle;
const func = vi.fn(); const func = vi.fn();
runWhenIdleIfSupported(func, 10); runWhenIdleIfSupported(func, 10);
expect(requestIdle).toBeCalledWith(func, { timeout: 10 }); expect(requestIdle).toBeCalledWith(func, { timeout: 10 });
@@ -298,6 +300,7 @@ describe('recursivelyMergeObjectsNotArrays', () => {
it('should recursively merge objects but replace arrays', () => { it('should recursively merge objects but replace arrays', () => {
expect( expect(
recursivelyMergeObjectsNotArrays( recursivelyMergeObjectsNotArrays(
{},
{ {
a: { a: {
b: { b: {
@@ -356,3 +359,50 @@ describe('aspectRatioToStyle', () => {
expect(aspectRatioToStyle({ ratio: [4] })).toEqual({ 'aspect-ratio': 'auto' }); expect(aspectRatioToStyle({ ratio: [4] })).toEqual({ 'aspect-ratio': 'auto' });
}); });
}); });
describe('desparsifyArrays', () => {
it('number', () => {
expect(desparsifyArrays(1)).toBe(1);
});
it('string', () => {
expect(desparsifyArrays('foo')).toBe('foo');
});
describe('array', () => {
it('simple', () => {
expect(desparsifyArrays([1, 2, undefined, 3])).toEqual([1, 2, 3]);
});
it('nested', () => {
expect(
desparsifyArrays([
1,
2,
undefined,
{
subArray: [undefined, 3],
},
4,
]),
).toEqual([1, 2, { subArray: [3] }, 4]);
});
});
describe('object', () => {
it('simple', () => {
expect(
desparsifyArrays({ foo: [1, 2, undefined, 3], bar: [undefined, 4] }),
).toEqual({
foo: [1, 2, 3],
bar: [4],
});
});
it('nested', () => {
expect(
desparsifyArrays({ foo: { bar: [1, undefined, 2], empty: [undefined] } }),
).toEqual({
foo: {
bar: [1, 2],
empty: [],
},
});
});
});
});
+17
View File
@@ -49,6 +49,23 @@ describe('deepRemoveDefaults', () => {
const result = deepRemoveDefaults(schema).parse({ string: 'moo' }); const result = deepRemoveDefaults(schema).parse({ string: 'moo' });
expect(result.string).toBe('moo'); expect(result.string).toBe('moo');
}); });
describe('should still enforce array length', () => {
it('min', () => {
const schema = z.number().array().min(1);
const result = deepRemoveDefaults(schema).safeParse([]);
expect(result.success).toBeFalsy();
});
it('max', () => {
const schema = z.number().array().max(1);
const result = deepRemoveDefaults(schema).safeParse([1, 2]);
expect(result.success).toBeFalsy();
});
it('exact', () => {
const schema = z.number().array().length(1);
const result = deepRemoveDefaults(schema).safeParse([]);
expect(result.success).toBeFalsy();
});
});
}); });
describe('getParseErrorKeys', () => { describe('getParseErrorKeys', () => {
+67 -57
View File
@@ -800,6 +800,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@jridgewell/trace-mapping@npm:^0.3.23":
version: 0.3.25
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
dependencies:
"@jridgewell/resolve-uri": "npm:^3.1.0"
"@jridgewell/sourcemap-codec": "npm:^1.4.14"
checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4
languageName: node
linkType: hard
"@lit-labs/scoped-registry-mixin@npm:^1.0.3": "@lit-labs/scoped-registry-mixin@npm:^1.0.3":
version: 1.0.3 version: 1.0.3
resolution: "@lit-labs/scoped-registry-mixin@npm:1.0.3" resolution: "@lit-labs/scoped-registry-mixin@npm:1.0.3"
@@ -1467,76 +1477,76 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/coverage-istanbul@npm:^1.3.1": "@vitest/coverage-istanbul@npm:^1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "@vitest/coverage-istanbul@npm:1.3.1" resolution: "@vitest/coverage-istanbul@npm:1.5.0"
dependencies: dependencies:
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-instrument: "npm:^6.0.1" istanbul-lib-instrument: "npm:^6.0.1"
istanbul-lib-report: "npm:^3.0.1" istanbul-lib-report: "npm:^3.0.1"
istanbul-lib-source-maps: "npm:^4.0.1" istanbul-lib-source-maps: "npm:^5.0.4"
istanbul-reports: "npm:^3.1.6" istanbul-reports: "npm:^3.1.6"
magicast: "npm:^0.3.3" magicast: "npm:^0.3.3"
picocolors: "npm:^1.0.0" picocolors: "npm:^1.0.0"
test-exclude: "npm:^6.0.0" test-exclude: "npm:^6.0.0"
peerDependencies: peerDependencies:
vitest: 1.3.1 vitest: 1.5.0
checksum: 10c0/40efacde51e5998bde8032dd45d64fe214674f1efb97c094c4ae9ef1893afc4e0ef97f3fa4cc4923561b67b1196c27a877a0fa5bc71a049b8e48e3ef1f5c48db checksum: 10c0/12abbaf8925c797e1fafef05c8b7d554c9fc61d13eb70c3f8532ade72db305a3cb1fec9bb7642763de27471e3f3da3d684d6533aa954756c5edf2707d2fc25cd
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/expect@npm:1.3.1": "@vitest/expect@npm:1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "@vitest/expect@npm:1.3.1" resolution: "@vitest/expect@npm:1.5.0"
dependencies: dependencies:
"@vitest/spy": "npm:1.3.1" "@vitest/spy": "npm:1.5.0"
"@vitest/utils": "npm:1.3.1" "@vitest/utils": "npm:1.5.0"
chai: "npm:^4.3.10" chai: "npm:^4.3.10"
checksum: 10c0/ea66a1e912d896a481a27631b68089b885af7e8ed62ba8aaa119c37a9beafe6c094fd672775a20e6e23460af66e294f9ca259e6e0562708d1b7724eaaf53c7bb checksum: 10c0/12138caf0831a9bcdf475750fdff27588f03f11ee5101124f9d39951bbcdec39aa6a19a1a30f3ab0ca17374ddfc7399b861e2fcb1548bba302943e8e88512a9c
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/runner@npm:1.3.1": "@vitest/runner@npm:1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "@vitest/runner@npm:1.3.1" resolution: "@vitest/runner@npm:1.5.0"
dependencies: dependencies:
"@vitest/utils": "npm:1.3.1" "@vitest/utils": "npm:1.5.0"
p-limit: "npm:^5.0.0" p-limit: "npm:^5.0.0"
pathe: "npm:^1.1.1" pathe: "npm:^1.1.1"
checksum: 10c0/d732de2368d2bc32cbc27f0bbc5477f6e36088ddfb873c036935a45b1b252ebc529b932cf5cd944eed9b692243acebef828f6d3218583cb8a6817a8270712050 checksum: 10c0/a7b693e4121b6159a77f6cbffc03c228cd18c3734e27a968fe26579777f6d7a4d55a02fba396d564df996a91881ee76de80ee5a526d27a4b508f44a9af62d95a
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/snapshot@npm:1.3.1": "@vitest/snapshot@npm:1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "@vitest/snapshot@npm:1.3.1" resolution: "@vitest/snapshot@npm:1.5.0"
dependencies: dependencies:
magic-string: "npm:^0.30.5" magic-string: "npm:^0.30.5"
pathe: "npm:^1.1.1" pathe: "npm:^1.1.1"
pretty-format: "npm:^29.7.0" pretty-format: "npm:^29.7.0"
checksum: 10c0/cad0844270852c6d53c1ca6b7ca279034880d2140837ff245d5bd2376f4356cc924929c58dc69bcf9fad83ba934d4a06000c908971cc24b5d7a9ec2656b72d29 checksum: 10c0/d897070b3a7c008eb58d44ca0dc324c69dfc1f787335e59fb930ae2e8d02a148e6b3c13497a588083a0654bdc430281fdd84276ead4888230314a2984dd9f93d
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/spy@npm:1.3.1": "@vitest/spy@npm:1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "@vitest/spy@npm:1.3.1" resolution: "@vitest/spy@npm:1.5.0"
dependencies: dependencies:
tinyspy: "npm:^2.2.0" tinyspy: "npm:^2.2.0"
checksum: 10c0/efc42f679d2a51fc6583ca3136ccd47581cb27c923ed3cb0500f5dee9aac99b681bfdd400c16ef108f2e0761daa642bc190816a6411931a2aba99ebf8b213dd4 checksum: 10c0/0206f1e8431f543474dc6d252b553227d0f286b27226b987b63babb18865e8320e6c3d822c67782aae68555728a57ecdeb7c4e16283dbad49791608a691f26d1
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/utils@npm:1.3.1": "@vitest/utils@npm:1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "@vitest/utils@npm:1.3.1" resolution: "@vitest/utils@npm:1.5.0"
dependencies: dependencies:
diff-sequences: "npm:^29.6.3" diff-sequences: "npm:^29.6.3"
estree-walker: "npm:^3.0.3" estree-walker: "npm:^3.0.3"
loupe: "npm:^2.3.7" loupe: "npm:^2.3.7"
pretty-format: "npm:^29.7.0" pretty-format: "npm:^29.7.0"
checksum: 10c0/d604c8ad3b1aee30d4dcd889098f591407bfe18547ff96485b1d1ed54eff58219c756a9544a7fbd4e37886863abacd7a89a76334cb3ea7f84c3d496bb757db23 checksum: 10c0/b9d779ea5c1a5759df4f59e3ba3f3a94816d6c600afe7a2d14b963ea114ce1acedbfe678cbfacb0a20d33cabcce890a08b2ce3fd52e3465f2e0969cc39f7686b
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3855,7 +3865,7 @@ __metadata:
"@types/masonry-layout": "npm:^4.2.5" "@types/masonry-layout": "npm:^4.2.5"
"@typescript-eslint/eslint-plugin": "npm:^5.36.2" "@typescript-eslint/eslint-plugin": "npm:^5.36.2"
"@typescript-eslint/parser": "npm:^5.36.2" "@typescript-eslint/parser": "npm:^5.36.2"
"@vitest/coverage-istanbul": "npm:^1.3.1" "@vitest/coverage-istanbul": "npm:^1.5.0"
component-emitter: "npm:^1.3.0" component-emitter: "npm:^1.3.0"
crypto: "npm:^1.0.1" crypto: "npm:^1.0.1"
date-fns: "npm:^2.29.2" date-fns: "npm:^2.29.2"
@@ -3895,7 +3905,7 @@ __metadata:
vis-data: "npm:^7.1.9" vis-data: "npm:^7.1.9"
vis-timeline: "npm:^7.7.3" vis-timeline: "npm:^7.7.3"
vis-util: "npm:^5.0.2" vis-util: "npm:^5.0.2"
vitest: "npm:^1.3.1" vitest: "npm:^1.5.0"
vitest-mock-extended: "npm:^1.3.1" vitest-mock-extended: "npm:^1.3.1"
web-dialog: "npm:^0.0.11" web-dialog: "npm:^0.0.11"
xss: "npm:^1.0.14" xss: "npm:^1.0.14"
@@ -4938,14 +4948,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"istanbul-lib-source-maps@npm:^4.0.1": "istanbul-lib-source-maps@npm:^5.0.4":
version: 4.0.1 version: 5.0.4
resolution: "istanbul-lib-source-maps@npm:4.0.1" resolution: "istanbul-lib-source-maps@npm:5.0.4"
dependencies: dependencies:
"@jridgewell/trace-mapping": "npm:^0.3.23"
debug: "npm:^4.1.1" debug: "npm:^4.1.1"
istanbul-lib-coverage: "npm:^3.0.0" istanbul-lib-coverage: "npm:^3.0.0"
source-map: "npm:^0.6.1" checksum: 10c0/48b48294590675005ba439888a53157fc71a99d78321428f3ce5f64e28cdfb6bc6eb45871333f448437118ef56a0ef371f4958163e2c2d066d3a703415a71b2e
checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8011,10 +8021,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tinypool@npm:^0.8.2": "tinypool@npm:^0.8.3":
version: 0.8.2 version: 0.8.3
resolution: "tinypool@npm:0.8.2" resolution: "tinypool@npm:0.8.3"
checksum: 10c0/8998626614172fc37c394e9a14e701dc437727fc6525488a4d4fd42044a4b2b59d6f076d750cbf5c699f79c58dd4e40599ab09e2f1ae0df4b23516b98c9c3055 checksum: 10c0/c219d0cfb69de8e3cf17403034a508d773f2fccaad79a13cdbad68600c4fb10186ad814d2320bcaa8f6e774fff5666d2a3d3b241dc8a7ad9d970ee63fe620a32
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8487,9 +8497,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-node@npm:1.3.1": "vite-node@npm:1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "vite-node@npm:1.3.1" resolution: "vite-node@npm:1.5.0"
dependencies: dependencies:
cac: "npm:^6.7.14" cac: "npm:^6.7.14"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
@@ -8498,7 +8508,7 @@ __metadata:
vite: "npm:^5.0.0" vite: "npm:^5.0.0"
bin: bin:
vite-node: vite-node.mjs vite-node: vite-node.mjs
checksum: 10c0/b50665ef224f3527f856ab88a0cfabab36dd6e2dd1e3edca8f8f25d5d33754e1050495472c2c82147d0dcf7c5280971dae2f37a531c10f3941d8d3344e34ce0b checksum: 10c0/85f9e8616f3612c71193a2bbdfb00f610685108a65eaa7ac535655ca1bd65dda5436ee577465436f715966f34e4b25b54140bbd584fe2c95ea0726acf4ecd351
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8554,15 +8564,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vitest@npm:^1.3.1": "vitest@npm:^1.5.0":
version: 1.3.1 version: 1.5.0
resolution: "vitest@npm:1.3.1" resolution: "vitest@npm:1.5.0"
dependencies: dependencies:
"@vitest/expect": "npm:1.3.1" "@vitest/expect": "npm:1.5.0"
"@vitest/runner": "npm:1.3.1" "@vitest/runner": "npm:1.5.0"
"@vitest/snapshot": "npm:1.3.1" "@vitest/snapshot": "npm:1.5.0"
"@vitest/spy": "npm:1.3.1" "@vitest/spy": "npm:1.5.0"
"@vitest/utils": "npm:1.3.1" "@vitest/utils": "npm:1.5.0"
acorn-walk: "npm:^8.3.2" acorn-walk: "npm:^8.3.2"
chai: "npm:^4.3.10" chai: "npm:^4.3.10"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
@@ -8574,15 +8584,15 @@ __metadata:
std-env: "npm:^3.5.0" std-env: "npm:^3.5.0"
strip-literal: "npm:^2.0.0" strip-literal: "npm:^2.0.0"
tinybench: "npm:^2.5.1" tinybench: "npm:^2.5.1"
tinypool: "npm:^0.8.2" tinypool: "npm:^0.8.3"
vite: "npm:^5.0.0" vite: "npm:^5.0.0"
vite-node: "npm:1.3.1" vite-node: "npm:1.5.0"
why-is-node-running: "npm:^2.2.2" why-is-node-running: "npm:^2.2.2"
peerDependencies: peerDependencies:
"@edge-runtime/vm": "*" "@edge-runtime/vm": "*"
"@types/node": ^18.0.0 || >=20.0.0 "@types/node": ^18.0.0 || >=20.0.0
"@vitest/browser": 1.3.1 "@vitest/browser": 1.5.0
"@vitest/ui": 1.3.1 "@vitest/ui": 1.5.0
happy-dom: "*" happy-dom: "*"
jsdom: "*" jsdom: "*"
peerDependenciesMeta: peerDependenciesMeta:
@@ -8600,7 +8610,7 @@ __metadata:
optional: true optional: true
bin: bin:
vitest: vitest.mjs vitest: vitest.mjs
checksum: 10c0/66d312a3dc12e67bba22d31332d939e89cd17d38531893c7b13b8826704564031c1dde795df2799b855660572c19a595301e920710c7775d072ee6332502efc5 checksum: 10c0/87f6666cccd52678a0e1dbf32e7492e4003580a168114221e4c2d404d53097d886a8be4c5c379e347d0e749affa0ea5416e4e3a0356aeca228a77ee54891ca3c
languageName: node languageName: node
linkType: hard linkType: hard