mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-24 17:07:26 +00:00
Compare commits
3 Commits
picture-el
...
auto-jsdoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b72e2d7a5 | ||
|
|
542be12673 | ||
|
|
dba6841a93 |
@@ -3,6 +3,9 @@ import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
import yaml from "js-yaml";
|
||||
import { marked } from "marked";
|
||||
import ts from "typescript";
|
||||
import { create } from "@custom-elements-manifest/analyzer";
|
||||
import { litPlugin } from "@custom-elements-manifest/analyzer/src/features/framework-plugins/lit/lit.js";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import "./clean.js";
|
||||
@@ -13,6 +16,28 @@ import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task("generate-component-docs", async function generateComponentDocs() {
|
||||
const filePaths = ["src/components/ha-alert.ts"];
|
||||
|
||||
const modules = await Promise.all(
|
||||
filePaths.map(async (file) => {
|
||||
const filePath = path.resolve(file);
|
||||
console.log(`Reading ${file} -> ${filePath}`);
|
||||
const source = fs.readFileSync(filePath).toString();
|
||||
|
||||
return ts.createSourceFile(file, source, ts.ScriptTarget.ES2015, true);
|
||||
})
|
||||
);
|
||||
|
||||
const manifest = create({
|
||||
modules,
|
||||
plugins: litPlugin(),
|
||||
context: { dev: true },
|
||||
});
|
||||
|
||||
console.log(manifest);
|
||||
});
|
||||
|
||||
gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
const pageDir = path.resolve(paths.gallery_dir, "src/pages");
|
||||
const files = await glob(path.resolve(pageDir, "**/*"));
|
||||
|
||||
186
custom-elements.json
Normal file
186
custom-elements.json
Normal file
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"readme": "",
|
||||
"modules": [
|
||||
{
|
||||
"kind": "javascript-module",
|
||||
"path": "src/components/ha-alert.ts",
|
||||
"declarations": [
|
||||
{
|
||||
"kind": "class",
|
||||
"description": "A custom alert component for displaying messages with various alert types.",
|
||||
"name": "HaAlert",
|
||||
"cssProperties": [
|
||||
{
|
||||
"description": "The color used for \"info\" alerts.",
|
||||
"name": "--info-color"
|
||||
},
|
||||
{
|
||||
"description": "The color used for \"warning\" alerts.",
|
||||
"name": "--warning-color"
|
||||
},
|
||||
{
|
||||
"description": "The color used for \"error\" alerts.",
|
||||
"name": "--error-color"
|
||||
},
|
||||
{
|
||||
"description": "The color used for \"success\" alerts.",
|
||||
"name": "--success-color"
|
||||
},
|
||||
{
|
||||
"description": "The primary text color used in the alert.",
|
||||
"name": "--primary-text-color"
|
||||
}
|
||||
],
|
||||
"cssParts": [
|
||||
{
|
||||
"description": "The container for the alert.",
|
||||
"name": "issue-type"
|
||||
},
|
||||
{
|
||||
"description": "The container for the alert icon.",
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"description": "The container for the alert content.",
|
||||
"name": "content"
|
||||
},
|
||||
{
|
||||
"description": "The container for the alert actions.",
|
||||
"name": "action"
|
||||
},
|
||||
{
|
||||
"description": "The container for the alert title.",
|
||||
"name": "title"
|
||||
}
|
||||
],
|
||||
"slots": [
|
||||
{
|
||||
"description": "The main content of the alert.",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"description": "Slot for providing a custom icon for the alert.",
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"description": "Slot for providing custom actions or buttons for the alert.",
|
||||
"name": "action"
|
||||
}
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "title",
|
||||
"type": {
|
||||
"text": "string"
|
||||
},
|
||||
"privacy": "public",
|
||||
"default": "\"\"",
|
||||
"description": "The title of the alert. Defaults to an empty string.",
|
||||
"attribute": "title"
|
||||
},
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "alertType",
|
||||
"type": {
|
||||
"text": "\"info\" | \"warning\" | \"error\" | \"success\""
|
||||
},
|
||||
"privacy": "public",
|
||||
"default": "\"info\"",
|
||||
"description": "The type of alert to display. Defaults to \"info\". Determines the styling and icon used.",
|
||||
"attribute": "alert-type"
|
||||
},
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "dismissable",
|
||||
"type": {
|
||||
"text": "boolean"
|
||||
},
|
||||
"privacy": "public",
|
||||
"default": "false",
|
||||
"description": "Whether the alert can be dismissed. Defaults to `false`. If `true`, a dismiss button is displayed.",
|
||||
"attribute": "dismissable"
|
||||
},
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "narrow",
|
||||
"type": {
|
||||
"text": "boolean"
|
||||
},
|
||||
"privacy": "public",
|
||||
"default": "false",
|
||||
"description": "Whether the alert should use a narrow layout. Defaults to `false`.",
|
||||
"attribute": "narrow"
|
||||
},
|
||||
{
|
||||
"kind": "method",
|
||||
"name": "_dismissClicked",
|
||||
"privacy": "private"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"description": "Fired when the dismiss button is clicked.",
|
||||
"name": "alert-dismissed-clicked"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": {
|
||||
"text": "string"
|
||||
},
|
||||
"default": "\"\"",
|
||||
"description": "The title of the alert. Defaults to an empty string.",
|
||||
"fieldName": "title"
|
||||
},
|
||||
{
|
||||
"name": "alert-type",
|
||||
"type": {
|
||||
"text": "\"info\" | \"warning\" | \"error\" | \"success\""
|
||||
},
|
||||
"default": "\"info\"",
|
||||
"description": "The type of alert to display. Defaults to \"info\". Determines the styling and icon used.",
|
||||
"fieldName": "alertType"
|
||||
},
|
||||
{
|
||||
"name": "dismissable",
|
||||
"type": {
|
||||
"text": "boolean"
|
||||
},
|
||||
"default": "false",
|
||||
"description": "Whether the alert can be dismissed. Defaults to `false`. If `true`, a dismiss button is displayed.",
|
||||
"fieldName": "dismissable"
|
||||
},
|
||||
{
|
||||
"name": "narrow",
|
||||
"type": {
|
||||
"text": "boolean"
|
||||
},
|
||||
"default": "false",
|
||||
"description": "Whether the alert should use a narrow layout. Defaults to `false`.",
|
||||
"fieldName": "narrow"
|
||||
}
|
||||
],
|
||||
"superclass": {
|
||||
"name": "LitElement",
|
||||
"package": "lit"
|
||||
},
|
||||
"tagName": "ha-alert",
|
||||
"customElement": true
|
||||
}
|
||||
],
|
||||
"exports": [
|
||||
{
|
||||
"kind": "custom-element-definition",
|
||||
"name": "ha-alert",
|
||||
"declaration": {
|
||||
"name": "HaAlert",
|
||||
"module": "src/components/ha-alert.ts"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,7 +20,9 @@
|
||||
"prepack": "pinst --disable",
|
||||
"postpack": "pinst --enable",
|
||||
"test": "vitest run --config test/vitest.config.ts",
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage",
|
||||
"analyze": "cem analyze --litelement --globs \"src/components/ha-alert.ts\" --dev",
|
||||
"doc": "gulp generate-component-docs"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
@@ -153,6 +155,8 @@
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||
"@custom-elements-manifest/analyzer": "0.10.4",
|
||||
"@custom-elements-manifest/to-markdown": "0.1.0",
|
||||
"@lokalise/node-api": "15.4.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -28,7 +18,7 @@ export const deepEqual = (
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +35,7 @@ export const deepEqual = (
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -103,28 +93,11 @@ export const deepEqual = (
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare Symbol properties if requested
|
||||
if (options?.compareSymbols) {
|
||||
const symbolsA = Object.getOwnPropertySymbols(a);
|
||||
const symbolsB = Object.getOwnPropertySymbols(b);
|
||||
if (symbolsA.length !== symbolsB.length) {
|
||||
return false;
|
||||
}
|
||||
for (const sym of symbolsA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[sym], b[sym], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,36 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom alert component for displaying messages with various alert types.
|
||||
*
|
||||
* @element ha-alert
|
||||
*
|
||||
* @property {string} title - The title of the alert. Defaults to an empty string.
|
||||
* @property {"info" | "warning" | "error" | "success"} alertType - The type of alert to display.
|
||||
* Defaults to "info". Determines the styling and icon used.
|
||||
* @property {boolean} dismissable - Whether the alert can be dismissed. Defaults to `false`.
|
||||
* If `true`, a dismiss button is displayed.
|
||||
* @property {boolean} narrow - Whether the alert should use a narrow layout. Defaults to `false`.
|
||||
*
|
||||
* @slot - The main content of the alert.
|
||||
* @slot icon - Slot for providing a custom icon for the alert.
|
||||
* @slot action - Slot for providing custom actions or buttons for the alert.
|
||||
*
|
||||
* @fires alert-dismissed-clicked - Fired when the dismiss button is clicked.
|
||||
*
|
||||
* @csspart issue-type - The container for the alert.
|
||||
* @csspart icon - The container for the alert icon.
|
||||
* @csspart content - The container for the alert content.
|
||||
* @csspart action - The container for the alert actions.
|
||||
* @csspart title - The container for the alert title.
|
||||
*
|
||||
* @cssprop --info-color - The color used for "info" alerts.
|
||||
* @cssprop --warning-color - The color used for "warning" alerts.
|
||||
* @cssprop --error-color - The color used for "error" alerts.
|
||||
* @cssprop --success-color - The color used for "success" alerts.
|
||||
* @cssprop --primary-text-color - The primary text color used in the alert.
|
||||
*/
|
||||
@customElement("ha-alert")
|
||||
class HaAlert extends LitElement {
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@@ -35,7 +65,7 @@ class HaAlert extends LitElement {
|
||||
| "warning"
|
||||
| "error"
|
||||
| "success" = "info";
|
||||
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -11,10 +11,7 @@ import { findEntities } from "../common/find-entities";
|
||||
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "./types";
|
||||
import type { PictureElementsCardConfig } from "./types";
|
||||
import type { PersonEntity } from "../../../data/person";
|
||||
|
||||
@customElement("hui-picture-elements-card")
|
||||
@@ -169,7 +166,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
.aspectRatio=${this._config.aspect_ratio}
|
||||
.darkModeFilter=${this._config.dark_mode_filter}
|
||||
.darkModeImage=${darkModeImage}
|
||||
@click=${this._handleImageClick}
|
||||
></hui-image>
|
||||
${this._elements}
|
||||
</div>
|
||||
@@ -225,19 +221,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
curCardEl === elToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
|
||||
private _handleImageClick(ev: MouseEvent): void {
|
||||
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const x = ((ev.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((ev.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
// only the edited card has this callback
|
||||
this._config[PREVIEW_CLICK_CALLBACK](x, y);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -483,10 +483,6 @@ export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
alt_text?: string;
|
||||
}
|
||||
|
||||
// Symbol for preview click callback - preserved through spreads, not serialized
|
||||
// This allows the editor to attach a callback that only exists on the edited card's config
|
||||
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
|
||||
|
||||
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
image?: string | MediaSelectorValue;
|
||||
@@ -501,7 +497,6 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
dark_mode_image?: string | MediaSelectorValue;
|
||||
dark_mode_filter?: string;
|
||||
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
|
||||
}
|
||||
|
||||
export interface PictureEntityCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -15,16 +15,12 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { PictureElementsCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../hui-sub-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
@@ -32,6 +28,7 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "../hui-picture-elements-card-row-editor";
|
||||
import type { LovelaceElementConfig } from "../../elements/types";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const genericElementConfigStruct = type({
|
||||
@@ -69,44 +66,6 @@ export class HuiPictureElementsCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _onPreviewClick = (x: number, y: number): void => {
|
||||
if (this._subElementEditorConfig?.type === "element") {
|
||||
this._handlePositionClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
private _handlePositionClick(x: number, y: number): void {
|
||||
if (
|
||||
!this._subElementEditorConfig?.elementConfig ||
|
||||
this._subElementEditorConfig.type !== "element" ||
|
||||
this._subElementEditorConfig.elementConfig.type === "conditional"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementConfig = this._subElementEditorConfig
|
||||
.elementConfig as LovelaceElementConfig;
|
||||
const currentPosition = (elementConfig.style as Record<string, string>)
|
||||
?.position;
|
||||
if (currentPosition && currentPosition !== "absolute") {
|
||||
return;
|
||||
}
|
||||
|
||||
const newElement = {
|
||||
...elementConfig,
|
||||
style: {
|
||||
...((elementConfig.style as Record<string, string>) || {}),
|
||||
left: `${Math.round(x)}%`,
|
||||
top: `${Math.round(y)}%`,
|
||||
},
|
||||
};
|
||||
|
||||
const updateEvent = new CustomEvent("config-changed", {
|
||||
detail: { config: newElement },
|
||||
});
|
||||
this._handleSubElementChanged(updateEvent);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
@@ -179,16 +138,6 @@ export class HuiPictureElementsCardEditor
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
return html`
|
||||
${this._subElementEditorConfig.type === "element" &&
|
||||
this._subElementEditorConfig.elementConfig?.type !== "conditional"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
@@ -232,7 +181,6 @@ export class HuiPictureElementsCardEditor
|
||||
return;
|
||||
}
|
||||
|
||||
// no need to attach the preview click callback here, no element is being edited
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
@@ -243,8 +191,7 @@ export class HuiPictureElementsCardEditor
|
||||
const config = {
|
||||
...this._config,
|
||||
elements: ev.detail.elements as LovelaceElementConfig[],
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
} as PictureElementsCardConfig;
|
||||
} as LovelaceCardConfig;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
|
||||
@@ -285,12 +232,7 @@ export class HuiPictureElementsCardEditor
|
||||
elementConfig: value,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...this._config,
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
},
|
||||
});
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||
|
||||
@@ -10,9 +10,7 @@ export const getElementStubConfig = async (
|
||||
): Promise<LovelaceElementConfig> => {
|
||||
let elementConfig: LovelaceElementConfig = { type };
|
||||
|
||||
if (type === "conditional") {
|
||||
elementConfig = { type, conditions: [], elements: [] };
|
||||
} else {
|
||||
if (type !== "conditional") {
|
||||
elementConfig.style = { left: "50%", top: "50%" };
|
||||
}
|
||||
|
||||
|
||||
@@ -89,11 +89,7 @@ export abstract class HuiElementEditor<
|
||||
}
|
||||
|
||||
public set value(config: T | undefined) {
|
||||
// Compare symbols to detect callback changes (e.g., preview click handlers)
|
||||
if (
|
||||
this._config &&
|
||||
deepEqual(config, this._config, { compareSymbols: true })
|
||||
) {
|
||||
if (this._config && deepEqual(config, this._config)) {
|
||||
return;
|
||||
}
|
||||
this._config = config;
|
||||
|
||||
@@ -8217,7 +8217,6 @@
|
||||
"dark_mode_image": "Dark mode image path",
|
||||
"state_filter": "State filter",
|
||||
"dark_mode_filter": "Dark mode state filter",
|
||||
"position_hint": "Click on the image preview to position this element",
|
||||
"element_types": {
|
||||
"state-badge": "State badge",
|
||||
"state-icon": "State icon",
|
||||
|
||||
Reference in New Issue
Block a user