mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-22 14:47:56 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d36d35467 | ||
|
|
9ba34bdf9a | ||
|
|
f0f28789de | ||
|
|
f007ea9da1 | ||
|
|
8c51adf77f | ||
|
|
876c4d3e2e | ||
|
|
cff72770e6 | ||
|
|
ff49fa78f8 | ||
|
|
914b90ffae | ||
|
|
1f1d520fdf | ||
|
|
1cca5f3108 | ||
|
|
0859202043 | ||
|
|
125629ed39 | ||
|
|
bbfa71e974 | ||
|
|
3e8c528863 | ||
|
|
ffc4731205 | ||
|
|
3b699da86c | ||
|
|
b316ef5f45 | ||
|
|
2c3e61b126 | ||
|
|
3bd1d45fe1 | ||
|
|
ae66cfc12e | ||
|
|
de45578c4c | ||
|
|
1221e74776 | ||
|
|
04ae6eb3b4 | ||
|
|
089849d283 | ||
|
|
70a20d8bcc | ||
|
|
6c448be3f1 | ||
|
|
c6e5ae21e2 | ||
|
|
b8ac4f3f3e | ||
|
|
fab4022dee | ||
|
|
d70a42930b | ||
|
|
b91a087ab1 |
@@ -2,7 +2,7 @@
|
||||
|
||||
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
|
||||
|
||||
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
|
||||
**Note**: This file contains high-level guidelines and references to implementation patterns. For gallery-specific documentation, demos, page structure, and usage examples, see [`gallery/AGENTS.md`](gallery/AGENTS.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -338,11 +338,6 @@ Common patterns:
|
||||
- **Destructive actions**: `variant="danger"` for delete/remove operations (the generic confirmation dialog uses `variant="danger"` for its confirm button — see `src/dialogs/generic/dialog-box.ts`)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialogs.markdown`
|
||||
|
||||
### Form Component (ha-form)
|
||||
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
@@ -361,10 +356,6 @@ Common patterns:
|
||||
></ha-form>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-form.markdown`
|
||||
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
- Types: `error`, `warning`, `info`, `success`
|
||||
@@ -378,10 +369,6 @@ Common patterns:
|
||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-alert.markdown`
|
||||
|
||||
### Keyboard Shortcuts (ShortcutManager)
|
||||
|
||||
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
|
||||
@@ -405,7 +392,6 @@ The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming
|
||||
|
||||
- **Component definition**: `src/components/ha-tooltip.ts`
|
||||
- **Usage example**: `src/components/ha-label.ts`
|
||||
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
@@ -435,7 +421,7 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
|
||||
#### Creating a Lovelace Card
|
||||
|
||||
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
|
||||
**Purpose**: Cards allow users to tell different stories about their house.
|
||||
|
||||
```typescript
|
||||
@customElement("hui-my-card")
|
||||
@@ -508,6 +494,10 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
4. **Test**: `yarn test` - Add and run tests
|
||||
5. **Build**: `script/build_frontend` - Test production build
|
||||
|
||||
### Gallery
|
||||
|
||||
For Gallery-specific structure, page/demo naming, sidebar behavior, content standards, and commands, see [`gallery/AGENTS.md`](gallery/AGENTS.md).
|
||||
|
||||
### Common Pitfalls to Avoid
|
||||
|
||||
- Don't manually query the DOM with `querySelector` - use the `@query`/`@queryAll` decorators or component properties
|
||||
@@ -538,7 +528,7 @@ When creating a pull request, you **must** use the PR template located at `.gith
|
||||
|
||||
#### Terminology Standards
|
||||
|
||||
**Delete vs Remove** (Based on gallery/src/pages/Text/remove-delete-add-create.markdown)
|
||||
**Delete vs Remove**
|
||||
|
||||
- **Use "Remove"** for actions that can be restored or reapplied:
|
||||
- Removing a user's permission
|
||||
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0
|
||||
- uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -33,7 +33,9 @@ const isWsl =
|
||||
* compiler: import("@rspack/core").Compiler,
|
||||
* contentBase: string,
|
||||
* port: number,
|
||||
* listenHost?: string
|
||||
* listenHost?: string,
|
||||
* open?: boolean,
|
||||
* logUrlAfterFirstBuild?: boolean,
|
||||
* }}
|
||||
*/
|
||||
const runDevServer = async ({
|
||||
@@ -41,16 +43,31 @@ const runDevServer = async ({
|
||||
contentBase,
|
||||
port,
|
||||
listenHost = undefined,
|
||||
open = true,
|
||||
logUrlAfterFirstBuild = false,
|
||||
proxy = undefined,
|
||||
}) => {
|
||||
if (listenHost === undefined) {
|
||||
// For dev container, we need to listen on all hosts
|
||||
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
|
||||
}
|
||||
const url = `http://localhost:${port}`;
|
||||
let loggedUrl = false;
|
||||
if (logUrlAfterFirstBuild) {
|
||||
compiler.hooks.done.tap("log-dev-server-url", () => {
|
||||
if (loggedUrl) {
|
||||
return;
|
||||
}
|
||||
loggedUrl = true;
|
||||
setTimeout(() => {
|
||||
log("[rspack-dev-server]", `Project is running at ${url}`);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
const server = new RspackDevServer(
|
||||
{
|
||||
hot: false,
|
||||
open: true,
|
||||
open,
|
||||
host: listenHost,
|
||||
port,
|
||||
static: {
|
||||
@@ -70,7 +87,9 @@ const runDevServer = async ({
|
||||
|
||||
await server.start();
|
||||
// Server listening
|
||||
log("[rspack-dev-server]", `Project is running at http://localhost:${port}`);
|
||||
if (!logUrlAfterFirstBuild) {
|
||||
log("[rspack-dev-server]", `Project is running at ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
const doneHandler = (done) => (err, stats) => {
|
||||
@@ -172,6 +191,8 @@ gulp.task("rspack-dev-server-gallery", () =>
|
||||
contentBase: paths.gallery_output_root,
|
||||
port: 8100,
|
||||
listenHost: "0.0.0.0",
|
||||
open: false,
|
||||
logUrlAfterFirstBuild: true,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
+7
-1
@@ -1,5 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import js from "@eslint/js";
|
||||
@@ -11,6 +13,10 @@ import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
import importX from "eslint-plugin-import-x";
|
||||
|
||||
const rspackConfigPath = fileURLToPath(
|
||||
new URL("./rspack.config.cjs", import.meta.url)
|
||||
);
|
||||
|
||||
export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
@@ -50,7 +56,7 @@ export default tseslint.config(
|
||||
settings: {
|
||||
"import-x/resolver": {
|
||||
webpack: {
|
||||
config: "./rspack.config.cjs",
|
||||
config: rspackConfigPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Gallery Agent Instructions
|
||||
|
||||
This file applies to all files under `gallery/`. Follow the root `AGENTS.md` for repository-wide Home Assistant frontend, TypeScript, Lit, accessibility, and copy standards. This file adds gallery-specific structure, page, demo, and verification guidance.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
Run commands from the repository root unless noted otherwise:
|
||||
|
||||
```bash
|
||||
gallery/script/develop_gallery # Start the gallery development server
|
||||
gallery/script/build_gallery # Build the static gallery
|
||||
yarn lint # ESLint, Prettier, TypeScript, and Lit checks
|
||||
yarn lint:types # TypeScript compiler, without file arguments
|
||||
```
|
||||
|
||||
Never run `yarn lint:types` or `tsc` with file arguments. See the root `AGENTS.md` for the generated `.js` file risk.
|
||||
|
||||
## Purpose
|
||||
|
||||
The gallery is a developer and designer reference for Home Assistant frontend UI patterns. It documents component APIs, shows realistic Lovelace and more-info states, captures brand and copy guidance, and provides reproducible demos that are safe to inspect outside a running Home Assistant instance.
|
||||
|
||||
- Prefer demonstrating real production components from `src/` instead of creating gallery-only replacements.
|
||||
- Keep fake state, sample data, and demo-only helpers inside `gallery/`.
|
||||
- Do not move gallery stubs or demo data into production code unless a production feature explicitly needs them.
|
||||
- Do not hand-edit generated output under `gallery/build/` or `gallery/dist/`.
|
||||
|
||||
## Structure
|
||||
|
||||
- `sidebar.js`: Defines gallery sections, headers, and explicit page ordering.
|
||||
- `script/develop_gallery`: Wrapper for the `develop-gallery` gulp task.
|
||||
- `script/build_gallery`: Wrapper for the `build-gallery` gulp task.
|
||||
- `src/entrypoint.js`: Creates the `<ha-gallery>` shell.
|
||||
- `src/ha-gallery.ts`: Renders the drawer, page routing, markdown descriptions, demos, edit links, and RTL toggle.
|
||||
- `src/html/index.html.template`: HTML template used by the gallery build.
|
||||
- `src/pages/<category>/<page>.markdown`: Optional page description and frontmatter.
|
||||
- `src/pages/<category>/<page>.ts`: Optional live demo module for the same page id.
|
||||
- `src/components/`: Gallery-only demo wrappers like `demo-card`, `demo-cards`, `demo-more-info`, and `page-description`.
|
||||
- `src/data/`: Fake `hass`, demo states, mock traces, and reusable sample data.
|
||||
- `public/`: Static assets copied into the gallery output.
|
||||
|
||||
## Page Model
|
||||
|
||||
Gallery pages are generated by `gather-gallery-pages` in `build-scripts/gulp/gallery.js`.
|
||||
|
||||
- A page id is the path under `src/pages/` without the extension, like `components/ha-button`.
|
||||
- A `.markdown` file and a `.ts` file with the same page id become one gallery page.
|
||||
- A page may have only markdown, only a TypeScript demo, or both.
|
||||
- Markdown can contain YAML frontmatter with `title` and optional `subtitle`.
|
||||
- Markdown that contains only frontmatter contributes metadata without rendering a description block.
|
||||
- TypeScript demo modules are dynamically imported for side effects when the page is opened.
|
||||
- A demo module must define a custom element named `demo-${category}-${page}` with slashes replaced by hyphens, like `demo-components-ha-button` for `components/ha-button`.
|
||||
- `ha-gallery.ts` renders that element with `dynamicElement()` based on the current page id.
|
||||
|
||||
## Sidebar
|
||||
|
||||
Use `sidebar.js` when a page needs a visible section, section header, or deterministic ordering.
|
||||
|
||||
- `category` must match the first directory name under `src/pages/`.
|
||||
- `header` is the section label shown in the drawer.
|
||||
- `pages` is optional. When present, listed pages keep that exact order.
|
||||
- Pages in a category that are not listed are appended alphabetically after the listed pages.
|
||||
- New categories without a sidebar entry are appended by the generator with their category name as the header.
|
||||
- If a listed page does not exist, the generator logs an error during `gather-gallery-pages`.
|
||||
|
||||
## Markdown Pages
|
||||
|
||||
Use markdown pages for explanations, design guidance, API notes, and copy standards.
|
||||
|
||||
- Start with frontmatter when the page needs a title or subtitle.
|
||||
- Use sentence case for titles, headings, labels, and UI copy.
|
||||
- Put the live example before the reference API when that makes the page easier to scan.
|
||||
- Use fenced code blocks with a language tag for copyable examples.
|
||||
- Keep examples short and focused on the behavior being documented.
|
||||
- Prefer real component names and attributes over prose-only descriptions.
|
||||
- Use Home Assistant terminology from the root `AGENTS.md`.
|
||||
- For remove/delete and add/create wording, follow `src/pages/misc/remove-delete-add-create.markdown`.
|
||||
|
||||
Gallery markdown is documentation content and is not localized with `localize`. If demo code creates production UI strings, keep those strings aligned with the root localization and copy guidance.
|
||||
|
||||
## Demo Components
|
||||
|
||||
Use TypeScript demo pages for interactive or stateful examples.
|
||||
|
||||
- Import production components from `../../../src/...` or the correct relative path from the demo file.
|
||||
- Import reusable gallery helpers from `gallery/src/components/` when they already model the pattern.
|
||||
- Use `demo-card` and `demo-cards` for Lovelace card examples that render YAML card configs.
|
||||
- Use `demo-more-info` and `demo-more-infos` for more-info dialog examples.
|
||||
- Use shared mock data from `src/data/` instead of repeating large fake state objects inline.
|
||||
- Show meaningful states, such as loading, unavailable, empty, error, active, inactive, and disabled when relevant.
|
||||
- Check responsive behavior and the gallery RTL toggle when layout or direction-sensitive UI changes.
|
||||
- Keep unavoidable casts or loose demo parsing local to the demo helper or demo page.
|
||||
|
||||
The gallery ESLint config allows `console` for gallery diagnostics. Do not copy that exception into production frontend code.
|
||||
|
||||
## Content Standards
|
||||
|
||||
The root copy standards still apply: use American English, sentence case, active voice, inclusive language, direct user-focused wording, and consistent Home Assistant terminology.
|
||||
|
||||
- Use `Home Assistant` in full, not `HA` or `HASS`.
|
||||
- Use `integration` instead of `component` for product concepts.
|
||||
- Use `Remove` for reversible disassociation and `Delete` for permanent deletion.
|
||||
- Use `Add` for existing items and `Create` for something made from scratch.
|
||||
- Avoid Latin abbreviations like `e.g.` and `i.e.` in prose.
|
||||
- Avoid stitching sentence fragments together in production UI examples.
|
||||
|
||||
## Verification
|
||||
|
||||
- For markdown, sidebar, and page-generation changes, run `gallery/script/build_gallery`.
|
||||
- For TypeScript demo or gallery shell changes, run the smallest relevant check plus `yarn lint` when practical.
|
||||
- For type checking, run `yarn lint:types` without file arguments.
|
||||
- For visual changes, run `gallery/script/develop_gallery` and check the affected page on desktop, narrow viewport, and RTL when relevant.
|
||||
- If verification is skipped, state which command was skipped and why.
|
||||
+44
-18
@@ -1,20 +1,50 @@
|
||||
import {
|
||||
mdiAccountGroup,
|
||||
mdiCalendarClock,
|
||||
mdiDotsHorizontal,
|
||||
mdiHome,
|
||||
mdiInformationOutline,
|
||||
mdiPalette,
|
||||
mdiPuzzle,
|
||||
mdiRobot,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
|
||||
export default [
|
||||
{
|
||||
// This section has no header and so all page links are shown directly in the sidebar
|
||||
category: "concepts",
|
||||
icon: mdiHome,
|
||||
pages: ["home"],
|
||||
},
|
||||
|
||||
{
|
||||
category: "brand",
|
||||
icon: mdiPalette,
|
||||
header: "Brand",
|
||||
},
|
||||
{
|
||||
category: "components",
|
||||
icon: mdiPuzzle,
|
||||
header: "Components",
|
||||
},
|
||||
{
|
||||
category: "lovelace",
|
||||
icon: mdiViewDashboard,
|
||||
// Label for in the sidebar
|
||||
header: "Dashboards",
|
||||
// Specify order of pages. Any pages in the category folder but not listed here will
|
||||
// automatically be added after the pages listed here.
|
||||
pages: ["introduction"],
|
||||
},
|
||||
{
|
||||
category: "more-info",
|
||||
icon: mdiInformationOutline,
|
||||
header: "More Info dialogs",
|
||||
},
|
||||
{
|
||||
category: "automation",
|
||||
icon: mdiRobot,
|
||||
header: "Automation",
|
||||
pages: [
|
||||
"editor-trigger",
|
||||
@@ -24,33 +54,29 @@ export default [
|
||||
"trace-timeline",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "components",
|
||||
header: "Components",
|
||||
},
|
||||
{
|
||||
category: "more-info",
|
||||
header: "More Info dialogs",
|
||||
},
|
||||
{
|
||||
category: "misc",
|
||||
header: "Miscellaneous",
|
||||
},
|
||||
{
|
||||
category: "brand",
|
||||
header: "Brand",
|
||||
},
|
||||
{
|
||||
category: "user-test",
|
||||
icon: mdiAccountGroup,
|
||||
header: "Users",
|
||||
pages: ["user-types", "configuration-menu"],
|
||||
},
|
||||
{
|
||||
category: "date-time",
|
||||
icon: mdiCalendarClock,
|
||||
header: "Date and Time",
|
||||
},
|
||||
{
|
||||
category: "design.home-assistant.io",
|
||||
header: "About",
|
||||
category: "misc",
|
||||
icon: mdiDotsHorizontal,
|
||||
header: "Miscellaneous",
|
||||
pages: [
|
||||
"entity-state",
|
||||
"ha-markdown",
|
||||
"integration-card",
|
||||
"box-shadow",
|
||||
"util-long-press",
|
||||
"remove-delete-add-create",
|
||||
"editing",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import { extractVars } from "../../../src/common/style/derived-css-vars";
|
||||
import { animationStyles } from "../../../src/resources/theme/animations.globals";
|
||||
import { coreStyles } from "../../../src/resources/theme/core.globals";
|
||||
import { colorStyles } from "../../../src/resources/theme/color/color.globals";
|
||||
import { coreColorStyles } from "../../../src/resources/theme/color/core.globals";
|
||||
import { semanticColorStyles } from "../../../src/resources/theme/color/semantic.globals";
|
||||
import { waColorStyles } from "../../../src/resources/theme/color/wa.globals";
|
||||
import { mainStyles } from "../../../src/resources/theme/main.globals";
|
||||
import { semanticStyles } from "../../../src/resources/theme/semantic.globals";
|
||||
import { typographyStyles } from "../../../src/resources/theme/typography.globals";
|
||||
import { waMainStyles } from "../../../src/resources/theme/wa.globals";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../../src/types";
|
||||
|
||||
export const GALLERY_THEME_STORAGE_KEY = "gallery-theme";
|
||||
|
||||
export const loadGalleryThemeSettings = (): ThemeSettings => {
|
||||
const stored = localStorage.getItem(GALLERY_THEME_STORAGE_KEY);
|
||||
if (!stored) {
|
||||
return { theme: "default" };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(stored) as unknown;
|
||||
const value =
|
||||
parsed && typeof parsed === "object"
|
||||
? (parsed as Partial<ThemeSettings>)
|
||||
: {};
|
||||
return {
|
||||
theme: "default",
|
||||
dark: typeof value.dark === "boolean" ? value.dark : undefined,
|
||||
primaryColor:
|
||||
typeof value.primaryColor === "string" ? value.primaryColor : undefined,
|
||||
accentColor:
|
||||
typeof value.accentColor === "string" ? value.accentColor : undefined,
|
||||
};
|
||||
} catch (_err) {
|
||||
return { theme: "default" };
|
||||
}
|
||||
};
|
||||
|
||||
const LIGHT_THEME_STYLES = [
|
||||
coreStyles,
|
||||
mainStyles,
|
||||
typographyStyles,
|
||||
semanticStyles,
|
||||
coreColorStyles,
|
||||
semanticColorStyles,
|
||||
colorStyles,
|
||||
waColorStyles,
|
||||
waMainStyles,
|
||||
animationStyles,
|
||||
];
|
||||
|
||||
const LIGHT_THEME_VARIABLES = LIGHT_THEME_STYLES.reduce<Record<string, string>>(
|
||||
(variables, style) => {
|
||||
for (const [key, value] of Object.entries(extractVars(style))) {
|
||||
variables[`--${key}`] = value;
|
||||
}
|
||||
return variables;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const LIGHT_THEME_VARIABLE_KEYS = Object.keys(LIGHT_THEME_VARIABLES);
|
||||
const LIGHT_THEME_DEFAULTS_APPLIED = new WeakSet<HTMLElement>();
|
||||
|
||||
export const effectiveGalleryDarkMode = (
|
||||
themeSettings: ThemeSettings,
|
||||
systemDark: boolean
|
||||
): boolean => themeSettings.dark ?? systemDark;
|
||||
|
||||
const galleryThemes = (darkMode: boolean): HomeAssistant["themes"] => ({
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode,
|
||||
theme: "default",
|
||||
});
|
||||
|
||||
const applyLightThemeDefaults = (element: HTMLElement, lightMode: boolean) => {
|
||||
if (lightMode) {
|
||||
for (const [key, value] of Object.entries(LIGHT_THEME_VARIABLES)) {
|
||||
element.style.setProperty(key, value);
|
||||
}
|
||||
LIGHT_THEME_DEFAULTS_APPLIED.add(element);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LIGHT_THEME_DEFAULTS_APPLIED.has(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of LIGHT_THEME_VARIABLE_KEYS) {
|
||||
element.style.removeProperty(key);
|
||||
}
|
||||
LIGHT_THEME_DEFAULTS_APPLIED.delete(element);
|
||||
};
|
||||
|
||||
export const applyFlippedGalleryTheme = (
|
||||
element: HTMLElement,
|
||||
themeSettings: ThemeSettings,
|
||||
systemDark: boolean
|
||||
) => {
|
||||
const darkMode = !effectiveGalleryDarkMode(themeSettings, systemDark);
|
||||
|
||||
if (!darkMode) {
|
||||
applyThemesOnElement(element, galleryThemes(false), undefined, {
|
||||
dark: false,
|
||||
});
|
||||
applyLightThemeDefaults(element, true);
|
||||
} else {
|
||||
applyLightThemeDefaults(element, false);
|
||||
}
|
||||
|
||||
applyThemesOnElement(element, galleryThemes(darkMode), "default", {
|
||||
...themeSettings,
|
||||
dark: darkMode,
|
||||
});
|
||||
element.style.colorScheme = darkMode ? "dark" : "light";
|
||||
};
|
||||
@@ -1,25 +1,83 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, css, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import type { HaButton } from "../../../src/components/ha-button";
|
||||
import type { ThemeSettings } from "../../../src/types";
|
||||
import {
|
||||
applyFlippedGalleryTheme,
|
||||
effectiveGalleryDarkMode,
|
||||
loadGalleryThemeSettings,
|
||||
} from "../common/theme";
|
||||
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
@customElement("demo-black-white-row")
|
||||
class DemoBlackWhiteRow extends LitElement {
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() title!: string;
|
||||
|
||||
@property() value?: any;
|
||||
@property({ attribute: false }) value?: unknown;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _themeSettings = loadGalleryThemeSettings();
|
||||
|
||||
@state() private _systemDark = mql.matches;
|
||||
|
||||
@query(".flipped") private _flipped?: HTMLElement;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
mql.addEventListener("change", this._systemDarkChanged);
|
||||
window.addEventListener(
|
||||
"theme-settings-changed",
|
||||
this._themeSettingsChanged as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
mql.removeEventListener("change", this._systemDarkChanged);
|
||||
window.removeEventListener(
|
||||
"theme-settings-changed",
|
||||
this._themeSettingsChanged as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._applyFlippedTheme();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (
|
||||
changedProperties.has("_themeSettings") ||
|
||||
changedProperties.has("_systemDark")
|
||||
) {
|
||||
this._applyFlippedTheme();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentLabel = effectiveGalleryDarkMode(
|
||||
this._themeSettings,
|
||||
this._systemDark
|
||||
)
|
||||
? "Dark mode"
|
||||
: "Light mode";
|
||||
const flippedLabel =
|
||||
currentLabel === "Dark mode" ? "Light mode" : "Dark mode";
|
||||
|
||||
return html`
|
||||
<div class="row">
|
||||
<div class="content light">
|
||||
<section class="content current" aria-label=${currentLabel}>
|
||||
<h2>${currentLabel}</h2>
|
||||
<ha-card .header=${this.title}>
|
||||
<div class="card-content">
|
||||
<slot name="light"></slot>
|
||||
@@ -30,8 +88,9 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="content dark">
|
||||
</section>
|
||||
<section class="content flipped" aria-label=${flippedLabel}>
|
||||
<h2>${flippedLabel}</h2>
|
||||
<ha-card .header=${this.title}>
|
||||
<div class="card-content">
|
||||
<slot name="dark"></slot>
|
||||
@@ -45,65 +104,84 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
${this.value
|
||||
? html`<pre>${JSON.stringify(this.value, undefined, 2)}</pre>`
|
||||
: nothing}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
handleSubmit(ev: Event) {
|
||||
const content = (ev.target as HaButton).closest(".content");
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSubmit(ev) {
|
||||
const content = (ev.target as HaButton).closest(".content")!;
|
||||
fireEvent(this, "submitted" as any, {
|
||||
slot: content.classList.contains("light") ? "light" : "dark",
|
||||
slot: content.classList.contains("current") ? "light" : "dark",
|
||||
});
|
||||
}
|
||||
|
||||
private _themeSettingsChanged = (
|
||||
ev: HASSDomEvent<Partial<ThemeSettings>>
|
||||
) => {
|
||||
this._themeSettings = {
|
||||
...this._themeSettings,
|
||||
...ev.detail,
|
||||
theme: "default",
|
||||
};
|
||||
};
|
||||
|
||||
private _systemDarkChanged = (ev: MediaQueryListEvent) => {
|
||||
this._systemDark = ev.matches;
|
||||
};
|
||||
|
||||
private _applyFlippedTheme() {
|
||||
if (!this._flipped) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyFlippedGalleryTheme(
|
||||
this._flipped,
|
||||
this._themeSettings,
|
||||
this._systemDark
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-block-size: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
inline-size: 100%;
|
||||
min-block-size: 100%;
|
||||
}
|
||||
.content {
|
||||
padding: 50px 0;
|
||||
box-sizing: border-box;
|
||||
min-inline-size: 0;
|
||||
padding: var(--ha-space-8);
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
.light {
|
||||
flex: 1;
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.light ha-card {
|
||||
margin-left: auto;
|
||||
}
|
||||
.dark {
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-left: 50px;
|
||||
box-sizing: border-box;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
ha-card {
|
||||
width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
}
|
||||
pre {
|
||||
width: 300px;
|
||||
margin: 0 16px 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -112,27 +190,18 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
flex-direction: row-reverse;
|
||||
border-top: none;
|
||||
}
|
||||
@media only screen and (max-width: 1500px) {
|
||||
.light {
|
||||
flex: initial;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.light,
|
||||
.dark {
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.row,
|
||||
.dark {
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
pre {
|
||||
margin: 16px auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
@@ -16,17 +15,12 @@ class DemoCards extends LitElement {
|
||||
|
||||
@state() private _showConfig = false;
|
||||
|
||||
@query("#container") private _container!: HTMLElement;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-demo-options>
|
||||
<ha-formfield label="Show config">
|
||||
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
</ha-demo-options>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
@@ -48,12 +42,6 @@ class DemoCards extends LitElement {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
private _darkThemeToggled(ev) {
|
||||
applyThemesOnElement(this._container, { themes: {} } as any, "default", {
|
||||
dark: ev.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.cards {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
@@ -21,9 +20,6 @@ class DemoMoreInfos extends LitElement {
|
||||
<ha-formfield label="Show config">
|
||||
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
|
||||
</ha-formfield>
|
||||
</ha-demo-options>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
@@ -51,33 +47,16 @@ class DemoMoreInfos extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
demo-more-info {
|
||||
margin: 16px 16px 32px;
|
||||
margin: var(--ha-space-4) var(--ha-space-4) var(--ha-space-8);
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
margin-right: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
|
||||
private _showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
private _darkThemeToggled(ev) {
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector("#container"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
theme: "default",
|
||||
},
|
||||
"default",
|
||||
{
|
||||
dark: ev.target.checked,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
||||
import type { ThemeSettings } from "../../../src/types";
|
||||
import {
|
||||
applyFlippedGalleryTheme,
|
||||
effectiveGalleryDarkMode,
|
||||
loadGalleryThemeSettings,
|
||||
} from "../common/theme";
|
||||
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
export const THEME_COMPARISON_PANELS = [
|
||||
{ slot: "current" },
|
||||
{ slot: "flipped" },
|
||||
] as const;
|
||||
|
||||
@customElement("demo-theme-comparison")
|
||||
export class DemoThemeComparison extends LitElement {
|
||||
@state() private _themeSettings = loadGalleryThemeSettings();
|
||||
|
||||
@state() private _systemDark = mql.matches;
|
||||
|
||||
@query(".flipped") private _flipped?: HTMLElement;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
mql.addEventListener("change", this._systemDarkChanged);
|
||||
window.addEventListener(
|
||||
"theme-settings-changed",
|
||||
this._themeSettingsChanged as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
mql.removeEventListener("change", this._systemDarkChanged);
|
||||
window.removeEventListener(
|
||||
"theme-settings-changed",
|
||||
this._themeSettingsChanged as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._applyFlippedTheme();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (
|
||||
changedProperties.has("_themeSettings") ||
|
||||
changedProperties.has("_systemDark")
|
||||
) {
|
||||
this._applyFlippedTheme();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentLabel = effectiveGalleryDarkMode(
|
||||
this._themeSettings,
|
||||
this._systemDark
|
||||
)
|
||||
? "Dark mode"
|
||||
: "Light mode";
|
||||
const flippedLabel =
|
||||
currentLabel === "Dark mode" ? "Light mode" : "Dark mode";
|
||||
|
||||
return html`
|
||||
<section class="panel" aria-label=${currentLabel}>
|
||||
<h2>${currentLabel}</h2>
|
||||
<slot name="current"></slot>
|
||||
</section>
|
||||
<section class="panel flipped" aria-label=${flippedLabel}>
|
||||
<h2>${flippedLabel}</h2>
|
||||
<slot name="flipped"></slot>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
private _themeSettingsChanged = (
|
||||
ev: HASSDomEvent<Partial<ThemeSettings>>
|
||||
) => {
|
||||
this._themeSettings = {
|
||||
...this._themeSettings,
|
||||
...ev.detail,
|
||||
theme: "default",
|
||||
};
|
||||
};
|
||||
|
||||
private _systemDarkChanged = (ev: MediaQueryListEvent) => {
|
||||
this._systemDark = ev.matches;
|
||||
};
|
||||
|
||||
private _applyFlippedTheme() {
|
||||
if (!this._flipped) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyFlippedGalleryTheme(
|
||||
this._flipped,
|
||||
this._themeSettings,
|
||||
this._systemDark
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
flex: 1;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
inline-size: 100%;
|
||||
min-block-size: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-sizing: border-box;
|
||||
min-block-size: 100%;
|
||||
min-inline-size: 0;
|
||||
padding: var(--ha-space-6);
|
||||
background-color: var(--primary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--ha-space-4);
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
box-sizing: border-box;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
:host {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-theme-comparison": DemoThemeComparison;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import "../../../src/components/ha-theme-settings";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../../src/types";
|
||||
|
||||
@customElement("gallery-settings")
|
||||
class GallerySettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public themeSettings!: ThemeSettings;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public rtl = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card .header=${"Appearance"}>
|
||||
<div class="card-content">
|
||||
Configure how the gallery renders component previews and examples.
|
||||
</div>
|
||||
<ha-theme-settings
|
||||
.hass=${this.hass}
|
||||
.selectedTheme=${this.themeSettings}
|
||||
.narrow=${this.narrow}
|
||||
.heading=${"Theme"}
|
||||
.description=${"Choose the mode and colors used throughout the gallery."}
|
||||
.labels=${{
|
||||
mode: "Theme mode",
|
||||
autoMode: "Auto",
|
||||
lightMode: "Light",
|
||||
darkMode: "Dark",
|
||||
primaryColor: "Primary color",
|
||||
accentColor: "Accent color",
|
||||
reset: "Reset",
|
||||
}}
|
||||
.showThemePicker=${false}
|
||||
></ha-theme-settings>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">Right-to-left layout</span>
|
||||
<span slot="description">
|
||||
Preview the gallery with right-to-left text direction.
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this.rtl}
|
||||
@change=${this._rtlChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rtlChanged(ev: Event) {
|
||||
fireEvent(this, "gallery-rtl-changed", {
|
||||
rtl: (ev.currentTarget as HaSwitch).checked,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"gallery-rtl-changed": { rtl: boolean };
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"gallery-settings": GallerySettings;
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,10 @@ class PageDescription extends HaMarkdown {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const subtitle = PAGES[this.page].metadata.subtitle;
|
||||
|
||||
return html`
|
||||
<div class="heading">
|
||||
<div class="title">
|
||||
${PAGES[this.page].metadata.title || this.page.split("/")[1]}
|
||||
</div>
|
||||
<div class="subtitle">${PAGES[this.page].metadata.subtitle}</div>
|
||||
</div>
|
||||
${subtitle ? html`<div class="subtitle">${subtitle}</div>` : nothing}
|
||||
${until(
|
||||
PAGES[this.page]
|
||||
.description()
|
||||
@@ -32,16 +29,9 @@ class PageDescription extends HaMarkdown {
|
||||
static styles = [
|
||||
HaMarkdown.styles,
|
||||
css`
|
||||
.heading {
|
||||
.subtitle {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--secondary-background-color);
|
||||
}
|
||||
.title {
|
||||
font-size: 42px;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: var(--ha-font-size-l);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
}
|
||||
|
||||
@@ -16,22 +16,9 @@ class HaDemoOptions extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background-color: var(--light-primary-color);
|
||||
margin-left: 60px
|
||||
margin-right: 60px;
|
||||
display: var(--layout-horizontal_-_display);
|
||||
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(
|
||||
--layout-horizontal_-_-webkit-flex-direction
|
||||
);
|
||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
|
||||
align-items: var(--layout-center_-_align-items);
|
||||
background-color: var(--primary-background-color);
|
||||
position: relative;
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
padding: var(--ha-space-2) var(--ha-space-16) var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
`,
|
||||
|
||||
+533
-156
@@ -1,161 +1,183 @@
|
||||
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
|
||||
import { mdiCog, mdiMenu } from "@mdi/js";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../src/common/dom/fire_event";
|
||||
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-drawer";
|
||||
import type { HaDrawer } from "../../src/components/ha-drawer";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/components/ha-sidebar";
|
||||
import "../../src/components/item/ha-list-item-button";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/ha-top-app-bar-fixed";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../src/types";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
import {
|
||||
GALLERY_THEME_STORAGE_KEY,
|
||||
loadGalleryThemeSettings,
|
||||
} from "./common/theme";
|
||||
import "./components/gallery-settings";
|
||||
import "./components/page-description";
|
||||
|
||||
const RTL_STORAGE_KEY = "gallery-rtl";
|
||||
const SETTINGS_PAGE = "settings";
|
||||
|
||||
const GITHUB_DEMO_URL =
|
||||
"https://github.com/home-assistant/frontend/blob/dev/gallery/src/pages/";
|
||||
|
||||
const FAKE_HASS = {
|
||||
// Just enough for computeRTL for notification-manager
|
||||
language: "en",
|
||||
translationMetadata: {
|
||||
translations: {},
|
||||
interface GalleryPage {
|
||||
metadata: Record<string, unknown>;
|
||||
description?: unknown;
|
||||
demo?: unknown;
|
||||
}
|
||||
|
||||
interface GallerySidebarGroup {
|
||||
category: string;
|
||||
header?: string;
|
||||
icon?: string;
|
||||
pages: string[];
|
||||
}
|
||||
|
||||
const GALLERY_SIDEBAR = SIDEBAR as GallerySidebarGroup[];
|
||||
const DEFAULT_PAGE = `${GALLERY_SIDEBAR[0].category}/${GALLERY_SIDEBAR[0].pages[0]}`;
|
||||
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const galleryLocalize = (key: string) =>
|
||||
(
|
||||
({
|
||||
"ui.sidebar.sidebar_toggle": "Toggle sidebar",
|
||||
"ui.notification_drawer.title": "Notifications",
|
||||
"ui.sidebar.external_app_configuration": "App configuration",
|
||||
"panel.config": "Settings",
|
||||
}) as Record<string, string>
|
||||
)[key] ?? key;
|
||||
|
||||
const galleryConnection = {
|
||||
subscribeMessage(
|
||||
callback: (message: unknown) => void,
|
||||
message: { type?: string }
|
||||
) {
|
||||
if (message.type === "frontend/subscribe_user_data") {
|
||||
callback({ value: { panelOrder: [], hiddenPanels: [] } });
|
||||
} else if (message.type === "persistent_notification/subscribe") {
|
||||
callback({ type: "current", notifications: {} });
|
||||
}
|
||||
return Promise.resolve(() => undefined);
|
||||
},
|
||||
};
|
||||
sendMessagePromise() {
|
||||
return Promise.resolve({ value: null });
|
||||
},
|
||||
} as unknown as Connection;
|
||||
|
||||
@customElement("ha-gallery")
|
||||
class HaGallery extends LitElement {
|
||||
@state() private _page =
|
||||
document.location.hash.substring(1) ||
|
||||
`${SIDEBAR[0].category}/${SIDEBAR[0].pages![0]}`;
|
||||
@state() private _page = this._pageFromLocation();
|
||||
|
||||
@state() private _rtl = localStorage.getItem(RTL_STORAGE_KEY) === "true";
|
||||
|
||||
@state() private _themeSettings = loadGalleryThemeSettings();
|
||||
|
||||
@state() private _systemDark = mql.matches;
|
||||
|
||||
@query("notification-manager")
|
||||
private _notifications!: HTMLElementTagNameMap["notification-manager"];
|
||||
|
||||
@query("ha-drawer")
|
||||
private _drawer!: HaDrawer;
|
||||
@query("ha-sidebar")
|
||||
private _sidebar?: HTMLElementTagNameMap["ha-sidebar"];
|
||||
|
||||
@query(".gallery-nav-item[selected]")
|
||||
private _selectedNavigationItem?: HTMLElementTagNameMap["ha-list-item-button"];
|
||||
|
||||
private _narrow = window.matchMedia("(max-width: 600px)").matches;
|
||||
|
||||
@state() private _drawerOpen = !this._narrow;
|
||||
|
||||
render() {
|
||||
const sidebar: unknown[] = [];
|
||||
|
||||
for (const group of SIDEBAR) {
|
||||
const links: unknown[] = [];
|
||||
|
||||
for (const page of group.pages!) {
|
||||
const key = `${group.category}/${page}`;
|
||||
const active = this._page === key;
|
||||
if (!(key in PAGES)) {
|
||||
console.error("Undefined page referenced in sidebar.js:", key);
|
||||
continue;
|
||||
}
|
||||
const title = PAGES[key].metadata.title || page;
|
||||
links.push(html`
|
||||
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>
|
||||
`);
|
||||
}
|
||||
|
||||
sidebar.push(
|
||||
group.header
|
||||
? html`
|
||||
<ha-expansion-panel .header=${group.header}>
|
||||
${links}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: links
|
||||
);
|
||||
}
|
||||
const isSettingsPage = this._page === SETTINGS_PAGE;
|
||||
const page = isSettingsPage ? undefined : PAGES[this._page];
|
||||
|
||||
return html`
|
||||
<ha-drawer
|
||||
.direction=${this._rtl ? "rtl" : "ltr"}
|
||||
.open=${!this._narrow}
|
||||
.open=${this._drawerOpen}
|
||||
.type=${this._narrow ? "modal" : "dismissible"}
|
||||
>
|
||||
<div class="drawer-title">Home Assistant Design</div>
|
||||
<div class="sidebar">${sidebar}</div>
|
||||
<ha-sidebar
|
||||
.hass=${this._galleryHass}
|
||||
.narrow=${this._narrow}
|
||||
.route=${{ prefix: "", path: this._page }}
|
||||
.alwaysExpand=${true}
|
||||
sidebar-title="Home Assistant Design"
|
||||
@hass-toggle-menu=${this._toggleDrawer}
|
||||
>
|
||||
${this._renderSidebarNavigation()} ${this._renderSettingsItem()}
|
||||
</ha-sidebar>
|
||||
<div slot="appContent" class="app-content">
|
||||
<ha-top-app-bar-fixed>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._menuTapped}
|
||||
.path=${mdiMenu}
|
||||
></ha-icon-button>
|
||||
<ha-top-app-bar-fixed .narrow=${this._narrow}>
|
||||
${this._narrow || !this._drawerOpen
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._toggleDrawer}
|
||||
.path=${mdiMenu}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
|
||||
<div slot="title">
|
||||
${PAGES[this._page].metadata.title || this._page.split("/")[1]}
|
||||
${isSettingsPage
|
||||
? "Settings"
|
||||
: page?.metadata.title || this._page.split("/")[1]}
|
||||
</div>
|
||||
<div class="content">
|
||||
${isSettingsPage
|
||||
? html`<gallery-settings
|
||||
.hass=${this._galleryHass}
|
||||
.themeSettings=${this._themeSettings}
|
||||
.narrow=${this._narrow}
|
||||
.rtl=${this._rtl}
|
||||
@theme-settings-changed=${this._themeSettingsChanged}
|
||||
@gallery-rtl-changed=${this._rtlChanged}
|
||||
></gallery-settings>`
|
||||
: html`
|
||||
${page?.description
|
||||
? html`
|
||||
<page-description .page=${this._page}>
|
||||
</page-description>
|
||||
`
|
||||
: nothing}
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
`}
|
||||
</div>
|
||||
${isSettingsPage || !page ? nothing : this._renderPageFooter(page)}
|
||||
</ha-top-app-bar-fixed>
|
||||
<div class="content">
|
||||
${PAGES[this._page].description
|
||||
? html`
|
||||
<page-description .page=${this._page}></page-description>
|
||||
`
|
||||
: ""}
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
</div>
|
||||
<div class="page-footer">
|
||||
<div class="edit-docs">
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for this
|
||||
page.
|
||||
</div>
|
||||
<div>
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rtl-toggle">
|
||||
<ha-icon-button
|
||||
@click=${this._toggleRtl}
|
||||
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-drawer>
|
||||
<notification-manager
|
||||
.hass=${FAKE_HASS}
|
||||
.hass=${this._galleryHass}
|
||||
id="notifications"
|
||||
></notification-manager>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
mql.addEventListener("change", this._systemDarkChanged);
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._applyDirection();
|
||||
this._applyTheme();
|
||||
|
||||
this.addEventListener("show-notification", (ev) =>
|
||||
this._notifications.showDialog({ message: ev.detail.message })
|
||||
@@ -171,16 +193,26 @@ class HaGallery extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
document.location.hash = this._page;
|
||||
if (document.location.hash.substring(1) !== this._page) {
|
||||
document.location.hash = this._page;
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", () => {
|
||||
this._page = document.location.hash.substring(1);
|
||||
if (this._narrow) {
|
||||
this._drawer.open = false;
|
||||
}
|
||||
});
|
||||
window.addEventListener("hashchange", this._hashChanged);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
mql.removeEventListener("change", this._systemDarkChanged);
|
||||
window.removeEventListener("hashchange", this._hashChanged);
|
||||
}
|
||||
|
||||
private _hashChanged = () => {
|
||||
this._page = this._pageFromLocation();
|
||||
if (this._narrow) {
|
||||
this._drawerOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
@@ -188,37 +220,335 @@ class HaGallery extends LitElement {
|
||||
this._applyDirection();
|
||||
}
|
||||
|
||||
if (changedProps.has("_themeSettings") || changedProps.has("_systemDark")) {
|
||||
this._applyTheme();
|
||||
}
|
||||
|
||||
if (!changedProps.has("_page")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._page === SETTINGS_PAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PAGES[this._page].demo) {
|
||||
PAGES[this._page].demo();
|
||||
}
|
||||
|
||||
const menuItem = this.shadowRoot!.querySelector(
|
||||
`a[href="#${this._page}"]`
|
||||
)!;
|
||||
void this._scrollSelectedNavigationItemIntoView();
|
||||
}
|
||||
|
||||
// Make sure section is expanded
|
||||
private async _scrollSelectedNavigationItemIntoView() {
|
||||
const menuItem = this._selectedNavigationItem;
|
||||
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure section is expanded before measuring the selected item.
|
||||
if (menuItem.parentElement instanceof HaExpansionPanel) {
|
||||
menuItem.parentElement.expanded = true;
|
||||
await menuItem.parentElement.updateComplete;
|
||||
}
|
||||
|
||||
const scrollable = this._sidebar?.shadowRoot?.querySelector<HTMLElement>(
|
||||
"ha-list-nav.before-spacer"
|
||||
);
|
||||
|
||||
if (!scrollable) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const itemRect = menuItem.getBoundingClientRect();
|
||||
const scrollableRect = scrollable.getBoundingClientRect();
|
||||
const targetScrollTop =
|
||||
scrollable.scrollTop +
|
||||
itemRect.top -
|
||||
scrollableRect.top -
|
||||
(scrollableRect.height - itemRect.height) / 2;
|
||||
|
||||
scrollable.scrollTo({
|
||||
top: Math.min(
|
||||
Math.max(0, targetScrollTop),
|
||||
scrollable.scrollHeight - scrollable.clientHeight
|
||||
),
|
||||
left: 0,
|
||||
});
|
||||
scrollable.scrollLeft = 0;
|
||||
});
|
||||
}
|
||||
|
||||
private _menuTapped() {
|
||||
this._drawer.open = !this._drawer.open;
|
||||
private _renderSidebarNavigation() {
|
||||
const sidebar: unknown[] = [];
|
||||
|
||||
for (const group of GALLERY_SIDEBAR) {
|
||||
const links: unknown[] = [];
|
||||
const expanded = group.pages.some(
|
||||
(page) => this._page === `${group.category}/${page}`
|
||||
);
|
||||
|
||||
for (const page of group.pages) {
|
||||
const key = `${group.category}/${page}`;
|
||||
if (!(key in PAGES)) {
|
||||
console.error("Undefined page referenced in sidebar.js:", key);
|
||||
continue;
|
||||
}
|
||||
links.push(
|
||||
this._renderPageLink(
|
||||
key,
|
||||
PAGES[key].metadata.title || page,
|
||||
group.header ? undefined : "main-navigation",
|
||||
group.header ? undefined : group.icon
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sidebar.push(
|
||||
group.header
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
slot="main-navigation"
|
||||
class="gallery-sidebar-section"
|
||||
.header=${group.header}
|
||||
?expanded=${expanded}
|
||||
>
|
||||
${group.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="gallery-sidebar-icon"
|
||||
.path=${group.icon}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${links}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: links
|
||||
);
|
||||
}
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
private _toggleRtl() {
|
||||
this._rtl = !this._rtl;
|
||||
localStorage.setItem(RTL_STORAGE_KEY, String(this._rtl));
|
||||
private _renderPageLink(
|
||||
page: string,
|
||||
title: string,
|
||||
slot?: string,
|
||||
iconPath?: string
|
||||
) {
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
slot=${ifDefined(slot)}
|
||||
class=${classMap({
|
||||
"gallery-nav-item": true,
|
||||
"has-icon": Boolean(iconPath),
|
||||
selected: this._page === page,
|
||||
})}
|
||||
?selected=${this._page === page}
|
||||
href=${`#${page}`}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${title}</span>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSettingsItem() {
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
slot="fixed-navigation"
|
||||
class=${classMap({
|
||||
"gallery-settings-item": true,
|
||||
selected: this._page === SETTINGS_PAGE,
|
||||
})}
|
||||
?selected=${this._page === SETTINGS_PAGE}
|
||||
href="#settings"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
<span slot="headline">Settings</span>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPageFooter(page: GalleryPage) {
|
||||
return html`<div class="page-footer">
|
||||
<div class="edit-docs">
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for this page.
|
||||
</div>
|
||||
<div>
|
||||
${page.description || Object.keys(page.metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
${page.demo
|
||||
? html`
|
||||
<a href=${`${GITHUB_DEMO_URL}${this._page}.ts`} target="_blank">
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _toggleDrawer(ev?: Event) {
|
||||
ev?.stopPropagation();
|
||||
this._drawerOpen = !this._drawerOpen;
|
||||
}
|
||||
|
||||
private _applyDirection() {
|
||||
setDirectionStyles(this._rtl ? "rtl" : "ltr", this);
|
||||
}
|
||||
|
||||
private _themeSettingsChanged(ev: HASSDomEvent<Partial<ThemeSettings>>) {
|
||||
this._themeSettings = {
|
||||
...this._themeSettings,
|
||||
...ev.detail,
|
||||
theme: "default",
|
||||
};
|
||||
localStorage.setItem(
|
||||
GALLERY_THEME_STORAGE_KEY,
|
||||
JSON.stringify(this._themeSettings)
|
||||
);
|
||||
}
|
||||
|
||||
private _rtlChanged(ev: HASSDomEvent<{ rtl: boolean }>) {
|
||||
this._rtl = ev.detail.rtl;
|
||||
localStorage.setItem(RTL_STORAGE_KEY, String(this._rtl));
|
||||
}
|
||||
|
||||
private _systemDarkChanged = (ev: MediaQueryListEvent) => {
|
||||
this._systemDark = ev.matches;
|
||||
};
|
||||
|
||||
private _applyTheme() {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
this._themes,
|
||||
"default",
|
||||
this._themeSettings,
|
||||
true
|
||||
);
|
||||
|
||||
let schemeMeta = document.querySelector("meta[name=color-scheme]");
|
||||
if (!schemeMeta) {
|
||||
schemeMeta = document.createElement("meta");
|
||||
schemeMeta.setAttribute("name", "color-scheme");
|
||||
document.head.appendChild(schemeMeta);
|
||||
}
|
||||
schemeMeta.setAttribute(
|
||||
"content",
|
||||
this._effectiveDarkMode ? "dark" : "light"
|
||||
);
|
||||
document.documentElement.style.colorScheme = this._effectiveDarkMode
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
const themeMeta = document.querySelector("meta[name=theme-color]");
|
||||
if (themeMeta) {
|
||||
if (!themeMeta.hasAttribute("default-content")) {
|
||||
themeMeta.setAttribute(
|
||||
"default-content",
|
||||
themeMeta.getAttribute("content") ?? ""
|
||||
);
|
||||
}
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const themeColor =
|
||||
styles.getPropertyValue("--app-theme-color").trim() ||
|
||||
styles.getPropertyValue("--primary-background-color").trim() ||
|
||||
themeMeta.getAttribute("default-content") ||
|
||||
"";
|
||||
themeMeta.setAttribute("content", themeColor);
|
||||
}
|
||||
}
|
||||
|
||||
private _pageFromLocation() {
|
||||
const page = document.location.hash.substring(1);
|
||||
return page === SETTINGS_PAGE || page in PAGES ? page : DEFAULT_PAGE;
|
||||
}
|
||||
|
||||
private get _effectiveDarkMode() {
|
||||
return this._themeSettings.dark ?? this._systemDark;
|
||||
}
|
||||
|
||||
private get _themes(): HomeAssistant["themes"] {
|
||||
return {
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: this._effectiveDarkMode,
|
||||
theme: "default",
|
||||
};
|
||||
}
|
||||
|
||||
private get _galleryHass(): HomeAssistant {
|
||||
return {
|
||||
auth: {},
|
||||
areas: {},
|
||||
config: {},
|
||||
connected: true,
|
||||
connection: galleryConnection,
|
||||
debugConnection: false,
|
||||
devices: {},
|
||||
dockedSidebar: "docked",
|
||||
enableShortcuts: true,
|
||||
entities: {},
|
||||
floors: {},
|
||||
hassUrl: (path) => path,
|
||||
kioskMode: false,
|
||||
language: "en",
|
||||
loadBackendTranslation: async () => galleryLocalize,
|
||||
loadFragmentTranslation: async () => undefined,
|
||||
locale: {
|
||||
language: "en",
|
||||
number_format: "language",
|
||||
time_format: "language",
|
||||
date_format: "language",
|
||||
first_weekday: "language",
|
||||
time_zone: "local",
|
||||
},
|
||||
localize: galleryLocalize,
|
||||
panelUrl: this._page,
|
||||
panels: {},
|
||||
selectedLanguage: null,
|
||||
selectedTheme: this._themeSettings,
|
||||
services: {},
|
||||
states: {},
|
||||
suspendWhenHidden: false,
|
||||
systemData: {},
|
||||
themes: this._themes,
|
||||
translationMetadata: { fragments: [], translations: {} },
|
||||
user: {
|
||||
id: "gallery",
|
||||
is_admin: false,
|
||||
is_owner: false,
|
||||
name: "Settings",
|
||||
credentials: [],
|
||||
mfa_modules: [],
|
||||
},
|
||||
userData: {},
|
||||
vibrate: false,
|
||||
callApi: async () => undefined,
|
||||
callApiRaw: async () => new Response(),
|
||||
callService: async () => ({ context: { id: "gallery" } }),
|
||||
callWS: async () => undefined,
|
||||
fetchWithAuth: async () => new Response(),
|
||||
sendWS: () => undefined,
|
||||
} as unknown as HomeAssistant;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
@@ -226,49 +556,103 @@ class HaGallery extends LitElement {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
--ha-sidebar-width: 256px;
|
||||
--header-height: 64px;
|
||||
--ha-sidebar-width: 300px;
|
||||
--ha-sidebar-expanded-width: 300px;
|
||||
--ha-sidebar-expanded-item-width: 292px;
|
||||
--ha-sidebar-expanded-section-item-width: 256px;
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.gallery-sidebar-section {
|
||||
color: var(--sidebar-text-color);
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - var(--header-height));
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
margin: 0 var(--ha-space-1) var(--ha-space-1);
|
||||
overflow-x: hidden;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--expansion-panel-summary-padding: 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
align-items: center;
|
||||
.gallery-sidebar-section::part(summary) {
|
||||
min-height: var(--ha-space-10);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
min-height: var(--header-height);
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
color: var(--primary-text-color);
|
||||
display: block;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
.gallery-sidebar-section .gallery-nav-item {
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
width: var(--ha-sidebar-expanded-section-item-width, 248px);
|
||||
}
|
||||
|
||||
.gallery-sidebar-icon,
|
||||
.gallery-nav-item ha-svg-icon[slot="start"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
flex-shrink: 0;
|
||||
height: var(--ha-space-6);
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.gallery-sidebar-icon {
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.gallery-nav-item,
|
||||
.gallery-settings-item {
|
||||
flex-shrink: 0;
|
||||
margin: 0 var(--ha-space-1) var(--ha-space-1);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-row-item-min-height: var(--ha-space-10);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--ha-row-item-padding-inline: var(--ha-space-3);
|
||||
position: relative;
|
||||
width: var(--ha-sidebar-expanded-item-width, 248px);
|
||||
color: var(--sidebar-text-color);
|
||||
}
|
||||
|
||||
.sidebar a[active]::before {
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
.gallery-nav-item.has-icon,
|
||||
.gallery-settings-item {
|
||||
--ha-row-item-gap: var(--ha-space-3);
|
||||
--ha-row-item-padding-inline: var(--ha-space-2) var(--ha-space-3);
|
||||
}
|
||||
|
||||
.gallery-nav-item::part(headline),
|
||||
.gallery-settings-item::part(headline) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.gallery-nav-item[selected],
|
||||
.gallery-settings-item[selected] {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
.gallery-nav-item[selected]::before,
|
||||
.gallery-settings-item[selected]::before {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 2px;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
}
|
||||
|
||||
.gallery-settings-item ha-svg-icon[slot="start"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
flex-shrink: 0;
|
||||
height: var(--ha-space-6);
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.gallery-settings-item[selected] ha-svg-icon[slot="start"] {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
.gallery-nav-item[selected] ha-svg-icon[slot="start"] {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
@@ -283,11 +667,16 @@ class HaGallery extends LitElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
page-description {
|
||||
margin: 16px;
|
||||
display: block;
|
||||
margin: 0 var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
@@ -324,18 +713,6 @@ class HaGallery extends LitElement {
|
||||
margin: 0 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.rtl-toggle {
|
||||
padding: var(--ha-space-4);
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
|
||||
.rtl-toggle ha-icon-button {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-logo-svg";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const alerts: {
|
||||
title?: string;
|
||||
@@ -135,10 +135,10 @@ const alerts: {
|
||||
export class DemoHaAlert extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-alert ${mode} demo">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
${alerts.map(
|
||||
(alert) => html`
|
||||
@@ -154,43 +154,19 @@ export class DemoHaAlert extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { mdiButtonCursor, mdiHome } from "@mdi/js";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-badge";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const badges: {
|
||||
type?: "badge" | "button";
|
||||
@@ -60,10 +60,10 @@ const badges: {
|
||||
export class DemoHaBadge extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-badge ${mode} demo">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
${badges.map(
|
||||
(badge) => html`
|
||||
@@ -78,45 +78,23 @@ export class DemoHaBadge extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-6);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { mdiHome } from "@mdi/js";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import { titleCase } from "../../../../src/common/string/title-case";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const appearances = ["accent", "filled", "plain"];
|
||||
const variants = ["brand", "danger", "neutral", "warning", "success"];
|
||||
@@ -16,10 +16,10 @@ const variants = ["brand", "danger", "neutral", "warning", "success"];
|
||||
export class DemoHaButton extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-button in ${mode}">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
${variants.map(
|
||||
(variant) => html`
|
||||
@@ -112,45 +112,22 @@ export class DemoHaButton extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
.button {
|
||||
padding: unset;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
@@ -159,6 +136,7 @@ export class DemoHaButton extends LitElement {
|
||||
}
|
||||
.card-content div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -26,7 +26,7 @@ const chips: {
|
||||
export class DemoHaChips extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-chip demo">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p>Action chip</p>
|
||||
<ha-chip-set>
|
||||
@@ -82,7 +82,7 @@ export class DemoHaChips extends LitElement {
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${chip.content}
|
||||
</ha-input-chip>
|
||||
`
|
||||
|
||||
@@ -9,9 +9,11 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-switch";
|
||||
import type { HaControlSwitch } from "../../../../src/components/ha-control-switch";
|
||||
import type { HASSDomTargetEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const switches: {
|
||||
id: string;
|
||||
@@ -45,106 +47,72 @@ const switches: {
|
||||
export class DemoHaControlSwitch extends LitElement {
|
||||
@state() private checked = false;
|
||||
|
||||
handleValueChanged(e: any) {
|
||||
this.checked = e.target.checked as boolean;
|
||||
handleValueChanged(e: HASSDomTargetEvent<HaControlSwitch>) {
|
||||
this.checked = e.target.checked;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="themes">
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-control-switch ${mode}">
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<div class="card-content">
|
||||
<label id="${mode}-${id}">${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<div class="card-content">
|
||||
<label id="${slot}-${id}">${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.pathOn=${mdiLightbulb}
|
||||
.pathOff=${mdiLightbulbOff}
|
||||
.label=${label}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Vertical</b></p>
|
||||
<div class="vertical-switches">
|
||||
${repeat(switches, (sw) => {
|
||||
const { label, ...config } = sw;
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.pathOn=${mdiLightbulb}
|
||||
.pathOff=${mdiLightbulbOff}
|
||||
.label=${label}
|
||||
.pathOn=${mdiGarageOpen}
|
||||
.pathOff=${mdiGarage}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Vertical</b></p>
|
||||
<div class="vertical-switches">
|
||||
${repeat(switches, (sw) => {
|
||||
const { label, ...config } = sw;
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.label=${label}
|
||||
.pathOn=${mdiGarageOpen}
|
||||
.pathOff=${mdiGarage}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.themes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 16px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -8,25 +8,25 @@ import {
|
||||
mdiContentPaste,
|
||||
mdiDelete,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dropdown";
|
||||
import "../../../../src/components/ha-dropdown-item";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
@customElement("demo-components-ha-dropdown")
|
||||
export class DemoHaDropdown extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-button in ${mode}">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
<ha-dropdown>
|
||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
||||
@@ -74,45 +74,22 @@ export class DemoHaDropdown extends LitElement {
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
.button {
|
||||
padding: unset;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@@ -12,7 +12,7 @@ const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
||||
export class DemoHaFaded extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-faded demo">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h3>Long text directly as slotted content</h3>
|
||||
<ha-faded>${LONG_TEXT}</ha-faded>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/input/ha-input";
|
||||
@@ -11,6 +10,15 @@ import "../../../../src/components/input/ha-input-copy";
|
||||
import "../../../../src/components/input/ha-input-multi";
|
||||
import "../../../../src/components/input/ha-input-search";
|
||||
import { internationalizationContext } from "../../../../src/data/context";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../../../src/types";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const LOCALIZE_KEYS: Record<string, string> = {
|
||||
"ui.common.copy": "Copy",
|
||||
@@ -22,6 +30,25 @@ const LOCALIZE_KEYS: Record<string, string> = {
|
||||
"ui.common.copied_clipboard": "Copied to clipboard",
|
||||
};
|
||||
|
||||
const localize = (key: string) => LOCALIZE_KEYS[key] ?? key;
|
||||
|
||||
const DEMO_I18N: HomeAssistantInternationalization = {
|
||||
localize,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
},
|
||||
translationMetadata: { fragments: [], translations: {} },
|
||||
loadBackendTranslation: async () => localize,
|
||||
loadFragmentTranslation: async () => localize,
|
||||
};
|
||||
|
||||
@customElement("demo-components-ha-input")
|
||||
export class DemoHaInput extends LitElement {
|
||||
constructor() {
|
||||
@@ -29,185 +56,171 @@ export class DemoHaInput extends LitElement {
|
||||
// Provides internationalizationContext for ha-input-copy, ha-input-multi and ha-input-search
|
||||
new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
initialValue: {
|
||||
localize: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {} as any,
|
||||
translationMetadata: {} as any,
|
||||
loadBackendTranslation: (async () => (key: string) => key) as any,
|
||||
loadFragmentTranslation: (async () => (key: string) => key) as any,
|
||||
},
|
||||
initialValue: DEMO_I18N,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-input in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Default"></ha-input>
|
||||
<ha-input label="With value" value="Hello"></ha-input>
|
||||
<ha-input
|
||||
label="With placeholder"
|
||||
placeholder="Type here..."
|
||||
></ha-input>
|
||||
</div>
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<div slot=${slot} class="panel-content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Default"></ha-input>
|
||||
<ha-input label="With value" value="Hello"></ha-input>
|
||||
<ha-input
|
||||
label="With placeholder"
|
||||
placeholder="Type here..."
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>Input types</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Text" type="text" value="Text"></ha-input>
|
||||
<ha-input label="Number" type="number" value="42"></ha-input>
|
||||
<ha-input
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Password"
|
||||
type="password"
|
||||
value="secret"
|
||||
password-toggle
|
||||
></ha-input>
|
||||
<ha-input label="URL" type="url" placeholder="https://...">
|
||||
</ha-input>
|
||||
<ha-input label="Date" type="date"></ha-input>
|
||||
</div>
|
||||
<h3>Input types</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Text" type="text" value="Text"></ha-input>
|
||||
<ha-input
|
||||
label="Number"
|
||||
type="number"
|
||||
value="42"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Password"
|
||||
type="password"
|
||||
value="secret"
|
||||
password-toggle
|
||||
></ha-input>
|
||||
<ha-input label="URL" type="url" placeholder="https://...">
|
||||
</ha-input>
|
||||
<ha-input label="Date" type="date"></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>States</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="Readonly"
|
||||
readonly
|
||||
value="Readonly"
|
||||
></ha-input>
|
||||
<ha-input label="Required" required></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Invalid"
|
||||
invalid
|
||||
validation-message="This field is required"
|
||||
value=""
|
||||
></ha-input>
|
||||
<ha-input label="With hint" hint="This is a hint"></ha-input>
|
||||
<ha-input
|
||||
label="With clear"
|
||||
with-clear
|
||||
value="Clear me"
|
||||
></ha-input>
|
||||
</div>
|
||||
<h3>States</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="Readonly"
|
||||
readonly
|
||||
value="Readonly"
|
||||
></ha-input>
|
||||
<ha-input label="Required" required></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Invalid"
|
||||
invalid
|
||||
validation-message="This field is required"
|
||||
value=""
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="With hint"
|
||||
hint="This is a hint"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="With clear"
|
||||
with-clear
|
||||
value="Clear me"
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>With slots</h3>
|
||||
<div class="row">
|
||||
<ha-input label="With prefix">
|
||||
<span slot="start">$</span>
|
||||
</ha-input>
|
||||
<ha-input label="With suffix">
|
||||
<span slot="end">kg</span>
|
||||
</ha-input>
|
||||
<ha-input label="With icon">
|
||||
<ha-svg-icon .path=${mdiMagnify} slot="start"></ha-svg-icon>
|
||||
</ha-input>
|
||||
<h3>With slots</h3>
|
||||
<div class="row">
|
||||
<ha-input label="With prefix">
|
||||
<span slot="start">$</span>
|
||||
</ha-input>
|
||||
<ha-input label="With suffix">
|
||||
<span slot="end">kg</span>
|
||||
</ha-input>
|
||||
<ha-input label="With icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiMagnify}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
</ha-input>
|
||||
</div>
|
||||
|
||||
<h3>Appearance: outlined</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined"
|
||||
value="Hello"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined invalid"
|
||||
invalid
|
||||
validation-message="Required"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
placeholder="Placeholder only"
|
||||
></ha-input>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<h3>Appearance: outlined</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined"
|
||||
value="Hello"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined invalid"
|
||||
invalid
|
||||
validation-message="Required"
|
||||
></ha-input>
|
||||
<ha-card header="Derivatives">
|
||||
<div class="card-content">
|
||||
<h3>ha-input-search</h3>
|
||||
<ha-input-search label="Search label"></ha-input-search>
|
||||
<ha-input-search appearance="outlined"></ha-input-search>
|
||||
|
||||
<h3>ha-input-copy</h3>
|
||||
<ha-input-copy
|
||||
value="my-api-token-123"
|
||||
masked-value="••••••••••••••••••"
|
||||
masked-toggle
|
||||
></ha-input-copy>
|
||||
|
||||
<h3>ha-input-multi</h3>
|
||||
<ha-input-multi
|
||||
label="URL"
|
||||
add-label="Add URL"
|
||||
.value=${["https://example.com"]}
|
||||
></ha-input-multi>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
placeholder="Placeholder only"
|
||||
></ha-input>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Derivatives in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>ha-input-search</h3>
|
||||
<ha-input-search label="Search label"></ha-input-search>
|
||||
<ha-input-search appearance="outlined"></ha-input-search>
|
||||
|
||||
<h3>ha-input-copy</h3>
|
||||
<ha-input-copy
|
||||
value="my-api-token-123"
|
||||
masked-value="••••••••••••••••••"
|
||||
masked-toggle
|
||||
></ha-input-copy>
|
||||
|
||||
<h3>ha-input-multi</h3>
|
||||
<ha-input-multi
|
||||
label="URL"
|
||||
add-label="Add URL"
|
||||
.value=${["https://example.com"]}
|
||||
></ha-input-multi>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6);
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
@@ -224,10 +237,11 @@ export class DemoHaInput extends LitElement {
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
@customElement("demo-components-ha-progress-button")
|
||||
export class DemoHaProgressButton extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-progress-button in ${mode}">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
<ha-progress-button @click=${this._clickedSuccess}>
|
||||
Success
|
||||
@@ -59,32 +60,17 @@ export class DemoHaProgressButton extends LitElement {
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private async _clickedSuccess(ev: CustomEvent): Promise<void> {
|
||||
private _clickedSuccess(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-progress-button"]>
|
||||
) {
|
||||
console.log("Clicked success");
|
||||
const button = ev.currentTarget as any;
|
||||
const button = ev.currentTarget;
|
||||
button.progress = true;
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -93,8 +79,10 @@ export class DemoHaProgressButton extends LitElement {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private async _clickedFail(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
private _clickedFail(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-progress-button"]>
|
||||
) {
|
||||
const button = ev.currentTarget;
|
||||
button.progress = true;
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -105,20 +93,14 @@ export class DemoHaProgressButton extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
.button {
|
||||
padding: unset;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-bar";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import "../../../../src/components/ha-slider";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
@customElement("demo-components-ha-slider")
|
||||
export class DemoHaSlider extends LitElement {
|
||||
@@ -14,10 +14,10 @@ export class DemoHaSlider extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-slider ${mode} demo">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
<span>Default (disabled)</span>
|
||||
<ha-slider
|
||||
@@ -45,44 +45,19 @@ export class DemoHaSlider extends LitElement {
|
||||
></ha-slider>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
margin: 16px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-bar";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
@customElement("demo-components-ha-spinner")
|
||||
export class DemoHaSpinner extends LitElement {
|
||||
@@ -13,10 +13,10 @@ export class DemoHaSpinner extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-badge ${mode} demo">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
<ha-spinner></ha-spinner>
|
||||
<ha-spinner size="tiny"></ha-spinner>
|
||||
@@ -27,44 +27,19 @@ export class DemoHaSpinner extends LitElement {
|
||||
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
margin: 16px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
@customElement("demo-components-ha-switch")
|
||||
export class DemoHaSwitch extends LitElement {
|
||||
@@ -12,10 +12,10 @@ export class DemoHaSwitch extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-switch ${mode}">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
<span>Unchecked</span>
|
||||
@@ -35,44 +35,19 @@ export class DemoHaSwitch extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
margin: 16px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-textarea";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const LONG_VALUE = Array.from(
|
||||
{ length: 30 },
|
||||
(_, i) => `Line ${i + 1}: this content overflows the max-height and scrolls.`
|
||||
).join("\n");
|
||||
|
||||
@customElement("demo-components-ha-textarea")
|
||||
export class DemoHaTextarea extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-textarea in ${mode}">
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
@@ -38,6 +43,11 @@ export class DemoHaTextarea extends LitElement {
|
||||
resize="auto"
|
||||
value="This textarea will grow as you type more content into it. Try adding more lines to see the effect."
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="Autogrow capped (scrolls past max-height)"
|
||||
resize="auto"
|
||||
.value=${LONG_VALUE}
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>States</h3>
|
||||
@@ -84,42 +94,19 @@ export class DemoHaTextarea extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { provide } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-tip";
|
||||
import { internationalizationContext } from "../../../../src/data/context";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../../../src/types";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const tips: (string | TemplateResult)[] = [
|
||||
"Test tip",
|
||||
@@ -14,68 +21,57 @@ const tips: (string | TemplateResult)[] = [
|
||||
html`<i>Tip</i> <b>with</b> <sub>HTML</sub>`,
|
||||
];
|
||||
|
||||
const localize = (key: string) => key;
|
||||
|
||||
const DEMO_I18N: HomeAssistantInternationalization = {
|
||||
localize,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
},
|
||||
translationMetadata: { fragments: [], translations: {} },
|
||||
loadBackendTranslation: async () => localize,
|
||||
loadFragmentTranslation: async () => localize,
|
||||
};
|
||||
|
||||
@customElement("demo-components-ha-tip")
|
||||
export class DemoHaTip extends LitElement {
|
||||
@provide({ context: internationalizationContext })
|
||||
@state()
|
||||
protected _i18n: HomeAssistantInternationalization = {
|
||||
localize: ((key: string) => key) as any,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {} as any,
|
||||
translationMetadata: {} as any,
|
||||
loadBackendTranslation: (async () => (key: string) => key) as any,
|
||||
loadFragmentTranslation: (async () => (key: string) => key) as any,
|
||||
};
|
||||
protected _i18n = DEMO_I18N;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html` ${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-tip ${mode} demo">
|
||||
<div class="card-content">
|
||||
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
return html`
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<ha-card slot=${slot}>
|
||||
<div class="card-content">
|
||||
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-tip {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const SHADOWS = ["s", "m", "l"] as const;
|
||||
|
||||
@@ -9,67 +8,32 @@ const SHADOWS = ["s", "m", "l"] as const;
|
||||
export class DemoMiscBoxShadow extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<h2>${mode}</h2>
|
||||
<div class="grid">
|
||||
${SHADOWS.map(
|
||||
(size) => html`
|
||||
<div
|
||||
class="box"
|
||||
style="box-shadow: var(--ha-box-shadow-${size})"
|
||||
>
|
||||
${size}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<demo-theme-comparison>
|
||||
${THEME_COMPARISON_PANELS.map(
|
||||
({ slot }) => html`
|
||||
<div slot=${slot} class="panel-content">
|
||||
<div class="grid">
|
||||
${SHADOWS.map(
|
||||
(size) => html`
|
||||
<div
|
||||
class="box"
|
||||
style="box-shadow: var(--ha-box-shadow-${size})"
|
||||
>
|
||||
${size}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
)}
|
||||
</demo-theme-comparison>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 48px;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.light,
|
||||
.dark {
|
||||
flex: 1;
|
||||
background-color: var(--primary-background-color);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 24px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--primary-text-color);
|
||||
text-transform: capitalize;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are gr
|
||||
|
||||
## Development
|
||||
|
||||
You can develop design.home-assistant.io locally by checking out [the Home Assistant frontend repository](https://github.com/home-assistant/frontend). The command to run the gallery is `gallery/script/develop_gallery`. It will automatically open a browser window and load the development version of the website.
|
||||
You can develop design.home-assistant.io locally by checking out [the Home Assistant frontend repository](https://github.com/home-assistant/frontend). The command to run the gallery is `gallery/script/develop_gallery`. After the first build finishes, the command prints the local URL for the development version of the website.
|
||||
|
||||
## Creating a page
|
||||
|
||||
+16
-16
@@ -29,7 +29,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
@@ -40,15 +40,15 @@
|
||||
"@codemirror/view": "6.43.0",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.7",
|
||||
"@formatjs/intl-displaynames": "7.3.9",
|
||||
"@formatjs/intl-durationformat": "0.10.13",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.9",
|
||||
"@formatjs/intl-listformat": "8.3.9",
|
||||
"@formatjs/intl-locale": "5.3.8",
|
||||
"@formatjs/intl-numberformat": "9.3.10",
|
||||
"@formatjs/intl-pluralrules": "6.3.9",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.9",
|
||||
"@formatjs/intl-datetimeformat": "7.4.8",
|
||||
"@formatjs/intl-displaynames": "7.3.10",
|
||||
"@formatjs/intl-durationformat": "0.10.14",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.10",
|
||||
"@formatjs/intl-listformat": "8.3.10",
|
||||
"@formatjs/intl-locale": "5.3.9",
|
||||
"@formatjs/intl-numberformat": "9.3.11",
|
||||
"@formatjs/intl-pluralrules": "6.3.10",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.10",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -70,8 +70,8 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.1.2",
|
||||
"@tsparticles/preset-links": "4.1.2",
|
||||
"@tsparticles/engine": "4.1.3",
|
||||
"@tsparticles/preset-links": "4.1.3",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -88,13 +88,13 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.4.1",
|
||||
"fuse.js": "7.4.2",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.5",
|
||||
"intl-messageformat": "11.2.7",
|
||||
"intl-messageformat": "11.2.8",
|
||||
"js-yaml": "4.2.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -102,7 +102,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "18.0.4",
|
||||
"marked": "18.0.5",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -187,7 +187,7 @@
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.8.3",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.16",
|
||||
|
||||
@@ -4,8 +4,7 @@ import { ensureArray } from "../array/ensure-array";
|
||||
import { isComponentLoaded } from "./is_component_loaded";
|
||||
|
||||
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
(isCore(page) || isLoadedIntegration(hass, page)) &&
|
||||
isNotLoadedIntegration(hass, page);
|
||||
isCore(page) || isLoadedIntegration(hass, page);
|
||||
|
||||
export const isLoadedIntegration = (
|
||||
hass: HomeAssistant,
|
||||
@@ -16,13 +15,4 @@ export const isLoadedIntegration = (
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isNotLoadedIntegration = (
|
||||
hass: HomeAssistant,
|
||||
page: PageNavigation
|
||||
) =>
|
||||
!page.not_component ||
|
||||
!ensureArray(page.not_component).some((integration) =>
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isCore = (page: PageNavigation) => page.core;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import {
|
||||
createQueryString,
|
||||
decodeQueryParams,
|
||||
queryParamsFromServiceTarget,
|
||||
serviceTargetFromQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
export type HistoryLogbookTargetParamKey =
|
||||
| "entity_id"
|
||||
| "label_id"
|
||||
| "floor_id"
|
||||
| "area_id"
|
||||
| "device_id";
|
||||
|
||||
export type HistoryLogbookDateParamKey = "start_date" | "end_date";
|
||||
|
||||
export type HistoryLogbookBooleanParamKey = "back";
|
||||
|
||||
export type HistoryLogbookQueryParams = QueryParamValues<
|
||||
HistoryLogbookTargetParamKey,
|
||||
HistoryLogbookDateParamKey,
|
||||
HistoryLogbookBooleanParamKey
|
||||
>;
|
||||
|
||||
export const historyLogbookTargetParamKeys: HistoryLogbookTargetParamKey[] = [
|
||||
"entity_id",
|
||||
"label_id",
|
||||
"floor_id",
|
||||
"area_id",
|
||||
"device_id",
|
||||
];
|
||||
|
||||
export const historyLogbookQueryParamConfig = {
|
||||
list: historyLogbookTargetParamKeys,
|
||||
date: ["start_date", "end_date"],
|
||||
boolean: [{ key: "back", trueValue: "1" }],
|
||||
} satisfies QueryParamConfig<
|
||||
HistoryLogbookTargetParamKey,
|
||||
HistoryLogbookDateParamKey,
|
||||
HistoryLogbookBooleanParamKey
|
||||
>;
|
||||
|
||||
export const decodeHistoryLogbookQueryParams = (
|
||||
searchParams: SearchParamsSource
|
||||
): HistoryLogbookQueryParams =>
|
||||
decodeQueryParams(searchParams, historyLogbookQueryParamConfig);
|
||||
|
||||
export const historyLogbookTargetFromQueryParams = (
|
||||
params: HistoryLogbookQueryParams
|
||||
): HassServiceTarget | undefined =>
|
||||
serviceTargetFromQueryParams(params, historyLogbookTargetParamKeys);
|
||||
|
||||
export const createHistoryLogbookUrl = (
|
||||
path: string,
|
||||
target: HassServiceTarget,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): string => {
|
||||
const queryString = createQueryString(
|
||||
{
|
||||
...queryParamsFromServiceTarget(target, historyLogbookTargetParamKeys),
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
historyLogbookQueryParamConfig
|
||||
);
|
||||
|
||||
return queryString ? `${path}?${queryString}` : path;
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
export type SearchParamsSource =
|
||||
| URLSearchParams
|
||||
| Record<string, string>
|
||||
| string;
|
||||
|
||||
export interface QueryParamConfig<
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
> {
|
||||
list?: readonly ListKey[];
|
||||
date?: readonly DateKey[];
|
||||
boolean?: readonly {
|
||||
key: BooleanKey;
|
||||
trueValue: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type QueryParamValues<
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
> = Partial<
|
||||
Record<ListKey, string[]> &
|
||||
Record<DateKey, Date> &
|
||||
Record<BooleanKey, boolean>
|
||||
>;
|
||||
|
||||
export type ServiceTargetQueryParams<
|
||||
Key extends keyof HassServiceTarget & string,
|
||||
> = Partial<Record<Key, string[]>>;
|
||||
|
||||
const getSearchParam = (
|
||||
searchParams: SearchParamsSource,
|
||||
key: string
|
||||
): string | null => {
|
||||
if (typeof searchParams === "string") {
|
||||
return new URLSearchParams(searchParams).get(key);
|
||||
}
|
||||
if (searchParams instanceof URLSearchParams) {
|
||||
return searchParams.get(key);
|
||||
}
|
||||
return searchParams[key] ?? null;
|
||||
};
|
||||
|
||||
export const decodeQueryParams = <
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
>(
|
||||
searchParams: SearchParamsSource,
|
||||
config: QueryParamConfig<ListKey, DateKey, BooleanKey>
|
||||
): QueryParamValues<ListKey, DateKey, BooleanKey> => {
|
||||
const params: QueryParamValues<ListKey, DateKey, BooleanKey> = {};
|
||||
for (const key of config.list ?? []) {
|
||||
const value = getSearchParam(searchParams, key);
|
||||
if (value) {
|
||||
params[key] = value.split(",") as (typeof params)[typeof key];
|
||||
}
|
||||
}
|
||||
for (const key of config.date ?? []) {
|
||||
const value = getSearchParam(searchParams, key);
|
||||
if (value) {
|
||||
params[key] = new Date(value) as (typeof params)[typeof key];
|
||||
}
|
||||
}
|
||||
for (const { key, trueValue } of config.boolean ?? []) {
|
||||
if (getSearchParam(searchParams, key) === trueValue) {
|
||||
params[key] = true as (typeof params)[typeof key];
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
export const createQueryString = <
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
>(
|
||||
values: QueryParamValues<ListKey, DateKey, BooleanKey>,
|
||||
config: QueryParamConfig<ListKey, DateKey, BooleanKey>
|
||||
): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const key of config.list ?? []) {
|
||||
const value = values[key] as string[] | undefined;
|
||||
if (value?.length) {
|
||||
searchParams.append(key, value.join(","));
|
||||
}
|
||||
}
|
||||
for (const key of config.date ?? []) {
|
||||
const value = values[key] as Date | undefined;
|
||||
if (value) {
|
||||
searchParams.append(key, value.toISOString());
|
||||
}
|
||||
}
|
||||
for (const { key, trueValue } of config.boolean ?? []) {
|
||||
if (values[key]) {
|
||||
searchParams.append(key, trueValue);
|
||||
}
|
||||
}
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
export const serviceTargetFromQueryParams = <
|
||||
Key extends keyof HassServiceTarget & string,
|
||||
>(
|
||||
params: ServiceTargetQueryParams<Key>,
|
||||
keys: readonly Key[]
|
||||
): HassServiceTarget | undefined => {
|
||||
if (!keys.some((key) => params[key])) {
|
||||
return undefined;
|
||||
}
|
||||
const target: HassServiceTarget = {};
|
||||
for (const key of keys) {
|
||||
const value = params[key];
|
||||
if (value) {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
export const queryParamsFromServiceTarget = <
|
||||
Key extends keyof HassServiceTarget & string,
|
||||
>(
|
||||
target: HassServiceTarget,
|
||||
keys: readonly Key[]
|
||||
): ServiceTargetQueryParams<Key> => {
|
||||
const params: ServiceTargetQueryParams<Key> = {};
|
||||
for (const key of keys) {
|
||||
const value = target[key];
|
||||
if (value) {
|
||||
params[key] = ensureArray(value);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
@@ -162,7 +162,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@state() private _filteredData: DataTableRowData[] = [];
|
||||
@state() private _filteredData?: DataTableRowData[];
|
||||
|
||||
@state() private _headerHeight = 0;
|
||||
|
||||
@@ -204,7 +204,7 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
this._checkedRows = this._filteredData
|
||||
this._checkedRows = (this._filteredData || [])
|
||||
.filter((data) => data.selectable !== false)
|
||||
.map((data) => data[this.id]);
|
||||
this._lastSelectedRowId = null;
|
||||
@@ -216,7 +216,7 @@ export class HaDataTable extends LitElement {
|
||||
this._checkedRows = [];
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
const row = this._filteredData.find((data) => data[this.id] === id);
|
||||
const row = this._filteredData?.find((data) => data[this.id] === id);
|
||||
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
|
||||
this._checkedRows.push(id);
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._filteredData.length) {
|
||||
if (this._filteredData?.length) {
|
||||
// Force update of location of rows
|
||||
this._filteredData = [...this._filteredData];
|
||||
}
|
||||
@@ -366,7 +366,10 @@ export class HaDataTable extends LitElement {
|
||||
this._lastSelectedRowId = null;
|
||||
}
|
||||
|
||||
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
||||
if (
|
||||
this._filteredData &&
|
||||
(properties.has("selectable") || properties.has("hiddenColumns"))
|
||||
) {
|
||||
this._filteredData = [...this._filteredData];
|
||||
}
|
||||
}
|
||||
@@ -409,6 +412,8 @@ export class HaDataTable extends LitElement {
|
||||
const renderRow = (row: DataTableRowData, index: number) =>
|
||||
this._renderRow(columns, this.narrow, row, index);
|
||||
|
||||
const filteredDataLength = this._filteredData?.length || 0;
|
||||
|
||||
return html`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
@@ -429,10 +434,10 @@ export class HaDataTable extends LitElement {
|
||||
"auto-height": this.autoHeight,
|
||||
})}"
|
||||
role="table"
|
||||
aria-rowcount=${this._filteredData.length + 1}
|
||||
aria-rowcount=${filteredDataLength + 1}
|
||||
style=${styleMap({
|
||||
height: this.autoHeight
|
||||
? `${(this._filteredData.length || 1) * 53 + 53}px`
|
||||
? `${(filteredDataLength || 1) * 53 + 53}px`
|
||||
: `calc(100% - ${this._headerHeight}px)`,
|
||||
})}
|
||||
>
|
||||
@@ -521,16 +526,23 @@ export class HaDataTable extends LitElement {
|
||||
})}
|
||||
</slot>
|
||||
</div>
|
||||
${!this._filteredData.length
|
||||
${!this._filteredData?.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div class="mdc-data-table__cell grows center" role="cell">
|
||||
${this.noDataText ||
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"}
|
||||
${!this._filteredData
|
||||
? this._i18n?.localize?.("ui.common.loading") ||
|
||||
"Loading"
|
||||
: this.data.length
|
||||
? this._i18n?.localize?.(
|
||||
"ui.components.data-table.no_match_filter"
|
||||
) || "No rows matching current filters"
|
||||
: this.noDataText ||
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -903,7 +915,7 @@ export class HaDataTable extends LitElement {
|
||||
const rowId = checkboxElement.rowId;
|
||||
|
||||
const groupedData = this._groupData(
|
||||
this._filteredData,
|
||||
this._filteredData || [],
|
||||
this._i18n?.localize,
|
||||
this._i18n?.locale,
|
||||
this.appendRow,
|
||||
@@ -1005,7 +1017,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
if (this._filteredData.length) {
|
||||
if (this._filteredData?.length) {
|
||||
this._filteredData = [...this._filteredData];
|
||||
}
|
||||
fireEvent(this, "selection-changed", {
|
||||
@@ -1465,6 +1477,11 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__table.auto-height .scroller {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.mdc-data-table__table.auto-height lit-virtualizer {
|
||||
overscroll-behavior-y: auto;
|
||||
}
|
||||
|
||||
.grows {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
@@ -115,6 +115,20 @@ export class HaEntityStatePicker extends LitElement {
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _computeDefaultLabel(): string {
|
||||
// When an attribute is configured, default to the attribute's friendly
|
||||
// name (e.g. "Source") instead of the generic "State". Requires a concrete
|
||||
// entity to resolve the translated name; otherwise fall back to "State".
|
||||
if (this.attribute && this.entityId) {
|
||||
const entityId = ensureArray(this.entityId)[0];
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
if (stateObj) {
|
||||
return this.hass.formatEntityAttributeName(stateObj, this.attribute);
|
||||
}
|
||||
}
|
||||
return this.hass.localize("ui.components.entity.entity-state-picker.state");
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -129,8 +143,7 @@ export class HaEntityStatePicker extends LitElement {
|
||||
.disabled=${this.disabled || noEntity}
|
||||
.autofocus=${this.autofocus}
|
||||
.required=${this.required}
|
||||
.label=${this.label ??
|
||||
this.hass.localize("ui.components.entity.entity-state-picker.state")}
|
||||
.label=${this.label ?? this._computeDefaultLabel()}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getFilteredItems}
|
||||
|
||||
@@ -1600,8 +1600,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
// Filter states based on what's typed
|
||||
const filteredStates = typedText
|
||||
? states.filter((entityState) =>
|
||||
entityState.label
|
||||
.toLowerCase()
|
||||
entityState.displayLabel
|
||||
?.toLowerCase()
|
||||
.startsWith(typedText.toLowerCase())
|
||||
)
|
||||
: states;
|
||||
@@ -1658,8 +1658,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
// Filter states based on what's typed
|
||||
const filteredStates = typedText
|
||||
? states.filter((entityState) =>
|
||||
entityState.label
|
||||
.toLowerCase()
|
||||
entityState.displayLabel
|
||||
?.toLowerCase()
|
||||
.startsWith(typedText.toLowerCase())
|
||||
)
|
||||
: states;
|
||||
|
||||
@@ -170,6 +170,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
@property({ attribute: "always-expand", type: Boolean })
|
||||
public alwaysExpand = false;
|
||||
|
||||
@property({ attribute: "sidebar-title" }) public sidebarTitle =
|
||||
"Home Assistant";
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _updatesCount = 0;
|
||||
@@ -346,8 +349,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
@action=${this._toggleSidebar}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="title">Home Assistant</div>
|
||||
: nothing}
|
||||
<div class="title">${this.sidebarTitle}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -362,16 +365,28 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
>`;
|
||||
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
return html`<div class="panels-list">
|
||||
<div class="wrapper">
|
||||
${renderList(
|
||||
html`<slot name="main-navigation">
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
</slot>`,
|
||||
"before-spacer",
|
||||
true
|
||||
)}
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
${this._renderSpacer()}
|
||||
${renderList(
|
||||
html`${this._renderFixedPanels(selectedPanel)}`,
|
||||
html`<slot name="fixed-navigation">
|
||||
${this._renderFixedPanels(selectedPanel)}
|
||||
</slot>`,
|
||||
"after-spacer",
|
||||
false
|
||||
)}
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
@@ -388,7 +403,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
return html`<div class="panels-list">
|
||||
<div class="wrapper">
|
||||
${renderList(
|
||||
this._renderPanels(beforeSpacer, selectedPanel),
|
||||
html`<slot name="main-navigation">
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
</slot>`,
|
||||
"before-spacer",
|
||||
true
|
||||
)}
|
||||
@@ -396,10 +413,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
</div>
|
||||
${this._renderSpacer()}
|
||||
${renderList(
|
||||
html`
|
||||
html`<slot name="fixed-navigation">
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this._renderFixedPanels(selectedPanel)}
|
||||
`,
|
||||
</slot>`,
|
||||
"after-spacer",
|
||||
false
|
||||
)}
|
||||
@@ -541,7 +558,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
>
|
||||
<ha-user-badge slot="start" .user=${this.hass.user}></ha-user-badge>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>${this.hass.user ? this.hass.user.name : nothing}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand && this.hass.user
|
||||
@@ -665,7 +682,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + var(--safe-area-inset-left, 0px));
|
||||
width: calc(
|
||||
var(--ha-sidebar-expanded-width, 256px) +
|
||||
var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
}
|
||||
:host([narrow][expanded]) .menu {
|
||||
width: 100%;
|
||||
@@ -748,7 +768,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
color: var(--sidebar-text-color);
|
||||
}
|
||||
:host([expanded]) ha-list-item-button {
|
||||
width: 248px;
|
||||
width: var(--ha-sidebar-expanded-item-width, 248px);
|
||||
}
|
||||
:host([narrow][expanded]) ha-list-item-button {
|
||||
width: calc(240px - var(--safe-area-inset-left, 0px));
|
||||
|
||||
@@ -258,6 +258,14 @@ export class HaTextArea extends WaInputMixin(LitElement) {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* The size-adjuster shares a grid cell with the textarea and is given an
|
||||
inline height matching the content's scrollHeight. Without capping it
|
||||
too, it inflates the grid row past the max-height and pushes the
|
||||
textarea down instead of scrolling. */
|
||||
:host([resize="auto"]) wa-textarea::part(textarea-adjuster) {
|
||||
max-height: var(--ha-textarea-max-height, 200px);
|
||||
}
|
||||
|
||||
wa-textarea:hover::part(base),
|
||||
wa-textarea:hover::part(label) {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { normalizeLuminance } from "../common/color/palette";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
DefaultAccentColor,
|
||||
DefaultPrimaryColor,
|
||||
} from "../resources/theme/color/color.globals";
|
||||
import type { HomeAssistant, ThemeSettings, ValueChangedEvent } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-theme-picker";
|
||||
import "./input/ha-input";
|
||||
import "./radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "./radio/ha-radio-group";
|
||||
import "./radio/ha-radio-option";
|
||||
|
||||
const HOME_ASSISTANT_THEME = "default";
|
||||
|
||||
export interface ThemeSettingsLabels {
|
||||
theme?: string;
|
||||
noTheme?: string;
|
||||
mode?: string;
|
||||
autoMode?: string;
|
||||
lightMode?: string;
|
||||
darkMode?: string;
|
||||
primaryColor?: string;
|
||||
accentColor?: string;
|
||||
reset?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-theme-settings")
|
||||
export class HaThemeSettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selectedTheme?: ThemeSettings | null;
|
||||
|
||||
@property({ attribute: false }) public labels?: ThemeSettingsLabels;
|
||||
|
||||
@property({ attribute: false }) public description?: TemplateResult | string;
|
||||
|
||||
@property() public heading?: string;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "include-default", type: Boolean })
|
||||
public includeDefault = false;
|
||||
|
||||
@property({ attribute: "show-theme-picker", type: Boolean })
|
||||
public showThemePicker = true;
|
||||
|
||||
@property({ attribute: "theme-picker-disabled", type: Boolean })
|
||||
public themePickerDisabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const themeSettings = this.selectedTheme ?? this.hass.selectedTheme;
|
||||
const curThemeIsUseDefault = themeSettings?.theme === "";
|
||||
const curTheme = themeSettings?.theme
|
||||
? themeSettings.theme
|
||||
: this.hass.themes.darkMode
|
||||
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow} ?empty=${!this.showThemePicker}>
|
||||
${this.heading
|
||||
? html`<span slot="heading">${this.heading}</span>`
|
||||
: nothing}
|
||||
${this.description
|
||||
? html`<span slot="description">${this.description}</span>`
|
||||
: nothing}
|
||||
${this.showThemePicker
|
||||
? html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.labels?.theme}
|
||||
.noThemeLabel=${this.labels?.noTheme}
|
||||
.value=${themeSettings?.theme || undefined}
|
||||
.disabled=${this.themePickerDisabled}
|
||||
?include-default=${this.includeDefault}
|
||||
@value-changed=${this._handleThemeSelection}
|
||||
></ha-theme-picker>
|
||||
`
|
||||
: nothing}
|
||||
</ha-settings-row>
|
||||
${curTheme === HOME_ASSISTANT_THEME ||
|
||||
(curThemeIsUseDefault &&
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme)
|
||||
? html`<div class="inputs">
|
||||
<ha-radio-group
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
.ariaLabel=${this.labels?.mode ?? "Theme mode"}
|
||||
.value=${themeSettings?.dark === undefined
|
||||
? "auto"
|
||||
: themeSettings.dark
|
||||
? "dark"
|
||||
: "light"}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<ha-radio-option value="auto">
|
||||
${this.labels?.autoMode ?? "Auto"}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="light">
|
||||
${this.labels?.lightMode ?? "Light"}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="dark">
|
||||
${this.labels?.darkMode ?? "Dark"}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
${curTheme === HOME_ASSISTANT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-input
|
||||
.value=${themeSettings?.primaryColor || DefaultPrimaryColor}
|
||||
type="color"
|
||||
.label=${this.labels?.primaryColor ?? "Primary color"}
|
||||
.name=${"primaryColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
<ha-input
|
||||
.value=${themeSettings?.accentColor || DefaultAccentColor}
|
||||
type="color"
|
||||
.label=${this.labels?.accentColor ?? "Accent color"}
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
${themeSettings?.primaryColor || themeSettings?.accentColor
|
||||
? html` <ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
@click=${this._resetColors}
|
||||
>
|
||||
${this.labels?.reset ?? "Reset"}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleColorChange(ev: Event) {
|
||||
const target = ev.currentTarget as HTMLInputElement;
|
||||
const value =
|
||||
target.name === "primaryColor"
|
||||
? normalizeLuminance(target.value)
|
||||
: target.value;
|
||||
|
||||
target.value = value;
|
||||
fireEvent(this, "theme-settings-changed", {
|
||||
[target.name]: value,
|
||||
} as Partial<ThemeSettings>);
|
||||
}
|
||||
|
||||
private _resetColors() {
|
||||
fireEvent(this, "theme-settings-changed", {
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _supportsModeSelection(themeName: string): boolean {
|
||||
const theme = this.hass.themes.themes[themeName];
|
||||
if (!theme) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!(theme.modes && "light" in theme.modes && "dark" in theme.modes);
|
||||
}
|
||||
|
||||
private _handleDarkMode(ev: Event) {
|
||||
let dark: boolean | undefined;
|
||||
switch ((ev.currentTarget as HaRadioGroup).value) {
|
||||
case "light":
|
||||
dark = false;
|
||||
break;
|
||||
case "dark":
|
||||
dark = true;
|
||||
break;
|
||||
}
|
||||
fireEvent(this, "theme-settings-changed", { dark });
|
||||
}
|
||||
|
||||
private _handleThemeSelection(
|
||||
ev: ValueChangedEvent<string | undefined>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
const theme = ev.detail.value;
|
||||
|
||||
if (theme === undefined) {
|
||||
if (this.selectedTheme?.theme || this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "theme-settings-changed", {
|
||||
theme: "",
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === (this.selectedTheme ?? this.hass.selectedTheme)?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "theme-settings-changed", {
|
||||
theme,
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
ha-radio-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
}
|
||||
.color-pickers {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
ha-input {
|
||||
min-width: 75px;
|
||||
flex-grow: 1;
|
||||
margin: 0 var(--ha-space-1);
|
||||
}
|
||||
|
||||
ha-theme-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"theme-settings-changed": Partial<ThemeSettings>;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-theme-settings": HaThemeSettings;
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ export class HaListVirtualized extends HaListBase {
|
||||
this._activeItemFocus = focusItem;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement
|
||||
?.element(index)
|
||||
?.element(this.activeItemIndex)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
export const DEFAULT_DIRTY_STATE_KEY = "__default__";
|
||||
|
||||
export type DefaultDirtyStateKey = typeof DEFAULT_DIRTY_STATE_KEY;
|
||||
|
||||
export interface DirtyStateContext<
|
||||
State = unknown,
|
||||
Key extends string = DefaultDirtyStateKey,
|
||||
> {
|
||||
/** Whether any contributor's current slice differs from its initial snapshot */
|
||||
isDirty: boolean;
|
||||
/**
|
||||
* Push a state slice. The first push for a slice sets its baseline.
|
||||
* Subsequent pushes are compared against that baseline using the provider's
|
||||
* compare strategy.
|
||||
*/
|
||||
setState: (state: State, key: Key) => void;
|
||||
/** Reset every slice baseline to its current value (marks clean). */
|
||||
markClean: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton context key for dirty-state tracking.
|
||||
*
|
||||
* Because Lit context keys are singletons, the value type is
|
||||
* `DirtyStateContext<unknown, DefaultDirtyStateKey>`. Providers and consumers
|
||||
* can use narrower `DirtyStateContext<State, Key>` annotations at the type
|
||||
* boundary.
|
||||
*/
|
||||
export const dirtyStateContext = createContext<DirtyStateContext>("dirtyState");
|
||||
@@ -116,12 +116,14 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
);
|
||||
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
|
||||
return html`${(supportsFeature(
|
||||
this.stateObj!,
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
) ||
|
||||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
|
||||
stateActive(this.stateObj!)
|
||||
(stateActive(this.stateObj!) || assumedState)
|
||||
? html`
|
||||
<div class="volume">
|
||||
${supportsMute
|
||||
|
||||
@@ -63,6 +63,9 @@ import { subscribeLabFeature } from "../../data/labs";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import type { EntitySettingsState } from "../../panels/config/entities/entity-registry-settings-editor";
|
||||
import type { Helper } from "../../panels/config/helpers/const";
|
||||
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import {
|
||||
@@ -121,9 +124,10 @@ declare global {
|
||||
const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends SubscribeMixin(
|
||||
ScrollableFadeMixin(LitElement)
|
||||
) {
|
||||
export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
EntitySettingsState | Helper | null,
|
||||
"entity-registry" | "helper"
|
||||
>()(SubscribeMixin(ScrollableFadeMixin(LitElement))) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
@@ -633,6 +637,18 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const childViewContent = this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
@@ -640,7 +656,8 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
@closed=${this._dialogClosed}
|
||||
@opened=${this._handleOpened}
|
||||
@show-child-view=${this._showChildView}
|
||||
.preventScrimClose=${this._currView === "settings" ||
|
||||
.preventScrimClose=${(this._currView === "settings" &&
|
||||
this.isDirtyState) ||
|
||||
!this._isEscapeEnabled}
|
||||
flexcontent
|
||||
>
|
||||
@@ -863,70 +880,65 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-info
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
.data=${this._data}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "settings"
|
||||
${this._currView === "settings"
|
||||
? html`
|
||||
<div ?hidden=${!!this._childView}>
|
||||
<ha-more-info-settings
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
</div>
|
||||
${childViewContent}
|
||||
`
|
||||
: cache(
|
||||
this._childView
|
||||
? childViewContent
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-settings
|
||||
<ha-more-info-info
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
.editMode=${this._infoEditMode}
|
||||
.data=${this._data}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "related"
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "add_to"
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-more-info-add-to
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: this._currView === "details"
|
||||
: this._currView === "add_to"
|
||||
? html`
|
||||
<ha-more-info-details
|
||||
.hass=${this.hass}
|
||||
.entry=${this._entry}
|
||||
.params=${{ entityId }}
|
||||
.yamlMode=${this._detailsYamlMode}
|
||||
></ha-more-info-details>
|
||||
<ha-more-info-add-to
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
: this._currView === "details"
|
||||
? html`
|
||||
<ha-more-info-details
|
||||
.hass=${this.hass}
|
||||
.entry=${this._entry}
|
||||
.params=${{ entityId }}
|
||||
.yamlMode=${this._detailsYamlMode}
|
||||
></ha-more-info-details>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
@@ -949,6 +961,10 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
| MoreInfoView
|
||||
| undefined;
|
||||
|
||||
if (previousView === "settings" && this._currView !== "settings") {
|
||||
this._discardDirtyStateChanges();
|
||||
}
|
||||
|
||||
if (previousView === "details" && this._currView !== "details") {
|
||||
const dialog =
|
||||
this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
|
||||
@@ -957,6 +973,12 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_currView") || changedProps.has("_entry")) {
|
||||
if (this._currView === "settings" && this._entry) {
|
||||
this._initDirtyTracking({ type: "deep" });
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_currView")) {
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
|
||||
@@ -5,6 +5,7 @@ in core bundle slows things down and causes duplicate registration.
|
||||
This is the entry point for providing external app stuff from app entrypoint.
|
||||
*/
|
||||
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { navigate } from "../common/navigate";
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
EMIncomingMessageBarCodeScanResult,
|
||||
EMIncomingMessageCommands,
|
||||
ImprovDiscoveredDevice,
|
||||
MatterCommissionFinish,
|
||||
} from "./external_messaging";
|
||||
|
||||
const barCodeListeners = new Set<
|
||||
@@ -91,6 +93,8 @@ export const handleExternalMessage = (
|
||||
fireEvent(window, "improv-discovered-device", msg.payload);
|
||||
} else if (msg.command === "improv/device_setup_done") {
|
||||
fireEvent(window, "improv-device-setup-done");
|
||||
} else if (msg.command === "matter/commission/finish") {
|
||||
fireEvent(window, "matter-commission-finish", msg.payload);
|
||||
} else if (msg.command === "bar_code/scan_result") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
@@ -115,5 +119,10 @@ declare global {
|
||||
interface HASSDomEvents {
|
||||
"improv-discovered-device": ImprovDiscoveredDevice;
|
||||
"improv-device-setup-done": undefined;
|
||||
"matter-commission-finish": MatterCommissionFinish;
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"matter-commission-finish": HASSDomEvent<MatterCommissionFinish>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,18 @@ export interface EMIncomingMessageKioskModeSet {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MatterCommissionFinish {
|
||||
name: string | null;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface EMIncomingMessageMatterCommissionFinish extends EMMessage {
|
||||
id: number;
|
||||
type: "command";
|
||||
command: "matter/commission/finish";
|
||||
payload: MatterCommissionFinish;
|
||||
}
|
||||
|
||||
export type EMIncomingMessageCommands =
|
||||
| EMIncomingMessageRestart
|
||||
| EMIncomingMessageNavigate
|
||||
@@ -331,6 +343,7 @@ export type EMIncomingMessageCommands =
|
||||
| EMIncomingMessageBarCodeScanAborted
|
||||
| EMIncomingMessageImprovDeviceDiscovered
|
||||
| EMIncomingMessageImprovDeviceSetupDone
|
||||
| EMIncomingMessageMatterCommissionFinish
|
||||
| EMIncomingMessageKioskModeSet;
|
||||
|
||||
type EMIncomingMessage =
|
||||
@@ -346,6 +359,7 @@ export interface ExternalConfig {
|
||||
canWriteTag?: boolean;
|
||||
hasExoPlayer?: boolean;
|
||||
canCommissionMatter?: boolean;
|
||||
hasMatterStatusReport?: boolean;
|
||||
canImportThreadCredentials?: boolean;
|
||||
canTransferThreadCredentialsToKeychain?: boolean;
|
||||
hasAssist?: boolean;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFormatFunctions } from "../common/translations/entity-state";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
import type { IconCategory } from "../data/icons";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -20,6 +21,7 @@ import { getLocalLanguage, getTranslation } from "../util/common-translation";
|
||||
import { demoConfig } from "./demo_config";
|
||||
import { demoPanels } from "./demo_panels";
|
||||
import { demoServices } from "./demo_services";
|
||||
import { ENTITY_COMPONENT_ICONS } from "./entity_component_icons";
|
||||
import { getEntity } from "./entities/registry";
|
||||
import type { EntityInput } from "./entities/types";
|
||||
|
||||
@@ -33,6 +35,12 @@ type MockRestCallback = (
|
||||
parameters: Record<string, any> | undefined
|
||||
) => any;
|
||||
|
||||
interface MockGetIconsMessage {
|
||||
type: "frontend/get_icons";
|
||||
category: IconCategory;
|
||||
integration?: string;
|
||||
}
|
||||
|
||||
export interface MockHomeAssistant extends HomeAssistant {
|
||||
mockEntities: any;
|
||||
updateHass(obj: Partial<MockHomeAssistant>);
|
||||
@@ -415,6 +423,10 @@ export const provideHass = (
|
||||
...overrideData,
|
||||
};
|
||||
|
||||
hassObj.mockWS("frontend/get_icons", ({ category }: MockGetIconsMessage) => ({
|
||||
resources: category === "entity_component" ? ENTITY_COMPONENT_ICONS : {},
|
||||
}));
|
||||
|
||||
// Set hass if required
|
||||
if (setHassProperty) {
|
||||
elements.forEach((el) => {
|
||||
|
||||
@@ -33,7 +33,6 @@ export interface PageNavigation {
|
||||
translationKey?: string;
|
||||
component?: string | string[];
|
||||
name?: string;
|
||||
not_component?: string | string[];
|
||||
core?: boolean;
|
||||
/** Hide from non-admin users in filtered navigation and quick bar. */
|
||||
adminOnly?: boolean;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { provide } from "@lit/context";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { LitElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { shallowEqual } from "../common/util/shallow-equal";
|
||||
import {
|
||||
DEFAULT_DIRTY_STATE_KEY,
|
||||
dirtyStateContext,
|
||||
type DefaultDirtyStateKey,
|
||||
type DirtyStateContext,
|
||||
} from "../data/context/dirty-state";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
export type CompareStrategy<State> =
|
||||
| { type: "deep" }
|
||||
| { type: "shallow" }
|
||||
| { type: "custom"; compare: (a: State, b: State) => boolean };
|
||||
|
||||
/**
|
||||
* Mixin that provides dirty-state tracking via Lit context.
|
||||
*
|
||||
* The provider holds a map of named slices. Each slice has its own initial
|
||||
* snapshot and current value, and is compared with the configured compare
|
||||
* strategy. `isDirty` is true when any slice differs from its initial value,
|
||||
* so independent contributors (e.g. a helper form alongside the entity
|
||||
* registry editor) can coexist without overwriting each other.
|
||||
*
|
||||
* @example Eager init for the provider's own slice:
|
||||
* ```ts
|
||||
* class MyDialog extends DirtyStateProviderMixin<MyDialogState>()(LitElement) {
|
||||
* open() {
|
||||
* this._initDirtyTracking({ type: "shallow" }, { name: "", icon: "" });
|
||||
* // Update later with `this._updateDirtyState({ name, icon })`.
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Deferred init with child consumers:
|
||||
* ```ts
|
||||
* class MyPage extends DirtyStateProviderMixin<MyState, "their-key">()(LitElement) {
|
||||
* connectedCallback() {
|
||||
* super.connectedCallback();
|
||||
* this._initDirtyTracking({ type: "deep" });
|
||||
* // Child consumers push slices via `setState(value, "their-key")`.
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Child consumers:
|
||||
* ```ts
|
||||
* @consume({ context: dirtyStateContext, subscribe: true })
|
||||
* @state()
|
||||
* private _dirtyState?: DirtyStateContext<MyState, "my-section">;
|
||||
*
|
||||
* // Read: this._dirtyState?.isDirty
|
||||
* // Write: this._dirtyState?.setState(value, "my-section")
|
||||
* ```
|
||||
*/
|
||||
export const DirtyStateProviderMixin =
|
||||
<State = unknown, Key extends string = DefaultDirtyStateKey>() =>
|
||||
<Base extends Constructor<LitElement>>(superClass: Base) => {
|
||||
class DirtyStateProviderMixinClass extends superClass {
|
||||
private _dirtySlices = new Map<
|
||||
Key | DefaultDirtyStateKey,
|
||||
{ initial: State; current: State }
|
||||
>();
|
||||
|
||||
private _dirtyCompareFn: (a: State, b: State) => boolean = deepEqual;
|
||||
|
||||
private _dirtyCloneFn: (value: State) => State = (value) => value;
|
||||
|
||||
@provide({ context: dirtyStateContext })
|
||||
@state()
|
||||
private _dirtyStateContext: DirtyStateContext<State, Key> =
|
||||
this._buildContextValue();
|
||||
|
||||
private _buildContextValue(): DirtyStateContext<State, Key> {
|
||||
return {
|
||||
isDirty: Array.from(this._dirtySlices.values()).some(
|
||||
({ initial, current }) => !this._dirtyCompareFn(initial, current)
|
||||
),
|
||||
setState: (value: State, key: Key) => {
|
||||
this._writeSlice(key, value);
|
||||
},
|
||||
markClean: () => {
|
||||
this._markDirtyStateClean();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _publishContext(): void {
|
||||
this._dirtyStateContext = this._buildContextValue();
|
||||
}
|
||||
|
||||
private _writeSlice(key: Key | DefaultDirtyStateKey, value: State): void {
|
||||
const slice = this._dirtySlices.get(key);
|
||||
if (!slice) {
|
||||
// First push for this key becomes the baseline.
|
||||
this._dirtySlices.set(key, {
|
||||
initial: this._dirtyCloneFn(value),
|
||||
current: value,
|
||||
});
|
||||
this._publishContext();
|
||||
return;
|
||||
}
|
||||
if (this._dirtyCompareFn(slice.current, value)) {
|
||||
return;
|
||||
}
|
||||
slice.current = value;
|
||||
this._publishContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dirty state tracking.
|
||||
*
|
||||
* When `initialState` is provided, it seeds the provider's own slice so
|
||||
* `_updateDirtyState` can be used immediately. When omitted, the first
|
||||
* push for any key (via the provider helper or a consumer's `setState`)
|
||||
* becomes that key's baseline.
|
||||
*
|
||||
* Call again to reset (e.g. when the underlying entity changes).
|
||||
*/
|
||||
protected _initDirtyTracking(
|
||||
strategy: CompareStrategy<State>,
|
||||
initialState?: State
|
||||
): void {
|
||||
switch (strategy.type) {
|
||||
case "deep":
|
||||
this._dirtyCompareFn = (a, b) => deepEqual(a, b);
|
||||
this._dirtyCloneFn = (value) => deepClone(value);
|
||||
break;
|
||||
case "shallow":
|
||||
this._dirtyCompareFn = (a, b) => shallowEqual(a, b);
|
||||
this._dirtyCloneFn = (value) => value;
|
||||
break;
|
||||
default:
|
||||
this._dirtyCompareFn = strategy.compare;
|
||||
this._dirtyCloneFn = (value) => value;
|
||||
}
|
||||
this._dirtySlices.clear();
|
||||
if (initialState !== undefined) {
|
||||
this._dirtySlices.set(DEFAULT_DIRTY_STATE_KEY, {
|
||||
initial: this._dirtyCloneFn(initialState),
|
||||
current: initialState,
|
||||
});
|
||||
}
|
||||
this._publishContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the provider's own state slice. Triggers dirty comparison
|
||||
* against the provider's baseline (or sets the baseline if this is the
|
||||
* first push after a deferred init).
|
||||
*/
|
||||
protected _updateDirtyState(newState: State): void {
|
||||
this._writeSlice(DEFAULT_DIRTY_STATE_KEY, newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset every slice's baseline to its current value. Call this after a
|
||||
* successful save.
|
||||
*/
|
||||
protected _markDirtyStateClean(): void {
|
||||
for (const slice of this._dirtySlices.values()) {
|
||||
slice.initial = this._dirtyCloneFn(slice.current);
|
||||
}
|
||||
this._publishContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard pending changes by restoring each slice's current value back
|
||||
* to its baseline.
|
||||
*/
|
||||
protected _discardDirtyStateChanges(): void {
|
||||
for (const slice of this._dirtySlices.values()) {
|
||||
slice.current = this._dirtyCloneFn(slice.initial);
|
||||
}
|
||||
this._publishContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any slice's current value differs from its baseline.
|
||||
* This passes the protected getter to the consuming class.
|
||||
*/
|
||||
public get isDirtyState(): boolean {
|
||||
return this._dirtyStateContext.isDirty;
|
||||
}
|
||||
}
|
||||
return DirtyStateProviderMixinClass;
|
||||
};
|
||||
@@ -45,7 +45,6 @@ class SupervisorAppsState extends LitElement {
|
||||
}
|
||||
.dot.state-started {
|
||||
background-color: var(--ha-color-green-80);
|
||||
animation: state-dot-pulse 1.8s infinite;
|
||||
}
|
||||
.dot.state-startup {
|
||||
background-color: var(--ha-color-on-warning-normal);
|
||||
@@ -56,19 +55,6 @@ class SupervisorAppsState extends LitElement {
|
||||
ha-svg-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
@keyframes state-dot-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--rgb-success-color), 0.6);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(var(--rgb-success-color), 0);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion) {
|
||||
.dot.state-started {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
import { saveFabStyles } from "./styles";
|
||||
|
||||
@@ -19,7 +24,9 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ type: Boolean }) public dirty = false;
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
protected get _config(): BlueprintAutomationConfig {
|
||||
return this.config;
|
||||
@@ -58,7 +65,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
class=${this._dirtyState?.isDirty ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._saveAutomation}
|
||||
>
|
||||
|
||||
@@ -421,7 +421,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
></blueprint-automation-editor>
|
||||
@@ -434,7 +433,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
.stateObj=${stateObj}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@@ -554,7 +552,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
class=${this.isDirtyState ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._handleSaveAutomation}
|
||||
>
|
||||
@@ -602,7 +600,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
!this.entityId
|
||||
) {
|
||||
const initData = getAutomationEditorInitData();
|
||||
this.dirty = !!initData;
|
||||
let baseConfig: Partial<AutomationConfig> = { description: "" };
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig = {
|
||||
@@ -617,6 +614,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
...baseConfig,
|
||||
...(initData ? normalizeAutomationConfig(initData) : initData),
|
||||
} as AutomationConfig;
|
||||
this._initDirtyTracking({ type: "deep" }, baseConfig as AutomationConfig);
|
||||
this._updateDirtyState(this.config);
|
||||
this.currentEntityId = undefined;
|
||||
this.readOnly = false;
|
||||
}
|
||||
@@ -624,10 +623,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this.config = normalizeAutomationConfig(c.config);
|
||||
this._initDirtyTracking({ type: "deep" }, this.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
|
||||
@@ -690,7 +689,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
@@ -762,7 +761,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
@@ -772,11 +770,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
id: this.config?.id,
|
||||
...normalizeAutomationConfig(ev.detail.value),
|
||||
};
|
||||
this._updateDirtyState(this.config!);
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
if (!this.isDirtyState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -787,7 +786,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.automationId || String(Date.now());
|
||||
@@ -901,7 +900,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -918,7 +917,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(config);
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -1009,7 +1008,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
}
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this._markDirtyStateClean();
|
||||
} catch (errors: any) {
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showEditorToast(this, {
|
||||
@@ -1068,7 +1067,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
private _applyUndoRedo(config: AutomationConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import type { Constructor, HomeAssistant, Route } from "../../../types";
|
||||
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
|
||||
@@ -87,7 +88,9 @@ export interface EditorDomainHooks<TConfig> {
|
||||
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
class AutomationScriptEditorClass extends superClass {
|
||||
class AutomationScriptEditorClass extends DirtyStateProviderMixin<TConfig>()(
|
||||
superClass
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
@@ -102,8 +105,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entityRegistry?: EntityRegistryEntry[];
|
||||
|
||||
@state() protected dirty = false;
|
||||
|
||||
@state() protected errors?: string;
|
||||
|
||||
@state() protected yamlErrors?: string;
|
||||
@@ -217,7 +218,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
|
||||
protected takeControlSave() {
|
||||
this.readOnly = false;
|
||||
this.dirty = true;
|
||||
// Force dirty: set baseline to null so current config always differs
|
||||
this._initDirtyTracking({ type: "deep" }, null as unknown as TConfig);
|
||||
this._updateDirtyState(this.config!);
|
||||
this.blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
@@ -237,10 +240,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
}
|
||||
};
|
||||
|
||||
protected get isDirty() {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this.confirmUnsavedChanged();
|
||||
}
|
||||
@@ -259,9 +258,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
const domain = hooks.domain;
|
||||
try {
|
||||
const config = await hooks.fetchFileConfig(this.hass, id);
|
||||
this.dirty = false;
|
||||
this.readOnly = false;
|
||||
this.config = hooks.normalizeConfig(config);
|
||||
this._initDirtyTracking({ type: "deep" }, this.config);
|
||||
hooks.checkValidation();
|
||||
} catch (err: any) {
|
||||
if (err.status_code !== 404) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import {
|
||||
html,
|
||||
@@ -19,6 +20,10 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { SidebarConfig } from "../../../data/automation";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type {
|
||||
Constructor,
|
||||
HomeAssistant,
|
||||
@@ -46,7 +51,13 @@ export const ManualEditorMixin = <TConfig>(
|
||||
|
||||
@property({ attribute: false }) public config!: TConfig;
|
||||
|
||||
@property({ attribute: false }) public dirty = false;
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
protected get dirty(): boolean {
|
||||
return this._dirtyState?.isDirty ?? false;
|
||||
}
|
||||
|
||||
@state() protected pastedConfig?: TConfig;
|
||||
|
||||
|
||||
@@ -15,13 +15,19 @@ import type {
|
||||
} from "../../../data/category_registry";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { ValueChangedEvent } from "../../../types";
|
||||
import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail";
|
||||
|
||||
interface CategoryFormState {
|
||||
name: string;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
@customElement("dialog-category-registry-detail")
|
||||
class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParams>(
|
||||
LitElement
|
||||
class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
DialogMixin<CategoryRegistryDetailDialogParams>(LitElement)
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@@ -44,6 +50,10 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
this._name = this.params?.suggestedName || "";
|
||||
this._icon = null;
|
||||
}
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{ name: this._name, icon: this._icon }
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -52,13 +62,14 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
}
|
||||
const entry = this.params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
const isCreate = !entry;
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
header-title=${entry
|
||||
? this._i18n.localize("ui.panel.config.category.editor.edit")
|
||||
: this._i18n.localize("ui.panel.config.category.editor.create")}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -96,7 +107,9 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
.disabled=${nameInvalid ||
|
||||
!!this._submitting ||
|
||||
(!isCreate && !this.isDirtyState)}
|
||||
>
|
||||
${entry
|
||||
? this._i18n.localize("ui.common.save")
|
||||
@@ -114,15 +127,17 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState({ name: this._name, icon: this._icon });
|
||||
}
|
||||
|
||||
private _iconChanged(ev: ValueChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._icon = ev.detail.value;
|
||||
this._updateDirtyState({ name: this._name, icon: this._icon });
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
const create = !this.params!.entry;
|
||||
const create = !this.params?.entry;
|
||||
this._submitting = true;
|
||||
let newValue: CategoryRegistryEntry | undefined;
|
||||
try {
|
||||
@@ -131,10 +146,11 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
icon: this._icon || (create ? undefined : null),
|
||||
};
|
||||
if (create) {
|
||||
newValue = await this.params!.createEntry!(values);
|
||||
newValue = await this.params?.createEntry?.(values);
|
||||
} else {
|
||||
newValue = await this.params!.updateEntry!(values);
|
||||
newValue = await this.params?.updateEntry?.(values);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { DeviceRegistryEntry } from "../../../../../../data/device/device_r
|
||||
import { fetchZHADevice } from "../../../../../../data/zha";
|
||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { showZHAManageZigbeeDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device";
|
||||
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
|
||||
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||
|
||||
@@ -64,8 +63,7 @@ export const getZHADeviceActions = async (
|
||||
{
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.manage"),
|
||||
icon: mdiGroup,
|
||||
action: () =>
|
||||
showZHAManageZigbeeDeviceDialog(el, { device: zhaDevice }),
|
||||
action: () => navigate(`/config/zha/device/${zhaDevice.ieee}/clusters`),
|
||||
},
|
||||
{
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.view_network"),
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consume } from "@lit/context";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeEntityEntryName } from "../../../../../common/entity/compute_entity_name";
|
||||
import "../../../../../components/ha-button";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../../../data/context/dirty-state";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import { removeEntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import type { Helper } from "../../../helpers/const";
|
||||
import "../../../helpers/forms/ha-counter-form";
|
||||
import "../../../helpers/forms/ha-input_boolean-form";
|
||||
@@ -33,21 +38,21 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
|
||||
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext<Helper | null, "helper">;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _item?: Helper | null;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _componentLoaded?: boolean;
|
||||
|
||||
@query("entity-registry-settings-editor")
|
||||
private _registryEditor?: EntityRegistrySettingsEditor;
|
||||
|
||||
private _originalItemJson?: string;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._componentLoaded = isComponentLoaded(
|
||||
@@ -60,13 +65,9 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
this._error = undefined;
|
||||
if (
|
||||
this.entry.unique_id !==
|
||||
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
|
||||
) {
|
||||
if (this.entry.unique_id !== changedProperties.get("entry")?.unique_id) {
|
||||
this._item = undefined;
|
||||
}
|
||||
|
||||
this._getItem();
|
||||
}
|
||||
}
|
||||
@@ -107,7 +108,6 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.entry=${this.entry}
|
||||
.disabled=${!!this._submitting}
|
||||
@change=${this._entityRegistryChanged}
|
||||
hide-name
|
||||
hide-icon
|
||||
></entity-registry-settings-editor>
|
||||
@@ -124,7 +124,7 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateItem}
|
||||
.disabled=${!this._dirty ||
|
||||
.disabled=${!this._dirtyState?.isDirty ||
|
||||
!!this._submitting ||
|
||||
!!(this._item && !this._item.name)}
|
||||
>
|
||||
@@ -134,48 +134,36 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _isHelperDirty(): boolean {
|
||||
if (!this._item || !this._originalItemJson) return false;
|
||||
return JSON.stringify(this._item) !== this._originalItemJson;
|
||||
}
|
||||
|
||||
private _updateDirty() {
|
||||
this._dirty = (this._registryEditor?.dirty ?? false) || this._isHelperDirty;
|
||||
}
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
this._updateDirty();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
private _valueChanged(ev: ValueChangedEvent<Helper>): void {
|
||||
if (this._item === null) {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._item = ev.detail.value;
|
||||
this._updateDirty();
|
||||
this._dirtyState?.setState(this._item, "helper");
|
||||
}
|
||||
|
||||
private async _getItem() {
|
||||
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
|
||||
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
|
||||
this._originalItemJson = this._item
|
||||
? JSON.stringify(this._item)
|
||||
: undefined;
|
||||
const item =
|
||||
items.find((helper) => helper.id === this.entry.unique_id) || null;
|
||||
this._item = item;
|
||||
this._dirtyState?.setState(item, "helper");
|
||||
}
|
||||
|
||||
private async _updateItem(): Promise<void> {
|
||||
this._submitting = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS_CRUD[this.entry.platform].update(
|
||||
this.hass!,
|
||||
this.hass,
|
||||
this._item.id,
|
||||
this._item
|
||||
);
|
||||
}
|
||||
const result = await this._registryEditor!.updateEntry();
|
||||
this._dirtyState?.markClean();
|
||||
if (result.close) {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "@lit/context";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
@@ -45,6 +45,10 @@ import {
|
||||
STREAM_TYPE_HLS,
|
||||
updateCameraPrefs,
|
||||
} from "../../../data/camera";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { deleteConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
@@ -144,6 +148,28 @@ const SCANNER_SOURCE_TYPES = ["router", "bluetooth", "bluetooth_le"];
|
||||
|
||||
const ZONE_DOMAINS = ["zone"];
|
||||
|
||||
export interface EntitySettingsState {
|
||||
name: string | null;
|
||||
icon: string | null;
|
||||
entityId: string;
|
||||
areaId: string | null;
|
||||
labels: string[];
|
||||
deviceClass: string | undefined;
|
||||
disabledBy: EntityRegistryEntry["disabled_by"];
|
||||
hiddenBy: EntityRegistryEntry["hidden_by"];
|
||||
unitOfMeasurement: string | null | undefined;
|
||||
precision: number | null | undefined;
|
||||
defaultCode: string | null | undefined;
|
||||
calendarColor: string | null;
|
||||
precipitationUnit: string | null | undefined;
|
||||
pressureUnit: string | null | undefined;
|
||||
temperatureUnit: string | null | undefined;
|
||||
visibilityUnit: string | null | undefined;
|
||||
windSpeedUnit: string | null | undefined;
|
||||
switchAsDomain: string;
|
||||
switchAsInvert: boolean;
|
||||
}
|
||||
|
||||
@customElement("entity-registry-settings-editor")
|
||||
export class EntityRegistrySettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -158,41 +184,53 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext<
|
||||
EntitySettingsState,
|
||||
"entity-registry"
|
||||
>;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@state() private _icon!: string;
|
||||
|
||||
@state() private _entityId!: string;
|
||||
@state() private _entityId!: EntitySettingsState["entityId"];
|
||||
|
||||
@state() private _deviceClass?: string;
|
||||
@state() private _deviceClass?: EntitySettingsState["deviceClass"];
|
||||
|
||||
@state() private _switchAsDomain = "switch";
|
||||
@state() private _switchAsDomain: EntitySettingsState["switchAsDomain"] =
|
||||
"switch";
|
||||
|
||||
@state() private _switchAsInvert = false;
|
||||
@state() private _switchAsInvert: EntitySettingsState["switchAsInvert"] =
|
||||
false;
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _labels?: string[] | null;
|
||||
|
||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
||||
@state() private _disabledBy!: EntitySettingsState["disabledBy"];
|
||||
|
||||
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
|
||||
@state() private _hiddenBy!: EntitySettingsState["hiddenBy"];
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _unit_of_measurement?: string | null;
|
||||
@state()
|
||||
private _unit_of_measurement?: EntitySettingsState["unitOfMeasurement"];
|
||||
|
||||
@state() private _precision?: number | null;
|
||||
@state() private _precision?: EntitySettingsState["precision"];
|
||||
|
||||
@state() private _precipitation_unit?: string | null;
|
||||
@state()
|
||||
private _precipitation_unit?: EntitySettingsState["precipitationUnit"];
|
||||
|
||||
@state() private _pressure_unit?: string | null;
|
||||
@state() private _pressure_unit?: EntitySettingsState["pressureUnit"];
|
||||
|
||||
@state() private _temperature_unit?: string | null;
|
||||
@state()
|
||||
private _temperature_unit?: EntitySettingsState["temperatureUnit"];
|
||||
|
||||
@state() private _visibility_unit?: string | null;
|
||||
@state() private _visibility_unit?: EntitySettingsState["visibilityUnit"];
|
||||
|
||||
@state() private _wind_speed_unit?: string | null;
|
||||
@state() private _wind_speed_unit?: EntitySettingsState["windSpeedUnit"];
|
||||
|
||||
@state() private _cameraPrefs?: CameraPreferences;
|
||||
|
||||
@@ -204,9 +242,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@state() private _weatherConvertibleUnits?: WeatherUnits;
|
||||
|
||||
@state() private _defaultCode?: string | null;
|
||||
@state() private _defaultCode?: EntitySettingsState["defaultCode"];
|
||||
|
||||
@state() private _calendarColor?: string | null;
|
||||
@state() private _calendarColor?: EntitySettingsState["calendarColor"];
|
||||
|
||||
@state() private _associatedZone?: string;
|
||||
|
||||
@@ -216,34 +254,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
|
||||
private _initialStateJson!: string;
|
||||
|
||||
private _lastDirty = false;
|
||||
|
||||
private _currentState() {
|
||||
return {
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
entityId: this._entityId.trim(),
|
||||
areaId: this._areaId ?? null,
|
||||
labels: this._labels ?? [],
|
||||
deviceClass: this._deviceClass,
|
||||
disabledBy: this._disabledBy,
|
||||
hiddenBy: this._hiddenBy,
|
||||
unitOfMeasurement: this._unit_of_measurement,
|
||||
precision: this._precision,
|
||||
defaultCode: this._defaultCode,
|
||||
calendarColor: this._calendarColor ?? null,
|
||||
precipitationUnit: this._precipitation_unit,
|
||||
pressureUnit: this._pressure_unit,
|
||||
temperatureUnit: this._temperature_unit,
|
||||
visibilityUnit: this._visibility_unit,
|
||||
windSpeedUnit: this._wind_speed_unit,
|
||||
switchAsDomain: this._switchAsDomain,
|
||||
switchAsInvert: this._switchAsInvert,
|
||||
};
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (
|
||||
@@ -315,9 +325,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
|
||||
}
|
||||
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
|
||||
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
|
||||
if (!deviceClasses || this._hideDeviceClassOverride(domain)) {
|
||||
@@ -365,6 +372,45 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
protected async updated(changedProps: PropertyValues): Promise<void> {
|
||||
if (changedProps.has("helperConfigEntry")) {
|
||||
if (this.helperConfigEntry?.domain === "switch_as_x") {
|
||||
this._switchAsDomain = computeDomain(this.entry.entity_id);
|
||||
this.hass.loadBackendTranslation("title", SWITCH_AS_DOMAINS, false);
|
||||
} else {
|
||||
this._switchAsDomain = "switch";
|
||||
this._switchAsInvert = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._name === undefined || this._entityId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirtyState?.setState(
|
||||
{
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
entityId: this._entityId.trim(),
|
||||
areaId: this._areaId ?? null,
|
||||
labels: this._labels ?? [],
|
||||
deviceClass: this._deviceClass,
|
||||
disabledBy: this._disabledBy,
|
||||
hiddenBy: this._hiddenBy,
|
||||
unitOfMeasurement: this._unit_of_measurement,
|
||||
precision: this._precision,
|
||||
defaultCode: this._defaultCode,
|
||||
calendarColor: this._calendarColor ?? null,
|
||||
precipitationUnit: this._precipitation_unit,
|
||||
pressureUnit: this._pressure_unit,
|
||||
temperatureUnit: this._temperature_unit,
|
||||
visibilityUnit: this._visibility_unit,
|
||||
windSpeedUnit: this._wind_speed_unit,
|
||||
switchAsDomain: this._switchAsDomain,
|
||||
switchAsInvert: this._switchAsInvert,
|
||||
},
|
||||
"entity-registry"
|
||||
);
|
||||
|
||||
if (changedProps.has("_deviceClass")) {
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
@@ -408,25 +454,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._weatherConvertibleUnits = undefined;
|
||||
}
|
||||
}
|
||||
if (changedProps.has("helperConfigEntry")) {
|
||||
if (this.helperConfigEntry?.domain === "switch_as_x") {
|
||||
this._switchAsDomain = computeDomain(this.entry.entity_id);
|
||||
this.hass.loadBackendTranslation("title", SWITCH_AS_DOMAINS, false);
|
||||
} else {
|
||||
this._switchAsDomain = "switch";
|
||||
this._switchAsInvert = false;
|
||||
}
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
}
|
||||
|
||||
if (this._initialStateJson) {
|
||||
const dirty = this.dirty;
|
||||
if (dirty !== this._lastDirty) {
|
||||
this._lastDirty = dirty;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -1144,10 +1171,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public get dirty(): boolean {
|
||||
return JSON.stringify(this._currentState()) !== this._initialStateJson;
|
||||
}
|
||||
|
||||
public async updateEntry(): Promise<{
|
||||
close: boolean;
|
||||
entry: ExtEntityRegistryEntry;
|
||||
@@ -1433,12 +1456,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._name = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _iconChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1457,22 +1478,18 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _entityIdChanged(ev: InputEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._entityId = `${computeDomain(this._origEntityId)}.${(ev.target as HTMLInputElement).value}`;
|
||||
}
|
||||
|
||||
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>): void {
|
||||
fireEvent(this, "change");
|
||||
this._deviceClass = ev.detail.value;
|
||||
}
|
||||
|
||||
private _unitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._unit_of_measurement = ev.detail.value;
|
||||
}
|
||||
|
||||
private _defaultcodeChanged(ev: InputEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._defaultCode =
|
||||
(ev.target as HTMLInputElement).value === ""
|
||||
? null
|
||||
@@ -1480,43 +1497,35 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _calendarColorChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._calendarColor = ev.detail.value || null;
|
||||
}
|
||||
|
||||
private _associatedZoneChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._associatedZone = ev.detail.value || "zone.home";
|
||||
}
|
||||
|
||||
private _precipitationUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._precipitation_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _precisionChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._precision =
|
||||
ev.detail.value === "default" ? null : Number(ev.detail.value);
|
||||
}
|
||||
|
||||
private _pressureUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._pressure_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _temperatureUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._temperature_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _visibilityUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._visibility_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _windSpeedUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._wind_speed_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1551,7 +1560,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _areaPicked(ev: CustomEvent) {
|
||||
fireEvent(this, "change");
|
||||
this._areaId = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1624,8 +1632,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _resetNameAndOpenDeviceSettings() {
|
||||
this._name = this.entry.name || "";
|
||||
fireEvent(this, "change");
|
||||
|
||||
this._openDeviceSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consume } from "@lit/context";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceName } from "../../../common/entity/compute_device_name";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
@@ -13,6 +14,10 @@ import {
|
||||
deleteConfigEntry,
|
||||
getConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import { updateDeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
@@ -38,14 +43,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
|
||||
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
@state() private _helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@query("entity-registry-settings-editor")
|
||||
private _registryEditor?: EntityRegistrySettingsEditor;
|
||||
|
||||
@@ -133,7 +140,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.entry=${this.entry}
|
||||
.helperConfigEntry=${this._helperConfigEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
@change=${this._entityRegistryChanged}
|
||||
></entity-registry-settings-editor>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
@@ -148,7 +154,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._dirty || !!this._submitting}
|
||||
.disabled=${!this._dirtyState?.isDirty || !!this._submitting}
|
||||
.loading=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
|
||||
@@ -157,11 +163,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
this._dirty = this._registryEditor?.dirty ?? false;
|
||||
}
|
||||
|
||||
private _openDeviceSettings() {
|
||||
const device = this.hass.devices[this.entry.device_id!];
|
||||
|
||||
@@ -207,8 +208,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
private async _updateEntry(): Promise<void> {
|
||||
this._submitting = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
const result = await this._registryEditor!.updateEntry();
|
||||
this._dirtyState?.markClean();
|
||||
if (result.close) {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@state() private _helperEntities: HassEntity[] = [];
|
||||
@state() private _helperEntities?: HassEntity[];
|
||||
|
||||
@state() private _disabledEntityEntries?: EntityRegistryEntry[];
|
||||
|
||||
@@ -229,7 +229,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||
|
||||
@state() private _entitySource: Record<string, string> = {};
|
||||
@state() private _entitySource?: Record<string, string>;
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
@@ -499,7 +499,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
configEntry !== undefined || entityState.attributes.editable,
|
||||
type: configEntry
|
||||
? configEntry.domain
|
||||
: this._entitySource[entityState.entity_id] ||
|
||||
: this._entitySource![entityState.entity_id] ||
|
||||
computeStateDomain(entityState),
|
||||
configEntry,
|
||||
entity: entityState,
|
||||
@@ -830,6 +830,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _applyFilters() {
|
||||
if (!this._helperEntities) {
|
||||
return;
|
||||
}
|
||||
const filters = Object.entries(this._filters);
|
||||
|
||||
let items: Set<string> | undefined;
|
||||
@@ -1003,10 +1006,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
if (!entityReg) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.no_category_support"
|
||||
"ui.panel.config.helpers.picker.no_category_support"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.no_category_entity_reg"
|
||||
"ui.panel.config.helpers.picker.no_category_entity_reg"
|
||||
),
|
||||
});
|
||||
return;
|
||||
@@ -1224,7 +1227,7 @@ ${rejected
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
|
||||
if (!this._entityEntries || !this._configEntries) {
|
||||
if (!this._entityEntries || !this._configEntries || !this._entitySource) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1265,6 +1268,7 @@ ${rejected
|
||||
);
|
||||
|
||||
if (
|
||||
!this._helperEntities ||
|
||||
this._helperEntities.length !== newHelpers.length ||
|
||||
!this._helperEntities.every((val, idx) => newHelpers[idx] === val)
|
||||
) {
|
||||
|
||||
@@ -151,6 +151,7 @@ class HaDomainIntegrations extends LitElement {
|
||||
.map(
|
||||
([dom, val]) =>
|
||||
html`<ha-integration-list-item
|
||||
.domain=${dom}
|
||||
.integration=${{
|
||||
...val,
|
||||
domain: dom,
|
||||
|
||||
+62
-1
@@ -2,6 +2,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
|
||||
@@ -10,6 +11,7 @@ import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import type { MatterCommissionFinish } from "../../../../../external_app/external_messaging";
|
||||
import {
|
||||
commissionMatterDevice,
|
||||
watchForNewMatterDevice,
|
||||
@@ -82,6 +84,10 @@ class DialogMatterAddDevice extends LitElement {
|
||||
|
||||
@state() private _mainEntity?: ExtEntityRegistryEntry;
|
||||
|
||||
@state() private _proposedDeviceName?: string;
|
||||
|
||||
@state() private _commissioningFinished = false;
|
||||
|
||||
@state() private _deviceAddedState: {
|
||||
name: string;
|
||||
area: string | undefined;
|
||||
@@ -104,15 +110,63 @@ class DialogMatterAddDevice extends LitElement {
|
||||
// make sure a refresh of the page will navigate to the device page, old iOS apps will refresh the webview when commissioning is done
|
||||
setRefreshUrl(`/config/devices/device/${device.id}`);
|
||||
this._newDevice = device;
|
||||
this._step = "device_added";
|
||||
this._fetchMainEntity();
|
||||
this._maybeShowDeviceAdded();
|
||||
});
|
||||
if (this._waitForCommissioningFinish) {
|
||||
window.addEventListener(
|
||||
"matter-commission-finish",
|
||||
this._handleCommissionFinish
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private get _waitForCommissioningFinish(): boolean {
|
||||
// When the app supports reporting Matter commissioning status, defer
|
||||
// advancing past the spinner until we receive matter/commission/finish.
|
||||
return !!this.hass.auth.external?.config.hasMatterStatusReport;
|
||||
}
|
||||
|
||||
private _maybeShowDeviceAdded(): void {
|
||||
if (!this._newDevice) {
|
||||
return;
|
||||
}
|
||||
if (this._waitForCommissioningFinish && !this._commissioningFinished) {
|
||||
return;
|
||||
}
|
||||
this._step = "device_added";
|
||||
}
|
||||
|
||||
private _handleCommissionFinish = (
|
||||
ev: HASSDomEvent<MatterCommissionFinish>
|
||||
) => {
|
||||
const { name, success } = ev.detail;
|
||||
if (!success) {
|
||||
if (this._newDevice) {
|
||||
// Device already showed up in the registry — ignore the failure signal
|
||||
// and let the user finish the rename flow.
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.dialogs.matter-add-device.add_device_failed"
|
||||
),
|
||||
duration: 2000,
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._commissioningFinished = true;
|
||||
if (name) {
|
||||
this._proposedDeviceName = name;
|
||||
}
|
||||
this._maybeShowDeviceAdded();
|
||||
};
|
||||
|
||||
protected updated(changedProps: Map<string, unknown>): void {
|
||||
// Retry fetching main entity when hass updates (entities may not be available immediately)
|
||||
if (
|
||||
@@ -160,6 +214,8 @@ class DialogMatterAddDevice extends LitElement {
|
||||
this._newDevice = undefined;
|
||||
this._mainEntity = undefined;
|
||||
this._mainEntityFetched = false;
|
||||
this._proposedDeviceName = undefined;
|
||||
this._commissioningFinished = false;
|
||||
this._deviceAddedState = {
|
||||
name: "",
|
||||
area: undefined,
|
||||
@@ -168,6 +224,10 @@ class DialogMatterAddDevice extends LitElement {
|
||||
};
|
||||
this._unsub?.();
|
||||
this._unsub = undefined;
|
||||
window.removeEventListener(
|
||||
"matter-commission-finish",
|
||||
this._handleCommissionFinish
|
||||
);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -211,6 +271,7 @@ class DialogMatterAddDevice extends LitElement {
|
||||
hass: this.hass,
|
||||
device: this._newDevice,
|
||||
mainEntity: this._mainEntity,
|
||||
proposedName: this._proposedDeviceName,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
+16
-2
@@ -36,6 +36,8 @@ class MatterAddDeviceDeviceAdded extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public mainEntity?: ExtEntityRegistryEntry;
|
||||
|
||||
@property({ attribute: false }) public proposedName?: string;
|
||||
|
||||
@state() private _deviceName = "";
|
||||
|
||||
@state() private _area: string | undefined;
|
||||
@@ -49,8 +51,18 @@ class MatterAddDeviceDeviceAdded extends LitElement {
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (!this._initialized && this.device) {
|
||||
this._initialized = true;
|
||||
this._deviceName = computeDeviceName(this.device) ?? "";
|
||||
this._deviceName =
|
||||
this.proposedName || (computeDeviceName(this.device) ?? "");
|
||||
this._area = this.device.area_id ?? undefined;
|
||||
} else if (
|
||||
changedProps.has("proposedName") &&
|
||||
this.proposedName &&
|
||||
this.device &&
|
||||
this._deviceName === (computeDeviceName(this.device) ?? "")
|
||||
) {
|
||||
// proposedName arrived after we initialized, and the user hasn't
|
||||
// changed the name yet — adopt it
|
||||
this._deviceName = this.proposedName;
|
||||
}
|
||||
if (
|
||||
!this._deviceClassInitialized &&
|
||||
@@ -158,7 +170,9 @@ class MatterAddDeviceDeviceAdded extends LitElement {
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div class="device-name">
|
||||
<span>${computeDeviceName(this.device)}</span>
|
||||
<span
|
||||
>${this.proposedName || computeDeviceName(this.device)}</span
|
||||
>
|
||||
<span class="secondary">Matter</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../../components/ha-card";
|
||||
import "../../../../../../components/ha-spinner";
|
||||
import type { ZHADevice, ZHAGroup } from "../../../../../../data/zha";
|
||||
import { fetchBindableDevices, fetchGroups } from "../../../../../../data/zha";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import "../zha-device-binding";
|
||||
import { sortZHADevices, sortZHAGroups } from "../functions";
|
||||
import "../zha-group-binding";
|
||||
|
||||
@customElement("zha-device-bindings-pane")
|
||||
export class ZHADeviceBindingsPane extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device?: ZHADevice;
|
||||
|
||||
@state() private _bindableDevices: ZHADevice[] = [];
|
||||
|
||||
@state() private _groups: ZHAGroup[] = [];
|
||||
|
||||
@state() private _loaded = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const oldDevice = changedProperties.get("device");
|
||||
const deviceChanged =
|
||||
changedProperties.has("device") && this.device?.ieee !== oldDevice?.ieee;
|
||||
|
||||
if (deviceChanged) {
|
||||
this._bindableDevices = [];
|
||||
this._groups = [];
|
||||
this._loaded = false;
|
||||
this._error = undefined;
|
||||
this._fetchBindings();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.device) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._loaded) {
|
||||
return html`
|
||||
<ha-card class="loading-card">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html`<ha-card class="empty-card">${this._error}</ha-card>`;
|
||||
}
|
||||
|
||||
if (!this._bindableDevices.length && !this._groups.length) {
|
||||
return html`
|
||||
<ha-card class="empty-card">
|
||||
${this.hass.localize("ui.panel.config.zha.device_page.no_bindings")}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._bindableDevices.length
|
||||
? html`
|
||||
<ha-card class="binding-card">
|
||||
<div class="binding-section-header">
|
||||
<div class="binding-section-title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.device_binding.header"
|
||||
)}
|
||||
</div>
|
||||
<div class="binding-section-description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.device_binding.introduction"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<zha-device-binding-control
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.bindableDevices=${this._bindableDevices}
|
||||
></zha-device-binding-control>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${this._groups.length
|
||||
? html`
|
||||
<ha-card class="binding-card">
|
||||
<div class="binding-section-header">
|
||||
<div class="binding-section-title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.group_binding.header"
|
||||
)}
|
||||
</div>
|
||||
<div class="binding-section-description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.group_binding.introduction"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<zha-group-binding-control
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.groups=${this._groups}
|
||||
></zha-group-binding-control>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchBindings(): Promise<void> {
|
||||
if (!this.device || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ieee = this.device.ieee;
|
||||
|
||||
try {
|
||||
const [bindableDevices, groups] = await Promise.all([
|
||||
this.device.device_type !== "Coordinator"
|
||||
? fetchBindableDevices(this.hass, ieee)
|
||||
: Promise.resolve([]),
|
||||
fetchGroups(this.hass),
|
||||
]);
|
||||
|
||||
if (this.device?.ieee !== ieee) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._bindableDevices = bindableDevices.sort(sortZHADevices);
|
||||
this._groups = groups.sort(sortZHAGroups);
|
||||
} catch (_err: any) {
|
||||
if (this.device?.ieee === ieee) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.bindings_error"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (this.device?.ieee === ieee) {
|
||||
this._loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.binding-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.binding-section-header {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.binding-section-title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.binding-section-description {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.loading-card,
|
||||
.empty-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-device-bindings-pane": ZHADeviceBindingsPane;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const zhaDevicePageCardStyles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.device-page-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
`;
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiCogRefresh,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFamilyTree,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import checkValidDate from "../../../../../../common/datetime/check_valid_date";
|
||||
import { formatDateTimeWithSeconds } from "../../../../../../common/datetime/format_date_time";
|
||||
import { navigate } from "../../../../../../common/navigate";
|
||||
import "../../../../../../components/ha-button";
|
||||
import "../../../../../../components/ha-card";
|
||||
import "../../../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../../../components/ha-dropdown";
|
||||
import "../../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../../components/ha-icon-button";
|
||||
import "../../../../../../components/ha-relative-time";
|
||||
import "../../../../../../components/ha-svg-icon";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../../../data/context";
|
||||
import type { ZHADevice } from "../../../../../../data/zha";
|
||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { formatAsPaddedHex } from "../functions";
|
||||
import { showZHAReconfigureDeviceDialog } from "../show-dialog-zha-reconfigure-device";
|
||||
|
||||
type ZHADeviceAction = "add-via" | "view-network" | "remove";
|
||||
|
||||
@customElement("zha-device-summary-card")
|
||||
export class ZHADeviceSummaryCard extends LitElement {
|
||||
@property({ attribute: false }) public device?: ZHADevice;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _processingRemove = false;
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.device || !this._config || !this._i18n) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = this.device.user_given_name || this.device.name;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="device-heading">
|
||||
<div class="device-name">${name}</div>
|
||||
${this.device.user_given_name
|
||||
? html`<div class="device-subtitle">${this.device.name}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.zha.device_page.information")}
|
||||
</div>
|
||||
<div class="summary-grid">
|
||||
${this._renderSummaryItem(
|
||||
this._i18n.localize("ui.panel.config.zha.device_page.ieee"),
|
||||
this.device.ieee
|
||||
)}
|
||||
${this._renderSummaryItem(
|
||||
this._i18n.localize("ui.panel.config.zha.device_page.nwk"),
|
||||
formatAsPaddedHex(this.device.nwk)
|
||||
)}
|
||||
${this._renderSummaryItem(
|
||||
this._i18n.localize(
|
||||
"ui.panel.config.zha.visualization.device_type"
|
||||
),
|
||||
this.device.device_type
|
||||
)}
|
||||
${this._renderSummaryItem(
|
||||
this._i18n.localize("ui.dialogs.zha_device_info.power_source"),
|
||||
this.device.power_source ||
|
||||
this._i18n.localize("ui.dialogs.zha_device_info.unknown")
|
||||
)}
|
||||
${this._renderLastSeenSummaryItem()}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${!this.device.active_coordinator
|
||||
? html`
|
||||
<ha-button appearance="plain" @click=${this._reconfigureDevice}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiCogRefresh}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.reconfigure"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-button appearance="plain" @click=${this._viewNetwork}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiFamilyTree}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.view_network"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
${this._renderDeviceActionMenu()}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDeviceActionMenu(): TemplateResult | typeof nothing {
|
||||
const canAddViaDevice =
|
||||
this.device!.power_source === "Mains" &&
|
||||
(this.device!.device_type === "Router" ||
|
||||
this.device!.device_type === "Coordinator");
|
||||
const canManageDevice = !this.device!.active_coordinator;
|
||||
|
||||
if (!canAddViaDevice && !canManageDevice) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleDeviceActionSelected}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this._i18n.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${canAddViaDevice
|
||||
? html`
|
||||
<ha-dropdown-item value="add-via">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._i18n.localize("ui.dialogs.zha_device_info.buttons.add")}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${canManageDevice
|
||||
? html`
|
||||
<ha-dropdown-item value="view-network">
|
||||
<ha-svg-icon slot="icon" .path=${mdiFamilyTree}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.view_network"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
value="remove"
|
||||
variant="danger"
|
||||
.disabled=${this._processingRemove}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.remove"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSummaryItem(
|
||||
label: string,
|
||||
value: string | number
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
<span class="summary-label">${label}</span>
|
||||
<span class="summary-value" title=${String(value)}>${value}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLastSeenSummaryItem(): TemplateResult {
|
||||
const label = this._i18n.localize("ui.dialogs.zha_device_info.last_seen");
|
||||
const lastSeen = this.device!.last_seen;
|
||||
|
||||
if (!lastSeen) {
|
||||
return this._renderSummaryItem(
|
||||
label,
|
||||
this._i18n.localize("ui.dialogs.zha_device_info.unknown")
|
||||
);
|
||||
}
|
||||
|
||||
const date = new Date(lastSeen);
|
||||
if (!checkValidDate(date)) {
|
||||
return this._renderSummaryItem(label, lastSeen);
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<span class="summary-label">${label}</span>
|
||||
<span
|
||||
class="summary-value"
|
||||
title=${formatDateTimeWithSeconds(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._config.config
|
||||
)}
|
||||
>
|
||||
<ha-relative-time .datetime=${lastSeen}></ha-relative-time>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _reconfigureDevice(): void {
|
||||
showZHAReconfigureDeviceDialog(this, { device: this.device! });
|
||||
}
|
||||
|
||||
private _addViaDevice(): void {
|
||||
navigate(`/config/zha/add/${this.device!.ieee}`);
|
||||
}
|
||||
|
||||
private _viewNetwork(): void {
|
||||
navigate(`/config/zha/visualization/${this.device!.device_reg_id}`);
|
||||
}
|
||||
|
||||
private _handleDeviceActionSelected(
|
||||
ev: HaDropdownSelectEvent<ZHADeviceAction>
|
||||
): void {
|
||||
switch (ev.detail.item.value) {
|
||||
case "add-via":
|
||||
this._addViaDevice();
|
||||
break;
|
||||
case "view-network":
|
||||
this._viewNetwork();
|
||||
break;
|
||||
case "remove":
|
||||
this._removeDevice();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeDevice(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this._i18n.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_title"
|
||||
),
|
||||
text: this._i18n.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_text"
|
||||
),
|
||||
confirmText: this._i18n.localize("ui.common.remove"),
|
||||
dismissText: this._i18n.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processingRemove = true;
|
||||
try {
|
||||
await this._api.callService("zha", "remove", {
|
||||
ieee: this.device!.ieee,
|
||||
});
|
||||
navigate("/config/devices", { replace: true });
|
||||
} finally {
|
||||
this._processingRemove = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host,
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.device-heading {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.device-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.device-subtitle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: var(--ha-space-1);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--ha-space-3);
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.summary-label,
|
||||
.summary-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
margin-top: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-l);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-1) var(--ha-space-4) var(--ha-space-1)
|
||||
var(--ha-space-1);
|
||||
}
|
||||
|
||||
.card-actions ha-button {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-actions ha-dropdown {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-device-summary-card": ZHADeviceSummaryCard;
|
||||
}
|
||||
}
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import "../../../../../components/ha-dialog-header";
|
||||
import "../../../../../components/ha-tab-group";
|
||||
import "../../../../../components/ha-tab-group-tab";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import type { ZHADevice, ZHAGroup } from "../../../../../data/zha";
|
||||
import { fetchBindableDevices, fetchGroups } from "../../../../../data/zha";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { sortZHADevices, sortZHAGroups } from "./functions";
|
||||
import type {
|
||||
Tab,
|
||||
ZHAManageZigbeeDeviceDialogParams,
|
||||
} from "./show-dialog-zha-manage-zigbee-device";
|
||||
import "./zha-cluster-attributes";
|
||||
import "./zha-cluster-commands";
|
||||
import "./zha-device-binding";
|
||||
import "./zha-device-neighbors";
|
||||
import "./zha-device-signature";
|
||||
import "./zha-group-binding";
|
||||
import "./zha-manage-clusters";
|
||||
|
||||
@customElement("dialog-zha-manage-zigbee-device")
|
||||
class DialogZHAManageZigbeeDevice extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _currTab: Tab = "clusters";
|
||||
|
||||
@state() private _device?: ZHADevice;
|
||||
|
||||
@state() private _bindableDevices: ZHADevice[] = [];
|
||||
|
||||
@state() private _groups: ZHAGroup[] = [];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
params: ZHAManageZigbeeDeviceDialogParams
|
||||
): Promise<void> {
|
||||
this._device = params.device;
|
||||
if (!this._device) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._currTab = params.tab || "clusters";
|
||||
this.large = false;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._device = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("close-dialog", () => this.closeDialog());
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this._device) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("_device")) {
|
||||
const tabs = this._getTabs(this._device);
|
||||
if (!tabs.includes(this._currTab)) {
|
||||
this._currTab = tabs[0];
|
||||
}
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._device) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const tabs = this._getTabs(this._device);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span
|
||||
slot="title"
|
||||
.title=${this.hass.localize("ui.dialogs.zha_manage_device.heading")}
|
||||
@click=${this._enlarge}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.zha_manage_device.heading")}
|
||||
</span>
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${this._currTab === tab}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.zha_manage_device.tabs.${tab}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
</ha-dialog-header>
|
||||
<div class="content" tabindex="-1" autofocus>
|
||||
${cache(
|
||||
this._currTab === "clusters"
|
||||
? html`
|
||||
<zha-manage-clusters
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
></zha-manage-clusters>
|
||||
`
|
||||
: this._currTab === "bindings"
|
||||
? html`
|
||||
${this._bindableDevices.length > 0
|
||||
? html`
|
||||
<zha-device-binding-control
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
.bindableDevices=${this._bindableDevices}
|
||||
></zha-device-binding-control>
|
||||
`
|
||||
: nothing}
|
||||
${this._device && this._groups.length > 0
|
||||
? html`
|
||||
<zha-group-binding-control
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
.groups=${this._groups}
|
||||
></zha-group-binding-control>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: this._currTab === "signature"
|
||||
? html`
|
||||
<zha-device-zigbee-info
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
></zha-device-zigbee-info>
|
||||
`
|
||||
: html`
|
||||
<zha-device-neighbors
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
.narrow=${!this.large}
|
||||
></zha-device-neighbors>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData(): Promise<void> {
|
||||
if (this._device && this.hass) {
|
||||
this._bindableDevices =
|
||||
this._device && this._device.device_type !== "Coordinator"
|
||||
? (await fetchBindableDevices(this.hass, this._device.ieee)).sort(
|
||||
sortZHADevices
|
||||
)
|
||||
: [];
|
||||
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
|
||||
}
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.name as Tab;
|
||||
if (newTab === this._currTab) {
|
||||
return;
|
||||
}
|
||||
this._currTab = newTab;
|
||||
}
|
||||
|
||||
private _getTabs = memoizeOne((device: ZHADevice | undefined) => {
|
||||
const tabs: Tab[] = ["clusters", "bindings", "signature"];
|
||||
|
||||
if (
|
||||
device &&
|
||||
(device.device_type === "Router" || device.device_type === "Coordinator")
|
||||
) {
|
||||
tabs.push("neighbors");
|
||||
}
|
||||
|
||||
return tabs;
|
||||
});
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zha-manage-zigbee-device": DialogZHAManageZigbeeDevice;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha";
|
||||
|
||||
export const formatAsPaddedHex = (value: string | number): string => {
|
||||
@@ -27,3 +28,16 @@ export const computeClusterKey = (cluster: Cluster): string =>
|
||||
`${cluster.name} (Endpoint id: ${
|
||||
cluster.endpoint_id
|
||||
}, Id: ${formatAsPaddedHex(cluster.id)}, Type: ${cluster.type})`;
|
||||
|
||||
export const computeClusterSecondary = (
|
||||
cluster: Cluster,
|
||||
localize: LocalizeFunc
|
||||
): string =>
|
||||
localize("ui.panel.config.zha.clusters.cluster_description", {
|
||||
endpoint: cluster.endpoint_id,
|
||||
id: formatAsPaddedHex(cluster.id),
|
||||
type: cluster.type,
|
||||
});
|
||||
|
||||
export const computeClusterValue = (cluster: Cluster): string =>
|
||||
`${cluster.endpoint_id}-${cluster.id}-${cluster.type}`;
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
|
||||
export type Tab = "clusters" | "bindings" | "signature" | "neighbors";
|
||||
|
||||
export interface ZHAManageZigbeeDeviceDialogParams {
|
||||
device: ZHADevice;
|
||||
tab?: Tab;
|
||||
}
|
||||
|
||||
export const loadZHAManageZigbeeDeviceDialog = () =>
|
||||
import("./dialog-zha-manage-zigbee-device");
|
||||
|
||||
export const showZHAManageZigbeeDeviceDialog = (
|
||||
element: HTMLElement,
|
||||
params: ZHAManageZigbeeDeviceDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zha-manage-zigbee-device",
|
||||
dialogImport: loadZHAManageZigbeeDeviceDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
@@ -60,7 +59,7 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-card class="content">
|
||||
<div class="content">
|
||||
<div class="attribute-picker">
|
||||
<ha-select
|
||||
.label=${this.hass!.localize(
|
||||
@@ -79,7 +78,7 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
${this._selectedAttributeId !== undefined
|
||||
? this._renderAttributeInteractions()
|
||||
: nothing}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -222,8 +221,12 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
border: none;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-select {
|
||||
@@ -235,12 +238,7 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-actions.warning ha-call-service-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.attribute-picker {
|
||||
align-items: center;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
padding-inline-start: 28px;
|
||||
@@ -256,15 +254,12 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
margin-top: var(--ha-space-2);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
gap: var(--ha-space-3);
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
@@ -54,7 +53,7 @@ export class ZHAClusterCommands extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-card class="content">
|
||||
<div class="content">
|
||||
<div class="command-picker">
|
||||
<ha-select
|
||||
.label=${this.hass!.localize(
|
||||
@@ -111,7 +110,7 @@ export class ZHAClusterCommands extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -184,8 +183,12 @@ export class ZHAClusterCommands extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
border: none;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-select {
|
||||
@@ -196,12 +199,7 @@ export class ZHAClusterCommands extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-actions.warning ha-call-service-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.command-picker {
|
||||
align-items: center;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
padding-inline-start: 28px;
|
||||
@@ -225,24 +223,10 @@ export class ZHAClusterCommands extends LitElement {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.toggle-help-icon {
|
||||
float: right;
|
||||
top: -6px;
|
||||
right: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: initial;
|
||||
padding-right: 0px;
|
||||
padding-inline-end: 0px;
|
||||
padding-inline-start: initial;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`,
|
||||
|
||||
+5
-1
@@ -35,6 +35,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
tag: "zha-add-group-page",
|
||||
load: () => import("./zha-add-group-page"),
|
||||
},
|
||||
device: {
|
||||
tag: "zha-device-page",
|
||||
load: () => import("./zha-device-page"),
|
||||
},
|
||||
visualization: {
|
||||
tag: "zha-network-visualization-page",
|
||||
load: () => import("./zha-network-visualization-page"),
|
||||
@@ -62,7 +66,7 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
if (this._currentPage === "group") {
|
||||
el.groupId = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "device") {
|
||||
el.ieee = this.routeTail.path.substr(1);
|
||||
el.ieee = this.routeTail.path.split("/")[1];
|
||||
} else if (this._currentPage === "visualization") {
|
||||
el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "section") {
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
@@ -16,24 +15,29 @@ export class ZHADeviceBindingControl extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public device?: ZHADevice;
|
||||
|
||||
@state() private _bindTargetIndex = -1;
|
||||
@property({ attribute: false }) public bindableDevices: ZHADevice[] = [];
|
||||
|
||||
@state() private bindableDevices: ZHADevice[] = [];
|
||||
@state() private _bindTargetIndex = -1;
|
||||
|
||||
@state() private _deviceToBind?: ZHADevice;
|
||||
|
||||
@state() private _bindingOperationInProgress = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("device")) {
|
||||
const oldDevice = changedProperties.get("device");
|
||||
const deviceChanged =
|
||||
changedProperties.has("device") && this.device?.ieee !== oldDevice?.ieee;
|
||||
|
||||
if (deviceChanged || changedProperties.has("bindableDevices")) {
|
||||
this._bindTargetIndex = -1;
|
||||
this._deviceToBind = undefined;
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card class="content">
|
||||
<div class="content">
|
||||
<div class="command-picker">
|
||||
<ha-select
|
||||
label=${this.hass!.localize(
|
||||
@@ -69,7 +73,7 @@ export class ZHADeviceBindingControl extends LitElement {
|
||||
${this.hass!.localize("ui.panel.config.zha.device_binding.bind")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -127,14 +131,11 @@ export class ZHADeviceBindingControl extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-4) 0 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.command-picker {
|
||||
align-items: center;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
padding-inline-start: 28px;
|
||||
@@ -142,14 +143,12 @@ export class ZHADeviceBindingControl extends LitElement {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
margin-top: var(--ha-space-2);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
gap: var(--ha-space-3);
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../../components/data-table/ha-data-table";
|
||||
@@ -7,11 +8,14 @@ import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import { narrowViewportContext } from "../../../../../data/context";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { fetchDevices } from "../../../../../data/zha";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { zhaDevicePageCardStyles } from "./device-page/zha-device-page-card-styles";
|
||||
|
||||
export interface DeviceRowData extends DataTableRowData {
|
||||
id: string;
|
||||
@@ -25,15 +29,25 @@ export interface DeviceRowData extends DataTableRowData {
|
||||
class ZHADeviceNeighbors extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public device?: ZHADevice;
|
||||
|
||||
@state()
|
||||
@consume({ context: narrowViewportContext, subscribe: true })
|
||||
private _narrow!: ContextType<typeof narrowViewportContext>;
|
||||
|
||||
@state() private _devices: Map<string, ZHADevice> | undefined;
|
||||
|
||||
@state() private _loaded = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (this.hass && changedProperties.has("device")) {
|
||||
const oldDevice = changedProperties.get("device");
|
||||
const deviceChanged =
|
||||
changedProperties.has("device") && this.device?.ieee !== oldDevice?.ieee;
|
||||
|
||||
if (this.hass && deviceChanged) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
@@ -47,15 +61,14 @@ class ZHADeviceNeighbors extends LitElement {
|
||||
if (device && devices) {
|
||||
device.neighbors.forEach((neighbor) => {
|
||||
const zhaDevice: ZHADevice | undefined = devices.get(neighbor.ieee);
|
||||
if (zhaDevice) {
|
||||
outputDevices.push({
|
||||
name: zhaDevice.user_given_name || zhaDevice.name,
|
||||
id: zhaDevice.device_reg_id,
|
||||
lqi: parseInt(neighbor.lqi),
|
||||
depth: parseInt(neighbor.depth),
|
||||
relationship: neighbor.relationship,
|
||||
});
|
||||
}
|
||||
outputDevices.push({
|
||||
name:
|
||||
zhaDevice?.user_given_name || zhaDevice?.name || neighbor.ieee,
|
||||
id: zhaDevice?.device_reg_id || neighbor.ieee,
|
||||
lqi: Number(neighbor.lqi),
|
||||
depth: Number(neighbor.depth),
|
||||
relationship: neighbor.relationship,
|
||||
});
|
||||
});
|
||||
}
|
||||
return outputDevices;
|
||||
@@ -110,35 +123,128 @@ class ZHADeviceNeighbors extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.device) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._loaded) {
|
||||
return html`
|
||||
<ha-card class="loading-card">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html`<ha-card class="empty-card">${this._error}</ha-card>`;
|
||||
}
|
||||
|
||||
const neighbors = this._deviceNeighbors(this.device, this._devices);
|
||||
|
||||
if (!neighbors.length) {
|
||||
return html`
|
||||
<ha-card class="device-page-card">
|
||||
${this._renderCardHeader()}
|
||||
<div class="empty-content">
|
||||
${this.hass.localize("ui.panel.config.zha.neighbors.no_neighbors")}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${!this._devices
|
||||
? html`<ha-spinner size="large"></ha-spinner>`
|
||||
: html`<ha-data-table
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._deviceNeighbors(this.device, this._devices)}
|
||||
auto-height
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.data-table.search"
|
||||
)}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.components.data-table.no-data"
|
||||
)}
|
||||
></ha-data-table>`}
|
||||
<ha-card class="device-page-card">
|
||||
${this._renderCardHeader()}
|
||||
<ha-data-table
|
||||
.columns=${this._columns(this._narrow)}
|
||||
.data=${neighbors}
|
||||
auto-height
|
||||
.searchLabel=${this.hass.localize("ui.components.data-table.search")}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.zha.neighbors.no_neighbors"
|
||||
)}
|
||||
></ha-data-table>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCardHeader(): TemplateResult {
|
||||
return html`
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.tabs.neighbors"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.tab_descriptions.neighbors"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData(): Promise<void> {
|
||||
if (this.device && this.hass) {
|
||||
const devices = await fetchDevices(this.hass!);
|
||||
this._devices = new Map(
|
||||
devices.map((device: ZHADevice) => [device.ieee, device])
|
||||
);
|
||||
const ieee = this.device.ieee;
|
||||
this._loaded = false;
|
||||
this._error = undefined;
|
||||
try {
|
||||
const devices = await fetchDevices(this.hass);
|
||||
if (this.device?.ieee !== ieee) {
|
||||
return;
|
||||
}
|
||||
this._devices = new Map(
|
||||
devices.map((device: ZHADevice) => [device.ieee, device])
|
||||
);
|
||||
} catch (_err: any) {
|
||||
if (this.device?.ieee === ieee) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zha.neighbors.load_failed"
|
||||
);
|
||||
this._devices = undefined;
|
||||
}
|
||||
} finally {
|
||||
if (this.device?.ieee === ieee) {
|
||||
this._loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
zhaDevicePageCardStyles,
|
||||
css`
|
||||
ha-data-table {
|
||||
--data-table-background-color: var(--card-background-color);
|
||||
--data-table-border-width: 0;
|
||||
--ha-border-radius-sm: 0;
|
||||
}
|
||||
|
||||
.loading-card,
|
||||
.empty-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
color: var(--secondary-text-color);
|
||||
padding: var(--ha-space-8);
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiAccessPointNetwork,
|
||||
mdiCodeJson,
|
||||
mdiHexagonMultipleOutline,
|
||||
mdiLinkVariant,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { goBack, navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import { narrowViewportContext } from "../../../../../data/context";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { fetchZHADevice } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-error-screen";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "./device-page/zha-device-bindings-pane";
|
||||
import "./device-page/zha-device-summary-card";
|
||||
import "./zha-device-neighbors";
|
||||
import "./zha-device-signature";
|
||||
import "./zha-manage-clusters";
|
||||
|
||||
type ZHADevicePageTab = "clusters" | "bindings" | "signature" | "neighbors";
|
||||
|
||||
const TAB_ICONS: Record<ZHADevicePageTab, string> = {
|
||||
clusters: mdiHexagonMultipleOutline,
|
||||
bindings: mdiLinkVariant,
|
||||
signature: mdiCodeJson,
|
||||
neighbors: mdiAccessPointNetwork,
|
||||
};
|
||||
|
||||
const DEVICE_REFRESH_INTERVAL = 60000;
|
||||
|
||||
@customElement("zha-device-page")
|
||||
class ZHADevicePage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: "ieee" }) public ieee!: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: narrowViewportContext, subscribe: true })
|
||||
private _narrow!: ContextType<typeof narrowViewportContext>;
|
||||
|
||||
@state() private _device?: ZHADevice;
|
||||
|
||||
@state() private _currTab: ZHADevicePageTab = "clusters";
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _deviceRefreshInterval?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearDeviceRefreshInterval();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("route")) {
|
||||
this._syncTabFromRoute();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("ieee")) {
|
||||
this._clearDeviceRefreshInterval();
|
||||
if (this.ieee) {
|
||||
this._fetchDevice();
|
||||
} else {
|
||||
this._device = undefined;
|
||||
this._loading = false;
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.not_found"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const header =
|
||||
this._device?.user_given_name ||
|
||||
this._device?.name ||
|
||||
this.hass.localize("ui.panel.config.zha.device_page.heading");
|
||||
|
||||
if (this._loading || (!this._device && !this._error)) {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this._narrow}
|
||||
.header=${header}
|
||||
.backCallback=${this._goBack}
|
||||
>
|
||||
<div class="loading">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._device || this._error) {
|
||||
return html`
|
||||
<hass-error-screen
|
||||
.hass=${this.hass}
|
||||
.error=${this._error ||
|
||||
this.hass.localize("ui.panel.config.zha.device_page.not_found")}
|
||||
></hass-error-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
const tabNavigation = this._getTabNavigation(this._device, this.ieee);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
.tabs=${tabNavigation}
|
||||
.backCallback=${this._goBack}
|
||||
>
|
||||
<div class="container">
|
||||
<zha-device-summary-card
|
||||
class="device-info"
|
||||
.device=${this._device}
|
||||
></zha-device-summary-card>
|
||||
|
||||
<div class="main-content">
|
||||
${cache(
|
||||
this._currTab === "clusters"
|
||||
? html`
|
||||
<zha-manage-clusters
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
></zha-manage-clusters>
|
||||
`
|
||||
: this._currTab === "bindings"
|
||||
? html`
|
||||
<zha-device-bindings-pane
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
></zha-device-bindings-pane>
|
||||
`
|
||||
: this._currTab === "signature"
|
||||
? html`
|
||||
<zha-device-zigbee-info
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
></zha-device-zigbee-info>
|
||||
`
|
||||
: html`
|
||||
<zha-device-neighbors
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
></zha-device-neighbors>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchDevice(): Promise<void> {
|
||||
const ieee = this.ieee;
|
||||
this._loading = true;
|
||||
this._error = undefined;
|
||||
this._device = undefined;
|
||||
|
||||
try {
|
||||
const device = await fetchZHADevice(this.hass, ieee);
|
||||
if (this.ieee !== ieee) {
|
||||
return;
|
||||
}
|
||||
this._device = device;
|
||||
const tabs = this._getTabs(device);
|
||||
if (!tabs.includes(this._currTab)) {
|
||||
this._currTab = tabs[0];
|
||||
}
|
||||
this._startDeviceRefreshInterval();
|
||||
} catch (_err: any) {
|
||||
if (this.ieee === ieee) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.not_found"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (this.ieee === ieee) {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _startDeviceRefreshInterval(): void {
|
||||
this._clearDeviceRefreshInterval();
|
||||
this._deviceRefreshInterval = window.setInterval(
|
||||
() => this._refreshDevice(),
|
||||
DEVICE_REFRESH_INTERVAL
|
||||
);
|
||||
}
|
||||
|
||||
private _clearDeviceRefreshInterval(): void {
|
||||
if (this._deviceRefreshInterval) {
|
||||
window.clearInterval(this._deviceRefreshInterval);
|
||||
this._deviceRefreshInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _refreshDevice(): Promise<void> {
|
||||
if (!this._device) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ieee = this.ieee;
|
||||
|
||||
try {
|
||||
const device = await fetchZHADevice(this.hass, ieee);
|
||||
if (this.ieee === ieee) {
|
||||
this._device = device;
|
||||
}
|
||||
} catch (_err: any) {
|
||||
// Keep showing the current device details until a full page refresh fails.
|
||||
}
|
||||
}
|
||||
|
||||
private _syncTabFromRoute(): void {
|
||||
const pathParts = this.route?.path.split("/").filter(Boolean) || [];
|
||||
if (this.ieee && pathParts.length === 1) {
|
||||
navigate(`/config/zha/device/${this.ieee}/clusters`, { replace: true });
|
||||
return;
|
||||
}
|
||||
const newTab = (pathParts[1] as ZHADevicePageTab | undefined) || "clusters";
|
||||
if (newTab === this._currTab) {
|
||||
return;
|
||||
}
|
||||
this._currTab = this._isValidTab(newTab) ? newTab : "clusters";
|
||||
}
|
||||
|
||||
private _isValidTab(tab: string): tab is ZHADevicePageTab {
|
||||
return ["clusters", "bindings", "signature", "neighbors"].includes(tab);
|
||||
}
|
||||
|
||||
private _goBack = (): void => {
|
||||
goBack(
|
||||
this._device
|
||||
? `/config/devices/device/${this._device.device_reg_id}`
|
||||
: "/config/zha/dashboard"
|
||||
);
|
||||
};
|
||||
|
||||
private _getTabs = memoizeOne((device: ZHADevice | undefined) => {
|
||||
const tabs: ZHADevicePageTab[] = ["clusters", "bindings", "signature"];
|
||||
|
||||
if (
|
||||
device &&
|
||||
(device.device_type === "Router" || device.device_type === "Coordinator")
|
||||
) {
|
||||
tabs.push("neighbors");
|
||||
}
|
||||
|
||||
return tabs;
|
||||
});
|
||||
|
||||
private _getTabNavigation = memoizeOne(
|
||||
(device: ZHADevice, ieee: string): PageNavigation[] =>
|
||||
this._getTabs(device).map((tab) => ({
|
||||
path: `/config/zha/device/${ieee}/${tab}`,
|
||||
translationKey: `ui.panel.config.zha.device_page.tabs.${tab}`,
|
||||
iconPath: TAB_ICONS[tab],
|
||||
}))
|
||||
);
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
hass-tabs-subpage {
|
||||
--app-header-text-color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
|
||||
gap: var(--ha-space-4);
|
||||
align-items: start;
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: var(--ha-space-4) var(--ha-space-4)
|
||||
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-12);
|
||||
}
|
||||
|
||||
.device-info,
|
||||
.main-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.container {
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-device-page": ZHADevicePage;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { zhaDevicePageCardStyles } from "./device-page/zha-device-page-card-styles";
|
||||
|
||||
@customElement("zha-device-zigbee-info")
|
||||
class ZHADeviceZigbeeInfo extends LitElement {
|
||||
@@ -35,10 +38,33 @@ class ZHADeviceZigbeeInfo extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-code-editor mode="yaml" read-only .value=${this._signature} dir="ltr">
|
||||
</ha-code-editor>
|
||||
<ha-card class="device-page-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.tabs.signature"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.device_page.tab_descriptions.signature"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
read-only
|
||||
.value=${this._signature}
|
||||
dir="ltr"
|
||||
>
|
||||
</ha-code-editor>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [haStyle, zhaDevicePageCardStyles];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import type { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha";
|
||||
@@ -24,9 +23,9 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public device?: ZHADevice;
|
||||
|
||||
@state() private _bindTargetIndex = -1;
|
||||
@property({ attribute: false }) public groups: ZHAGroup[] = [];
|
||||
|
||||
@state() private groups: ZHAGroup[] = [];
|
||||
@state() private _bindTargetIndex = -1;
|
||||
|
||||
@state() private _selectedClusters: string[] = [];
|
||||
|
||||
@@ -42,10 +41,17 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
private _zhaClustersDataTable!: ZHAClustersDataTable;
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("device")) {
|
||||
const oldDevice = changedProperties.get("device");
|
||||
const deviceChanged =
|
||||
changedProperties.has("device") && this.device?.ieee !== oldDevice?.ieee;
|
||||
|
||||
if (deviceChanged || changedProperties.has("groups")) {
|
||||
this._bindTargetIndex = -1;
|
||||
this._groupToBind = undefined;
|
||||
this._selectedClusters = [];
|
||||
this._clustersToBind = [];
|
||||
}
|
||||
if (deviceChanged) {
|
||||
this._fetchClustersForZhaNode();
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
@@ -53,31 +59,31 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card class="content">
|
||||
<div class="command-picker">
|
||||
<ha-select
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zha.group_binding.group_picker_label"
|
||||
)}
|
||||
class="menu"
|
||||
.value=${String(this._bindTargetIndex)}
|
||||
@selected=${this._bindTargetIndexChanged}
|
||||
.options=${this.groups.map((group, idx) => ({
|
||||
value: String(idx),
|
||||
label: group.name,
|
||||
}))}
|
||||
>
|
||||
</ha-select>
|
||||
</div>
|
||||
<div class="command-picker">
|
||||
<zha-clusters-data-table
|
||||
.hass=${this.hass}
|
||||
.clusters=${this._clusters}
|
||||
@selection-changed=${this._handleClusterSelectionChanged}
|
||||
class="menu"
|
||||
></zha-clusters-data-table>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="content">
|
||||
<div class="command-picker">
|
||||
<ha-select
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zha.group_binding.group_picker_label"
|
||||
)}
|
||||
class="menu"
|
||||
.value=${String(this._bindTargetIndex)}
|
||||
@selected=${this._bindTargetIndexChanged}
|
||||
.options=${this.groups.map((group, idx) => ({
|
||||
value: String(idx),
|
||||
label: group.name,
|
||||
}))}
|
||||
>
|
||||
</ha-select>
|
||||
</div>
|
||||
<div class="command-picker">
|
||||
<zha-clusters-data-table
|
||||
.hass=${this.hass}
|
||||
.clusters=${this._clusters}
|
||||
@selection-changed=${this._handleClusterSelectionChanged}
|
||||
class="menu"
|
||||
></zha-clusters-data-table>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@click=${this._onUnbindGroupClick}
|
||||
.disabled=${!this._canBind || this._bindingOperationInProgress}
|
||||
@@ -88,17 +94,16 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
"ui.panel.config.zha.group_binding.unbind_button_label"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._onBindGroupClick}
|
||||
.disabled=${!this._canBind || this._bindingOperationInProgress}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.group_binding.bind_button_label"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
<ha-progress-button
|
||||
@click=${this._onBindGroupClick}
|
||||
.disabled=${!this._canBind || this._bindingOperationInProgress}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.group_binding.bind_button_label"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -171,10 +176,10 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchClustersForZhaNode(): Promise<void> {
|
||||
if (this.hass) {
|
||||
if (this.hass && this.device) {
|
||||
this._clusters = await fetchClustersForZhaDevice(
|
||||
this.hass,
|
||||
this.device!.ieee
|
||||
this.device.ieee
|
||||
);
|
||||
this._clusters = this._clusters
|
||||
.filter((cluster) => cluster.type.toLowerCase() === "out")
|
||||
@@ -199,12 +204,11 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: var(--ha-space-2);
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.command-picker {
|
||||
align-items: center;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
padding-inline-start: 28px;
|
||||
@@ -212,22 +216,12 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.input-text {
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
padding-inline-start: 28px;
|
||||
padding-inline-end: 28px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
gap: var(--ha-space-1);
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-tab-group";
|
||||
import "../../../../../components/ha-tab-group-tab";
|
||||
import "../../../../../components/ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../../../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { Cluster, ZHADevice } from "../../../../../data/zha";
|
||||
import { fetchClustersForZhaDevice } from "../../../../../data/zha";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { computeClusterKey } from "./functions";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import { computeClusterSecondary, computeClusterValue } from "./functions";
|
||||
import "./zha-cluster-attributes";
|
||||
import "./zha-cluster-commands";
|
||||
|
||||
@@ -30,11 +34,9 @@ const tabs = ["attributes", "commands"] as const;
|
||||
export class ZHAManageClusters extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public device?: ZHADevice;
|
||||
|
||||
@state() private _selectedClusterIndex = -1;
|
||||
@state() private _selectedClusterValue?: string;
|
||||
|
||||
@state() private _clusters: Cluster[] = [];
|
||||
|
||||
@@ -44,6 +46,10 @@ export class ZHAManageClusters extends LitElement {
|
||||
|
||||
@state() private _clustersLoaded = false;
|
||||
|
||||
@state() private _clustersError?: string;
|
||||
|
||||
@query("ha-generic-picker") private _clusterPicker?: HaGenericPicker;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.device) {
|
||||
@@ -55,138 +61,307 @@ export class ZHAManageClusters extends LitElement {
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("device")) {
|
||||
const oldDevice = changedProperties.get("device");
|
||||
const deviceChanged =
|
||||
changedProperties.has("device") && this.device?.ieee !== oldDevice?.ieee;
|
||||
|
||||
if (deviceChanged) {
|
||||
this._clusters = [];
|
||||
this._selectedClusterIndex = -1;
|
||||
this._selectedClusterValue = undefined;
|
||||
this._selectedCluster = undefined;
|
||||
this._clustersLoaded = false;
|
||||
this._clustersError = undefined;
|
||||
this._fetchClustersForZhaDevice();
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.device || !this._clustersLoaded) {
|
||||
if (!this.device) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._clustersLoaded) {
|
||||
return html`
|
||||
<ha-card class="loading-card">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._clustersError) {
|
||||
return html`<ha-card class="empty-card">${this._clustersError}</ha-card>`;
|
||||
}
|
||||
|
||||
if (!this._clusters.length) {
|
||||
return html`
|
||||
<ha-card class="empty-card">
|
||||
${this.hass.localize("ui.panel.config.zha.clusters.no_clusters")}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card class="content">
|
||||
<div class="node-picker">
|
||||
<ha-select
|
||||
.label=${this.hass!.localize("ui.panel.config.zha.common.clusters")}
|
||||
class="menu"
|
||||
.value=${String(this._selectedClusterIndex)}
|
||||
@selected=${this._selectedClusterChanged}
|
||||
.options=${this._clusters.map((entry, idx) => ({
|
||||
value: String(idx),
|
||||
label: computeClusterKey(entry),
|
||||
}))}
|
||||
>
|
||||
</ha-select>
|
||||
</div>
|
||||
<ha-card class="cluster-detail-card">
|
||||
${this._renderClusterHeader()}
|
||||
${this._selectedCluster
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${this._currTab === tab}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.zha.clusters.tabs.${tab}`
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
|
||||
<div class="content" tabindex="-1" dialogInitialFocus>
|
||||
${cache(
|
||||
this._currTab === "attributes"
|
||||
? html`
|
||||
<zha-cluster-attributes
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.selectedCluster=${this._selectedCluster}
|
||||
></zha-cluster-attributes>
|
||||
`
|
||||
: html`
|
||||
<zha-cluster-commands
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.selectedCluster=${this._selectedCluster}
|
||||
></zha-cluster-commands>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this._renderClusterSegmentedTabs()}
|
||||
${this._renderSelectedClusterPanel()}
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderClusterHeader(): TemplateResult {
|
||||
return html`
|
||||
<div class="cluster-header">
|
||||
<div class="cluster-heading">
|
||||
<div class="cluster-name">${this._selectedCluster?.name}</div>
|
||||
<div class="cluster-description">
|
||||
${this._selectedCluster
|
||||
? computeClusterSecondary(
|
||||
this._selectedCluster,
|
||||
this.hass.localize
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${this._renderClusterPicker()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderClusterPicker(): TemplateResult {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
class="menu"
|
||||
.label=${this.hass.localize("ui.panel.config.zha.clusters.header")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.zha.clusters.change_cluster"
|
||||
)}
|
||||
.getItems=${this._clusterItems(this._clusters, this.hass.localize)}
|
||||
.value=${this._selectedClusterValue}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.panel.config.zha.clusters.no_clusters"
|
||||
)}
|
||||
@value-changed=${this._selectedClusterChanged}
|
||||
hide-clear-icon
|
||||
>
|
||||
<ha-button
|
||||
slot="field"
|
||||
appearance="plain"
|
||||
@click=${this._openClusterPicker}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.zha.clusters.change_cluster")}
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronDown}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderClusterSegmentedTabs(): TemplateResult {
|
||||
return html`
|
||||
<div class="cluster-tabs" role="tablist">
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<button
|
||||
role="tab"
|
||||
type="button"
|
||||
data-tab=${tab}
|
||||
aria-selected=${this._currTab === tab}
|
||||
class=${this._currTab === tab ? "active" : ""}
|
||||
@click=${this._clusterTabClicked}
|
||||
>
|
||||
${this.hass.localize(`ui.panel.config.zha.clusters.tabs.${tab}`)}
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSelectedClusterPanel(): TemplateResult {
|
||||
return cache(
|
||||
this._currTab === "attributes"
|
||||
? html`
|
||||
<zha-cluster-attributes
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.selectedCluster=${this._selectedCluster}
|
||||
></zha-cluster-attributes>
|
||||
`
|
||||
: html`
|
||||
<zha-cluster-commands
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.selectedCluster=${this._selectedCluster}
|
||||
></zha-cluster-commands>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchClustersForZhaDevice(): Promise<void> {
|
||||
if (this.hass) {
|
||||
this._clusters = await fetchClustersForZhaDevice(
|
||||
this.hass,
|
||||
this.device!.ieee
|
||||
);
|
||||
this._clusters.sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (this._clusters.length > 0) {
|
||||
this._selectedClusterIndex = 0;
|
||||
this._selectedCluster = this._clusters[0];
|
||||
if (this.hass && this.device) {
|
||||
const ieee = this.device.ieee;
|
||||
try {
|
||||
this._clusters = await fetchClustersForZhaDevice(this.hass, ieee);
|
||||
if (this.device?.ieee !== ieee) {
|
||||
return;
|
||||
}
|
||||
this._clusters.sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (this._clusters.length > 0) {
|
||||
this._selectCluster(this._clusters[0]);
|
||||
}
|
||||
} catch (_err: any) {
|
||||
if (this.device?.ieee === ieee) {
|
||||
this._clustersError = this.hass.localize(
|
||||
"ui.panel.config.zha.clusters.load_failed"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (this.device?.ieee === ieee) {
|
||||
this._clustersLoaded = true;
|
||||
}
|
||||
}
|
||||
this._clustersLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.name;
|
||||
private _clusterTabClicked(event: Event): void {
|
||||
this._selectClusterTab(
|
||||
(event.currentTarget as HTMLElement).dataset.tab as (typeof tabs)[number]
|
||||
);
|
||||
}
|
||||
|
||||
private _selectClusterTab(newTab: (typeof tabs)[number]): void {
|
||||
if (newTab === this._currTab) {
|
||||
return;
|
||||
}
|
||||
this._currTab = newTab;
|
||||
}
|
||||
|
||||
private _selectedClusterChanged(event: HaSelectSelectEvent): void {
|
||||
this._selectedClusterIndex = Number(event.detail.value);
|
||||
this._selectedCluster = this._clusters[this._selectedClusterIndex];
|
||||
private _selectedClusterChanged(event: ValueChangedEvent<string>): void {
|
||||
this._selectClusterValue(event.detail.value);
|
||||
}
|
||||
|
||||
private _openClusterPicker(event: Event): void {
|
||||
event.stopPropagation();
|
||||
this._clusterPicker?.open();
|
||||
}
|
||||
|
||||
private _selectClusterValue(value?: string): void {
|
||||
this._selectCluster(
|
||||
this._clusters.find((cluster) => computeClusterValue(cluster) === value)
|
||||
);
|
||||
}
|
||||
|
||||
private _selectCluster(cluster?: Cluster): void {
|
||||
this._selectedCluster = cluster;
|
||||
this._selectedClusterValue = cluster
|
||||
? computeClusterValue(cluster)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _clusterItems = memoizeOne(
|
||||
(clusters: Cluster[], localize: HomeAssistant["localize"]) =>
|
||||
(): PickerComboBoxItem[] =>
|
||||
clusters.map((cluster) => ({
|
||||
id: computeClusterValue(cluster),
|
||||
primary: cluster.name,
|
||||
secondary: computeClusterSecondary(cluster, localize),
|
||||
sorting_label: cluster.name,
|
||||
}))
|
||||
);
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: var(--ha-space-4) 0 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-select {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.menu {
|
||||
width: 100%;
|
||||
}
|
||||
.header {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.node-picker {
|
||||
align-items: center;
|
||||
margin: 0 var(--ha-space-5);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
ha-tab-group {
|
||||
margin: 0 var(--ha-space-5);
|
||||
}
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
width: 100%;
|
||||
.loading-card,
|
||||
.empty-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cluster-detail-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cluster-header {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-4);
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.cluster-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cluster-name {
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cluster-description {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
margin-top: var(--ha-space-1);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cluster-header ha-button {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.cluster-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cluster-tabs button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--primary-text-color);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
min-width: 0;
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.cluster-tabs button.active {
|
||||
border-bottom-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -25,6 +25,7 @@ import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityPickerDisplay } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
@@ -47,7 +48,11 @@ import {
|
||||
} from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
entityRegistryByEntityId,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type {
|
||||
SceneConfig,
|
||||
SceneEntities,
|
||||
@@ -70,6 +75,7 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -92,8 +98,8 @@ interface DeviceEntities {
|
||||
type DeviceEntitiesLookup = Record<string, string[]>;
|
||||
|
||||
@customElement("ha-scene-editor")
|
||||
export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
KeyboardShortcutMixin(LitElement)
|
||||
export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -107,10 +113,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
|
||||
@property({ attribute: false }) public scenes!: SceneEntity[];
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
private _sceneRevision = 0;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _config?: SceneConfig;
|
||||
@@ -317,7 +323,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
.disabled=${this._saving}
|
||||
@click=${this._saveScene}
|
||||
class=${classMap({
|
||||
dirty: this._dirty || !this.sceneId,
|
||||
dirty: this.isDirty || !this.sceneId,
|
||||
saving: this._saving,
|
||||
})}
|
||||
>
|
||||
@@ -344,6 +350,9 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._deviceEntityLookup,
|
||||
Object.values(this.hass.devices)
|
||||
);
|
||||
const entityRegistryLookup = entityRegistryByEntityId(
|
||||
this._entityRegistryEntries
|
||||
);
|
||||
return html` <div
|
||||
id="root"
|
||||
class=${classMap({
|
||||
@@ -423,9 +432,19 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
if (!entityStateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const { secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
entityStateObj
|
||||
);
|
||||
const platform =
|
||||
entityRegistryLookup[entityId]?.platform;
|
||||
const integrationName = platform
|
||||
? domainToName(this.hass.localize, platform)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
.graphic=${this._mode === "live"
|
||||
? "icon"
|
||||
: undefined}
|
||||
@@ -445,6 +464,14 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
`
|
||||
: nothing}
|
||||
${computeStateName(entityStateObj)}
|
||||
${secondary
|
||||
? html`<span slot="secondary">${secondary}</span>`
|
||||
: nothing}
|
||||
${integrationName
|
||||
? html`<span slot="meta" class="domain"
|
||||
>${integrationName}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
@@ -496,10 +523,19 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
if (!entityStateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const { secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
entityStateObj
|
||||
);
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
return html`
|
||||
<ha-list-item
|
||||
class="entity"
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
.graphic=${this._mode === "live"
|
||||
? "icon"
|
||||
: undefined}
|
||||
@@ -517,7 +553,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
></state-badge>`
|
||||
: nothing}
|
||||
${computeStateName(entityStateObj)}
|
||||
${secondary
|
||||
? html`<span slot="secondary"
|
||||
>${secondary}</span
|
||||
>`
|
||||
: nothing}
|
||||
<div slot="meta">
|
||||
<span class="domain">${domainName}</span>
|
||||
<ha-icon-button
|
||||
.path=${mdiDelete}
|
||||
.entityId=${entityId}
|
||||
@@ -600,7 +642,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
if (changedProps.has("sceneId") && !this.sceneId && this.hass) {
|
||||
this._dirty = false;
|
||||
this._sceneRevision = 0;
|
||||
const initData = getSceneEditorInitData();
|
||||
this._config = {
|
||||
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
|
||||
@@ -615,9 +657,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
category: "",
|
||||
};
|
||||
}
|
||||
this._dirty =
|
||||
this._initDirtyTracking({ type: "shallow" }, 0);
|
||||
if (
|
||||
initData !== undefined &&
|
||||
(initData.areaId !== undefined || initData.config !== undefined);
|
||||
(initData.areaId !== undefined || initData.config !== undefined)
|
||||
) {
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityRegistryEntries")) {
|
||||
@@ -775,7 +821,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _enterLiveMode() {
|
||||
if (this._dirty) {
|
||||
if (this.isDirty) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.scene.editor.enter_live_mode_unsaved"
|
||||
@@ -809,13 +855,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
this._errors = undefined;
|
||||
}
|
||||
|
||||
@@ -870,7 +916,8 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
(entity: SceneEntity) => entity.attributes.id === this.sceneId
|
||||
);
|
||||
|
||||
this._dirty = false;
|
||||
this._sceneRevision = 0;
|
||||
this._initDirtyTracking({ type: "shallow" }, 0);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@@ -919,7 +966,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._entities = [...this._entities, entityId];
|
||||
this._single_entities.push(entityId);
|
||||
this._storeState(entityId);
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _deleteEntity(ev: Event) {
|
||||
@@ -937,7 +984,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
if (this._config!.metadata) {
|
||||
delete this._config!.metadata[deleteEntityId];
|
||||
}
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _pickDevice(device_id: string) {
|
||||
@@ -953,7 +1000,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
deviceEntities.forEach((entityId) => {
|
||||
this._storeState(entityId);
|
||||
});
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _devicePicked(ev: CustomEvent) {
|
||||
@@ -977,7 +1024,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
delete this._config!.entities[entityId];
|
||||
});
|
||||
}
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _stateChanged(event: HassEvent) {
|
||||
@@ -985,7 +1032,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
event.context.id !== this._activateContextId &&
|
||||
this._entities.includes(event.data.entity_id)
|
||||
) {
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,7 +1078,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (this._dirty) {
|
||||
if (this.isDirty) {
|
||||
return showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.scene.editor.unsaved_confirm_title"
|
||||
@@ -1198,7 +1245,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
this._markDirtyStateClean();
|
||||
if (isNewScene) {
|
||||
navigate(`/config/scene/edit/${id}`, { replace: true });
|
||||
}
|
||||
@@ -1253,7 +1300,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
this._config = newConfig;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -1272,7 +1319,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
this._config = newConfig;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -1281,10 +1328,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
});
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this._confirmUnsavedChanged();
|
||||
}
|
||||
@@ -1355,10 +1398,21 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
ha-list-item {
|
||||
/* let the trailing label size to its content instead of the default
|
||||
fixed meta width, which would clip it */
|
||||
--mdc-list-item-meta-size: auto;
|
||||
}
|
||||
ha-list-item.entity {
|
||||
padding-right: 28px;
|
||||
}
|
||||
.domain {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import { css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type { BlueprintScriptConfig } from "../../../data/script";
|
||||
import { saveFabStyles } from "../automation/styles";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
@@ -15,7 +20,9 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ type: Boolean }) public dirty = false;
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
protected get _config(): BlueprintScriptConfig {
|
||||
return this.config;
|
||||
@@ -35,7 +42,7 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
class=${this._dirtyState?.isDirty ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._saveScript}
|
||||
>
|
||||
|
||||
@@ -377,7 +377,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-script=${this._handleSaveScript}
|
||||
></blueprint-script-editor>
|
||||
@@ -389,7 +388,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.isWide=${this.isWide}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@@ -470,7 +468,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${!this.readOnly && this.dirty ? "dirty" : ""}
|
||||
class=${!this.readOnly && this.isDirtyState ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._handleSaveScript}
|
||||
>
|
||||
@@ -522,7 +520,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
|
||||
const initData = getScriptEditorInitData();
|
||||
this.dirty = !!initData;
|
||||
const baseConfig: Partial<ScriptConfig> = {};
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig.sequence = [];
|
||||
@@ -531,12 +528,15 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
...baseConfig,
|
||||
...initData,
|
||||
} as ScriptConfig;
|
||||
this._initDirtyTracking({ type: "deep" }, baseConfig as ScriptConfig);
|
||||
this._updateDirtyState(this.config);
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this.config = normalizeScriptConfig(c.config);
|
||||
this._initDirtyTracking({ type: "deep" }, this.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
const regEntry = this.entityRegistry?.find(
|
||||
@@ -546,7 +546,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this.scriptId = regEntry.unique_id;
|
||||
}
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
}
|
||||
@@ -582,7 +581,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
this.config = ev.detail.value;
|
||||
this.errors = undefined;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config!);
|
||||
}
|
||||
|
||||
private async _runScript() {
|
||||
@@ -669,7 +668,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
this._manualEditor?.addFields();
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config!);
|
||||
}
|
||||
|
||||
private _preprocessYaml() {
|
||||
@@ -678,18 +677,18 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this.yamlErrors = undefined;
|
||||
this.config = ev.detail.value;
|
||||
this._updateDirtyState(this.config!);
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
if (!this.isDirtyState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -700,7 +699,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.scriptId || String(Date.now());
|
||||
@@ -815,7 +814,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -834,7 +833,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(config);
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -930,7 +929,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this._markDirtyStateClean();
|
||||
} catch (errors: any) {
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showEditorToast(this, {
|
||||
@@ -980,7 +979,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
private _applyUndoRedo(config: ScriptConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
|
||||
@@ -13,13 +13,16 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
createSearchParam,
|
||||
createHistoryLogbookUrl,
|
||||
decodeHistoryLogbookQueryParams,
|
||||
historyLogbookTargetFromQueryParams,
|
||||
} from "../../common/url/history-logbook-query-params";
|
||||
import {
|
||||
extractSearchParamsObject,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
@@ -230,43 +233,18 @@ class HaPanelHistory extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParams = extractSearchParamsObject();
|
||||
const entityIds = searchParams.entity_id;
|
||||
const deviceIds = searchParams.device_id;
|
||||
const areaIds = searchParams.area_id;
|
||||
const floorIds = searchParams.floor_id;
|
||||
const labelsIds = searchParams.label_id;
|
||||
if (entityIds || deviceIds || areaIds || floorIds || labelsIds) {
|
||||
this._targetPickerValue = {};
|
||||
const queryParams = decodeHistoryLogbookQueryParams(
|
||||
extractSearchParamsObject()
|
||||
);
|
||||
const targetPickerValue = historyLogbookTargetFromQueryParams(queryParams);
|
||||
if (targetPickerValue) {
|
||||
this._targetPickerValue = targetPickerValue;
|
||||
}
|
||||
if (entityIds) {
|
||||
const splitIds = entityIds.split(",");
|
||||
this._targetPickerValue!.entity_id = splitIds;
|
||||
if (queryParams.start_date) {
|
||||
this._startDate = queryParams.start_date;
|
||||
}
|
||||
if (deviceIds) {
|
||||
const splitIds = deviceIds.split(",");
|
||||
this._targetPickerValue!.device_id = splitIds;
|
||||
}
|
||||
if (areaIds) {
|
||||
const splitIds = areaIds.split(",");
|
||||
this._targetPickerValue!.area_id = splitIds;
|
||||
}
|
||||
if (floorIds) {
|
||||
const splitIds = floorIds.split(",");
|
||||
this._targetPickerValue!.floor_id = splitIds;
|
||||
}
|
||||
if (labelsIds) {
|
||||
const splitIds = labelsIds.split(",");
|
||||
this._targetPickerValue!.label_id = splitIds;
|
||||
}
|
||||
|
||||
const startDate = searchParams.start_date;
|
||||
if (startDate) {
|
||||
this._startDate = new Date(startDate);
|
||||
}
|
||||
const endDate = searchParams.end_date;
|
||||
if (endDate) {
|
||||
this._endDate = new Date(endDate);
|
||||
if (queryParams.end_date) {
|
||||
this._endDate = queryParams.end_date;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,37 +447,15 @@ class HaPanelHistory extends LitElement {
|
||||
}
|
||||
|
||||
private _updatePath() {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (this._targetPickerValue.entity_id) {
|
||||
params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
|
||||
","
|
||||
);
|
||||
}
|
||||
if (this._targetPickerValue.label_id) {
|
||||
params.label_id = ensureArray(this._targetPickerValue.label_id).join(",");
|
||||
}
|
||||
if (this._targetPickerValue.floor_id) {
|
||||
params.floor_id = ensureArray(this._targetPickerValue.floor_id).join(",");
|
||||
}
|
||||
if (this._targetPickerValue.area_id) {
|
||||
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
|
||||
}
|
||||
if (this._targetPickerValue.device_id) {
|
||||
params.device_id = ensureArray(this._targetPickerValue.device_id).join(
|
||||
","
|
||||
);
|
||||
}
|
||||
|
||||
if (this._startDate) {
|
||||
params.start_date = this._startDate.toISOString();
|
||||
}
|
||||
|
||||
if (this._endDate) {
|
||||
params.end_date = this._endDate.toISOString();
|
||||
}
|
||||
|
||||
navigate(`/history?${createSearchParam(params)}`, { replace: true });
|
||||
navigate(
|
||||
createHistoryLogbookUrl(
|
||||
"/history",
|
||||
this._targetPickerValue,
|
||||
this._startDate,
|
||||
this._endDate
|
||||
),
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
|
||||
@@ -4,12 +4,15 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
createSearchParam,
|
||||
createHistoryLogbookUrl,
|
||||
decodeHistoryLogbookQueryParams,
|
||||
historyLogbookTargetFromQueryParams,
|
||||
} from "../../common/url/history-logbook-query-params";
|
||||
import {
|
||||
extractSearchParamsObject,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
@@ -185,44 +188,17 @@ export class HaPanelLogbook extends LitElement {
|
||||
);
|
||||
|
||||
private _applyURLParams() {
|
||||
const searchParams = extractSearchParamsObject();
|
||||
const entityIds = searchParams.entity_id;
|
||||
const deviceIds = searchParams.device_id;
|
||||
const areaIds = searchParams.area_id;
|
||||
const floorIds = searchParams.floor_id;
|
||||
const labelsIds = searchParams.label_id;
|
||||
if (entityIds || deviceIds || areaIds || floorIds || labelsIds) {
|
||||
this._targetPickerValue = {};
|
||||
}
|
||||
if (entityIds) {
|
||||
const splitIds = entityIds.split(",");
|
||||
this._targetPickerValue!.entity_id = splitIds;
|
||||
}
|
||||
if (deviceIds) {
|
||||
const splitIds = deviceIds.split(",");
|
||||
this._targetPickerValue!.device_id = splitIds;
|
||||
}
|
||||
if (areaIds) {
|
||||
const splitIds = areaIds.split(",");
|
||||
this._targetPickerValue!.area_id = splitIds;
|
||||
}
|
||||
if (floorIds) {
|
||||
const splitIds = floorIds.split(",");
|
||||
this._targetPickerValue!.floor_id = splitIds;
|
||||
}
|
||||
if (labelsIds) {
|
||||
const splitIds = labelsIds.split(",");
|
||||
this._targetPickerValue!.label_id = splitIds;
|
||||
const queryParams = decodeHistoryLogbookQueryParams(
|
||||
extractSearchParamsObject()
|
||||
);
|
||||
const targetPickerValue = historyLogbookTargetFromQueryParams(queryParams);
|
||||
if (targetPickerValue) {
|
||||
this._targetPickerValue = targetPickerValue;
|
||||
}
|
||||
|
||||
const startDateStr = searchParams.start_date;
|
||||
const endDateStr = searchParams.end_date;
|
||||
|
||||
if (startDateStr || endDateStr) {
|
||||
const startDate = startDateStr
|
||||
? new Date(startDateStr)
|
||||
: this._time.range[0];
|
||||
const endDate = endDateStr ? new Date(endDateStr) : this._time.range[1];
|
||||
if (queryParams.start_date || queryParams.end_date) {
|
||||
const startDate = queryParams.start_date ?? this._time.range[0];
|
||||
const endDate = queryParams.end_date ?? this._time.range[1];
|
||||
|
||||
// Only set if date has changed.
|
||||
if (
|
||||
@@ -231,8 +207,8 @@ export class HaPanelLogbook extends LitElement {
|
||||
) {
|
||||
this._time = {
|
||||
range: [
|
||||
startDateStr ? new Date(startDateStr) : this._time.range[0],
|
||||
endDateStr ? new Date(endDateStr) : this._time.range[1],
|
||||
queryParams.start_date ?? this._time.range[0],
|
||||
queryParams.end_date ?? this._time.range[1],
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -254,37 +230,15 @@ export class HaPanelLogbook extends LitElement {
|
||||
}
|
||||
|
||||
private _updatePath() {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (this._targetPickerValue.entity_id) {
|
||||
params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
|
||||
","
|
||||
);
|
||||
}
|
||||
if (this._targetPickerValue.label_id) {
|
||||
params.label_id = ensureArray(this._targetPickerValue.label_id).join(",");
|
||||
}
|
||||
if (this._targetPickerValue.floor_id) {
|
||||
params.floor_id = ensureArray(this._targetPickerValue.floor_id).join(",");
|
||||
}
|
||||
if (this._targetPickerValue.area_id) {
|
||||
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
|
||||
}
|
||||
if (this._targetPickerValue.device_id) {
|
||||
params.device_id = ensureArray(this._targetPickerValue.device_id).join(
|
||||
","
|
||||
);
|
||||
}
|
||||
|
||||
if (this._time.range[0]) {
|
||||
params.start_date = this._time.range[0].toISOString();
|
||||
}
|
||||
|
||||
if (this._time.range[1]) {
|
||||
params.end_date = this._time.range[1].toISOString();
|
||||
}
|
||||
|
||||
navigate(`/logbook?${createSearchParam(params)}`, { replace: true });
|
||||
navigate(
|
||||
createHistoryLogbookUrl(
|
||||
"/logbook",
|
||||
this._targetPickerValue,
|
||||
this._time.range[0],
|
||||
this._time.range[1]
|
||||
),
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
|
||||
private _refreshLogbook() {
|
||||
|
||||
@@ -11,8 +11,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { batteryLevelIcon } from "../../../common/entity/battery_icon";
|
||||
import { batteryStateColorProperty } from "../../../common/entity/color/battery_color";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
@@ -109,6 +111,16 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const attributes = this._computeAttributes(stateObj);
|
||||
let batteryColorVar: string | undefined;
|
||||
if (attributes.includes("battery")) {
|
||||
const batteryLevel = stateObj.attributes.battery;
|
||||
const batteryColorProperty = batteryStateColorProperty(batteryLevel);
|
||||
if (batteryColorProperty) {
|
||||
batteryColorVar = computeCssVariable(batteryColorProperty);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${stateObj.attributes.entity_picture ? "has-plant-image" : ""}
|
||||
@@ -122,7 +134,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._computeAttributes(stateObj).map(
|
||||
${attributes.map(
|
||||
(item) => html`
|
||||
<div
|
||||
class="attributes"
|
||||
@@ -134,6 +146,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
|
||||
<div class="icon">
|
||||
${item === "battery"
|
||||
? html`<ha-icon
|
||||
style="color: ${batteryColorVar};"
|
||||
.icon=${batteryLevelIcon(stateObj.attributes.battery)}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
|
||||
@@ -247,6 +247,11 @@ export class HuiEntityEditor extends LitElement {
|
||||
.entity ha-entity-picker {
|
||||
flex-grow: 1;
|
||||
}
|
||||
ha-entity-picker:is([add-button]) {
|
||||
display: block;
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
ha-md-list {
|
||||
gap: 8px;
|
||||
padding-top: 0;
|
||||
|
||||
@@ -81,6 +81,7 @@ export class HuiEnergyGraphCardEditor
|
||||
? [
|
||||
{
|
||||
name: "show_legend",
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
|
||||
@@ -616,6 +616,10 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
margin-bottom: var(--ha-space-1);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-selector-select {
|
||||
display: block;
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1351,6 +1351,13 @@ class HUIRoot extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.edit-mode .action-items ha-icon-button[disabled] {
|
||||
--ha-color-on-disabled-quiet: color-mix(
|
||||
in srgb,
|
||||
var(--app-header-edit-text-color, #fff) 50%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
ha-tab-group {
|
||||
--ha-tab-indicator-color: var(
|
||||
--app-header-selection-bar-color,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
@@ -14,6 +15,10 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { TileCardConfig } from "../../lovelace/cards/types";
|
||||
import { BINARY_STATE_ON } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import {
|
||||
type EntityRegistryDisplayEntry,
|
||||
findBatteryChargingEntity,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
|
||||
export interface MaintenanceViewStrategyConfig {
|
||||
type: "maintenance";
|
||||
@@ -33,6 +38,16 @@ export const maintenanceEntityFilters: EntityFilter[] = [
|
||||
|
||||
const LOW_BATTERY_THRESHOLD = 20;
|
||||
|
||||
const _deviceEntities = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
entities: HomeAssistant["entities"]
|
||||
): EntityRegistryDisplayEntry[] => {
|
||||
const entries = Object.values(entities);
|
||||
return entries.filter((entity) => entity.device_id === deviceId);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterLowBatteryEntities = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[]
|
||||
@@ -42,6 +57,19 @@ export const filterLowBatteryEntities = (
|
||||
if (computeDomain(entityId) === "binary_sensor") {
|
||||
return state === BINARY_STATE_ON;
|
||||
}
|
||||
|
||||
const deviceId = hass.entities[entityId]?.device_id;
|
||||
const entities = deviceId ? _deviceEntities(deviceId, hass.entities) : [];
|
||||
|
||||
const batteryChargingEntity = findBatteryChargingEntity(hass, entities);
|
||||
const batteryCharging = batteryChargingEntity
|
||||
? hass.states[batteryChargingEntity?.entity_id]
|
||||
: undefined;
|
||||
|
||||
if (batteryCharging && batteryCharging.state === "on") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateValue = parseFloat(state);
|
||||
return !isNaN(stateValue) && stateValue <= LOW_BATTERY_THRESHOLD;
|
||||
});
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { normalizeLuminance } from "../../common/color/palette";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-settings-row";
|
||||
import "../../components/ha-theme-picker";
|
||||
import "../../components/input/ha-input";
|
||||
import "../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
|
||||
import "../../components/radio/ha-radio-option";
|
||||
import "../../components/ha-theme-settings";
|
||||
import {
|
||||
saveThemePreferences,
|
||||
subscribeThemePreferences,
|
||||
} from "../../data/theme";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import {
|
||||
DefaultAccentColor,
|
||||
DefaultPrimaryColor,
|
||||
} from "../../resources/theme/color/color.globals";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
ThemeSettings,
|
||||
ValueChangedEvent,
|
||||
} from "../../types";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { clearSelectedThemeState } from "../../util/ha-pref-storage";
|
||||
|
||||
const HOME_ASSISTANT_THEME = "default";
|
||||
|
||||
@customElement("ha-pick-theme-row")
|
||||
export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -54,14 +39,6 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
const hasThemes =
|
||||
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
|
||||
|
||||
const curThemeIsUseDefault = this.hass.selectedTheme?.theme === "";
|
||||
const curTheme = this.hass.selectedTheme?.theme
|
||||
? this.hass.selectedTheme?.theme
|
||||
: this.hass.themes.darkMode
|
||||
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
const themeSettings = this.hass.selectedTheme;
|
||||
const localTheme = this._getLocalTheme();
|
||||
const showMigration =
|
||||
this._userTheme !== undefined &&
|
||||
@@ -69,14 +46,15 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
localTheme !== null;
|
||||
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.panel.profile.themes.header")}</span
|
||||
>
|
||||
<span slot="description">
|
||||
<ha-theme-settings
|
||||
.hass=${this.hass}
|
||||
.selectedTheme=${this.hass.selectedTheme}
|
||||
.narrow=${this.narrow}
|
||||
.heading=${this.hass.localize("ui.panel.profile.themes.header")}
|
||||
.description=${html`
|
||||
${!hasThemes
|
||||
? this.hass.localize("ui.panel.profile.themes.error_no_theme")
|
||||
: ""}
|
||||
: nothing}
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
@@ -87,81 +65,32 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.link_promo")}
|
||||
</a>
|
||||
</span>
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
|
||||
.noThemeLabel=${this.hass.localize(
|
||||
"ui.panel.profile.themes.use_default"
|
||||
)}
|
||||
.value=${this.hass.selectedTheme?.theme || undefined}
|
||||
.disabled=${!hasThemes}
|
||||
include-default
|
||||
@value-changed=${this._handleThemeSelection}
|
||||
></ha-theme-picker>
|
||||
</ha-settings-row>
|
||||
${curTheme === HOME_ASSISTANT_THEME ||
|
||||
(curThemeIsUseDefault &&
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme)
|
||||
? html`<div class="inputs">
|
||||
<ha-radio-group
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.profile.themes.theme_mode"
|
||||
)}
|
||||
.value=${themeSettings?.dark === undefined
|
||||
? "auto"
|
||||
: themeSettings.dark
|
||||
? "dark"
|
||||
: "light"}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<ha-radio-option value="auto">
|
||||
${this.hass.localize("ui.panel.profile.themes.dark_mode.auto")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="light">
|
||||
${this.hass.localize("ui.panel.profile.themes.dark_mode.light")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="dark">
|
||||
${this.hass.localize("ui.panel.profile.themes.dark_mode.dark")}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
${curTheme === HOME_ASSISTANT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-input
|
||||
.value=${themeSettings?.primaryColor || DefaultPrimaryColor}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.primary_color"
|
||||
)}
|
||||
.name=${"primaryColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
<ha-input
|
||||
.value=${themeSettings?.accentColor || DefaultAccentColor}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.accent_color"
|
||||
)}
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
${themeSettings?.primaryColor || themeSettings?.accentColor
|
||||
? html` <ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
@click=${this._resetColors}
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.reset")}
|
||||
</ha-button>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
`}
|
||||
.labels=${{
|
||||
theme: this.hass.localize("ui.panel.profile.themes.dropdown_label"),
|
||||
noTheme: this.hass.localize("ui.panel.profile.themes.use_default"),
|
||||
mode: this.hass.localize("ui.panel.profile.themes.theme_mode"),
|
||||
autoMode: this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.auto"
|
||||
),
|
||||
lightMode: this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.light"
|
||||
),
|
||||
darkMode: this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.dark"
|
||||
),
|
||||
primaryColor: this.hass.localize(
|
||||
"ui.panel.profile.themes.primary_color"
|
||||
),
|
||||
accentColor: this.hass.localize(
|
||||
"ui.panel.profile.themes.accent_color"
|
||||
),
|
||||
reset: this.hass.localize("ui.panel.profile.themes.reset"),
|
||||
}}
|
||||
.themePickerDisabled=${!hasThemes}
|
||||
include-default
|
||||
@theme-settings-changed=${this._themeSettingsChanged}
|
||||
></ha-theme-settings>
|
||||
${showMigration
|
||||
? html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
@@ -183,77 +112,12 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleColorChange(ev: CustomEvent) {
|
||||
const target = ev.target as any;
|
||||
|
||||
// normalize primary color if needed for contrast
|
||||
if (target.name === "primaryColor") {
|
||||
target.value = normalizeLuminance(target.value);
|
||||
}
|
||||
|
||||
fireEvent(this, "settheme", { [target.name]: target.value });
|
||||
}
|
||||
|
||||
private _resetColors() {
|
||||
fireEvent(this, "settheme", {
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _supportsModeSelection(themeName: string): boolean {
|
||||
const theme = this.hass.themes.themes[themeName];
|
||||
if (!theme) {
|
||||
return false; // User's theme no longer exists
|
||||
}
|
||||
|
||||
return !!(theme.modes && "light" in theme.modes && "dark" in theme.modes);
|
||||
}
|
||||
|
||||
private _handleDarkMode(ev: Event) {
|
||||
let dark: boolean | undefined;
|
||||
switch ((ev.currentTarget as HaRadioGroup).value) {
|
||||
case "light":
|
||||
dark = false;
|
||||
break;
|
||||
case "dark":
|
||||
dark = true;
|
||||
break;
|
||||
}
|
||||
fireEvent(this, "settheme", { dark });
|
||||
}
|
||||
|
||||
private _handleThemeSelection(
|
||||
ev: ValueChangedEvent<string | undefined>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
const theme = ev.detail.value;
|
||||
|
||||
if (theme === undefined) {
|
||||
// undefined = "use default"
|
||||
if (this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "settheme", {
|
||||
theme: "",
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === this.hass.selectedTheme?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "settheme", {
|
||||
theme,
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
private _themeSettingsChanged(ev: HASSDomEvent<Partial<ThemeSettings>>) {
|
||||
fireEvent(this, "settheme", ev.detail);
|
||||
}
|
||||
|
||||
private _getLocalTheme(): ThemeSettings | null {
|
||||
@@ -285,33 +149,6 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin: 0 12px;
|
||||
}
|
||||
ha-radio-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
}
|
||||
.color-pickers {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
ha-input {
|
||||
min-width: 75px;
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
ha-theme-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+32
-12
@@ -1197,6 +1197,7 @@
|
||||
"data-table": {
|
||||
"search": "Search",
|
||||
"no-data": "No data",
|
||||
"no_match_filter": "No rows matching current filters",
|
||||
"filtering_by": "Filtering by",
|
||||
"hidden": "{number} hidden",
|
||||
"clear": "Clear",
|
||||
@@ -2254,15 +2255,6 @@
|
||||
"attribute": "Attribute",
|
||||
"min_max_change": "min/max/change"
|
||||
},
|
||||
"zha_manage_device": {
|
||||
"heading": "Manage Zigbee device",
|
||||
"tabs": {
|
||||
"clusters": "Clusters",
|
||||
"bindings": "Bindings",
|
||||
"signature": "Signature",
|
||||
"neighbors": "Neighbors"
|
||||
}
|
||||
},
|
||||
"zha_device_info": {
|
||||
"manuf": "by {manufacturer}",
|
||||
"no_area": "No area",
|
||||
@@ -4389,7 +4381,9 @@
|
||||
"error_information": "Error information",
|
||||
"delete_confirm_title": "Delete helper?",
|
||||
"delete_confirm_text": "Are you sure you want to delete {name}?",
|
||||
"delete_failed": "Failed to delete helper"
|
||||
"delete_failed": "Failed to delete helper",
|
||||
"no_category_support": "You can't assign a category to this helper",
|
||||
"no_category_entity_reg": "To assign a category to a helper it needs to have a unique ID."
|
||||
},
|
||||
"dialog": {
|
||||
"create": "Create",
|
||||
@@ -7334,10 +7328,32 @@
|
||||
"no_devices_found": "No devices were found, make sure they are in pairing mode and keep them awake while Home Assistant is searching.",
|
||||
"search_again": "Search again"
|
||||
},
|
||||
"device_page": {
|
||||
"heading": "Manage Zigbee device",
|
||||
"information": "Device information",
|
||||
"ieee": "IEEE",
|
||||
"nwk": "NWK",
|
||||
"bindings_error": "Failed to load binding options",
|
||||
"no_bindings": "No bindable devices or groups were found",
|
||||
"not_found": "Zigbee device not found",
|
||||
"tabs": {
|
||||
"clusters": "Clusters",
|
||||
"bindings": "Bindings",
|
||||
"signature": "Signature",
|
||||
"neighbors": "Neighbors"
|
||||
},
|
||||
"tab_descriptions": {
|
||||
"signature": "View the raw Zigbee signature for this device.",
|
||||
"neighbors": "View neighboring Zigbee devices reported by this device."
|
||||
}
|
||||
},
|
||||
"add_device": "Add device",
|
||||
"clusters": {
|
||||
"header": "Clusters",
|
||||
"help_cluster_dropdown": "Select a cluster to view attributes and commands.",
|
||||
"change_cluster": "Change cluster",
|
||||
"cluster_description": "Endpoint ID: {endpoint} · ID: {id} · Type: {type}",
|
||||
"load_failed": "Failed to load clusters",
|
||||
"no_clusters": "No clusters found",
|
||||
"tabs": {
|
||||
"attributes": "Attributes",
|
||||
"commands": "Commands"
|
||||
@@ -7414,6 +7430,8 @@
|
||||
"offline": "Offline"
|
||||
},
|
||||
"device_binding": {
|
||||
"header": "Device binding",
|
||||
"introduction": "Bind this device to another Zigbee device or unbind it.",
|
||||
"bind": "Bind",
|
||||
"unbind": "Unbind",
|
||||
"picker_label": "Bindable devices"
|
||||
@@ -7433,7 +7451,9 @@
|
||||
"name": "Name",
|
||||
"lqi": "LQI",
|
||||
"relationship": "Relationship",
|
||||
"depth": "Depth"
|
||||
"depth": "Depth",
|
||||
"load_failed": "Failed to load neighbors",
|
||||
"no_neighbors": "No neighbors found"
|
||||
},
|
||||
"change_channel_dialog": {
|
||||
"title": "Change Zigbee channel",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
canShowPage,
|
||||
isLoadedIntegration,
|
||||
isNotLoadedIntegration,
|
||||
isCore,
|
||||
} from "../../../src/common/config/can_show_page";
|
||||
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
|
||||
@@ -50,28 +49,6 @@ describe("isLoadedIntegration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNotLoadedIntegration", () => {
|
||||
it("should return true if the integration is not loaded", () => {
|
||||
const hass = {
|
||||
config: { components: ["test_component"] },
|
||||
} as unknown as HomeAssistant;
|
||||
const page = {
|
||||
not_component: "other_component",
|
||||
} as unknown as PageNavigation;
|
||||
expect(isNotLoadedIntegration(hass, page)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the integration is loaded", () => {
|
||||
const hass = {
|
||||
config: { components: ["test_component"] },
|
||||
} as unknown as HomeAssistant;
|
||||
const page = {
|
||||
not_component: "test_component",
|
||||
} as unknown as PageNavigation;
|
||||
expect(isNotLoadedIntegration(hass, page)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCore", () => {
|
||||
it("should return true if the page is core", () => {
|
||||
const page = { core: true } as unknown as PageNavigation;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createHistoryLogbookUrl,
|
||||
decodeHistoryLogbookQueryParams,
|
||||
historyLogbookQueryParamConfig,
|
||||
historyLogbookTargetParamKeys,
|
||||
historyLogbookTargetFromQueryParams,
|
||||
} from "../../../src/common/url/history-logbook-query-params";
|
||||
import {
|
||||
createQueryString,
|
||||
decodeQueryParams,
|
||||
queryParamsFromServiceTarget,
|
||||
serviceTargetFromQueryParams,
|
||||
} from "../../../src/common/url/query-params";
|
||||
|
||||
const panelQueryParams = [
|
||||
{
|
||||
type: "history",
|
||||
path: "/history",
|
||||
},
|
||||
{
|
||||
type: "logbook",
|
||||
path: "/logbook",
|
||||
},
|
||||
] as const;
|
||||
|
||||
describe.each(panelQueryParams)("$type query params", (panel) => {
|
||||
it("decodes target and date params", () => {
|
||||
const params = decodeQueryParams(
|
||||
"?entity_id=light.kitchen,switch.fan&device_id=device-1&area_id=kitchen&floor_id=downstairs&label_id=important&start_date=2026-06-05T10:00:00.000Z&end_date=2026-06-05T11:00:00.000Z&back=1",
|
||||
historyLogbookQueryParamConfig
|
||||
);
|
||||
|
||||
expect(params).toEqual({
|
||||
entity_id: ["light.kitchen", "switch.fan"],
|
||||
label_id: ["important"],
|
||||
floor_id: ["downstairs"],
|
||||
area_id: ["kitchen"],
|
||||
device_id: ["device-1"],
|
||||
start_date: new Date("2026-06-05T10:00:00.000Z"),
|
||||
end_date: new Date("2026-06-05T11:00:00.000Z"),
|
||||
back: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates target picker values only when target params are present", () => {
|
||||
expect(
|
||||
serviceTargetFromQueryParams(
|
||||
decodeQueryParams(
|
||||
"?start_date=2026-06-05T10:00:00.000Z",
|
||||
historyLogbookQueryParamConfig
|
||||
),
|
||||
historyLogbookTargetParamKeys
|
||||
)
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
serviceTargetFromQueryParams(
|
||||
decodeQueryParams(
|
||||
"?entity_id=light.kitchen&area_id=kitchen",
|
||||
historyLogbookQueryParamConfig
|
||||
),
|
||||
historyLogbookTargetParamKeys
|
||||
)
|
||||
).toEqual({
|
||||
entity_id: ["light.kitchen"],
|
||||
area_id: ["kitchen"],
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores empty target values", () => {
|
||||
expect(
|
||||
serviceTargetFromQueryParams(
|
||||
decodeQueryParams(
|
||||
"?entity_id=&device_id=",
|
||||
historyLogbookQueryParamConfig
|
||||
),
|
||||
historyLogbookTargetParamKeys
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("encodes target picker values", () => {
|
||||
expect(
|
||||
queryParamsFromServiceTarget(
|
||||
{
|
||||
entity_id: ["light.kitchen", "switch.fan"],
|
||||
area_id: "kitchen",
|
||||
},
|
||||
historyLogbookTargetParamKeys
|
||||
)
|
||||
).toEqual({
|
||||
entity_id: ["light.kitchen", "switch.fan"],
|
||||
area_id: ["kitchen"],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates deterministic query strings", () => {
|
||||
expect(
|
||||
createQueryString(
|
||||
{
|
||||
device_id: ["device-1"],
|
||||
entity_id: ["light.kitchen"],
|
||||
start_date: new Date("2026-06-05T10:00:00.000Z"),
|
||||
end_date: new Date("2026-06-05T11:00:00.000Z"),
|
||||
},
|
||||
historyLogbookQueryParamConfig
|
||||
)
|
||||
).toBe(
|
||||
"entity_id=light.kitchen&device_id=device-1&start_date=2026-06-05T10%3A00%3A00.000Z&end_date=2026-06-05T11%3A00%3A00.000Z"
|
||||
);
|
||||
});
|
||||
|
||||
it("creates typed URLs", () => {
|
||||
expect(
|
||||
createHistoryLogbookUrl(
|
||||
panel.path,
|
||||
{ entity_id: ["light.kitchen"] },
|
||||
new Date("2026-06-05T10:00:00.000Z"),
|
||||
new Date("2026-06-05T11:00:00.000Z")
|
||||
)
|
||||
).toBe(
|
||||
`${panel.path}?entity_id=light.kitchen&start_date=2026-06-05T10%3A00%3A00.000Z&end_date=2026-06-05T11%3A00%3A00.000Z`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("history logbook query params", () => {
|
||||
it("decodes query params", () => {
|
||||
expect(
|
||||
decodeHistoryLogbookQueryParams("?entity_id=light.kitchen&back=1")
|
||||
).toEqual({
|
||||
entity_id: ["light.kitchen"],
|
||||
back: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates target picker values only when target params are present", () => {
|
||||
expect(
|
||||
historyLogbookTargetFromQueryParams(
|
||||
decodeHistoryLogbookQueryParams("?start_date=2026-06-05T10:00:00.000Z")
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1238,15 +1238,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/autocomplete@npm:6.20.2, @codemirror/autocomplete@npm:^6.0.0":
|
||||
version: 6.20.2
|
||||
resolution: "@codemirror/autocomplete@npm:6.20.2"
|
||||
"@codemirror/autocomplete@npm:6.20.3, @codemirror/autocomplete@npm:^6.0.0":
|
||||
version: 6.20.3
|
||||
resolution: "@codemirror/autocomplete@npm:6.20.3"
|
||||
dependencies:
|
||||
"@codemirror/language": "npm:^6.0.0"
|
||||
"@codemirror/state": "npm:^6.0.0"
|
||||
"@codemirror/view": "npm:^6.17.0"
|
||||
"@lezer/common": "npm:^1.0.0"
|
||||
checksum: 10/ee0612e55a52b55262d897972741f1c54c92317b34735943c73cf1e8608fd3b21604b0f2fca0cbe67b9bddb3b673777023a45237086c1296f8f5103e0ad01219
|
||||
checksum: 10/1e66faedba3c7c1520b938838de2a95d4d4f16d168158e8dfd4d34bb72fe9538f4d9e73449bd2c8bba6f1f0a40f4a754e656637296d434216529c6426828aaf5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1646,135 +1646,135 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/bigdecimal@npm:0.2.5":
|
||||
version: 0.2.5
|
||||
resolution: "@formatjs/bigdecimal@npm:0.2.5"
|
||||
checksum: 10/035a70be4175d47d82d81025ad4386d7c248ef7afb2676b4e0773595d01df1078f3d5224e2f7f17721c9169bbb77d4282898624bf0112d2e6350438b7032a1cb
|
||||
"@formatjs/bigdecimal@npm:0.2.6":
|
||||
version: 0.2.6
|
||||
resolution: "@formatjs/bigdecimal@npm:0.2.6"
|
||||
checksum: 10/5ef248f0feadeb1bceb9fa65ba36ccd1768a41ea6165bc58ae794564ebb09a67621d894fc94ab8b328cc7bba60e4c181847562c92b7cec5cd1208a4191fbbe67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/fast-memoize@npm:3.1.5":
|
||||
version: 3.1.5
|
||||
resolution: "@formatjs/fast-memoize@npm:3.1.5"
|
||||
checksum: 10/5ff47e3cc5b46a72028cf0c6a3a291781cc3c7e198b6d4446119d870d6366ecc5a05d5826fce3568bab62abf9f7b4ebbe9bfb8420bb81bbe2a8a120b062ac909
|
||||
"@formatjs/fast-memoize@npm:3.1.6":
|
||||
version: 3.1.6
|
||||
resolution: "@formatjs/fast-memoize@npm:3.1.6"
|
||||
checksum: 10/7dec3e82586d4c4889671c6081d7b0d87b2c229ba551d8328f96ae8ab58b2cd6056fc5d360c0b0c9688b7164f12ef517428016fbce15b950987a77005e481824
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-messageformat-parser@npm:3.5.10":
|
||||
version: 3.5.10
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.10"
|
||||
"@formatjs/icu-messageformat-parser@npm:3.5.11":
|
||||
version: 3.5.11
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.11"
|
||||
dependencies:
|
||||
"@formatjs/icu-skeleton-parser": "npm:2.1.9"
|
||||
checksum: 10/44392248b9247cf83a21b43c749025bfbc23acd63782b9a1b7dc47bf5520b686f8a5dccfa56716bc81fe0680000029aba22f5eb5c821ec529646758bd2d6af79
|
||||
"@formatjs/icu-skeleton-parser": "npm:2.1.10"
|
||||
checksum: 10/59b8323b9615edb4ca6463d09108581469de7566f2aec106f1d87b259333cdbf9577d9c22a990bc454f3f461c799f4add683a9ff7185af6fa225fcd29ec336da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-skeleton-parser@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.9"
|
||||
checksum: 10/eacb8acd60d487092fc1a6b7fdbac87dfc32475db7001562034a8ca7b0a4be7a35f95c30928ae8314f5e680f63302180bece9462c872a042a0302a5f4cf6a842
|
||||
"@formatjs/icu-skeleton-parser@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.10"
|
||||
checksum: 10/ec30d106ce38de80f4128d0cdfac15699628652807695843254bf0d31650bd0dc4b57e48691d164556234494d59b2816e710fa12321234c85b803c5cda32bedf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-datetimeformat@npm:7.4.7":
|
||||
version: 7.4.7
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:7.4.7"
|
||||
"@formatjs/intl-datetimeformat@npm:7.4.8":
|
||||
version: 7.4.8
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:7.4.8"
|
||||
dependencies:
|
||||
"@formatjs/bigdecimal": "npm:0.2.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/ab3a1806aede409f2eef59abd9657c0281de5b999dc992111fdc0c3d3e7fe3934e951abb934f469514111c262200985963e24a8d6b7bce1945f6d36dbd64f06f
|
||||
"@formatjs/bigdecimal": "npm:0.2.6"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/9dde6796f1e260fbb486f27b1a5774a70aef2b4259b102b745b495d93ea5881f0df80d133bf92138cb003c77b7a016f125562f20360a92125680cc7f54621971
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-displaynames@npm:7.3.9":
|
||||
version: 7.3.9
|
||||
resolution: "@formatjs/intl-displaynames@npm:7.3.9"
|
||||
"@formatjs/intl-displaynames@npm:7.3.10":
|
||||
version: 7.3.10
|
||||
resolution: "@formatjs/intl-displaynames@npm:7.3.10"
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/c5c6295be1e89b1ae497aab1df166714580499b67eda86e6504e54b29d434482633ae050c57d6fc06fd7d33b2efc5bf0405ff90933c328ec1f8924f1e349db55
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/da8809106f59e42c80d71f64d6965cb0c57daf5309976dedf2516ef1bd57912d46a5076f67f396406056425f42d415d10c6ddf9ed760971b484ac799462eb147
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-durationformat@npm:0.10.13":
|
||||
version: 0.10.13
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.10.13"
|
||||
"@formatjs/intl-durationformat@npm:0.10.14":
|
||||
version: 0.10.14
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.10.14"
|
||||
dependencies:
|
||||
"@formatjs/bigdecimal": "npm:0.2.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/ae33f714dccd8a0be95b216e07fabfaf44de6dfb1a4de51a266a0f55e6fa510ce77a8da461ed106549ba5cb7f0bf35e3dc019b5e03f57388d9d5b1b8867e2b6f
|
||||
"@formatjs/bigdecimal": "npm:0.2.6"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/0ca4898860322330442e150cde351924401f95aa772de276662c97e0c3e1dff619cac2054b608ce171d2235a4f215639deebd480eb63f81d747c70e9cd191c01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-getcanonicallocales@npm:3.2.9":
|
||||
version: 3.2.9
|
||||
resolution: "@formatjs/intl-getcanonicallocales@npm:3.2.9"
|
||||
checksum: 10/818cf303c5fbc5607af6efc57720eabde1ea16eca4f58dde1f3f6f5ec21e7697d300e59dc61769c928ff9fc53b26ea510e5255e1d263f92f48e3baf43c8f984a
|
||||
"@formatjs/intl-getcanonicallocales@npm:3.2.10":
|
||||
version: 3.2.10
|
||||
resolution: "@formatjs/intl-getcanonicallocales@npm:3.2.10"
|
||||
checksum: 10/dbf704d141bd4efc4e2687bd745d1a847a7b94955c23d2f06fe26add8e5ab8ad6096168babad72f2b4568f1fbee32c1528082269273b750bed4bdd1dc5b5d396
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-listformat@npm:8.3.9":
|
||||
version: 8.3.9
|
||||
resolution: "@formatjs/intl-listformat@npm:8.3.9"
|
||||
"@formatjs/intl-listformat@npm:8.3.10":
|
||||
version: 8.3.10
|
||||
resolution: "@formatjs/intl-listformat@npm:8.3.10"
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/36bee2f177d3d705ba9b95133b35404263fe08c15f781978d12b2a349d6e570b4ffdfae63a7a9ef233dfe8067e26584cadf6f4e252cd482c1b3a045dcca6bc52
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/ebfc78061842ff45cfadab9e1676944093e421b04f21da16925b2ed04a71cdf8213cfc442f01e537d0de1255e745d03b74b935525d90c0df9f979627b1a70d97
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-locale@npm:5.3.8":
|
||||
version: 5.3.8
|
||||
resolution: "@formatjs/intl-locale@npm:5.3.8"
|
||||
"@formatjs/intl-locale@npm:5.3.9":
|
||||
version: 5.3.9
|
||||
resolution: "@formatjs/intl-locale@npm:5.3.9"
|
||||
dependencies:
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.9"
|
||||
"@formatjs/intl-supportedvaluesof": "npm:2.3.7"
|
||||
checksum: 10/86881fe5142b21f976e92c90ed34ac591ef4f470334f2d3f17f8a4784d64ba51d5a2a35f45e40da02a02227d2cc63dae2f20a0365299b0a6e44c5613f8777a92
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.10"
|
||||
"@formatjs/intl-supportedvaluesof": "npm:2.3.8"
|
||||
checksum: 10/e2af858ec3ff75611d5412a1e39abce882eab2cf13eb278e4cd1647481321823cc2303c74e86c38b95662e668f6900f9d666136debb2377231d5ddf0bdba3218
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.8.9":
|
||||
version: 0.8.9
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.8.9"
|
||||
"@formatjs/intl-localematcher@npm:0.8.10":
|
||||
version: 0.8.10
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.8.10"
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize": "npm:3.1.5"
|
||||
checksum: 10/05873efaae86a72b738dc1ce6296d975a74f27f90cd5dd4a27e704acd2389e58f5db84f4735e6b1043723cc65ab5636fb345ec88d1cf9a1dafad57180e4d06b0
|
||||
"@formatjs/fast-memoize": "npm:3.1.6"
|
||||
checksum: 10/d9d3f408363091bf35950a842f58c662d88d9f54d9a53b1238cb673a73d1345412d78e01a2d56ec58cef81b9c66a8ce0ee78ed9c31976bd56dc68a38ef3cbc0f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-numberformat@npm:9.3.10":
|
||||
version: 9.3.10
|
||||
resolution: "@formatjs/intl-numberformat@npm:9.3.10"
|
||||
"@formatjs/intl-numberformat@npm:9.3.11":
|
||||
version: 9.3.11
|
||||
resolution: "@formatjs/intl-numberformat@npm:9.3.11"
|
||||
dependencies:
|
||||
"@formatjs/bigdecimal": "npm:0.2.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/ef954ca4b60e46d98ee41514352a8977c8d9cf2ca1df684178f3571ecafa7d4c2a0e7270e45c9ccf54b2f1f43e4560a907fb06d9a70a7920fab5f239fe3a6205
|
||||
"@formatjs/bigdecimal": "npm:0.2.6"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/ef8221c37c0611911b06c9a94d02e9d334048c0f2c8e5f049804f067afb628677e72215a9729a742f6b701b12d95d443b2e58a33981af6b65417b26f69d0d038
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-pluralrules@npm:6.3.9":
|
||||
version: 6.3.9
|
||||
resolution: "@formatjs/intl-pluralrules@npm:6.3.9"
|
||||
"@formatjs/intl-pluralrules@npm:6.3.10":
|
||||
version: 6.3.10
|
||||
resolution: "@formatjs/intl-pluralrules@npm:6.3.10"
|
||||
dependencies:
|
||||
"@formatjs/bigdecimal": "npm:0.2.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/c35cc1594596d3df557a683c89d5be18ee02150d2f9d2ba946e457e4ad2067e7f4d482487da1855f8f36ff83a773cef3f8aeee8c80ae7660d8c6eb6fe28f5985
|
||||
"@formatjs/bigdecimal": "npm:0.2.6"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/88ade192dca87d3efa3367e68efb703495a8fbcca9bd30b0aea62286134407dfffada0adb5125618c157eacd83575ffaf890db478fabd82c3f67f1ea0b5472ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-relativetimeformat@npm:12.3.9":
|
||||
version: 12.3.9
|
||||
resolution: "@formatjs/intl-relativetimeformat@npm:12.3.9"
|
||||
"@formatjs/intl-relativetimeformat@npm:12.3.10":
|
||||
version: 12.3.10
|
||||
resolution: "@formatjs/intl-relativetimeformat@npm:12.3.10"
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher": "npm:0.8.9"
|
||||
checksum: 10/cbeaa2d801f09ca6aad66aa15e1827d93997410029fbd1f7e55ae0a1cf56cde1973c8ba724afc0da349e2d30a670b1ec5f207d89f7aa502628d4c3f90a0a5b3c
|
||||
"@formatjs/intl-localematcher": "npm:0.8.10"
|
||||
checksum: 10/f94f642d112ddaf0dd922c1887bf53d174bafed2aeb10d6305933cb9cb5fc7929e894516aa2d795fd4f5bfe5074330198e4dc3652a413d3e8bf3a272abcff7ea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-supportedvaluesof@npm:2.3.7":
|
||||
version: 2.3.7
|
||||
resolution: "@formatjs/intl-supportedvaluesof@npm:2.3.7"
|
||||
"@formatjs/intl-supportedvaluesof@npm:2.3.8":
|
||||
version: 2.3.8
|
||||
resolution: "@formatjs/intl-supportedvaluesof@npm:2.3.8"
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize": "npm:3.1.5"
|
||||
checksum: 10/89695ea2c2af7b0abbe5e2fe9b1c96047e61bfcceef57749b66693cf01bff66943fbefbdbaec5ec1015f003d2af500a81bae531d2f9a60d423e883532133be01
|
||||
"@formatjs/fast-memoize": "npm:3.1.6"
|
||||
checksum: 10/d9d4e4d5dda1c26c771d0f2746e1031f9695dee663fb2d2ef9f83794c1e8683de88caf6cdb9718022c592cc879c5c1afa7f72af4c77da9ce5e1d98b54267dfff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4041,161 +4041,161 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/basic@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/basic@npm:4.1.2"
|
||||
"@tsparticles/basic@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/basic@npm:4.1.3"
|
||||
dependencies:
|
||||
"@tsparticles/engine": "npm:4.1.2"
|
||||
"@tsparticles/plugin-blend": "npm:4.1.2"
|
||||
"@tsparticles/plugin-hex-color": "npm:4.1.2"
|
||||
"@tsparticles/plugin-hsl-color": "npm:4.1.2"
|
||||
"@tsparticles/plugin-move": "npm:4.1.2"
|
||||
"@tsparticles/plugin-rgb-color": "npm:4.1.2"
|
||||
"@tsparticles/shape-circle": "npm:4.1.2"
|
||||
"@tsparticles/updater-opacity": "npm:4.1.2"
|
||||
"@tsparticles/updater-out-modes": "npm:4.1.2"
|
||||
"@tsparticles/updater-paint": "npm:4.1.2"
|
||||
"@tsparticles/updater-size": "npm:4.1.2"
|
||||
checksum: 10/1c145d25373562cd3b45f20664610226c050a0a6867396c2d138a76761d3e7a5796cf107d8bdcbb8eb8cca19a3a1e4192eb356d37387d6547bf6b1ff796b71b2
|
||||
"@tsparticles/engine": "npm:4.1.3"
|
||||
"@tsparticles/plugin-blend": "npm:4.1.3"
|
||||
"@tsparticles/plugin-hex-color": "npm:4.1.3"
|
||||
"@tsparticles/plugin-hsl-color": "npm:4.1.3"
|
||||
"@tsparticles/plugin-move": "npm:4.1.3"
|
||||
"@tsparticles/plugin-rgb-color": "npm:4.1.3"
|
||||
"@tsparticles/shape-circle": "npm:4.1.3"
|
||||
"@tsparticles/updater-opacity": "npm:4.1.3"
|
||||
"@tsparticles/updater-out-modes": "npm:4.1.3"
|
||||
"@tsparticles/updater-paint": "npm:4.1.3"
|
||||
"@tsparticles/updater-size": "npm:4.1.3"
|
||||
checksum: 10/daf230e24bb7557aa33b630958d7c858a14c8b446e56bf610a36d5365682fe4f635455f6c03717ce4cf6985057290155bfea0d6571bb69523968e63b79658062
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/canvas-utils@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/canvas-utils@npm:4.1.2"
|
||||
"@tsparticles/canvas-utils@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/canvas-utils@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/ffedc8400b5ff758331bdf1ef2362aac713c3e41d5bcece00153acc04c725947c41d545202d73bd6ef290d2ec220353f1801fb4ad22e99870989b0c68d7540c4
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/e69c6f9f49a32f4aa8d1e4444e93390615dddf95d62c2b14bfe53a113d417e5ffaf0cc0a8336456a6f1a6c559459454d6829d629e4ef28df9643c9ec12b9dbd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/engine@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/engine@npm:4.1.2"
|
||||
checksum: 10/6fe6aa50bba564a8a8691945cb378267e695ec58323066a4157e8a036e7a3caa99e1ced08d05bffd173045bbc33c4d41523afc28624b1bea015393c1a88ef2bb
|
||||
"@tsparticles/engine@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/engine@npm:4.1.3"
|
||||
checksum: 10/6506f055b044deb13fd910ec19fa9ebba7ac3cab8cd6d67ec5137e0a05ed88ff6a40514c62056c2f6c86882062ad42a9af440a24dede08fff6a3b0f270b1eb3c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/interaction-particles-links@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/interaction-particles-links@npm:4.1.2"
|
||||
"@tsparticles/interaction-particles-links@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/interaction-particles-links@npm:4.1.3"
|
||||
dependencies:
|
||||
"@tsparticles/canvas-utils": "npm:4.1.2"
|
||||
"@tsparticles/canvas-utils": "npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
"@tsparticles/plugin-interactivity": 4.1.2
|
||||
checksum: 10/b9e1e85bdb3226a25d7a4486556abfe458c81c109855c04857623d2d3506ef325fcc2dc4fcee962414e216aa6346bc0849df1bf7199a3e4e75bed2fb7f069702
|
||||
"@tsparticles/engine": 4.1.3
|
||||
"@tsparticles/plugin-interactivity": 4.1.3
|
||||
checksum: 10/770b77ef1f2e6caee5e3c364a40a3a184d93f2989f2ade7b83db8a043d2ffeb1ac5328c7fad8a9c00cc362bc467ccac215282c22c0fd9cd30005ad12393cabab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/plugin-blend@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/plugin-blend@npm:4.1.2"
|
||||
"@tsparticles/plugin-blend@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/plugin-blend@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/1f51bcf76d6d749a0a18799fd3a814a82e62f060fd3c20d28f8bb170311815aeba8efc361e73cef7356c51151c7debafc75d34bff6ab2d4e58099b51774a28c6
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/418a37e49dccf8c680953715be358ffe381a5198a7b683fe445d1052468fac9e4cc5b4026d43b1e9b9c43f341b27a7c1ce84d7e8fe24348580b9c07aa645951f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/plugin-hex-color@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/plugin-hex-color@npm:4.1.2"
|
||||
"@tsparticles/plugin-hex-color@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/plugin-hex-color@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/d32a39bbd6732b9e630f84a7d226753735f02d65b70a1d5d11fa680421d5f1378aee75af1ecb5ada5af9875822bbaf2b5d1f213d1e0cbce2a50820743eb9c2fc
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/782cef17a059ffeba447c0f797d332b24bf91088c783f952f7d3d4f1c79c4ff95c35be404db952c9e03a091a3520a603638f20e9313d64014d41d41d7d2ab0fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/plugin-hsl-color@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/plugin-hsl-color@npm:4.1.2"
|
||||
"@tsparticles/plugin-hsl-color@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/plugin-hsl-color@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/c130e80b0fd2750402fa0e309d0bb624b2138474069ae4ccb76610722c4f0966f278ad1a35c85fbcf5449914936f1c53322246f1cdd028106d83f779beee538c
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/0c53b7143288dd4e5f39525bac96c2bff70b0da32b49fc11be569077bc8c87b204bf541a9e4cbb007d0e697f00957fe8ca974ee39eddead355455f1e8719fe56
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/plugin-interactivity@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/plugin-interactivity@npm:4.1.2"
|
||||
"@tsparticles/plugin-interactivity@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/plugin-interactivity@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/78efd4ffe3a07752f26262fe27ee0a931b45fea6c81d62e156d8f965dc22a2141d64395eb99477624ca9aac9e152f9e2585fc8aced9c822c61cb22ecfad184a4
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/286a1348d474a58f60151922e78e34dd7a3507b7c97f86490e09e80bc762539f02e7ee8c693536aafbd83beaf8a3b6708bab05941e1f82ab840f2d9325f77e47
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/plugin-move@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/plugin-move@npm:4.1.2"
|
||||
"@tsparticles/plugin-move@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/plugin-move@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/fac5b90904d3fa62b310a1ce19647c5ae4a592111b5eeccfc6e946c6d17d677b0f3931ba3c114f04045827a92c1a0ed66791e99b352cb75d6c50f3749f389bfc
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/fe7ccb2d060ca80858676b2baea7f1a75f7ef7b794cae7e73044467c9d931ceed712aa78ebf768ac57c4a34b9997f4b508bf4b03e152fce209b6feba458b3aca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/plugin-rgb-color@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/plugin-rgb-color@npm:4.1.2"
|
||||
"@tsparticles/plugin-rgb-color@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/plugin-rgb-color@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/7e3e83171f74f7e9e3f68ce8d0aeb4685b277ecac3c4874a951e86228d3db78978d6197ef9e04b17190471294323c561c18c92c9d9ae8e8a115e6238cbecdb43
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/ee32e1ce0f8670090c1dc4a1f89ecdc13a0d6a78c8deedfe925405ebebdecc386cc92cea71110259e3adc5d12f3b181e2125abdfefef791f86cf2f8bc8df0836
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/preset-links@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/preset-links@npm:4.1.2"
|
||||
"@tsparticles/preset-links@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/preset-links@npm:4.1.3"
|
||||
dependencies:
|
||||
"@tsparticles/basic": "npm:4.1.2"
|
||||
"@tsparticles/engine": "npm:4.1.2"
|
||||
"@tsparticles/interaction-particles-links": "npm:4.1.2"
|
||||
"@tsparticles/plugin-interactivity": "npm:4.1.2"
|
||||
checksum: 10/d907884c4e9fd023aa8e0e1ff2388ef24d06a446433cc4ffbd620a4d22801f52ce97fe5f441b9fdea854f36d6f421d7571648925e5ea5c0976c121194156a2b1
|
||||
"@tsparticles/basic": "npm:4.1.3"
|
||||
"@tsparticles/engine": "npm:4.1.3"
|
||||
"@tsparticles/interaction-particles-links": "npm:4.1.3"
|
||||
"@tsparticles/plugin-interactivity": "npm:4.1.3"
|
||||
checksum: 10/7f9121f2c459c9ecacb2793332ae6bd7d8819eb74ab4de44258f28925a5e22f30052ed5167d3006270c1adc955de9b84bfc505b20d54dd3422e5e7500feb81a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/shape-circle@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/shape-circle@npm:4.1.2"
|
||||
"@tsparticles/shape-circle@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/shape-circle@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/60a87755d4e598c278c1850b58b07bcfbef603de242de3979702cd8120cf24fd95d985b2092c7a8a2cccd43eb6a1939ab215f74a93898ea1e2d425be5deb405c
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/da8800a93096774d915989fc55499c0fc6775257a4d3b3141edb7d5cd5bf7354f4183d067041d094eba9e6ed1cfda79f1dd7ea51a06b3383490f1ed3b0513b61
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/updater-opacity@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/updater-opacity@npm:4.1.2"
|
||||
"@tsparticles/updater-opacity@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/updater-opacity@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/4dc426175bb3e4c5d3bc35e7d83ace7130c4f8817c7f88ee6bf1b4b1a52b28511d99b1fc7ded3c549a5baae401f7c22cc701d83919694387154aef86ea6974c2
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/298ad4ef6fd9043d15e3ab5704d405e81cc3ed95638e480991b8eb44fae8dc459c96e80c730b436d035f78dcc16d66a07554c25752982e7b231dedfd8801bdd2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/updater-out-modes@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/updater-out-modes@npm:4.1.2"
|
||||
"@tsparticles/updater-out-modes@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/updater-out-modes@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/901be7b7dfa97d3d4ff857715a6b61dd4bc2ca09f23fee38d86cd6827351134c5cd4c3fa702d2aec8601544b1f6935f2b4034c4567a00128f9cf36cbaf379722
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/14a9aa5262dadcd2cc557936e24e64b5e4df57f96d3b327f8d195ed509b8d94f6fc07e3f9d7f3add67d665df9a9ecb9642704dc97455fd5c51475efdbdca9b71
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/updater-paint@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/updater-paint@npm:4.1.2"
|
||||
"@tsparticles/updater-paint@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/updater-paint@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/5a0b4bc0a4c6061e1eb93a6af7a97b472e5d552677dafe8c888b0f9a71b68ed10ef255d86f2cf8d8466ecd1d33ba8f5e52920947237fe1218cbcd002c50dff47
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/8e29683068e57deedbea1d902b582e4b0457b284e62dbcf77c85330ab52b13bb08fef09f0c6aa3e77533f22e763bb7b4369069985436a6421569a3cdd62b3f57
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsparticles/updater-size@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@tsparticles/updater-size@npm:4.1.2"
|
||||
"@tsparticles/updater-size@npm:4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@tsparticles/updater-size@npm:4.1.3"
|
||||
peerDependencies:
|
||||
"@tsparticles/engine": 4.1.2
|
||||
checksum: 10/d5c4f79d5c1d3d977dd5ebac56c9679df134ddba919b8e2ee1473820f2c551f5b162356b49b22e6e54134b87a27c51cb5a6c2e2441ba2569b0dab62974e91a47
|
||||
"@tsparticles/engine": 4.1.3
|
||||
checksum: 10/ec4376facea1ca6ab79adf76237975ce7762d89ed58f31ed52f0061f23c5e12e04a60786898e65fcfa323b33f21d0278d6918ff3cf225fb9817f76c9f8b93e79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7957,10 +7957,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fuse.js@npm:7.4.1":
|
||||
version: 7.4.1
|
||||
resolution: "fuse.js@npm:7.4.1"
|
||||
checksum: 10/581941d5015968ee624feb10a56d9b49d5d954672b2c9ec189d4ca513da6f8a3dea2d5f6637386d8298ffc5846f6d83435210d40a47c58e14d11dc5707544c75
|
||||
"fuse.js@npm:7.4.2":
|
||||
version: 7.4.2
|
||||
resolution: "fuse.js@npm:7.4.2"
|
||||
checksum: 10/1605bb929331056f9215a8f0a19b2d22615d9195e17794f41254bdeeb77b0145ed0ad6b6842227b329783a4ab53586d795924e7a4a3fb664295f365ef6e2405c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8437,7 +8437,7 @@ __metadata:
|
||||
"@babel/runtime": "npm:7.29.7"
|
||||
"@braintree/sanitize-url": "npm:7.1.2"
|
||||
"@bundle-stats/plugin-webpack-filter": "npm:4.22.2"
|
||||
"@codemirror/autocomplete": "npm:6.20.2"
|
||||
"@codemirror/autocomplete": "npm:6.20.3"
|
||||
"@codemirror/commands": "npm:6.10.3"
|
||||
"@codemirror/lang-jinja": "npm:6.0.1"
|
||||
"@codemirror/lang-yaml": "npm:6.1.3"
|
||||
@@ -8449,15 +8449,15 @@ __metadata:
|
||||
"@date-fns/tz": "npm:1.5.0"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@eslint/js": "npm:10.0.1"
|
||||
"@formatjs/intl-datetimeformat": "npm:7.4.7"
|
||||
"@formatjs/intl-displaynames": "npm:7.3.9"
|
||||
"@formatjs/intl-durationformat": "npm:0.10.13"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.9"
|
||||
"@formatjs/intl-listformat": "npm:8.3.9"
|
||||
"@formatjs/intl-locale": "npm:5.3.8"
|
||||
"@formatjs/intl-numberformat": "npm:9.3.10"
|
||||
"@formatjs/intl-pluralrules": "npm:6.3.9"
|
||||
"@formatjs/intl-relativetimeformat": "npm:12.3.9"
|
||||
"@formatjs/intl-datetimeformat": "npm:7.4.8"
|
||||
"@formatjs/intl-displaynames": "npm:7.3.10"
|
||||
"@formatjs/intl-durationformat": "npm:0.10.14"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.10"
|
||||
"@formatjs/intl-listformat": "npm:8.3.10"
|
||||
"@formatjs/intl-locale": "npm:5.3.9"
|
||||
"@formatjs/intl-numberformat": "npm:9.3.11"
|
||||
"@formatjs/intl-pluralrules": "npm:6.3.10"
|
||||
"@formatjs/intl-relativetimeformat": "npm:12.3.10"
|
||||
"@fullcalendar/core": "npm:6.1.20"
|
||||
"@fullcalendar/daygrid": "npm:6.1.20"
|
||||
"@fullcalendar/interaction": "npm:6.1.20"
|
||||
@@ -8487,8 +8487,8 @@ __metadata:
|
||||
"@rspack/dev-server": "npm:2.0.3"
|
||||
"@swc/helpers": "npm:0.5.23"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
"@tsparticles/engine": "npm:4.1.2"
|
||||
"@tsparticles/preset-links": "npm:4.1.2"
|
||||
"@tsparticles/engine": "npm:4.1.3"
|
||||
"@tsparticles/preset-links": "npm:4.1.3"
|
||||
"@types/chromecast-caf-receiver": "npm:6.0.26"
|
||||
"@types/chromecast-caf-sender": "npm:1.0.11"
|
||||
"@types/color-name": "npm:2.0.0"
|
||||
@@ -8534,7 +8534,7 @@ __metadata:
|
||||
eslint-plugin-wc: "npm:3.1.0"
|
||||
fancy-log: "npm:2.0.0"
|
||||
fs-extra: "npm:11.3.5"
|
||||
fuse.js: "npm:7.4.1"
|
||||
fuse.js: "npm:7.4.2"
|
||||
generate-license-file: "npm:4.2.1"
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.6.0"
|
||||
@@ -8549,7 +8549,7 @@ __metadata:
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.5"
|
||||
intl-messageformat: "npm:11.2.7"
|
||||
intl-messageformat: "npm:11.2.8"
|
||||
js-yaml: "npm:4.2.0"
|
||||
jsdom: "npm:29.1.1"
|
||||
jszip: "npm:3.10.1"
|
||||
@@ -8565,7 +8565,7 @@ __metadata:
|
||||
lodash.template: "npm:4.18.1"
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:18.0.4"
|
||||
marked: "npm:18.0.5"
|
||||
memoize-one: "npm:6.0.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
object-hash: "npm:3.0.0"
|
||||
@@ -8576,7 +8576,7 @@ __metadata:
|
||||
qrcode: "npm:1.5.4"
|
||||
roboto-fontface: "npm:0.10.0"
|
||||
rrule: "npm:2.8.1"
|
||||
rspack-manifest-plugin: "npm:5.2.1"
|
||||
rspack-manifest-plugin: "npm:5.2.2"
|
||||
serve: "npm:14.2.6"
|
||||
sinon: "npm:22.0.0"
|
||||
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
|
||||
@@ -8885,13 +8885,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"intl-messageformat@npm:11.2.7":
|
||||
version: 11.2.7
|
||||
resolution: "intl-messageformat@npm:11.2.7"
|
||||
"intl-messageformat@npm:11.2.8":
|
||||
version: 11.2.8
|
||||
resolution: "intl-messageformat@npm:11.2.8"
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize": "npm:3.1.5"
|
||||
"@formatjs/icu-messageformat-parser": "npm:3.5.10"
|
||||
checksum: 10/ccd566358c90c7d33fbc71206d46501c18e66dac10cfdab1ea226bc16c3b31d25d987619ef355ca24c5b9f415075d2abe5cf383c69dbbe758bd78e7f72017ab9
|
||||
"@formatjs/fast-memoize": "npm:3.1.6"
|
||||
"@formatjs/icu-messageformat-parser": "npm:3.5.11"
|
||||
checksum: 10/451585274cdb1ff798b8d9bfa9aed6623870d2a3bf7c315a4cc37e57793d13a51ec09da705a228f7a64a1267fd6afe91b5bf2fd0c4223fd4f1d158a2adb82d41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10221,12 +10221,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:18.0.4":
|
||||
version: 18.0.4
|
||||
resolution: "marked@npm:18.0.4"
|
||||
"marked@npm:18.0.5":
|
||||
version: 18.0.5
|
||||
resolution: "marked@npm:18.0.5"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/87d8e309690c9b8e1a5c41487262f19c811e876b8351fbb24432ab348e79c59ee7c68745ec47bfc91c1306a1756077717ff564454e7a68a58a5bd064d732d5dd
|
||||
checksum: 10/61f1c64304a7e2615ac096bd7b6e5da453561a57393a57f85f8ddef07a64eaeadf4d57c4c6e47d9179d116d36326c159d15993490686fc17434a39bdab9ddb8a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12019,17 +12019,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rspack-manifest-plugin@npm:5.2.1":
|
||||
version: 5.2.1
|
||||
resolution: "rspack-manifest-plugin@npm:5.2.1"
|
||||
"rspack-manifest-plugin@npm:5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "rspack-manifest-plugin@npm:5.2.2"
|
||||
dependencies:
|
||||
"@rspack/lite-tapable": "npm:^1.0.1"
|
||||
peerDependencies:
|
||||
"@rspack/core": ^1.0.0 || ^2.0.0-0
|
||||
"@rspack/core": ^1.0.0 || ^2.0.0
|
||||
peerDependenciesMeta:
|
||||
"@rspack/core":
|
||||
optional: true
|
||||
checksum: 10/fe0ddf92a881e45859f8dc4991823e3773292cfbd087db0d83d5856785069b1774139116fe0ca0adc02bdca8f7392165a990b104bc0298f4e1824c9d73672b05
|
||||
checksum: 10/1acae737b83bcb16d4d9fa07a2b8542c97844bc7328ebdca4db76d3d010f507eeb9ba05f639dc01961234fedbe99dc49d7c128533b415be13a75b91e21101410
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user