mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-12 13:44:27 +00:00
Compare commits
10 Commits
template-b
...
migrate-ha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c27d127fed | ||
|
|
4ceb4c3c2c | ||
|
|
cebdb46989 | ||
|
|
5aeae9ffa5 | ||
|
|
2ce62841cf | ||
|
|
63c9b85e6c | ||
|
|
03ace97a7e | ||
|
|
9edcfaf6b3 | ||
|
|
5cb7fdbfed | ||
|
|
5a0e1e89e6 |
@@ -3,37 +3,68 @@ title: Switch / Toggle
|
||||
---
|
||||
|
||||
<style>
|
||||
ha-switch {
|
||||
display: block;
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Switch `<ha-switch>`
|
||||
|
||||
A toggle switch can represent two states: on and off.
|
||||
A toggle switch representing two states: on and off.
|
||||
|
||||
## Examples
|
||||
## Implementation
|
||||
|
||||
Switch in on state
|
||||
### Example usage
|
||||
|
||||
<div class="wrapper">
|
||||
<ha-switch checked></ha-switch>
|
||||
<ha-switch></ha-switch>
|
||||
<ha-switch disabled></ha-switch>
|
||||
<ha-switch disabled checked></ha-switch>
|
||||
</div>
|
||||
|
||||
```html
|
||||
<ha-switch checked></ha-switch>
|
||||
|
||||
Switch in off state
|
||||
<ha-switch></ha-switch>
|
||||
|
||||
Disabled switch
|
||||
<ha-switch disabled></ha-switch>
|
||||
|
||||
## CSS variables
|
||||
<ha-switch disabled checked></ha-switch>
|
||||
```
|
||||
|
||||
For the switch / toggle there are always two variables, one for the on / checked state and one for the off / unchecked state.
|
||||
### API
|
||||
|
||||
The track element (background rounded rectangle that the round circular handle travels on) is set to being half transparent, so the final color will also be impacted by the color behind the track.
|
||||
This component is based on the webawesome switch component.
|
||||
Check the [webawesome documentation](https://webawesome.com/docs/components/switch/) for more details.
|
||||
|
||||
`switch-checked-color` / `switch-unchecked-color`
|
||||
Set both the color of the round handle and the track behind it. If you want to control them separately, use the variables below instead.
|
||||
**Properties/Attributes**
|
||||
|
||||
`switch-checked-button-color` / `switch-unchecked-button-color`
|
||||
Color of the round handle
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| checked | Boolean | false | The checked state of the switch. |
|
||||
| disabled | Boolean | false | Disables the switch and prevents user interaction. |
|
||||
| required | Boolean | false | Makes the switch a required field. |
|
||||
| haptic | Boolean | false | Enables haptic vibration on toggle. Only use when the new state is applied immediately (not when save is required). |
|
||||
|
||||
`switch-checked-track-color` / `switch-unchecked-track-color`
|
||||
Color of the track behind the round handle
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-switch-size` - The size of the switch track height. Defaults to `14px`.
|
||||
- `--ha-switch-thumb-size` - The size of the thumb. Defaults to `20px`.
|
||||
- `--ha-switch-width` - The width of the switch track. Defaults to `36px`.
|
||||
- `--ha-switch-box-shadow` - The box shadow of the thumb. Defaults to `var(--ha-box-shadow-s)`.
|
||||
- `--ha-switch-background-color` - Background color of the unchecked track.
|
||||
- `--ha-switch-border-color` - Border color of the unchecked track and thumb.
|
||||
- `--ha-switch-thumb-background-color` - Background color of the unchecked thumb.
|
||||
- `--ha-switch-background-color-hover` - Background color of the unchecked track on hover.
|
||||
- `--ha-switch-thumb-background-color-hover` - Background color of the unchecked thumb on hover.
|
||||
- `--ha-switch-checked-background-color` - Background color of the checked track.
|
||||
- `--ha-switch-checked-border-color` - Border color of the checked track.
|
||||
- `--ha-switch-checked-thumb-background-color` - Background color of the checked thumb.
|
||||
- `--ha-switch-checked-thumb-border-color` - Border color of the checked thumb.
|
||||
- `--ha-switch-checked-background-color-hover` - Background color of the checked track on hover.
|
||||
- `--ha-switch-checked-thumb-background-color-hover` - Background color of the checked thumb on hover.
|
||||
- `--ha-switch-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
- `--ha-switch-required-marker-offset` - Offset of the required marker. Defaults to `0.1rem`.
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.6",
|
||||
"marked": "18.0.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -140,7 +140,7 @@
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.59.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@lokalise/node-api": "15.7.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
@@ -189,7 +189,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.0.1",
|
||||
"jsdom": "29.0.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.4.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
|
||||
@@ -174,6 +174,7 @@ export class HaChartBase extends LitElement {
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
this._updateSankeyRoam();
|
||||
// drag to zoom
|
||||
this.chart?.dispatchAction({
|
||||
type: "takeGlobalCursor",
|
||||
@@ -192,6 +193,7 @@ export class HaChartBase extends LitElement {
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
this._updateSankeyRoam();
|
||||
this.chart?.dispatchAction({
|
||||
type: "takeGlobalCursor",
|
||||
key: "dataZoomSelect",
|
||||
@@ -267,6 +269,9 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
if (Object.keys(chartOptions).length > 0) {
|
||||
this._setChartOptions(chartOptions);
|
||||
if (chartOptions.series) {
|
||||
this._updateSankeyRoam();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +456,22 @@ export class HaChartBase extends LitElement {
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
});
|
||||
this.chart.on("sankeyroam", () => {
|
||||
const option = this.chart!.getOption();
|
||||
const series = option.series as any[];
|
||||
const sankeySeries = series?.find((s: any) => s.type === "sankey");
|
||||
const zoomed = sankeySeries.zoom !== 1;
|
||||
this._isZoomed = zoomed;
|
||||
if (!zoomed) {
|
||||
// Reset center when fully zoomed out
|
||||
this.chart!.setOption({
|
||||
series: [{ id: sankeySeries.id, center: null }],
|
||||
});
|
||||
}
|
||||
fireEvent(this, "chart-sankeyroam", { zoom: sankeySeries.zoom });
|
||||
// Clear cached emphasis states so labels don't revert to pre-zoom sizes
|
||||
this.chart!.dispatchAction({ type: "downplay" });
|
||||
});
|
||||
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart.getZr().on("dblclick", this._handleClickZoom);
|
||||
@@ -549,6 +570,7 @@ export class HaChartBase extends LitElement {
|
||||
...this._createOptions(),
|
||||
series: this._getSeries(),
|
||||
});
|
||||
this._updateSankeyRoam();
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
@@ -988,6 +1010,26 @@ export class HaChartBase extends LitElement {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
// Handle sankey chart double-click zoom
|
||||
const option = this.chart.getOption();
|
||||
const allSeries = option.series as any[];
|
||||
const sankeySeries = allSeries?.filter((s: any) => s.type === "sankey");
|
||||
if (sankeySeries?.length) {
|
||||
if (this._isZoomed) {
|
||||
this._handleZoomReset();
|
||||
} else {
|
||||
this.chart.setOption({
|
||||
series: sankeySeries.map((s: any) => ({
|
||||
id: s.id,
|
||||
zoom: 2,
|
||||
})),
|
||||
});
|
||||
this._isZoomed = true;
|
||||
}
|
||||
if (sankeySeries.length === allSeries?.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const range = this._isZoomed
|
||||
? [0, 100]
|
||||
: [
|
||||
@@ -1012,6 +1054,37 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||
// Reset sankey roam zoom
|
||||
const option = this.chart?.getOption();
|
||||
const sankeySeries = (option?.series as any[])?.filter(
|
||||
(s: any) => s.type === "sankey"
|
||||
);
|
||||
if (sankeySeries?.length) {
|
||||
this.chart?.setOption({
|
||||
series: sankeySeries.map((s: any) => ({
|
||||
id: s.id,
|
||||
zoom: 1,
|
||||
center: null,
|
||||
})),
|
||||
});
|
||||
this._isZoomed = false;
|
||||
fireEvent(this, "chart-sankeyroam", { zoom: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSankeyRoam() {
|
||||
const option = this.chart?.getOption();
|
||||
const sankeySeries = (option?.series as any[])?.filter(
|
||||
(s: any) => s.type === "sankey"
|
||||
);
|
||||
if (sankeySeries?.length) {
|
||||
this.chart?.setOption({
|
||||
series: sankeySeries.map((s: any) => ({
|
||||
id: s.id,
|
||||
roam: this._modifierPressed || this._isTouchDevice ? true : "move",
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDataZoomEvent(e: any) {
|
||||
@@ -1382,5 +1455,6 @@ declare global {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
"chart-sankeyroam": { zoom: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
public chart?: EChartsType;
|
||||
|
||||
private _currentZoom = 1;
|
||||
|
||||
@state() private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect,
|
||||
});
|
||||
@@ -84,11 +86,13 @@ export class HaSankeyChart extends LitElement {
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._createData(this.data, this._sizeController.value?.width)}
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-sankeyroam=${this._handleChartSankeyRoam}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
@@ -109,6 +113,10 @@ export class HaSankeyChart extends LitElement {
|
||||
return null;
|
||||
};
|
||||
|
||||
private _handleChartSankeyRoam = (ev: CustomEvent) => {
|
||||
this._currentZoom = ev.detail.zoom;
|
||||
};
|
||||
|
||||
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
|
||||
const detail = ev.detail;
|
||||
// Only handle node clicks (not links)
|
||||
@@ -180,6 +188,7 @@ export class HaSankeyChart extends LitElement {
|
||||
})),
|
||||
links,
|
||||
draggable: false,
|
||||
scaleLimit: { min: 1, max: 4 },
|
||||
orient: this.vertical ? "vertical" : "horizontal",
|
||||
nodeWidth: 15,
|
||||
nodeGap: NODE_GAP,
|
||||
@@ -210,7 +219,7 @@ export class HaSankeyChart extends LitElement {
|
||||
""
|
||||
);
|
||||
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
|
||||
const availableWidth = params.rect.width + 6;
|
||||
const availableWidth = (params.rect.width + 6) * this._currentZoom;
|
||||
const fontSize = Math.min(
|
||||
FONT_SIZE,
|
||||
(availableWidth / wordWidth) * FONT_SIZE
|
||||
@@ -223,7 +232,7 @@ export class HaSankeyChart extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
const availableHeight = params.rect.height + 8; // account for the margin
|
||||
const availableHeight = (params.rect.height + 8) * this._currentZoom; // account for the margin
|
||||
const fontSize = Math.min(
|
||||
(availableHeight / params.labelRect.height) * FONT_SIZE,
|
||||
FONT_SIZE
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiDevices, mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import { mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -235,22 +235,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
addGroup("views", views);
|
||||
addGroup("other_routes", otherRoutes);
|
||||
|
||||
if (
|
||||
searchString &&
|
||||
!this._navigationItems.some((navItem) => navItem.id === searchString)
|
||||
) {
|
||||
items.push({
|
||||
id: searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.add_custom_path"
|
||||
),
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
sorting_label: searchString,
|
||||
group: "other_routes",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,49 +1,195 @@
|
||||
import { SwitchBase } from "@material/mwc-switch/deprecated/mwc-switch-base";
|
||||
import { styles } from "@material/mwc-switch/deprecated/mwc-switch.css";
|
||||
import { css } from "lit";
|
||||
import Switch from "@home-assistant/webawesome/dist/components/switch/switch";
|
||||
import { css, type CSSResultGroup, type PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
|
||||
/**
|
||||
* Home Assistant switch component
|
||||
*
|
||||
* @element ha-switch
|
||||
* @extends {Switch}
|
||||
*
|
||||
* @summary
|
||||
* A toggle switch component supporting Home Assistant theming, based on the webawesome switch.
|
||||
* Represents two states: on and off.
|
||||
*
|
||||
* @cssprop --ha-switch-size - The size of the switch track height. Defaults to `14px`.
|
||||
* @cssprop --ha-switch-thumb-size - The size of the thumb. Defaults to `20px`.
|
||||
* @cssprop --ha-switch-width - The width of the switch track. Defaults to `36px`.
|
||||
* @cssprop --ha-switch-box-shadow - The box shadow of the thumb. Defaults to `var(--ha-box-shadow-s)`.
|
||||
* @cssprop --ha-switch-background-color - Background color of the unchecked track.
|
||||
* @cssprop --ha-switch-border-color - Border color of the unchecked track and thumb.
|
||||
* @cssprop --ha-switch-thumb-background-color - Background color of the unchecked thumb.
|
||||
* @cssprop --ha-switch-background-color-hover - Background color of the unchecked track on hover.
|
||||
* @cssprop --ha-switch-thumb-background-color-hover - Background color of the unchecked thumb on hover.
|
||||
* @cssprop --ha-switch-checked-background-color - Background color of the checked track.
|
||||
* @cssprop --ha-switch-checked-border-color - Border color of the checked track.
|
||||
* @cssprop --ha-switch-checked-thumb-background-color - Background color of the checked thumb.
|
||||
* @cssprop --ha-switch-checked-thumb-border-color - Border color of the checked thumb.
|
||||
* @cssprop --ha-switch-checked-background-color-hover - Background color of the checked track on hover.
|
||||
* @cssprop --ha-switch-checked-thumb-background-color-hover - Background color of the checked thumb on hover.
|
||||
* @cssprop --ha-switch-required-marker - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
* @cssprop --ha-switch-required-marker-offset - Offset of the required marker. Defaults to `0.1rem`.
|
||||
*
|
||||
* @attr {boolean} checked - The checked state of the switch.
|
||||
* @attr {boolean} disabled - Disables the switch and prevents user interaction.
|
||||
* @attr {boolean} required - Makes the switch a required field.
|
||||
* @attr {boolean} haptic - Enables haptic vibration on toggle. Only use when the new state is applied immediately (not when a save action is required).
|
||||
*/
|
||||
@customElement("ha-switch")
|
||||
export class HaSwitch extends SwitchBase {
|
||||
// Generate a haptic vibration.
|
||||
// Only set to true if the new value of the switch is applied right away when toggling.
|
||||
// Do not add haptic when a user is required to press save.
|
||||
export class HaSwitch extends Switch {
|
||||
/**
|
||||
* Enables haptic vibration on toggle.
|
||||
* Only set to true if the new value of the switch is applied right away when toggling.
|
||||
* Do not add haptic when a user is required to press save.
|
||||
*/
|
||||
@property({ type: Boolean }) public haptic = false;
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.addEventListener("change", () => {
|
||||
public updated(changedProperties: PropertyValues<typeof this>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("haptic")) {
|
||||
if (this.haptic) {
|
||||
forwardHaptic(this, "light");
|
||||
this.addEventListener("change", this._forwardHaptic);
|
||||
} else {
|
||||
this.removeEventListener("change", this._forwardHaptic);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-theme-secondary: var(--switch-checked-color);
|
||||
}
|
||||
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
|
||||
background-color: var(--switch-checked-button-color);
|
||||
border-color: var(--switch-checked-button-color);
|
||||
}
|
||||
.mdc-switch.mdc-switch--checked .mdc-switch__track {
|
||||
background-color: var(--switch-checked-track-color);
|
||||
border-color: var(--switch-checked-track-color);
|
||||
}
|
||||
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
|
||||
background-color: var(--switch-unchecked-button-color);
|
||||
border-color: var(--switch-unchecked-button-color);
|
||||
}
|
||||
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
|
||||
background-color: var(--switch-unchecked-track-color);
|
||||
border-color: var(--switch-unchecked-track-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("change", this._forwardHaptic);
|
||||
}
|
||||
|
||||
private _forwardHaptic = () => {
|
||||
forwardHaptic(this, "light");
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Switch.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-toggle-size: var(--ha-switch-size, 14px);
|
||||
--wa-form-control-required-content: var(
|
||||
--ha-switch-required-marker,
|
||||
var(--ha-input-required-marker, "*")
|
||||
);
|
||||
--wa-form-control-required-content-offset: var(
|
||||
--ha-switch-required-marker-offset,
|
||||
0.1rem
|
||||
);
|
||||
--thumb-size: var(--ha-switch-thumb-size, 20px);
|
||||
--width: var(--ha-switch-width, 36px);
|
||||
}
|
||||
|
||||
label {
|
||||
height: max(var(--thumb-size), var(--wa-form-control-toggle-size));
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.switch {
|
||||
background-color: var(
|
||||
--ha-switch-background-color,
|
||||
var(--ha-color-form-background)
|
||||
);
|
||||
border-color: var(
|
||||
--ha-switch-border-color,
|
||||
var(--ha-color-border-neutral-normal)
|
||||
);
|
||||
}
|
||||
|
||||
.switch .thumb {
|
||||
background-color: var(
|
||||
--ha-switch-thumb-background-color,
|
||||
var(--ha-color-form-background)
|
||||
);
|
||||
border-color: var(
|
||||
--ha-switch-border-color,
|
||||
var(--ha-color-border-neutral-normal)
|
||||
);
|
||||
border-style: var(--wa-form-control-border-style);
|
||||
border-width: var(--wa-form-control-border-width);
|
||||
box-shadow: var(--ha-switch-box-shadow, var(--ha-box-shadow-s));
|
||||
}
|
||||
|
||||
label:not(.disabled):hover .switch,
|
||||
label:not(.disabled) .input:focus-visible ~ .switch,
|
||||
label:not(.disabled):active .switch {
|
||||
background-color: var(
|
||||
--ha-switch-background-color-hover,
|
||||
var(
|
||||
--ha-switch-background-color,
|
||||
var(--ha-color-fill-neutral-normal-hover)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
label:not(.disabled):hover .switch .thumb,
|
||||
label:not(.disabled) .input:focus-visible ~ .switch .thumb,
|
||||
label:not(.disabled):active .switch .thumb {
|
||||
background-color: var(
|
||||
--ha-switch-thumb-background-color-hover,
|
||||
var(
|
||||
--ha-switch-thumb-background-color,
|
||||
var(--ha-color-form-background-hover)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.checked .switch {
|
||||
background-color: var(
|
||||
--ha-switch-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-resting)
|
||||
);
|
||||
border-color: var(
|
||||
--ha-switch-checked-border-color,
|
||||
var(--ha-color-border-primary-loud)
|
||||
);
|
||||
}
|
||||
|
||||
.checked .switch .thumb {
|
||||
background-color: var(
|
||||
--ha-switch-checked-thumb-background-color,
|
||||
var(--ha-color-fill-primary-loud-resting)
|
||||
);
|
||||
border-color: var(
|
||||
--ha-switch-checked-thumb-border-color,
|
||||
var(--ha-color-fill-primary-loud-resting)
|
||||
);
|
||||
}
|
||||
|
||||
label:not(.disabled).checked:hover .switch,
|
||||
label:not(.disabled).checked .input:focus-visible ~ .switch,
|
||||
label:not(.disabled).checked:active .switch {
|
||||
background-color: var(
|
||||
--ha-switch-checked-background-color-hover,
|
||||
var(
|
||||
--ha-switch-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-hover)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
label:not(.disabled).checked:hover .switch .thumb,
|
||||
label:not(.disabled).checked .input:focus-visible ~ .switch .thumb,
|
||||
label:not(.disabled).checked:active .switch .thumb {
|
||||
background-color: var(
|
||||
--ha-switch-checked-thumb-background-color-hover,
|
||||
var(
|
||||
--ha-switch-checked-thumb-background-color,
|
||||
var(--ha-color-fill-primary-loud-hover)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
label.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const isExternal =
|
||||
window.externalAppV2 ||
|
||||
window.externalApp ||
|
||||
window.webkit?.messageHandlers?.getExternalAuth ||
|
||||
location.search.includes("external_auth=1");
|
||||
export const isExternalAndroid = window.externalApp || window.externalAppV2;
|
||||
|
||||
@@ -5,6 +5,11 @@ import { Auth } from "home-assistant-js-websocket";
|
||||
import type { EMMessage } from "./external_messaging";
|
||||
import { ExternalMessaging } from "./external_messaging";
|
||||
|
||||
/**
|
||||
* WARNING: These constants should not be changed, as the native app relies on
|
||||
* these exact string values to know which callback to call.
|
||||
* This happens after getting a response from the native app.
|
||||
*/
|
||||
const CALLBACK_SET_TOKEN = "externalAuthSetToken";
|
||||
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
|
||||
|
||||
@@ -28,6 +33,9 @@ declare global {
|
||||
revokeExternalAuth(payload: string);
|
||||
externalBus(payload: string);
|
||||
};
|
||||
externalAppV2?: {
|
||||
postMessage(payload: string): void;
|
||||
};
|
||||
webkit?: {
|
||||
messageHandlers: {
|
||||
getExternalAuth: {
|
||||
@@ -44,9 +52,9 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.externalApp && !window.webkit) {
|
||||
if (!window.externalApp && !window.webkit && !window.externalAppV2) {
|
||||
throw new Error(
|
||||
"External auth requires either externalApp or webkit defined on Window object."
|
||||
"External auth requires either externalApp, externalAppV2, or webkit defined on Window object."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +103,11 @@ export class ExternalAuth extends Auth {
|
||||
// we sleep 1 microtask to get the promise to actually set it on the window object.
|
||||
await Promise.resolve();
|
||||
|
||||
if (window.externalApp) {
|
||||
if (window.externalAppV2) {
|
||||
window.externalAppV2.postMessage(
|
||||
JSON.stringify({ type: "getExternalAuth", payload })
|
||||
);
|
||||
} else if (window.externalApp) {
|
||||
window.externalApp.getExternalAuth(JSON.stringify(payload));
|
||||
} else {
|
||||
window.webkit!.messageHandlers.getExternalAuth.postMessage(payload);
|
||||
@@ -119,7 +131,11 @@ export class ExternalAuth extends Auth {
|
||||
// we sleep 1 microtask to get the promise to actually set it on the window object.
|
||||
await Promise.resolve();
|
||||
|
||||
if (window.externalApp) {
|
||||
if (window.externalAppV2) {
|
||||
window.externalAppV2.postMessage(
|
||||
JSON.stringify({ type: "revokeExternalAuth", payload })
|
||||
);
|
||||
} else if (window.externalApp) {
|
||||
window.externalApp.revokeExternalAuth(JSON.stringify(payload));
|
||||
} else {
|
||||
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(payload);
|
||||
@@ -132,6 +148,7 @@ export class ExternalAuth extends Auth {
|
||||
export const createExternalAuth = async (hassUrl: string) => {
|
||||
const auth = new ExternalAuth(hassUrl);
|
||||
if (
|
||||
window.externalAppV2 ||
|
||||
window.externalApp?.externalBus ||
|
||||
(window.webkit && window.webkit.messageHandlers.externalBus)
|
||||
) {
|
||||
|
||||
@@ -187,6 +187,11 @@ interface EMOutgoingMessageFocusElement extends EMMessage {
|
||||
};
|
||||
}
|
||||
|
||||
// These types are handled internally by the Android app via postMessage.
|
||||
// They are not sent by the frontend and should not be used directly.
|
||||
// They are intentionally listed here to prevent anyone from using them unintentionally.
|
||||
type RejectedEMMessageType = "onHomeAssistantSetTheme" | "handleBlob";
|
||||
|
||||
type EMOutgoingMessageWithoutAnswer =
|
||||
| EMMessageResultError
|
||||
| EMMessageResultSuccess
|
||||
@@ -393,8 +398,16 @@ export class ExternalMessaging {
|
||||
* Send message to external app that expects a response.
|
||||
* @param msg message to send
|
||||
*/
|
||||
public sendMessage<T extends keyof EMOutgoingMessageWithAnswer>(
|
||||
msg: EMOutgoingMessageWithAnswer[T]["request"]
|
||||
public sendMessage<
|
||||
T extends keyof EMOutgoingMessageWithAnswer,
|
||||
TType extends string = EMOutgoingMessageWithAnswer[T]["request"]["type"],
|
||||
>(
|
||||
msg: EMOutgoingMessageWithAnswer[T]["request"] & {
|
||||
type: TType &
|
||||
(TType extends RejectedEMMessageType
|
||||
? "ERROR: message type is rejected"
|
||||
: {});
|
||||
}
|
||||
): Promise<EMOutgoingMessageWithAnswer[T]["response"]> {
|
||||
const msgId = ++this.msgId;
|
||||
msg.id = msgId;
|
||||
@@ -412,7 +425,14 @@ export class ExternalMessaging {
|
||||
* Send message to external app without expecting a response.
|
||||
* @param msg message to send
|
||||
*/
|
||||
public fireMessage(msg: EMOutgoingMessageWithoutAnswer) {
|
||||
public fireMessage<T extends string>(
|
||||
msg: EMOutgoingMessageWithoutAnswer & {
|
||||
type: T &
|
||||
(T extends RejectedEMMessageType
|
||||
? "ERROR: message type is rejected"
|
||||
: {});
|
||||
}
|
||||
) {
|
||||
if (!msg.id) {
|
||||
msg.id = ++this.msgId;
|
||||
}
|
||||
@@ -473,7 +493,11 @@ export class ExternalMessaging {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Sending message to external app", msg);
|
||||
}
|
||||
if (window.externalApp) {
|
||||
if (window.externalAppV2) {
|
||||
window.externalAppV2.postMessage(
|
||||
JSON.stringify({ type: "externalBus", payload: msg })
|
||||
);
|
||||
} else if (window.externalApp) {
|
||||
window.externalApp.externalBus(JSON.stringify(msg));
|
||||
} else {
|
||||
window.webkit!.messageHandlers.externalBus.postMessage(msg);
|
||||
|
||||
@@ -122,11 +122,15 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: canInstallUpdates.length,
|
||||
})}
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
.isInstallable=${true}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</div>
|
||||
@@ -137,11 +141,18 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: notInstallableUpdates.length,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${notInstallableUpdates}
|
||||
.isInstallable=${false}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</div>
|
||||
@@ -236,6 +247,11 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.no-updates {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -293,62 +292,60 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
full-width
|
||||
>
|
||||
${repairsIssues.length || canInstallUpdates.length
|
||||
? html`<ha-card outlined>
|
||||
? html`<div class="dashboard-alerts">
|
||||
${repairsIssues.length
|
||||
? html`
|
||||
<ha-config-repairs
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalRepairIssues}
|
||||
.repairsIssues=${repairsIssues}
|
||||
></ha-config-repairs>
|
||||
${totalRepairIssues > repairsIssues.length
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
href="/config/repairs"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.repairs.more_repairs",
|
||||
{
|
||||
count:
|
||||
totalRepairIssues - repairsIssues.length,
|
||||
}
|
||||
)}
|
||||
>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
: ""}
|
||||
<ha-card outlined class="dashboard-alert-card">
|
||||
<div
|
||||
class="dashboard-alert-title"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
<a href="/config/repairs?historyBack=1">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.repairs.title",
|
||||
{
|
||||
count: totalRepairIssues,
|
||||
}
|
||||
)}
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</a>
|
||||
</div>
|
||||
<ha-config-repairs
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.repairsIssues=${repairsIssues}
|
||||
></ha-config-repairs>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${repairsIssues.length && canInstallUpdates.length
|
||||
? html`<hr />`
|
||||
: ""}
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalUpdates}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
.isInstallable=${true}
|
||||
></ha-config-updates>
|
||||
${totalUpdates > canInstallUpdates.length
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
href="/config/updates"
|
||||
label=${this.hass.localize(
|
||||
"ui.panel.config.updates.more_updates",
|
||||
{
|
||||
count:
|
||||
totalUpdates - canInstallUpdates.length,
|
||||
}
|
||||
)}
|
||||
>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
: ""}
|
||||
<ha-card outlined class="dashboard-alert-card">
|
||||
<div
|
||||
class="dashboard-alert-title"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
<a href="/config/updates?historyBack=1">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title",
|
||||
{
|
||||
count: totalUpdates,
|
||||
}
|
||||
)}
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</a>
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
></ha-config-updates>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>`
|
||||
</div>`
|
||||
: ""}
|
||||
${this._pages(
|
||||
this.cloudStatus,
|
||||
@@ -427,10 +424,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host(:not([narrow])) ha-card:last-child {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
ha-config-section {
|
||||
margin: auto;
|
||||
margin-top: -32px;
|
||||
@@ -439,20 +432,34 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-card a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-assist-chip {
|
||||
margin: 8px 16px 16px 16px;
|
||||
.dashboard-alerts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.title {
|
||||
.dashboard-alert-title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard-alert-title a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.dashboard-alert-title ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
@@ -477,16 +484,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
.keep-together {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
background-color: var(
|
||||
--ha-card-border-color,
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public updateEntities?: UpdateEntity[];
|
||||
|
||||
@property({ type: Number }) public total?: number;
|
||||
|
||||
@property({ attribute: false }) public isInstallable = true;
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
@@ -90,18 +86,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
const updates = this.updateEntities;
|
||||
|
||||
return html`
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.isInstallable
|
||||
? this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.total || this.updateEntities.length,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: this.total || this.updateEntities.length,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<ha-md-list>
|
||||
${updates.map((entity) => {
|
||||
const entityEntry = this.getEntityEntry(entity.entity_id);
|
||||
@@ -181,11 +165,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup[] {
|
||||
return [
|
||||
css`
|
||||
.title {
|
||||
font-size: var(--ha-font-size-l);
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.skipped {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
@@ -277,8 +277,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
|
||||
if (!deviceClasses || this._hideDeviceClassOverride(domain)) {
|
||||
this._deviceClassOptions = undefined;
|
||||
if (!deviceClasses) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,16 +291,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _hideDeviceClassOverride(domain: string): boolean {
|
||||
// Template binary sensor device_class should be edited via template options,
|
||||
// not the entity registry override UI used by other binary sensors.
|
||||
return (
|
||||
domain === "binary_sensor" &&
|
||||
this.entry.platform === "template" &&
|
||||
!!this.entry.config_entry_id
|
||||
);
|
||||
}
|
||||
|
||||
private _precisionLabel(precision?: number, stateValue?: string) {
|
||||
const stateValueNumber = Number(stateValue);
|
||||
const value = !isNaN(stateValueNumber) ? stateValue! : 0;
|
||||
@@ -536,7 +525,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
`
|
||||
: nothing} `
|
||||
: nothing}
|
||||
${this._deviceClassOptions && !this._hideDeviceClassOverride(domain)
|
||||
${this._deviceClassOptions
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
|
||||
@@ -24,12 +24,15 @@ import {
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
@@ -64,6 +67,7 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { multiTermSearch } from "../../../resources/fuseMultiTerm";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
@@ -74,7 +78,6 @@ import type { HaConfigEntryRow } from "./ha-config-entry-row";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { showPickConfigEntryDialog } from "./show-pick-config-entry-dialog";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
|
||||
export interface SubEntryData {
|
||||
subEntry: SubEntry;
|
||||
@@ -156,6 +159,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
window.location.hash.substring(1)
|
||||
);
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@state() private _subEntries: Record<string, SubEntry[]> = {};
|
||||
|
||||
private _subEntriesFetchId = 0;
|
||||
@@ -218,6 +223,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
this.hass.loadBackendTranslation("config", [this.domain]);
|
||||
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
|
||||
this._extraConfigEntries = undefined;
|
||||
this._filter = "";
|
||||
this._subEntries = {};
|
||||
this._fetchManifest();
|
||||
this._fetchDiagnostics();
|
||||
@@ -333,7 +339,16 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
this.hass.locale.language,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
const filteredNormalData = this._filterNormalTree(
|
||||
normalData,
|
||||
this._filter,
|
||||
this.hass.areas
|
||||
);
|
||||
const filteredAttentionData = this._filterAttentionTree(
|
||||
attentionData,
|
||||
this._filter,
|
||||
this.hass.areas
|
||||
);
|
||||
const devicesRegs = this._getDevices(configEntries, this.hass.devices);
|
||||
const entities = this._getEntities(configEntries, this._entities);
|
||||
let numberOfEntities = entities.length;
|
||||
@@ -652,6 +667,13 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${normalData.length + attentionData.length > 0
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
></ha-input-search>`
|
||||
: nothing}
|
||||
${this._logInfo?.level === LogSeverity.DEBUG
|
||||
? html`<div class="section">
|
||||
<ha-alert alert-type="warning">
|
||||
@@ -698,7 +720,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${attentionFlows.length || attentionData.length
|
||||
${attentionFlows.length || filteredAttentionData.length
|
||||
? html`
|
||||
<div class="section">
|
||||
<h3 class="section-header">
|
||||
@@ -738,7 +760,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
})}
|
||||
</ha-md-list>`
|
||||
: nothing}
|
||||
${attentionData.map(
|
||||
${filteredAttentionData.map(
|
||||
(data) =>
|
||||
html`<ha-config-entry-row
|
||||
class="attention"
|
||||
@@ -765,22 +787,26 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
`ui.panel.config.integrations.integration_page.entries`
|
||||
)}
|
||||
</h3>
|
||||
${normalData.length === 0
|
||||
${filteredNormalData.length === 0
|
||||
? html`<div class="card-content no-entries">
|
||||
${this._manifest &&
|
||||
!this._manifest.config_flow &&
|
||||
this.hass.config.components.find(
|
||||
(comp) => comp.split(".")[0] === this.domain
|
||||
)
|
||||
${this._filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.yaml_entry"
|
||||
"ui.panel.config.integrations.none_found"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.no_entries"
|
||||
)}
|
||||
: this._manifest &&
|
||||
!this._manifest.config_flow &&
|
||||
this.hass.config.components.find(
|
||||
(comp) => comp.split(".")[0] === this.domain
|
||||
)
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.yaml_entry"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.no_entries"
|
||||
)}
|
||||
</div>`
|
||||
: html`
|
||||
${normalData.map(
|
||||
${filteredNormalData.map(
|
||||
(data) =>
|
||||
html`<ha-config-entry-row
|
||||
.hass=${this.hass}
|
||||
@@ -968,6 +994,110 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _buildAttentionEntryData = memoizeOne(this._buildEntryData);
|
||||
|
||||
private _filterTree = memoizeOne(
|
||||
(
|
||||
data: ConfigEntryData[],
|
||||
filter: string,
|
||||
areas: HomeAssistant["areas"]
|
||||
): ConfigEntryData[] => {
|
||||
if (!filter) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const DEVICE_KEYS = [
|
||||
"name",
|
||||
"manufacturer",
|
||||
"model",
|
||||
"sw_version",
|
||||
"area",
|
||||
];
|
||||
|
||||
const buildDeviceSearchable = (device: DeviceRegistryEntry) => ({
|
||||
device,
|
||||
name: device.name_by_user || device.name || "",
|
||||
manufacturer: device.manufacturer || "",
|
||||
model: device.model || "",
|
||||
sw_version: device.sw_version || "",
|
||||
area: device.area_id ? areas[device.area_id]?.name || "" : "",
|
||||
});
|
||||
|
||||
const matchDevices = (devices: DeviceRegistryEntry[]) =>
|
||||
multiTermSearch(
|
||||
devices.map(buildDeviceSearchable),
|
||||
filter,
|
||||
DEVICE_KEYS,
|
||||
undefined,
|
||||
{ keys: DEVICE_KEYS }
|
||||
).map((r) => r.device);
|
||||
|
||||
const TITLE_KEYS = ["title"];
|
||||
|
||||
const titleMatches = (title: string) =>
|
||||
multiTermSearch([{ title }], filter, TITLE_KEYS, undefined, {
|
||||
keys: TITLE_KEYS,
|
||||
}).length > 0;
|
||||
|
||||
const result: ConfigEntryData[] = [];
|
||||
|
||||
for (const entryData of data) {
|
||||
if (titleMatches(entryData.entry.title)) {
|
||||
result.push(entryData);
|
||||
continue;
|
||||
}
|
||||
|
||||
const filteredDevices = matchDevices(entryData.devices);
|
||||
const filteredServices = matchDevices(entryData.services);
|
||||
|
||||
const filteredSubEntries = entryData.subEntries
|
||||
.map((subData): SubEntryData | null => {
|
||||
if (titleMatches(subData.subEntry.title)) {
|
||||
return subData;
|
||||
}
|
||||
const subDevices = matchDevices(subData.devices);
|
||||
const subServices = matchDevices(subData.services);
|
||||
if (subDevices.length || subServices.length) {
|
||||
return {
|
||||
subEntry: subData.subEntry,
|
||||
devices: subDevices,
|
||||
services: subServices,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((s): s is SubEntryData => s !== null);
|
||||
|
||||
if (
|
||||
filteredDevices.length ||
|
||||
filteredServices.length ||
|
||||
filteredSubEntries.length
|
||||
) {
|
||||
result.push({
|
||||
entry: entryData.entry,
|
||||
devices: filteredDevices,
|
||||
services: filteredServices,
|
||||
subEntries: filteredSubEntries,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private _filterNormalTree = memoizeOne(
|
||||
(data: ConfigEntryData[], filter: string, areas: HomeAssistant["areas"]) =>
|
||||
this._filterTree(data, filter, areas)
|
||||
);
|
||||
|
||||
private _filterAttentionTree = memoizeOne(
|
||||
(data: ConfigEntryData[], filter: string, areas: HomeAssistant["areas"]) =>
|
||||
this._filterTree(data, filter, areas)
|
||||
);
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
this._filter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private async _handleEnableDebugLogging() {
|
||||
const integration = this.domain;
|
||||
await setIntegrationLogLevel(
|
||||
@@ -1198,6 +1328,10 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
ha-input-search {
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,11 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
<div class="card-content">
|
||||
${issues.length
|
||||
? html`
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize("ui.panel.config.repairs.title", {
|
||||
count: issues.length,
|
||||
})}
|
||||
</div>
|
||||
<ha-config-repairs
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@@ -193,6 +198,11 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.no-repairs {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,6 @@ class HaConfigRepairs extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public repairsIssues?: RepairsIssue[];
|
||||
|
||||
@property({ type: Number })
|
||||
public total?: number;
|
||||
|
||||
protected render() {
|
||||
if (!this.repairsIssues?.length) {
|
||||
return nothing;
|
||||
@@ -43,11 +40,6 @@ class HaConfigRepairs extends LitElement {
|
||||
const issues = this.repairsIssues;
|
||||
|
||||
return html`
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize("ui.panel.config.repairs.title", {
|
||||
count: this.total || this.repairsIssues.length,
|
||||
})}
|
||||
</div>
|
||||
<ha-md-list>
|
||||
${issues.map((issue) => {
|
||||
const domainName = domainToName(this.hass.localize, issue.domain);
|
||||
@@ -191,11 +183,6 @@ class HaConfigRepairs extends LitElement {
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-l);
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ignored {
|
||||
opacity: var(--light-secondary-opacity);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -286,6 +288,9 @@ class HuiEnergySankeyCard
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: device.included_in_stat,
|
||||
entityId: isExternalStatistic(device.stat_consumption)
|
||||
? undefined
|
||||
: device.stat_consumption,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
@@ -418,9 +423,11 @@ class HuiEnergySankeyCard
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.hass=${this.hass}
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
@node-click=${this._handleNodeClick}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||
@@ -435,6 +442,13 @@ class HuiEnergySankeyCard
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
if (node.entityId) {
|
||||
fireEvent(this, "hass-more-info", { entityId: node.entityId });
|
||||
}
|
||||
}
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
no_area: {
|
||||
|
||||
@@ -565,6 +565,7 @@ class HuiPowerSankeyCard
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.hass=${this.hass}
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -222,6 +224,9 @@ class HuiWaterSankeyCard
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: device.included_in_stat,
|
||||
entityId: isExternalStatistic(device.stat_consumption)
|
||||
? undefined
|
||||
: device.stat_consumption,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
@@ -354,9 +359,11 @@ class HuiWaterSankeyCard
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.hass=${this.hass}
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
@node-click=${this._handleNodeClick}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||
@@ -369,6 +376,13 @@ class HuiWaterSankeyCard
|
||||
private _valueFormatter = (value: number) =>
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} ${this._data!.waterUnit}`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
if (node.entityId) {
|
||||
fireEvent(this, "hass-more-info", { entityId: node.entityId });
|
||||
}
|
||||
}
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
no_area: {
|
||||
|
||||
@@ -34,6 +34,7 @@ class EntityIdPickerRow extends LitElement {
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
haptic
|
||||
.checked=${!!this.coreUserData &&
|
||||
!!this.coreUserData.showEntityIdPicker}
|
||||
.disabled=${this.coreUserData === undefined}
|
||||
|
||||
@@ -266,11 +266,6 @@ export const colorStyles = css`
|
||||
--sidebar-selected-text-color: var(--primary-color);
|
||||
--sidebar-selected-icon-color: var(--primary-color);
|
||||
--sidebar-icon-color: rgba(var(--rgb-primary-text-color), 0.6);
|
||||
--switch-checked-color: var(--primary-color);
|
||||
--switch-checked-button-color: var(--switch-checked-color, var(--primary-background-color));
|
||||
--switch-checked-track-color: var(--switch-checked-color, #000000);
|
||||
--switch-unchecked-button-color: var(--switch-unchecked-color, var(--primary-background-color));
|
||||
--switch-unchecked-track-color: var(--switch-unchecked-color, #000000);
|
||||
--slider-color: var(--primary-color);
|
||||
--slider-secondary-color: var(--light-primary-color);
|
||||
--slider-track-color: var(--scrollbar-thumb-color);
|
||||
@@ -352,8 +347,6 @@ export const darkColorStyles = css`
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
--disabled-text-color: #6f6f6f;
|
||||
--switch-unchecked-button-color: #999999;
|
||||
--switch-unchecked-track-color: #9b9b9b;
|
||||
--divider-color: rgba(225, 225, 225, 0.12);
|
||||
--outline-color: rgba(225, 225, 225, 0.12);
|
||||
--outline-hover-color: rgba(225, 225, 225, 0.24);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isExternalAndroid } from "../data/external";
|
||||
|
||||
// 10 seconds gives the Android WebView download listener enough time
|
||||
// to open the blob before it is revoked, while still freeing memory
|
||||
// promptly. Revoking immediately would invalidate the URL before the
|
||||
@@ -17,7 +19,7 @@ export const fileDownload = (href: string, filename = ""): void => {
|
||||
if (href.startsWith("blob:")) {
|
||||
// Revoke blob URLs after a delay on Android so the WebView download
|
||||
// listener has time to fetch the blob before it becomes invalid.
|
||||
if (window.externalApp) {
|
||||
if (isExternalAndroid) {
|
||||
setTimeout(() => URL.revokeObjectURL(href), BLOB_REVOKE_DELAY_MS);
|
||||
} else {
|
||||
URL.revokeObjectURL(href);
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fileDownload } from "../../src/util/file_download";
|
||||
|
||||
describe("fileDownload", () => {
|
||||
let appendChildSpy: ReturnType<typeof vi.spyOn>;
|
||||
let removeChildSpy: ReturnType<typeof vi.spyOn>;
|
||||
let dispatchEventSpy: ReturnType<typeof vi.spyOn>;
|
||||
let createdElement: HTMLAnchorElement;
|
||||
let fileDownload: (href: string, filename?: string) => void;
|
||||
|
||||
/**
|
||||
* Because isExternalAndroid is a module-level constant, we must set
|
||||
* window.externalApp / externalAppV2 *before* importing the module.
|
||||
* This helper resets the module registry and re-imports file_download
|
||||
* so the constant is evaluated with the current window state.
|
||||
*/
|
||||
async function loadFileDownload() {
|
||||
vi.resetModules();
|
||||
const mod = await import("../../src/util/file_download");
|
||||
fileDownload = mod.fileDownload;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -25,9 +37,11 @@ describe("fileDownload", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete (window as any).externalApp;
|
||||
delete (window as any).externalAppV2;
|
||||
});
|
||||
|
||||
it("sets href, download, and triggers a click", () => {
|
||||
it("sets href, download, and triggers a click", async () => {
|
||||
await loadFileDownload();
|
||||
fileDownload("https://example.com/file.json", "file.json");
|
||||
|
||||
expect(createdElement.href).toBe("https://example.com/file.json");
|
||||
@@ -37,26 +51,42 @@ describe("fileDownload", () => {
|
||||
expect(removeChildSpy).toHaveBeenCalledWith(createdElement);
|
||||
});
|
||||
|
||||
it("defaults filename to empty string", () => {
|
||||
it("defaults filename to empty string", async () => {
|
||||
await loadFileDownload();
|
||||
fileDownload("https://example.com/file.json");
|
||||
expect(createdElement.download).toBe("");
|
||||
});
|
||||
|
||||
it("does not revoke non-blob URLs", () => {
|
||||
it("does not revoke non-blob URLs", async () => {
|
||||
await loadFileDownload();
|
||||
fileDownload("https://example.com/file.json", "file.json");
|
||||
vi.runAllTimers();
|
||||
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("revokes blob URLs immediately outside Android", () => {
|
||||
it("revokes blob URLs immediately outside Android", async () => {
|
||||
await loadFileDownload();
|
||||
fileDownload("blob:http://localhost/abc-123", "file.json");
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith(
|
||||
"blob:http://localhost/abc-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("revokes blob URL after delay on Android", () => {
|
||||
it("revokes blob URL after delay on Android (externalApp)", async () => {
|
||||
(window as any).externalApp = {};
|
||||
await loadFileDownload();
|
||||
fileDownload("blob:http://localhost/abc-123", "file.json");
|
||||
vi.advanceTimersByTime(9_999);
|
||||
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith(
|
||||
"blob:http://localhost/abc-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("revokes blob URL after delay on Android (externalAppV2)", async () => {
|
||||
(window as any).externalAppV2 = { postMessage: vi.fn() };
|
||||
await loadFileDownload();
|
||||
fileDownload("blob:http://localhost/abc-123", "file.json");
|
||||
vi.advanceTimersByTime(9_999);
|
||||
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
|
||||
|
||||
54
yarn.lock
54
yarn.lock
@@ -18,29 +18,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@asamuzakjp/css-color@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "@asamuzakjp/css-color@npm:5.0.1"
|
||||
"@asamuzakjp/css-color@npm:^5.1.5":
|
||||
version: 5.1.6
|
||||
resolution: "@asamuzakjp/css-color@npm:5.1.6"
|
||||
dependencies:
|
||||
"@csstools/css-calc": "npm:^3.1.1"
|
||||
"@csstools/css-color-parser": "npm:^4.0.2"
|
||||
"@csstools/css-parser-algorithms": "npm:^4.0.0"
|
||||
"@csstools/css-tokenizer": "npm:^4.0.0"
|
||||
lru-cache: "npm:^11.2.6"
|
||||
checksum: 10/941ee630cd037b35d1d95db03ea9e483958e0a444bde61b1f4a7f84a787df5abfa83be7b4dd33f742811c6f194aeedf81ce70fa5ca2713d5c9eeacac0930e64b
|
||||
checksum: 10/5151369d9369e478e03c0eee0f171b8f86306ebbdf5b352544cd745c360d97343f437bdd0690ff658e47d2876b466bffc8811fcef7f0347cb243c6483a7e95a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@asamuzakjp/dom-selector@npm:^7.0.3":
|
||||
version: 7.0.3
|
||||
resolution: "@asamuzakjp/dom-selector@npm:7.0.3"
|
||||
"@asamuzakjp/dom-selector@npm:^7.0.6":
|
||||
version: 7.0.7
|
||||
resolution: "@asamuzakjp/dom-selector@npm:7.0.7"
|
||||
dependencies:
|
||||
"@asamuzakjp/nwsapi": "npm:^2.3.9"
|
||||
bidi-js: "npm:^1.0.3"
|
||||
css-tree: "npm:^3.2.1"
|
||||
is-potential-custom-element-name: "npm:^1.0.1"
|
||||
lru-cache: "npm:^11.2.7"
|
||||
checksum: 10/2b030f912035426707efd0d6fe5bb4eda1cc4a1c5d5d0d90333d3bbc93719d795048b988ba00ce130d260254dcda5d95316f2b330c0245ee98d8faa7b345c349
|
||||
checksum: 10/18f40def8c775c6008c8fcd75d7d049ff92d99a494929ab2bf742341b348c78cbf4808d29c13b9cd87ca4fd272773cf5aa9e58fee48603c286df48148be8cb67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2266,10 +2264,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lokalise/node-api@npm:15.6.1":
|
||||
version: 15.6.1
|
||||
resolution: "@lokalise/node-api@npm:15.6.1"
|
||||
checksum: 10/0533046b1271b299d64d1eb9d7561a2d71e8dfead329c9f3fffa7141c069072ce975e4fe1935e5605b5fbbb9abb915b02e3150435b0d852541d7ae01d6ee0026
|
||||
"@lokalise/node-api@npm:15.7.1":
|
||||
version: 15.7.1
|
||||
resolution: "@lokalise/node-api@npm:15.7.1"
|
||||
checksum: 10/a950c379778de0a5da29eedfb9ed92d96b2a10b68818d96a982ad3be0a699ab0d386ef9600403c1195d164390b034e88c51d66e674c94fb5013ea0ba55e6a5cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8893,7 +8891,7 @@ __metadata:
|
||||
"@lit-labs/virtualizer": "npm:2.1.1"
|
||||
"@lit/context": "npm:1.1.6"
|
||||
"@lit/reactive-element": "npm:2.1.2"
|
||||
"@lokalise/node-api": "npm:15.6.1"
|
||||
"@lokalise/node-api": "npm:15.7.1"
|
||||
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
|
||||
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
|
||||
"@material/mwc-base": "npm:0.27.0"
|
||||
@@ -8986,7 +8984,7 @@ __metadata:
|
||||
idb-keyval: "npm:6.2.2"
|
||||
intl-messageformat: "npm:11.2.0"
|
||||
js-yaml: "npm:4.1.1"
|
||||
jsdom: "npm:29.0.1"
|
||||
jsdom: "npm:29.0.2"
|
||||
jszip: "npm:3.10.1"
|
||||
leaflet: "npm:1.9.4"
|
||||
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
@@ -8999,7 +8997,7 @@ __metadata:
|
||||
lodash.template: "npm:4.5.0"
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:17.0.6"
|
||||
marked: "npm:18.0.0"
|
||||
memoize-one: "npm:6.0.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
object-hash: "npm:3.0.0"
|
||||
@@ -10032,12 +10030,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsdom@npm:29.0.1":
|
||||
version: 29.0.1
|
||||
resolution: "jsdom@npm:29.0.1"
|
||||
"jsdom@npm:29.0.2":
|
||||
version: 29.0.2
|
||||
resolution: "jsdom@npm:29.0.2"
|
||||
dependencies:
|
||||
"@asamuzakjp/css-color": "npm:^5.0.1"
|
||||
"@asamuzakjp/dom-selector": "npm:^7.0.3"
|
||||
"@asamuzakjp/css-color": "npm:^5.1.5"
|
||||
"@asamuzakjp/dom-selector": "npm:^7.0.6"
|
||||
"@bramus/specificity": "npm:^2.4.2"
|
||||
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1"
|
||||
"@exodus/bytes": "npm:^1.15.0"
|
||||
@@ -10062,7 +10060,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
checksum: 10/93d6b4e17f8ed6428beab09aecb43737325be658b58f474df4fcc46ef391826e98b3fbf30c51cd5ead5d05de2c85a932be87fdeada064adb5617322e0c8476ea
|
||||
checksum: 10/3ad1d9a5b6aba067427bc43be98e1c51fab489bf689a6530e596278c6326fe053c94fc47a9c133f126fbe914f421283ae723fb92214dfe4959ca6cf2ee1666f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10597,7 +10595,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.6, lru-cache@npm:^11.2.7":
|
||||
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.7":
|
||||
version: 11.2.7
|
||||
resolution: "lru-cache@npm:11.2.7"
|
||||
checksum: 10/fbff4b8dee8189dde9b52cdfb3ea89b4c9cec094c1538cd30d1f47299477ff312efdb35f7994477ec72328f8e754e232b26a143feda1bd1f79ff22da6664d2c5
|
||||
@@ -10691,12 +10689,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:17.0.6":
|
||||
version: 17.0.6
|
||||
resolution: "marked@npm:17.0.6"
|
||||
"marked@npm:18.0.0":
|
||||
version: 18.0.0
|
||||
resolution: "marked@npm:18.0.0"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/46dac9481c028b6ab36f093084842f5c020329eb5529fa96ed22112f1f3a79b51b2f2c169a8b104b28c3ed09842ef372260dee08946fb1f7b1c70a7fb6f5cdc0
|
||||
checksum: 10/eb746a1f6e9b570ccc174cbf339504a432681bb76a1419d8b8b036c487235a55cbf94f4fa2c1e3c347a1d3f5f3666d4395bb74ea4ea4960c89b66d3a948c96d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user