mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-26 23:54:13 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72c435359e | ||
|
|
4307d59177 | ||
|
|
1420885d00 | ||
|
|
e535c04b04 | ||
|
|
e262c9e4b1 | ||
|
|
aa4c86cf22 | ||
|
|
a349ad16ad | ||
|
|
88c7ba5ca5 | ||
|
|
b2f31c7cd5 | ||
|
|
7b1ad2ea81 | ||
|
|
d6b405c9e4 | ||
|
|
26bb29408a | ||
|
|
b21432c951 | ||
|
|
e961261c97 | ||
|
|
50d80da0b2 | ||
|
|
3c6f164f14 | ||
|
|
f91aebf8cf | ||
|
|
b30024f744 | ||
|
|
caea88e184 | ||
|
|
052cff9955 | ||
|
|
cb784a6b95 | ||
|
|
bffed854fb | ||
|
|
4b215481b8 | ||
|
|
09a6cb11fa | ||
|
|
8afb4f69bb | ||
|
|
09638965f8 |
No files matched your search
@@ -52,8 +52,43 @@ const CONFIG_FILES = [
|
||||
path.join(__dirname, "lit-disable-dev-mode-loader.cjs"),
|
||||
path.join(__dirname, "babel-plugins", "custom-polyfill-plugin.js"),
|
||||
path.join(__dirname, "babel-plugins", "inline-constants-plugin.cjs"),
|
||||
// Its exports define the components source aliases below
|
||||
path.join(__dirname, "..", "packages", "components", "package.json"),
|
||||
];
|
||||
|
||||
// Resolve @home-assistant/components entry points to their sources, so the package
|
||||
// needs no build step during development. The entry points are read from the
|
||||
// "ha-source" export condition of the package; subpath patterns are expanded
|
||||
// for every element, i.e. every directory with a global.ts, like "button" and
|
||||
// "button/close".
|
||||
const COMPONENTS_PACKAGE_DIR = path.join(
|
||||
paths.root_dir,
|
||||
"packages",
|
||||
"components"
|
||||
);
|
||||
const COMPONENTS_SRC_DIR = path.join(
|
||||
COMPONENTS_PACKAGE_DIR,
|
||||
"src",
|
||||
"components"
|
||||
);
|
||||
const componentElements = fs
|
||||
.readdirSync(COMPONENTS_SRC_DIR, { recursive: true })
|
||||
.filter((file) => path.basename(file) === "global.ts")
|
||||
.map((file) => path.dirname(file).split(path.sep).join("/"));
|
||||
const componentsSourceAliases = Object.fromEntries(
|
||||
Object.entries(require("../packages/components/package.json").exports)
|
||||
.filter(([, target]) => target["ha-source"])
|
||||
.flatMap(([subpath, target]) =>
|
||||
(subpath.includes("*") ? componentElements : [undefined]).map((name) => [
|
||||
`${path.posix.join("@home-assistant/components", name ? subpath.replace("*", name) : subpath)}$`,
|
||||
path.join(
|
||||
COMPONENTS_PACKAGE_DIR,
|
||||
name ? target["ha-source"].replace("*", name) : target["ha-source"]
|
||||
),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
// Patches change a package's files but not its version, which is all the
|
||||
// node_modules snapshot checks, so their contents go into the version too.
|
||||
const PATCHES_DIR = path.join(paths.root_dir, "patches");
|
||||
@@ -380,6 +415,7 @@ const createRspackConfig = ({
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
...componentsSourceAliases,
|
||||
"lit/static-html$": "lit/static-html.js",
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
|
||||
@@ -301,6 +301,14 @@ export default tseslint.config(
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Published as native ESM, where package subpath imports such as
|
||||
// "lit/decorators.js" need their extension.
|
||||
files: ["packages/*/src/**/*.ts"],
|
||||
rules: {
|
||||
"import-x/extensions": ["error", "ignorePackages", { ts: "never" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "node ./node_modules/@typescript/native/bin/tsc && node ./node_modules/@typescript/native/bin/tsc -p .github/scripts",
|
||||
"lint:lit": "lit-analyzer \"{.,*}/src/**/*.ts\"",
|
||||
"lint:lit": "lit-analyzer \"{.,*,packages/*}/src/**/*.ts\"",
|
||||
"lint:element-imports": "eslint \"**/src/**/*.{js,ts,html}\" --ignore-pattern=.gitignore --rule '{\"ha/no-unused-element-import\": \"warn\"}'",
|
||||
"lint:licenses": "node --no-deprecation script/check-licenses",
|
||||
"lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:types && pnpm run lint:lit",
|
||||
@@ -145,6 +145,7 @@
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.3",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@gfx/zopfli": "1.0.15",
|
||||
"@home-assistant/components": "workspace:*",
|
||||
"@html-eslint/eslint-plugin": "0.67.0",
|
||||
"@lokalise/node-api": "16.6.0",
|
||||
"@octokit/auth-oauth-device": "8.0.5",
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# @home-assistant/components
|
||||
|
||||
Home Assistant Components (`hac-*`): framework-agnostic web components built
|
||||
with [Lit](https://lit.dev).
|
||||
|
||||
> Work in progress, not published yet.
|
||||
|
||||
## Loading modes
|
||||
|
||||
Every element is imported on its own, in one of two ways:
|
||||
|
||||
| Import | Registers in | Uses |
|
||||
| --------------------------------------------- | -------------------- | ------------------------------------------------ |
|
||||
| `@home-assistant/components/<element>` | Global registry | The bundled copy. Used by Home Assistant itself. |
|
||||
| `@home-assistant/components/<element>/scoped` | Your scoped registry | The bundled copy, isolated from other copies. |
|
||||
|
||||
`<element>` is the group, like `button` for `hac-button`, or the group and a
|
||||
variant, like `button/close` for `hac-button-close`.
|
||||
|
||||
Global registration skips tag names that are already registered, so the first
|
||||
registration wins and nothing throws.
|
||||
|
||||
### Global
|
||||
|
||||
```ts
|
||||
import "@home-assistant/components/button";
|
||||
import "@home-assistant/components/button/close";
|
||||
```
|
||||
|
||||
### Scoped
|
||||
|
||||
Pass the component's elements to `ScopedElementsMixin` for Lit, or to
|
||||
`createScopedElementRegistry` for plain custom elements. Forgetting the
|
||||
elements is a TypeScript error, and a runtime error in plain JavaScript.
|
||||
|
||||
Lit:
|
||||
|
||||
```ts
|
||||
import { LitElement, html } from "lit";
|
||||
import { ScopedElementsMixin } from "@home-assistant/components/scoped";
|
||||
import { hacCard } from "@home-assistant/components/card/scoped";
|
||||
|
||||
class MyCard extends ScopedElementsMixin(LitElement, [hacCard]) {
|
||||
render() {
|
||||
return html`<hac-card>…</hac-card>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Plain custom elements:
|
||||
|
||||
```js
|
||||
import {
|
||||
attachScopedShadow,
|
||||
createScopedElement,
|
||||
createScopedElementRegistry,
|
||||
} from "@home-assistant/components/scoped";
|
||||
import { hacCard } from "@home-assistant/components/card/scoped";
|
||||
|
||||
// Create the registry once, outside the class: all instances of the card share it
|
||||
const registry = createScopedElementRegistry([hacCard]);
|
||||
|
||||
class MyCard extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
const root = attachScopedShadow(this, { mode: "open" }, registry);
|
||||
root.innerHTML = "<hac-card>…</hac-card>";
|
||||
// or: root.append(createScopedElement(root, "hac-card"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`innerHTML`, `insertAdjacentHTML` and `createScopedElement` use the scoped
|
||||
registry. `document.createElement` uses the global one, so avoid it for
|
||||
scoped elements.
|
||||
|
||||
Scoped registries need native support (Chrome 146+, Safari 26) or
|
||||
`@webcomponents/scoped-custom-element-registry`, which Home Assistant loads.
|
||||
Without either, elements are registered globally instead, and an already
|
||||
registered tag name keeps its existing class.
|
||||
|
||||
Only elements rendered inside the shadow root use the scoped registry. Light
|
||||
DOM content, such as slotted elements, uses the registry of the tree it is
|
||||
written in.
|
||||
|
||||
`lit` is a peer dependency.
|
||||
|
||||
### TypeScript
|
||||
|
||||
Each element's entry points declare its tag in `HTMLElementTagNameMap`, so
|
||||
`document.querySelector("hac-card")` and templates are typed once the element
|
||||
is imported. For the class itself, import its type from the `scoped` entry
|
||||
point; `import type` loads nothing at runtime:
|
||||
|
||||
```ts
|
||||
import type { HacCard } from "@home-assistant/components/card/scoped";
|
||||
```
|
||||
|
||||
The package has no root entry point: each element and the scoped helpers
|
||||
(`@home-assistant/components/scoped`) have their own.
|
||||
|
||||
## Theming
|
||||
|
||||
Components read the Home Assistant theme variables when they are available
|
||||
and fall back to built-in defaults otherwise. Each component also exposes its
|
||||
own `--hac-*` custom properties, documented in its source.
|
||||
|
||||
## Structure
|
||||
|
||||
Elements are organized in groups. A group's directory holds its default
|
||||
element, and variants live in subdirectories, so the directories mirror the
|
||||
import paths:
|
||||
|
||||
```
|
||||
src/components/button/ <hac-button> -> @home-assistant/components/button
|
||||
hac-button.ts class, does not register itself
|
||||
info.ts tag name and version
|
||||
global.ts registers globally
|
||||
scoped.ts elements for a scoped registry -> …/scoped
|
||||
close/ <hac-button-close> -> @home-assistant/components/button/close
|
||||
hac-button-close.ts
|
||||
info.ts, global.ts, scoped.ts
|
||||
```
|
||||
|
||||
Shared code lives in `src/utils/`: `registration/` for defining elements
|
||||
(global and scoped) and `element/` for helpers that components use
|
||||
themselves, like slot checks.
|
||||
|
||||
A variant's tag name is `hac-<group>-<variant>`. `scoped` and `info` are
|
||||
reserved and cannot be variant names.
|
||||
|
||||
Every element has its own version in `info.ts`.
|
||||
|
||||
### Dependencies
|
||||
|
||||
A component that renders other hac components in its shadow root lists them
|
||||
in `static dependencies`, with the tag name from its `info.ts`, and imports
|
||||
only their classes:
|
||||
|
||||
```ts
|
||||
export class HacHeader extends LitElement {
|
||||
static readonly dependencies = [
|
||||
{ tagName: BUTTON_CLOSE_TAG_NAME, element: HacButtonClose },
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Never register a dependency yourself. The entry point decides where it goes:
|
||||
|
||||
- `global` registers dependencies globally, like the component. Inside Home
|
||||
Assistant everything stays in the global registry.
|
||||
- `scoped` registers a variant of the component whose shadow root has its own
|
||||
registry with the bundled dependencies. Its internals never pick up another
|
||||
copy from the page.
|
||||
|
||||
Entry points are mapped with subpath patterns in `package.json`, so a new
|
||||
element directory needs no `package.json` change. Only `global.ts` files have
|
||||
side effects, which `sideEffects` relies on for tree shaking.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm --filter @home-assistant/components build # ESM + type declarations to dist/
|
||||
pnpm --filter @home-assistant/components dev # rebuild on change
|
||||
```
|
||||
|
||||
Inside this repository the app uses the sources directly through
|
||||
the `ha-source` export condition, so no build is needed during development.
|
||||
The package is built with [Rslib](https://rslib.rs) in bundleless mode: every
|
||||
source file becomes its own ES module.
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@home-assistant/components",
|
||||
"version": "0.0.0",
|
||||
"description": "Home Assistant web components (hac-* elements) built with Lit",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/home-assistant/frontend.git",
|
||||
"directory": "packages/components"
|
||||
},
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"./dist/components/**/global.js",
|
||||
"./src/components/**/global.ts"
|
||||
],
|
||||
"exports": {
|
||||
"./scoped": {
|
||||
"ha-source": "./src/scoped.ts",
|
||||
"types": "./dist/scoped.d.ts",
|
||||
"import": "./dist/scoped.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./*/scoped": {
|
||||
"ha-source": "./src/components/*/scoped.ts",
|
||||
"types": "./dist/components/*/scoped.d.ts",
|
||||
"import": "./dist/components/*/scoped.js"
|
||||
},
|
||||
"./*/info": {
|
||||
"ha-source": "./src/components/*/info.ts",
|
||||
"types": "./dist/components/*/info.d.ts",
|
||||
"import": "./dist/components/*/info.js"
|
||||
},
|
||||
"./*": {
|
||||
"ha-source": "./src/components/*/global.ts",
|
||||
"types": "./dist/components/*/global.d.ts",
|
||||
"import": "./dist/components/*/global.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rslib build",
|
||||
"dev": "rslib build --watch",
|
||||
"prepublishOnly": "rslib build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lit": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rslib/core": "1.0.3",
|
||||
"lit": "3.3.3",
|
||||
"typescript": "6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from "@rslib/core";
|
||||
|
||||
// Bundleless build: every source file becomes its own ESM file, so consumers'
|
||||
// bundlers only include the entry points they import. Only global.ts files
|
||||
// register elements; everything else is free of side effects.
|
||||
export default defineConfig({
|
||||
source: {
|
||||
entry: { index: ["./src/**/*.ts"] },
|
||||
decorators: { version: "legacy" },
|
||||
tsconfigPath: "./tsconfig.json",
|
||||
},
|
||||
tools: {
|
||||
swc: { jsc: { transform: { decoratorMetadata: false } } },
|
||||
},
|
||||
lib: [
|
||||
{
|
||||
format: "esm",
|
||||
bundle: false,
|
||||
syntax: "es2021",
|
||||
dts: true,
|
||||
output: { target: "web", distPath: { root: "./dist" } },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineGlobal } from "../../../utils/registration/dependencies";
|
||||
import { HacButtonClose } from "./hac-button-close";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
// Registers <hac-button-close> and its dependencies in the global registry, as used by
|
||||
// Home Assistant
|
||||
defineGlobal(TAG_NAME, HacButtonClose);
|
||||
|
||||
export { HacButtonClose };
|
||||
@@ -0,0 +1,62 @@
|
||||
import { css, html } from "lit";
|
||||
import { HacButton } from "../hac-button";
|
||||
|
||||
// mdiClose from Material Design Icons (Apache-2.0)
|
||||
const CLOSE_ICON =
|
||||
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z";
|
||||
|
||||
/**
|
||||
* Icon button with a close icon.
|
||||
*
|
||||
* The accessible label defaults to "Close". Set `label` to a translated
|
||||
* text when the page is not in English.
|
||||
*
|
||||
* @csspart base - The native button.
|
||||
* @cssprop --hac-button-color - Icon color.
|
||||
* @cssprop --hac-button-height - Width and height.
|
||||
*/
|
||||
export class HacButtonClose extends HacButton {
|
||||
constructor() {
|
||||
super();
|
||||
this.appearance = "plain";
|
||||
}
|
||||
|
||||
protected get accessibleLabel() {
|
||||
return this.label ?? "Close";
|
||||
}
|
||||
|
||||
protected renderContent() {
|
||||
return html`
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d=${CLOSE_ICON}></path>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
HacButton.styles,
|
||||
css`
|
||||
:host([appearance="plain"]) {
|
||||
--_color: var(--hac-button-color, var(--secondary-text-color, #727272));
|
||||
}
|
||||
|
||||
button {
|
||||
width: var(--hac-button-height, 40px);
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hac-button-close": HacButtonClose;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Tag name of the element. */
|
||||
export const TAG_NAME = "hac-button-close";
|
||||
|
||||
/** Version of <hac-button-close>. */
|
||||
export const VERSION = "0.1.0";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ElementDefinition } from "../../../utils/registration/definition";
|
||||
import { scopedVariant } from "../../../utils/registration/dependencies";
|
||||
import { HacButtonClose } from "./hac-button-close";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
export { HacButtonClose };
|
||||
|
||||
/**
|
||||
* The bundled <hac-button-close> for a scoped registry:
|
||||
* `ScopedElementsMixin(LitElement, [hacButtonClose])` or
|
||||
* `createScopedElementRegistry([hacButtonClose])`.
|
||||
*/
|
||||
export const hacButtonClose: ElementDefinition<typeof HacButtonClose> = {
|
||||
tagName: TAG_NAME,
|
||||
element: scopedVariant(HacButtonClose),
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineGlobal } from "../../utils/registration/dependencies";
|
||||
import { HacButton } from "./hac-button";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
// Registers <hac-button> and its dependencies in the global registry, as used by
|
||||
// Home Assistant
|
||||
defineGlobal(TAG_NAME, HacButton);
|
||||
|
||||
export { HacButton };
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
/**
|
||||
* Button.
|
||||
*
|
||||
* @slot - Label of the button.
|
||||
* @slot icon - Icon displayed before the label.
|
||||
*
|
||||
* @csspart base - The native button.
|
||||
*
|
||||
* @cssprop --hac-button-color - Text and icon color.
|
||||
* @cssprop --hac-button-background - Background of filled buttons.
|
||||
* @cssprop --hac-button-border-radius - Border radius.
|
||||
* @cssprop --hac-button-height - Height.
|
||||
*/
|
||||
export class HacButton extends LitElement {
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/** Visual style of the button. */
|
||||
@property({ reflect: true }) public appearance:
|
||||
"filled" | "outlined" | "plain" = "filled";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
/** Accessible label, required for buttons without a visible label. */
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
part="base"
|
||||
?disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.accessibleLabel)}
|
||||
>
|
||||
${this.renderContent()}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
/** Accessible label of the native button, overridden by icon buttons. */
|
||||
protected get accessibleLabel(): string | undefined {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
/** Content of the button, overridden by specialized buttons. */
|
||||
protected renderContent(): TemplateResult {
|
||||
return html`<slot name="icon"></slot><slot></slot>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
--_color: var(--hac-button-color, var(--primary-color, #03a9f4));
|
||||
}
|
||||
|
||||
:host([appearance="filled"]) {
|
||||
--_color: var(--hac-button-color, var(--text-primary-color, #fff));
|
||||
}
|
||||
|
||||
:host([disabled]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-2, 8px);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: var(--hac-button-height, 40px);
|
||||
padding: 0 var(--ha-space-4, 16px);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(
|
||||
--hac-button-border-radius,
|
||||
var(--ha-border-radius-pill, 9999px)
|
||||
);
|
||||
background: transparent;
|
||||
color: var(--_color);
|
||||
font: inherit;
|
||||
font-weight: var(--ha-font-weight-medium, 500);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
:host([appearance="filled"]) button {
|
||||
background: var(--hac-button-background, var(--primary-color, #03a9f4));
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) button {
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
/* State layer for hover and press feedback */
|
||||
button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: currentColor;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
button:hover::before {
|
||||
opacity: 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
button:active::before {
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--primary-color, #03a9f4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
flex: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hac-button": HacButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Tag name of the element. */
|
||||
export const TAG_NAME = "hac-button";
|
||||
|
||||
/** Version of <hac-button>. */
|
||||
export const VERSION = "0.1.0";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ElementDefinition } from "../../utils/registration/definition";
|
||||
import { scopedVariant } from "../../utils/registration/dependencies";
|
||||
import { HacButton } from "./hac-button";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
export { HacButton };
|
||||
|
||||
/**
|
||||
* The bundled <hac-button> for a scoped registry:
|
||||
* `ScopedElementsMixin(LitElement, [hacButton])` or
|
||||
* `createScopedElementRegistry([hacButton])`.
|
||||
*/
|
||||
export const hacButton: ElementDefinition<typeof HacButton> = {
|
||||
tagName: TAG_NAME,
|
||||
element: scopedVariant(HacButton),
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineGlobal } from "../../utils/registration/dependencies";
|
||||
import { HacCard } from "./hac-card";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
// Registers <hac-card> and its dependencies in the global registry, as used by
|
||||
// Home Assistant
|
||||
defineGlobal(TAG_NAME, HacCard);
|
||||
|
||||
export { HacCard };
|
||||
@@ -0,0 +1,121 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import {
|
||||
hasAssignedContent,
|
||||
hasAssignedElements,
|
||||
} from "../../utils/element/slot";
|
||||
|
||||
/**
|
||||
* Container that groups related content.
|
||||
*
|
||||
* @slot header - Header of the card, usually a `hac-header`.
|
||||
* @slot - Content of the card. Its padding is omitted when empty.
|
||||
* @slot footer - Footer of the card, e.g. actions. Hidden when empty.
|
||||
*
|
||||
* @cssprop --hac-card-background - Background of the card.
|
||||
* @cssprop --hac-card-color - Text color of the card.
|
||||
* @cssprop --hac-card-border-color - Border color of the card.
|
||||
* @cssprop --hac-card-border-width - Border width of the card.
|
||||
* @cssprop --hac-card-border-radius - Border radius of the card.
|
||||
* @cssprop --hac-card-box-shadow - Box shadow of the card.
|
||||
* @cssprop --hac-card-content-padding - Padding of the content.
|
||||
* @cssprop --hac-card-footer-padding - Padding of the footer.
|
||||
*/
|
||||
export class HacCard extends LitElement {
|
||||
/** Visual style of the card. */
|
||||
@property({ reflect: true }) public appearance: "outlined" | "raised" =
|
||||
"outlined";
|
||||
|
||||
@state() private _hasHeader = false;
|
||||
|
||||
@state() private _hasFooter = false;
|
||||
|
||||
@state() private _hasContent = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<slot name="header" @slotchange=${this._headerChanged}></slot>
|
||||
<div
|
||||
class="content ${this._hasHeader ? "below-header" : ""}"
|
||||
?hidden=${!this._hasContent}
|
||||
>
|
||||
<slot @slotchange=${this._contentChanged}></slot>
|
||||
</div>
|
||||
<div class="footer" ?hidden=${!this._hasFooter}>
|
||||
<slot name="footer" @slotchange=${this._footerChanged}></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _headerChanged(ev: Event) {
|
||||
this._hasHeader = hasAssignedElements(ev);
|
||||
}
|
||||
|
||||
private _contentChanged(ev: Event) {
|
||||
this._hasContent = hasAssignedContent(ev);
|
||||
}
|
||||
|
||||
private _footerChanged(ev: Event) {
|
||||
this._hasFooter = hasAssignedElements(ev);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background: var(
|
||||
--hac-card-background,
|
||||
var(--card-background-color, #fff)
|
||||
);
|
||||
color: var(--hac-card-color, var(--primary-text-color, #141414));
|
||||
border: var(--hac-card-border-width, 1px) solid
|
||||
var(--hac-card-border-color, var(--divider-color, #e0e0e0));
|
||||
border-radius: var(
|
||||
--hac-card-border-radius,
|
||||
var(--ha-border-radius-lg, 12px)
|
||||
);
|
||||
box-shadow: var(--hac-card-box-shadow, none);
|
||||
}
|
||||
|
||||
:host([appearance="raised"]) {
|
||||
border-color: transparent;
|
||||
box-shadow: var(
|
||||
--hac-card-box-shadow,
|
||||
0 2px 1px -1px rgba(0, 0, 0, 0.2),
|
||||
0 1px 1px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--hac-card-content-padding, var(--ha-space-4, 16px));
|
||||
}
|
||||
|
||||
/* The header already provides the spacing above the content */
|
||||
.content.below-header {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2, 8px);
|
||||
padding: var(--hac-card-footer-padding, var(--ha-space-2, 8px));
|
||||
border-block-start: 1px solid var(--divider-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.content[hidden],
|
||||
.footer[hidden] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hac-card": HacCard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Tag name of the element. */
|
||||
export const TAG_NAME = "hac-card";
|
||||
|
||||
/** Version of <hac-card>. */
|
||||
export const VERSION = "0.1.0";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ElementDefinition } from "../../utils/registration/definition";
|
||||
import { scopedVariant } from "../../utils/registration/dependencies";
|
||||
import { HacCard } from "./hac-card";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
export { HacCard };
|
||||
|
||||
/**
|
||||
* The bundled <hac-card> for a scoped registry:
|
||||
* `ScopedElementsMixin(LitElement, [hacCard])` or
|
||||
* `createScopedElementRegistry([hacCard])`.
|
||||
*/
|
||||
export const hacCard: ElementDefinition<typeof HacCard> = {
|
||||
tagName: TAG_NAME,
|
||||
element: scopedVariant(HacCard),
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineGlobal } from "../../utils/registration/dependencies";
|
||||
import { HacHeader } from "./hac-header";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
// Registers <hac-header> and its dependencies in the global registry, as used by
|
||||
// Home Assistant
|
||||
defineGlobal(TAG_NAME, HacHeader);
|
||||
|
||||
export { HacHeader };
|
||||
@@ -0,0 +1,164 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { hasAssignedElements } from "../../utils/element/slot";
|
||||
import { HacButtonClose } from "../button/close/hac-button-close";
|
||||
import { TAG_NAME as BUTTON_CLOSE_TAG_NAME } from "../button/close/info";
|
||||
|
||||
/**
|
||||
* Header with an optional icon, a title, a subtitle and actions.
|
||||
*
|
||||
* Reusable in any container, e.g. `hac-card`. The title is exposed to
|
||||
* assistive technology as a heading of the given `level`.
|
||||
*
|
||||
* @slot - Title of the header.
|
||||
* @slot icon - Icon displayed before the title.
|
||||
* @slot subtitle - Secondary text displayed below the title.
|
||||
* @slot actions - Actions displayed at the end of the header.
|
||||
*
|
||||
* @fires hac-close - The close button was clicked, see `showClose`.
|
||||
*
|
||||
* @cssprop --hac-header-color - Title color.
|
||||
* @cssprop --hac-header-secondary-color - Color of the icon and subtitle.
|
||||
* @cssprop --hac-header-font-family - Title font family.
|
||||
* @cssprop --hac-header-font-size - Title font size.
|
||||
* @cssprop --hac-header-padding - Padding of the header.
|
||||
*/
|
||||
export class HacHeader extends LitElement {
|
||||
static readonly dependencies = [
|
||||
{ tagName: BUTTON_CLOSE_TAG_NAME, element: HacButtonClose },
|
||||
];
|
||||
|
||||
/** Heading level of the title for assistive technology. */
|
||||
@property({ type: Number }) public level: 1 | 2 | 3 | 4 | 5 | 6 = 2;
|
||||
|
||||
/** Shows a close button at the end, which fires `hac-close`. */
|
||||
@property({ type: Boolean, reflect: true, attribute: "show-close" })
|
||||
public showClose = false;
|
||||
|
||||
/** Accessible label of the close button, "Close" by default. */
|
||||
@property({ attribute: "close-label" }) public closeLabel?: string;
|
||||
|
||||
@state() private _hasIcon = false;
|
||||
|
||||
@state() private _hasSubtitle = false;
|
||||
|
||||
@state() private _hasActions = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="icon" ?hidden=${!this._hasIcon}>
|
||||
<slot name="icon" @slotchange=${this._iconChanged}></slot>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div class="title" role="heading" aria-level=${this.level}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="subtitle" ?hidden=${!this._hasSubtitle}>
|
||||
<slot name="subtitle" @slotchange=${this._subtitleChanged}></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" ?hidden=${!this._hasActions}>
|
||||
<slot name="actions" @slotchange=${this._actionsChanged}></slot>
|
||||
</div>
|
||||
${
|
||||
this.showClose
|
||||
? html`<hac-button-close
|
||||
.label=${this.closeLabel}
|
||||
@click=${this._close}
|
||||
></hac-button-close>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("hac-close", { bubbles: true, composed: true })
|
||||
);
|
||||
}
|
||||
|
||||
private _iconChanged(ev: Event) {
|
||||
this._hasIcon = hasAssignedElements(ev);
|
||||
}
|
||||
|
||||
private _subtitleChanged(ev: Event) {
|
||||
this._hasSubtitle = hasAssignedElements(ev);
|
||||
}
|
||||
|
||||
private _actionsChanged(ev: Event) {
|
||||
this._hasActions = hasAssignedElements(ev);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3, 12px);
|
||||
box-sizing: border-box;
|
||||
padding: var(
|
||||
--hac-header-padding,
|
||||
var(--ha-space-4, 16px) var(--ha-space-4, 16px) var(--ha-space-3, 12px)
|
||||
);
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
flex: none;
|
||||
color: var(
|
||||
--hac-header-secondary-color,
|
||||
var(--secondary-text-color, #727272)
|
||||
);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--hac-header-color, var(--primary-text-color, #141414));
|
||||
font-family: var(--hac-header-font-family, inherit);
|
||||
font-size: var(--hac-header-font-size, var(--ha-font-size-2xl, 24px));
|
||||
font-weight: var(--ha-font-weight-normal, 400);
|
||||
line-height: var(--ha-line-height-condensed, 1.2);
|
||||
letter-spacing: -0.012em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-block-start: var(--ha-space-1, 4px);
|
||||
color: var(
|
||||
--hac-header-secondary-color,
|
||||
var(--secondary-text-color, #727272)
|
||||
);
|
||||
font-size: var(--ha-font-size-m, 14px);
|
||||
line-height: var(--ha-line-height-normal, 1.6);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2, 8px);
|
||||
}
|
||||
|
||||
hac-button-close {
|
||||
flex: none;
|
||||
margin-inline-end: calc(var(--ha-space-2, 8px) * -1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hac-header": HacHeader;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"hac-close": CustomEvent<undefined>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Tag name of the element. */
|
||||
export const TAG_NAME = "hac-header";
|
||||
|
||||
/** Version of <hac-header>. */
|
||||
export const VERSION = "0.1.0";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ElementDefinition } from "../../utils/registration/definition";
|
||||
import { scopedVariant } from "../../utils/registration/dependencies";
|
||||
import { HacHeader } from "./hac-header";
|
||||
import { TAG_NAME } from "./info";
|
||||
|
||||
export { HacHeader };
|
||||
|
||||
/**
|
||||
* The bundled <hac-header> for a scoped registry:
|
||||
* `ScopedElementsMixin(LitElement, [hacHeader])` or
|
||||
* `createScopedElementRegistry([hacHeader])`.
|
||||
*/
|
||||
export const hacHeader: ElementDefinition<typeof HacHeader> = {
|
||||
tagName: TAG_NAME,
|
||||
element: scopedVariant(HacHeader),
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { define } from "./utils/registration/define";
|
||||
import type { ElementDefinition } from "./utils/registration/definition";
|
||||
import {
|
||||
createScopedRegistry,
|
||||
scopedCreationScope,
|
||||
scopedRegistrySupport,
|
||||
scopedShadowRootInit,
|
||||
} from "./utils/registration/scoped-registry";
|
||||
|
||||
export type { ElementDefinition } from "./utils/registration/definition";
|
||||
|
||||
/**
|
||||
* Creates a registry with the given elements, for `attachScopedShadow`:
|
||||
* definitions from `scoped` entry points, e.g. `[hacCard]`.
|
||||
* Create it once, outside the class, so all instances of a card share it.
|
||||
*
|
||||
* Without scoped registry support (native or polyfill), the elements are
|
||||
* registered in the global registry instead, unless their tag name is already
|
||||
* taken there, and the global registry is returned.
|
||||
*/
|
||||
export const createScopedElementRegistry = (
|
||||
elements: readonly ElementDefinition[]
|
||||
): CustomElementRegistry => {
|
||||
// Checked as unknown: Array.isArray would narrow the list to any[]
|
||||
if (!Array.isArray(elements as unknown)) {
|
||||
throw new TypeError(
|
||||
"@home-assistant/components: pass the elements to register, e.g. [hacCard]"
|
||||
);
|
||||
}
|
||||
const registry = createScopedRegistry() ?? customElements;
|
||||
for (const { tagName, element } of elements) {
|
||||
define(tagName, element, registry);
|
||||
}
|
||||
return registry;
|
||||
};
|
||||
|
||||
const shadowRootInit = (
|
||||
init: ShadowRootInit,
|
||||
registry: CustomElementRegistry
|
||||
): ShadowRootInit =>
|
||||
registry === customElements ? init : scopedShadowRootInit(init, registry);
|
||||
|
||||
const registries = new WeakMap<ShadowRoot, CustomElementRegistry>();
|
||||
|
||||
/**
|
||||
* Attaches a shadow root that uses `registry` for custom elements in it.
|
||||
*
|
||||
* Content added with `innerHTML` or `insertAdjacentHTML` uses the registry.
|
||||
* Create single elements with `createScopedElement`; `document.createElement`
|
||||
* would use the global registry.
|
||||
*
|
||||
* Light DOM content, like slotted elements, keeps using the registry of the
|
||||
* tree it is written in.
|
||||
*/
|
||||
export const attachScopedShadow = (
|
||||
host: HTMLElement,
|
||||
init: ShadowRootInit,
|
||||
registry: CustomElementRegistry
|
||||
): ShadowRoot => {
|
||||
const root = host.attachShadow(shadowRootInit(init, registry));
|
||||
registries.set(root, registry);
|
||||
return root;
|
||||
};
|
||||
|
||||
/** Creates an element from the registry of a shadow root. */
|
||||
export const createScopedElement = <K extends keyof HTMLElementTagNameMap>(
|
||||
root: ShadowRoot,
|
||||
tagName: K
|
||||
): HTMLElementTagNameMap[K] => {
|
||||
const registry = registries.get(root);
|
||||
if (!registry || registry === customElements) {
|
||||
return document.createElement(tagName);
|
||||
}
|
||||
if (scopedRegistrySupport() === "polyfill") {
|
||||
// The polyfill adds createElement to shadow roots
|
||||
return (
|
||||
root as unknown as {
|
||||
createElement(tagName: K): HTMLElementTagNameMap[K];
|
||||
}
|
||||
).createElement(tagName);
|
||||
}
|
||||
return (
|
||||
document.createElement as (
|
||||
tagName: K,
|
||||
options: { customElementRegistry: CustomElementRegistry }
|
||||
) => HTMLElementTagNameMap[K]
|
||||
)(tagName, { customElementRegistry: registry });
|
||||
};
|
||||
|
||||
type LitElementConstructor = abstract new (...args: any[]) => LitElement;
|
||||
|
||||
/**
|
||||
* Renders a Lit element into a shadow root with its own custom element
|
||||
* registry, containing `elements`:
|
||||
*
|
||||
* ```ts
|
||||
* class MyCard extends ScopedElementsMixin(LitElement, [hacCard]) {}
|
||||
* ```
|
||||
*
|
||||
* See `createScopedElementRegistry` and `attachScopedShadow` for the details.
|
||||
*/
|
||||
export const ScopedElementsMixin = <T extends LitElementConstructor>(
|
||||
superClass: T,
|
||||
elements: readonly ElementDefinition[]
|
||||
) => {
|
||||
// Created on first use, so defining the class has no side effects
|
||||
let registry: CustomElementRegistry | undefined;
|
||||
const getRegistry = () => {
|
||||
registry ??= createScopedElementRegistry(elements);
|
||||
return registry;
|
||||
};
|
||||
|
||||
abstract class ScopedElementsHost extends superClass {
|
||||
public attachShadow(init: ShadowRootInit): ShadowRoot {
|
||||
const root = super.attachShadow(shadowRootInit(init, getRegistry()));
|
||||
registries.set(root, getRegistry());
|
||||
return root;
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
const root = super.createRenderRoot();
|
||||
const scoped = getRegistry();
|
||||
if (scoped !== customElements && root instanceof ShadowRoot) {
|
||||
this.renderOptions.creationScope = scopedCreationScope(root, scoped);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
// Typed as the base class: the overrides add no public API
|
||||
return ScopedElementsHost as unknown as T;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Whether the slot of a `slotchange` event has elements assigned. */
|
||||
export const hasAssignedElements = (ev: Event): boolean =>
|
||||
(ev.target as HTMLSlotElement).assignedElements().length > 0;
|
||||
|
||||
/**
|
||||
* Whether the slot of a `slotchange` event has content assigned: elements or
|
||||
* text. Whitespace between tags is not content.
|
||||
*/
|
||||
export const hasAssignedContent = (ev: Event): boolean =>
|
||||
(ev.target as HTMLSlotElement)
|
||||
.assignedNodes()
|
||||
.some(
|
||||
(node) =>
|
||||
node.nodeType === Node.ELEMENT_NODE || Boolean(node.textContent?.trim())
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Registers a custom element unless the tag name is already taken in the
|
||||
* registry.
|
||||
*
|
||||
* Home Assistant and custom cards can each ship a copy of this library, so
|
||||
* registering the same tag name twice must not throw. The first registration
|
||||
* wins.
|
||||
*/
|
||||
export const define = (
|
||||
tagName: string,
|
||||
elementClass: CustomElementConstructor,
|
||||
registry: CustomElementRegistry = customElements
|
||||
): void => {
|
||||
if (!registry.get(tagName)) {
|
||||
registry.define(tagName, elementClass);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
/**
|
||||
* An element to register: its tag name and its class. The `scoped` entry
|
||||
* point of an element exports one, e.g. `hacCard`.
|
||||
*/
|
||||
export interface ElementDefinition<
|
||||
E extends CustomElementConstructor = CustomElementConstructor,
|
||||
> {
|
||||
readonly tagName: string;
|
||||
readonly element: E;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hac component class. Components list the elements they render in their
|
||||
* shadow root in `dependencies`, without registering them. The entry point a
|
||||
* component is loaded through decides where those are registered:
|
||||
*
|
||||
* - global: in the global registry, like the component itself
|
||||
* - scoped: in a scoped registry of the component's shadow root, so its
|
||||
* internals always use the bundled, same-version dependencies
|
||||
*/
|
||||
export type ComponentClass = (new () => LitElement) & {
|
||||
readonly dependencies?: readonly ElementDefinition<ComponentClass>[];
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ScopedElementsMixin } from "../../scoped";
|
||||
import { define } from "./define";
|
||||
import type { ComponentClass } from "./definition";
|
||||
|
||||
/** Registers a component and its dependencies in the global registry. */
|
||||
export const defineGlobal = (
|
||||
tagName: string,
|
||||
elementClass: ComponentClass
|
||||
): void => {
|
||||
for (const dependency of elementClass.dependencies ?? []) {
|
||||
defineGlobal(dependency.tagName, dependency.element);
|
||||
}
|
||||
define(tagName, elementClass);
|
||||
};
|
||||
|
||||
const variants = new WeakMap<ComponentClass, ComponentClass>();
|
||||
|
||||
/**
|
||||
* The class to register in a scoped registry: the component itself if it has
|
||||
* no dependencies, otherwise a subclass whose shadow root has its own
|
||||
* registry with scoped variants of its dependencies. Created once per class.
|
||||
*/
|
||||
export const scopedVariant = <T extends ComponentClass>(elementClass: T): T => {
|
||||
const dependencies = elementClass.dependencies ?? [];
|
||||
if (dependencies.length === 0) {
|
||||
return elementClass;
|
||||
}
|
||||
let variant = variants.get(elementClass);
|
||||
if (!variant) {
|
||||
// The mixin returns a new subclass, the variant
|
||||
variant = ScopedElementsMixin(
|
||||
elementClass,
|
||||
dependencies.map(({ tagName, element }) => ({
|
||||
tagName,
|
||||
element: scopedVariant(element),
|
||||
}))
|
||||
);
|
||||
variants.set(elementClass, variant);
|
||||
}
|
||||
return variant as T;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Scoped custom element registries come in two flavors: the standard API
|
||||
* (Chrome 146+, Safari 26) and the older API of
|
||||
* `@webcomponents/scoped-custom-element-registry`, which Home Assistant loads
|
||||
* and which replaces the native implementation when present.
|
||||
*/
|
||||
export type ScopedRegistrySupport = "polyfill" | "native" | "none";
|
||||
|
||||
let support: ScopedRegistrySupport | undefined;
|
||||
|
||||
export const scopedRegistrySupport = (): ScopedRegistrySupport => {
|
||||
if (support) {
|
||||
return support;
|
||||
}
|
||||
if ("CustomElementRegistryPolyfill" in globalThis) {
|
||||
support = "polyfill";
|
||||
} else {
|
||||
try {
|
||||
new CustomElementRegistry();
|
||||
support = "native";
|
||||
} catch (_err) {
|
||||
support = "none";
|
||||
}
|
||||
}
|
||||
return support;
|
||||
};
|
||||
|
||||
/** A new scoped registry, or `undefined` if the browser has no support. */
|
||||
export const createScopedRegistry = (): CustomElementRegistry | undefined =>
|
||||
scopedRegistrySupport() === "none" ? undefined : new CustomElementRegistry();
|
||||
|
||||
/** Shadow root options that make the shadow root use `registry`. */
|
||||
export const scopedShadowRootInit = (
|
||||
init: ShadowRootInit,
|
||||
registry: CustomElementRegistry
|
||||
): ShadowRootInit =>
|
||||
scopedRegistrySupport() === "polyfill"
|
||||
? ({ ...init, registry } as ShadowRootInit)
|
||||
: ({ ...init, customElementRegistry: registry } as ShadowRootInit);
|
||||
|
||||
interface CreationScope {
|
||||
importNode(node: Node, deep?: boolean): Node;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object Lit should create template content with, so custom elements in
|
||||
* it are created from the scoped registry (Lit's `creationScope`).
|
||||
*/
|
||||
export const scopedCreationScope = (
|
||||
root: ShadowRoot,
|
||||
registry: CustomElementRegistry
|
||||
): CreationScope => {
|
||||
if (scopedRegistrySupport() === "polyfill") {
|
||||
// The polyfill adds importNode to shadow roots
|
||||
return root as unknown as CreationScope;
|
||||
}
|
||||
// Standard API: the registry is passed to importNode
|
||||
return {
|
||||
importNode: (node, deep) =>
|
||||
(
|
||||
document.importNode as (
|
||||
node: Node,
|
||||
options: { selfOnly: boolean; customElementRegistry: unknown }
|
||||
) => Node
|
||||
)(node, { selfOnly: !deep, customElementRegistry: registry }),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { define } from "../src/utils/registration/define";
|
||||
|
||||
// Home Assistant and custom cards can each ship a copy of the library, so a
|
||||
// second registration of the same tag must be ignored instead of throwing.
|
||||
describe("define", () => {
|
||||
it("registers an element that is not registered yet", () => {
|
||||
class First extends HTMLElement {}
|
||||
define("hac-test-first", First);
|
||||
expect(customElements.get("hac-test-first")).toBe(First);
|
||||
});
|
||||
|
||||
it("keeps the first registration of a tag name", () => {
|
||||
class Original extends HTMLElement {}
|
||||
class Duplicate extends HTMLElement {}
|
||||
define("hac-test-duplicate", Original);
|
||||
expect(() => define("hac-test-duplicate", Duplicate)).not.toThrow();
|
||||
expect(customElements.get("hac-test-duplicate")).toBe(Original);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { LitElement } from "lit";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
defineGlobal,
|
||||
scopedVariant,
|
||||
} from "../src/utils/registration/dependencies";
|
||||
|
||||
// Entry points decide where a component's dependencies are registered. These
|
||||
// cases guard that global mode registers them globally and scoped mode never
|
||||
// shares classes with the global registry.
|
||||
class Leaf extends LitElement {}
|
||||
class Parent extends LitElement {
|
||||
static readonly dependencies = [{ tagName: "hac-test-leaf", element: Leaf }];
|
||||
}
|
||||
|
||||
describe("defineGlobal", () => {
|
||||
it("registers the dependencies with the component", () => {
|
||||
defineGlobal("hac-test-parent", Parent);
|
||||
expect(customElements.get("hac-test-parent")).toBe(Parent);
|
||||
expect(customElements.get("hac-test-leaf")).toBe(Leaf);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scopedVariant", () => {
|
||||
it("returns components without dependencies unchanged", () => {
|
||||
expect(scopedVariant(Leaf)).toBe(Leaf);
|
||||
});
|
||||
|
||||
it("returns one subclass per component with dependencies", () => {
|
||||
const variant = scopedVariant(Parent);
|
||||
expect(variant).not.toBe(Parent);
|
||||
expect(variant.prototype).toBeInstanceOf(Parent);
|
||||
expect(scopedVariant(Parent)).toBe(variant);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hasAssignedContent,
|
||||
hasAssignedElements,
|
||||
} from "../src/utils/element/slot";
|
||||
|
||||
// Components hide wrappers of empty slots, e.g. the padding of an empty card.
|
||||
// Whitespace between tags must not count as content.
|
||||
const slotEvent = (light: string, name?: string) => {
|
||||
const host = document.createElement("div");
|
||||
host.innerHTML = light;
|
||||
const slot = document.createElement("slot");
|
||||
if (name) {
|
||||
slot.name = name;
|
||||
}
|
||||
host.attachShadow({ mode: "open" }).append(slot);
|
||||
return { target: slot } as unknown as Event;
|
||||
};
|
||||
|
||||
describe("hasAssignedContent", () => {
|
||||
it("ignores whitespace", () => {
|
||||
expect(hasAssignedContent(slotEvent("\n \n"))).toBe(false);
|
||||
});
|
||||
|
||||
it("counts text", () => {
|
||||
expect(hasAssignedContent(slotEvent(" Hello "))).toBe(true);
|
||||
});
|
||||
|
||||
it("counts elements", () => {
|
||||
expect(hasAssignedContent(slotEvent("\n <p></p>\n"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasAssignedElements", () => {
|
||||
it("only counts elements assigned to the slot", () => {
|
||||
const light = '<span slot="icon"></span> text';
|
||||
expect(hasAssignedElements(slotEvent(light, "icon"))).toBe(true);
|
||||
expect(hasAssignedElements(slotEvent(light, "actions"))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Generated
+186
-6
@@ -490,6 +490,9 @@ importers:
|
||||
'@gfx/zopfli':
|
||||
specifier: 1.0.15
|
||||
version: 1.0.15
|
||||
'@home-assistant/components':
|
||||
specifier: workspace:*
|
||||
version: link:packages/components
|
||||
'@html-eslint/eslint-plugin':
|
||||
specifier: 0.67.0
|
||||
version: 0.67.0([email protected]([email protected]))
|
||||
@@ -507,7 +510,7 @@ importers:
|
||||
version: 1.64.1
|
||||
'@rsdoctor/rspack-plugin':
|
||||
specifier: 1.6.5
|
||||
version: 1.6.5(@emnapi/[email protected])(@emnapi/[email protected])(@rspack/[email protected](@swc/[email protected]))([email protected])
|
||||
version: 1.6.5(@emnapi/[email protected])(@emnapi/[email protected])(@rsbuild/[email protected]([email protected]))(@rspack/[email protected](@swc/[email protected]))([email protected])
|
||||
'@rspack/core':
|
||||
specifier: 2.2.9
|
||||
version: 2.2.9(@swc/[email protected])
|
||||
@@ -713,6 +716,18 @@ importers:
|
||||
specifier: 7.4.1
|
||||
version: 7.4.1(patch_hash=75e6346ad8d8af554c167f77ac7d9b9652f010398efdc6b399925a4795133099)([email protected])
|
||||
|
||||
packages/components:
|
||||
devDependencies:
|
||||
'@rslib/core':
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3([email protected])([email protected])
|
||||
lit:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
typescript:
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
|
||||
packages:
|
||||
|
||||
'@ampproject/[email protected]':
|
||||
@@ -733,6 +748,68 @@ packages:
|
||||
resolution: {integrity: sha512-3+777rEPsOOjoy5/sRmIzHnoRDaLpm3XWY2dmWn/N7f/XwDG0bcpnFaKL3X9FYJUbNwLmpcJC1bUYexJRxsFlw==}
|
||||
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-rMZJ6frSpR/xSbeV4hiA9WR5hHqk3rVYX8lOqEoUCaUnjEuFXvHdGN4WawJ8pVDq/IqM1UnTtsDSYgUdVmO5DA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-tIclZ5pM+YSgk8NNTBoTkaclbS39cU5lbVfSrFWX4LUnxXfUPG+F+ppxyFjKwJTBELsGHEO4LdX3/Lc+FDwPNA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-Bm8F7/oRHQkDXbjH+gBi9oB5huJ0q12r/AIOMIypf5PWlAVvh206Tsw468phw4Oh6kd4Ad1Z+gflNW1MutgK2g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-yr5xcRerV41GeRxmZ8SKbfGeyZRhQePoNNCgTAb4OrWMelWwbMI80aY7/IlF2bo1sN40BSW4hZdGIO/akTd1RQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-lluRK3hxfHMQdFuVID6seAKVx4FH0kEwF6rCh5/I40Sf3EtNR2UCBXAqd6RBA6hqlSckktvBxCfw+h4aescDmg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-09wJQKq86LqILqQtw0+X09SzhcPwUnMVFKyia7S3XLbLqSeQpAMF55Y+zll53JRQ1dCzFEZJyvTT/7j9vwfE3w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-/HiVgw2cdtzXLDLoG4eBBcauekNXewfagy7qiVbjkVu/KcCH8dJ9lpj8DzeWwuBNtZxuD8jpiwP0M1pz3Bc3uw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-/gwJrJQUw65uNtAFcG2rtSTr117Gqqy3bfjaljCPKsa65dTeRcMMnjKLlZ6V9/g8z+4Xf78kdhp5ECbir4yfSw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-CEZAMAsaDoQoVE3MT2c/ry3/ydfyaqlTjxv8DK5PG355SrdAYPgevGC2Me6tz4dhphJin89Y20VsnrS5UrYV5Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
resolution: {integrity: sha512-bjJsUt0g3UCwbn9bjv7MnoR15rszSm79yPCXn4DWWK0NuYUUqBU/srtNMR4WnpfR7TnfBRdYFoqSNx0pGCNWJg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -2850,6 +2927,16 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rsbuild/[email protected]':
|
||||
resolution: {integrity: sha512-UDSDrokgG70tmx+0bz9kuc64SkPQ5A1/Zhqsp8pD0L3Cko/xEnukAbrjouGihUTk55imhC4zx62DnQN/1VJ+GQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
core-js: '>= 3.0.0'
|
||||
peerDependenciesMeta:
|
||||
core-js:
|
||||
optional: true
|
||||
|
||||
'@rsbuild/[email protected]':
|
||||
resolution: {integrity: sha512-26xtEYN0QjZYoyt0lWnvIztBWjEZJvcfw7MN4f5B4SpNggmnF7F7aNPrgkY3EccXVFx1VGQBhnCkBV//OoS07Q==}
|
||||
peerDependencies:
|
||||
@@ -2892,6 +2979,19 @@ packages:
|
||||
'@rsdoctor/[email protected]':
|
||||
resolution: {integrity: sha512-/Ybsk9krXapFwn0LBS7iKqjVBreRbv2siySAKxEhMJiAiHmaWP8vFsgfyaQHK+TnjqQwiNlN/JeOnh8NF7+tyQ==}
|
||||
|
||||
'@rslib/[email protected]':
|
||||
resolution: {integrity: sha512-a5IZiyf9sNeeE9jcTsTmz16vxcThpbOSJg0BZXzJAxrUFOZX5xG25PJua9hr0LDWGryBX2UQTruVsLQ8BCpqng==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@microsoft/api-extractor': ^7
|
||||
typescript: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@microsoft/api-extractor':
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@rspack/[email protected]':
|
||||
resolution: {integrity: sha512-aIHkk/wngpwgWkwVQFz2AIkmG9WpWtWlkqy4BlYhs5a7l9z0BrB0kXnUb6VSbqZ7Tc3cRepKw7ipkGjKa8KKXw==}
|
||||
cpu: [arm64]
|
||||
@@ -6682,6 +6782,19 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wVxMYvWmVPam6SeZsz8OixHm+4JPfLJAAMTD0Q2te2SxksvjuHC3wuvU12teSmloQ3nFX1YC6guNzuYqDYw+vQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@microsoft/api-extractor': ^7
|
||||
'@rsbuild/core': ^2.0.0
|
||||
typescript: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@microsoft/api-extractor':
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-g2wW/ermwzGTLlzGdJBDRc4YKz2/yPBjf57w9g5CvhtpZ91Xq95OaWku0oNHYi+XsuV6v8QrCo2oJJR/pCUR8g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -7864,6 +7977,45 @@ snapshots:
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.5.3
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@ast-grep/[email protected]':
|
||||
optionalDependencies:
|
||||
'@ast-grep/napi-darwin-arm64': 0.45.3
|
||||
'@ast-grep/napi-darwin-x64': 0.45.3
|
||||
'@ast-grep/napi-linux-arm64-gnu': 0.45.3
|
||||
'@ast-grep/napi-linux-arm64-musl': 0.45.3
|
||||
'@ast-grep/napi-linux-x64-gnu': 0.45.3
|
||||
'@ast-grep/napi-linux-x64-musl': 0.45.3
|
||||
'@ast-grep/napi-win32-arm64-msvc': 0.45.3
|
||||
'@ast-grep/napi-win32-ia32-msvc': 0.45.3
|
||||
'@ast-grep/napi-win32-x64-msvc': 0.45.3
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
@@ -10485,19 +10637,30 @@ snapshots:
|
||||
'@rollup/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@rsbuild/[email protected]':
|
||||
'@rsbuild/[email protected]([email protected])':
|
||||
dependencies:
|
||||
'@rspack/core': 2.2.9(@swc/[email protected])
|
||||
'@swc/helpers': 0.5.23
|
||||
optionalDependencies:
|
||||
core-js: 3.50.0
|
||||
transitivePeerDependencies:
|
||||
- '@module-federation/runtime-tools'
|
||||
|
||||
'@rsbuild/[email protected](@rsbuild/[email protected]([email protected]))':
|
||||
dependencies:
|
||||
acorn: 8.18.0
|
||||
browserslist-to-es-version: 1.4.2
|
||||
htmlparser2: 10.0.0
|
||||
picocolors: 1.1.1
|
||||
source-map: 0.7.6
|
||||
optionalDependencies:
|
||||
'@rsbuild/core': 2.2.13([email protected])
|
||||
|
||||
'@rsdoctor/[email protected]': {}
|
||||
|
||||
'@rsdoctor/[email protected](@emnapi/[email protected])(@emnapi/[email protected])(@rspack/[email protected](@swc/[email protected]))([email protected])':
|
||||
'@rsdoctor/[email protected](@emnapi/[email protected])(@emnapi/[email protected])(@rsbuild/[email protected]([email protected]))(@rspack/[email protected](@swc/[email protected]))([email protected])':
|
||||
dependencies:
|
||||
'@rsbuild/plugin-check-syntax': 1.6.1
|
||||
'@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/[email protected]([email protected]))
|
||||
'@rsdoctor/graph': 1.6.5(@rspack/[email protected](@swc/[email protected]))
|
||||
'@rsdoctor/sdk': 1.6.5(@rspack/[email protected](@swc/[email protected]))([email protected])
|
||||
'@rsdoctor/types': 1.6.5(@rspack/[email protected](@swc/[email protected]))
|
||||
@@ -10531,9 +10694,9 @@ snapshots:
|
||||
- '@rspack/core'
|
||||
- webpack
|
||||
|
||||
'@rsdoctor/[email protected](@emnapi/[email protected])(@emnapi/[email protected])(@rspack/[email protected](@swc/[email protected]))([email protected])':
|
||||
'@rsdoctor/[email protected](@emnapi/[email protected])(@emnapi/[email protected])(@rsbuild/[email protected]([email protected]))(@rspack/[email protected](@swc/[email protected]))([email protected])':
|
||||
dependencies:
|
||||
'@rsdoctor/core': 1.6.5(@emnapi/[email protected])(@emnapi/[email protected])(@rspack/[email protected](@swc/[email protected]))([email protected])
|
||||
'@rsdoctor/core': 1.6.5(@emnapi/[email protected])(@emnapi/[email protected])(@rsbuild/[email protected]([email protected]))(@rspack/[email protected](@swc/[email protected]))([email protected])
|
||||
'@rsdoctor/graph': 1.6.5(@rspack/[email protected](@swc/[email protected]))
|
||||
'@rsdoctor/sdk': 1.6.5(@rspack/[email protected](@swc/[email protected]))([email protected])
|
||||
'@rsdoctor/types': 1.6.5(@rspack/[email protected](@swc/[email protected]))
|
||||
@@ -10596,6 +10759,16 @@ snapshots:
|
||||
- '@rspack/core'
|
||||
- webpack
|
||||
|
||||
'@rslib/[email protected]([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@rsbuild/core': 2.2.13([email protected])
|
||||
rsbuild-plugin-dts: 1.0.3(@rsbuild/[email protected]([email protected]))([email protected])
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- '@module-federation/runtime-tools'
|
||||
- core-js
|
||||
|
||||
'@rspack/[email protected]':
|
||||
optional: true
|
||||
|
||||
@@ -14525,6 +14698,13 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected](@rsbuild/[email protected]([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@ast-grep/napi': 0.45.3
|
||||
'@rsbuild/core': 2.2.13([email protected])
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@rspack/[email protected](@swc/[email protected])):
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
packages:
|
||||
- "."
|
||||
- "packages/*"
|
||||
|
||||
savePrefix: ""
|
||||
autoInstallPeers: false
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { mdiCellphone, mdiEarth, mdiLock, mdiMonitor, mdiTune } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "@home-assistant/components/card";
|
||||
import "@home-assistant/components/header";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
@@ -25,6 +27,8 @@ class HaProfileDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _greetingDismissed = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages: PageNavigation[] = [
|
||||
{
|
||||
@@ -71,6 +75,20 @@ class HaProfileDashboard extends LitElement {
|
||||
.header=${this.hass.localize("panel.profile")}
|
||||
>
|
||||
<div class="container">
|
||||
${
|
||||
this._greetingDismissed
|
||||
? nothing
|
||||
: html`<hac-card>
|
||||
<hac-header
|
||||
slot="header"
|
||||
show-close
|
||||
.closeLabel=${this.hass.localize("ui.common.close")}
|
||||
@hac-close=${this._dismissGreeting}
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.greeting_title")}
|
||||
</hac-header>
|
||||
</hac-card>`
|
||||
}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="heading">
|
||||
@@ -145,6 +163,10 @@ class HaProfileDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _dismissGreeting() {
|
||||
this._greetingDismissed = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -158,7 +180,8 @@ class HaProfileDashboard extends LitElement {
|
||||
calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
ha-card,
|
||||
hac-card {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
@@ -11899,6 +11899,7 @@
|
||||
"security": "Security"
|
||||
},
|
||||
"is_owner": "You are an owner.",
|
||||
"greeting_title": "Have a great day!",
|
||||
"user_preferences_header": "Appearance",
|
||||
"user_preferences_description": "Default dashboard and sidebar",
|
||||
"user_preferences_detail": "The following settings are tied to your account and will persist across all sessions and devices.",
|
||||
|
||||
+7
-1
@@ -8,6 +8,8 @@
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
// Resolve workspace packages to their sources, see packages/components
|
||||
"customConditions": ["ha-source"],
|
||||
"resolveJsonModule": true,
|
||||
// Babel handles transpiling and no need for declaration files
|
||||
"noEmit": true,
|
||||
@@ -32,6 +34,10 @@
|
||||
{
|
||||
"name": "ts-lit-plugin",
|
||||
"strict": true,
|
||||
// Follows imports of @home-assistant/components entry points (workspace
|
||||
// packages resolve through node_modules) to the component classes
|
||||
// that declare their tags
|
||||
"maxNodeModuleImportDepth": 2,
|
||||
"rules": {
|
||||
// Custom elements
|
||||
"no-unknown-tag-name": "error",
|
||||
@@ -161,5 +167,5 @@
|
||||
}
|
||||
},
|
||||
// Exclude e2e tests — they have their own tsconfig that adds node types.
|
||||
"exclude": ["test/e2e"]
|
||||
"exclude": ["test/e2e", "packages/*/dist"]
|
||||
}
|
||||
Reference in new issue
Block a user