Compare commits

..
97 changed files with 2049 additions and 1744 deletions

No files matched your search

@@ -16,7 +16,6 @@ runs:
with:
path: |
node_modules
packages/*/node_modules
~/.local/share/pnpm/store
key: >-
${{ inputs.node-modules-cache-key }}-${{ runner.os }}-${{ runner.arch }}-${{
@@ -36,7 +35,6 @@ runs:
with:
path: |
node_modules
packages/*/node_modules
~/.local/share/pnpm/store
key: >-
${{ inputs.node-modules-cache-key }}-${{ runner.os }}-${{ runner.arch }}-${{
-1
View File
@@ -28,7 +28,6 @@ runs:
with:
path: |
node_modules
packages/*/node_modules
~/.local/share/pnpm/store
key: >-
${{ inputs.node-modules-cache-key }}-${{ runner.os }}-${{ runner.arch }}-${{
-35
View File
@@ -68,42 +68,8 @@ 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 "card".
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.replaceAll("*", name) : subpath)}$`,
path.join(
COMPONENTS_PACKAGE_DIR,
name ? target["ha-source"].replaceAll("*", 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");
@@ -465,7 +431,6 @@ const createRspackConfig = ({
"src/resources/polyfills/bigdecimal.ts"
),
}),
...componentsSourceAliases,
"lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
-8
View File
@@ -301,14 +301,6 @@ 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,
+4 -5
View File
@@ -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 \"{.,*,packages/*}/src/**/*.ts\"",
"lint:lit": "lit-analyzer \"{.,*}/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",
@@ -80,7 +80,7 @@
"@swc/helpers": "0.5.23",
"@tsparticles/engine": "4.5.1",
"@tsparticles/preset-links": "4.5.1",
"@versatiles/style": "8.1.1",
"@versatiles/style": "8.0.0",
"@vibrant/color": "4.0.4",
"@vvo/tzdb": "6.202.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -98,7 +98,7 @@
"echarts": "6.1.0",
"echarts-extension-chart2music": "0.1.1",
"element-internals-polyfill": "3.0.2",
"fullcalendar": "7.1.4",
"fullcalendar": "7.1.3",
"fuse.js": "7.5.0",
"hls.js": "1.7.4",
"home-assistant-js-websocket": "9.8.1",
@@ -123,7 +123,7 @@
"sortablejs": "1.15.7",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"temporal-polyfill": "1.0.7",
"temporal-polyfill": "1.0.6",
"tinykeys": "4.0.1",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.4.1",
@@ -147,7 +147,6 @@
"@bundle-stats/plugin-webpack-filter": "4.23.1",
"@eslint/js": "10.0.1",
"@gfx/zopfli": "1.0.15",
"@home-assistant/components": "workspace:*",
"@html-eslint/eslint-plugin": "0.67.0",
"@lokalise/node-api": "17.0.1",
"@octokit/auth-oauth-device": "8.0.5",
-156
View File
@@ -1,156 +0,0 @@
# @home-assistant/components
Home Assistant Components (`hac-*`): framework-agnostic web components built
with [Lit](https://lit.dev).
> Work in progress, not published yet.
## Registration
Every element is imported on its own, in one of two ways:
| Import | Registers in | For |
| --------------------------------------------- | -------------------- | ----------------------------------------------------------- |
| `@home-assistant/components/<element>` | Global registry | Home Assistant itself |
| `@home-assistant/components/<element>/scoped` | Your scoped registry | Custom cards and other code that runs inside Home Assistant |
`<element>` is the tag name without the `hac-` prefix, like `card` for
`hac-card`.
Both use the copy bundled with your code. A scoped copy is isolated from the
one Home Assistant registers globally, so a custom card keeps the version it
was built with.
Global registration skips tag names that are already registered, so the first
registration wins and nothing throws.
### Global
```ts
import "@home-assistant/components/card";
```
### 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
Each component exposes its own `--hac-*` custom properties, documented in its
source, which take precedence. Next come the theme variables of the existing
Home Assistant element it replaces, so current themes keep working: `hac-card`
reads the variables of `ha-card` (`--ha-card-background`,
`--card-background-color`, `--ha-card-border-radius`, …). Last come the
semantic color tokens (`--ha-color-*`, like `--ha-color-surface-default`) and
their light values, so the components also render outside Home Assistant.
The legacy theme variables are a temporary bridge for this experiment, not the
target design. Home Assistant always defines some of them, like
`--card-background-color`, so inside Home Assistant they currently win over
the semantic tokens. Once themes move to the semantic tokens, the legacy
variables will be removed from the fallbacks.
## Structure
Each element has its own directory, named like its import path:
```
src/components/card/ <hac-card> -> @home-assistant/components/card
hac-card.ts class, does not register itself
info.ts tag name and version
global.ts registers globally
scoped.ts element for a scoped registry -> …/card/scoped
```
Shared code lives in `src/utils/`: `registration/` for defining elements
(global and scoped) and `element/` for helpers that components use
themselves, like slot checks.
Every element has its own version in `info.ts`.
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 and custom-elements.json 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. The build then generates a
[custom elements manifest](https://custom-elements-manifest.open-wc.org) from
the JSDoc of each element, so document new slots, parts, CSS properties and
events there.
@@ -1,84 +0,0 @@
// Generates dist/custom-elements.json from the JSDoc of the hac elements, for
// editors and documentation tools. Only hac-*.ts files declare elements; the
// entry points just re-export them.
const forEachDeclaration = (customElementsManifest, callback) => {
for (const module of customElementsManifest.modules) {
for (const declaration of module.declarations ?? []) {
callback(declaration);
}
}
};
// The analyzer leaves the readme empty. Paths in the manifest are relative to
// the package root, like the module paths.
const packageReadme = () => ({
name: "package-readme",
packageLinkPhase({ customElementsManifest }) {
customElementsManifest.readme = "README.md";
},
});
// Elements are documented with @summary, which the analyzer only puts into
// `summary`. Most tools show `description`, so fall back to the summary.
const summaryAsDescription = () => ({
name: "summary-as-description",
packageLinkPhase({ customElementsManifest }) {
forEachDeclaration(customElementsManifest, (declaration) => {
if (!declaration.description && declaration.summary) {
declaration.description = declaration.summary;
}
});
},
});
// Private and protected members are internal, so keep them out of the
// documented API.
const publicApiOnly = () => ({
name: "public-api-only",
packageLinkPhase({ customElementsManifest }) {
forEachDeclaration(customElementsManifest, (declaration) => {
declaration.members = declaration.members?.filter(
(member) => !member.privacy || member.privacy === "public"
);
});
},
});
const distPaths = () => {
const toDist = (value) => value.replace(/^src\/(.+)\.ts$/, "dist/$1.js");
const rewrite = (node) => {
if (Array.isArray(node)) {
node.forEach(rewrite);
return;
}
if (!node || typeof node !== "object") {
return;
}
for (const [key, value] of Object.entries(node)) {
if ((key === "path" || key === "module") && typeof value === "string") {
node[key] = toDist(value);
} else {
rewrite(value);
}
}
};
return {
name: "dist-paths",
packageLinkPhase({ customElementsManifest }) {
rewrite(customElementsManifest.modules);
},
};
};
export default {
globs: ["src/components/**/hac-*.ts"],
outdir: "dist",
litelement: true,
plugins: [
packageReadme(),
summaryAsDescription(),
publicApiOnly(),
distPaths(),
],
};
-60
View File
@@ -1,60 +0,0 @@
{
"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"
],
"customElements": "dist/custom-elements.json",
"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 && cem analyze",
"dev": "rslib build --watch",
"prepublishOnly": "pnpm run build"
},
"peerDependencies": {
"lit": "^3.3.0"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "0.11.0",
"@rslib/core": "1.0.5",
"lit": "3.3.3",
"typescript": "6.0.3"
}
}
-24
View File
@@ -1,24 +0,0 @@
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" } },
},
],
});
@@ -1,8 +0,0 @@
import { define } from "../../utils/registration/define";
import { HacCard } from "./hac-card";
import { TAG_NAME } from "./info";
// Registers <hac-card> in the global registry, as used by Home Assistant
define(TAG_NAME, HacCard);
export { HacCard };
@@ -1,165 +0,0 @@
import { css, html, LitElement } from "lit";
import { property, state } from "lit/decorators.js";
import {
hasAssignedContent,
hasAssignedElements,
} from "../../utils/element/slot";
/**
* @element hac-card
*
* @summary
* Card that groups related content, with an optional header and footer.
*
* @slot header - Header of the card. Its spacing replaces the top padding of
* the content.
* @slot - Content of the card. Its padding is omitted when empty.
* @slot footer - Footer of the card. Hidden when empty.
*
* @csspart header - Wrapper of the header slot.
* @csspart content - Wrapper of the default slot.
* @csspart footer - Wrapper of the footer slot.
*
* @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 and the footer divider.
* @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. `outlined` draws a border without a shadow.
* `raised` hides the border and lifts the card with a shadow instead.
*/
@property({ reflect: true }) public appearance: "outlined" | "raised" =
"outlined";
@state() private _hasHeader = false;
@state() private _hasFooter = false;
@state() private _hasContent = false;
protected render() {
return html`
<div class="header" part="header" ?hidden=${!this._hasHeader}>
<slot name="header" @slotchange=${this._headerChanged}></slot>
</div>
<div
class="content ${this._hasHeader ? "below-header" : ""}"
part="content"
?hidden=${!this._hasContent}
>
<slot @slotchange=${this._contentChanged}></slot>
</div>
<div class="footer" part="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);
}
/*
* Fallback order: --hac-card-*, then the theme variables of ha-card
* (--ha-card-*, --card-background-color, --divider-color,
* --primary-text-color) so existing themes keep working, then the semantic
* tokens and their light values for use outside Home Assistant.
* The ha-card variables are temporary, see "Theming" in the README.
*/
static styles = css`
:host {
display: block;
position: relative;
box-sizing: border-box;
background: var(
--hac-card-background,
var(
--ha-card-background,
var(--card-background-color, var(--ha-color-surface-default, #fff))
)
);
backdrop-filter: var(--ha-card-backdrop-filter, none);
color: var(
--hac-card-color,
var(--primary-text-color, var(--ha-color-on-surface-default, #141414))
);
border: var(--hac-card-border-width, var(--ha-card-border-width, 1px))
solid
var(
--hac-card-border-color,
var(
--ha-card-border-color,
var(--divider-color, var(--ha-color-border-neutral-quiet, #e6e6e6))
)
);
border-radius: var(
--hac-card-border-radius,
var(--ha-card-border-radius, var(--ha-border-radius-lg, 12px))
);
box-shadow: var(--hac-card-box-shadow, var(--ha-card-box-shadow, none));
}
:host([appearance="raised"]) {
border-color: transparent;
box-shadow: var(
--hac-card-box-shadow,
var(
--ha-card-box-shadow,
var(
--ha-box-shadow-s,
0 1px 2px 0 rgba(0, 0, 0, 0.08),
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;
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(
--hac-card-border-color,
var(--divider-color, var(--ha-color-border-neutral-quiet, #e6e6e6))
);
}
.header[hidden],
.content[hidden],
.footer[hidden] {
display: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hac-card": HacCard;
}
}
@@ -1,5 +0,0 @@
/** Tag name of the element. */
export const TAG_NAME = "hac-card";
/** Version of <hac-card>. */
export const VERSION = "0.1.0";
@@ -1,15 +0,0 @@
import type { ElementDefinition } from "../../utils/registration/definition";
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: HacCard,
};
-132
View File
@@ -1,132 +0,0 @@
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;
};
@@ -1,15 +0,0 @@
/** 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())
);
@@ -1,17 +0,0 @@
/**
* 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);
}
};
@@ -1,10 +0,0 @@
/**
* 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;
}
@@ -1,67 +0,0 @@
/**
* 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 }),
};
};
-20
View File
@@ -1,20 +0,0 @@
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);
});
});
-40
View File
@@ -1,40 +0,0 @@
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);
});
});
-20
View File
@@ -1,20 +0,0 @@
{
"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"]
}
+41 -700
View File
File diff suppressed because it is too large. Load diff
-5
View File
@@ -1,7 +1,3 @@
packages:
- "."
- "packages/*"
savePrefix: ""
autoInstallPeers: false
@@ -23,7 +19,6 @@ trustPolicyExclude:
allowBuilds:
"@tsparticles/engine": false
core-js: false
rs-module-lexer: false
unrs-resolver: false
overrides:
+4
View File
@@ -112,6 +112,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
@property({ attribute: "pin-block" })
public pinBlock?: "start" | "center" | "end" | "nearest";
// Skip the "unknown value" highlight and note for a value that is not in the
// list but that the value renderer presents on its own.
@property({ type: Boolean, attribute: "no-unknown-state" })
@@ -347,6 +350,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.searchKeys=${this.searchKeys}
.customValueLabel=${this.customValueLabel}
.noSort=${this.noSort}
.pinBlock=${this.pinBlock}
></ha-picker-combo-box>
`;
}
+62 -18
View File
@@ -1,13 +1,17 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type { TemplateResult } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { customIcons } from "../data/custom_icons";
import type { ValueChangedEvent } from "../types";
import "./ha-combo-box-item";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import "./ha-icon";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@@ -50,16 +54,17 @@ const createIconItem = (icon: IconData, prefix: string): PickerComboBoxItem => {
const loadIcons = async () => {
ICONS_LOADED = true;
const iconList = await import("../../build/mdi/iconList.json");
ICONS = iconList.default.map((icon) => createIconItem(icon, "mdi"));
const sortIcons = (icons: PickerComboBoxItem[]) =>
icons.sort((a, b) => caseInsensitiveStringCompare(a.id, b.id));
const customIconLoads: Promise<PickerComboBoxItem[]>[] = [];
Object.keys(customIcons).forEach((iconSet) => {
customIconLoads.push(loadCustomIconItems(iconSet));
});
(await Promise.all(customIconLoads)).forEach((customIconItems) => {
ICONS.push(...customIconItems);
});
const iconList = await import("../../build/mdi/iconList.json");
const mdiIcons = iconList.default.map((icon) => createIconItem(icon, "mdi"));
ICONS = sortIcons([...mdiIcons]);
const customIconItems = await Promise.all(
Object.keys(customIcons).map(loadCustomIconItems)
);
ICONS = sortIcons([...mdiIcons, ...customIconItems.flat()]);
};
const loadCustomIconItems = async (
@@ -81,8 +86,8 @@ const loadCustomIconItems = async (
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item>
<ha-icon .icon=${item.id} slot="start"></ha-icon>
${item.id}
<ha-icon .icon=${item.icon ?? item.id} slot="start"></ha-icon>
${item.icon ?? item.id}
</ha-combo-box-item>
`;
@@ -104,12 +109,32 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public invalid = false;
private _getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
@property({ attribute: false }) public suggestedIcons?: string[];
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
private _getIconPickerItems = () =>
this._itemsWithSuggestions(ICONS, this.suggestedIcons, this._localize);
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("suggestedIcons") || changedProps.has("_localize")) {
this._picker?.refreshItems();
}
}
protected render(): TemplateResult {
return html`
<ha-generic-picker
allow-custom-value
no-sort
.pinBlock=${
this.value && this.suggestedIcons?.includes(this.value)
? "end"
: undefined
}
.getItems=${this._getIconPickerItems}
.helper=${this.helper}
.disabled=${this.disabled}
@@ -128,15 +153,35 @@ export class HaIconPicker extends LitElement {
`;
}
private _itemsWithSuggestions = memoizeOne(
(
icons: PickerComboBoxItem[],
suggestedIcons: string[] | undefined,
localize: LocalizeFunc
): (PickerComboBoxItem | string)[] => {
if (!suggestedIcons?.length) {
return icons;
}
const suggested = new Set(suggestedIcons);
return [
localize("ui.components.icon_picker.suggested"),
...suggestedIcons.map((icon) => ({ id: icon, primary: icon, icon })),
localize("ui.components.icon_picker.all_icons"),
...icons.filter((item) => !suggested.has(item.id)),
];
}
);
// Filter can take a significant chunk of frame (up to 3-5 ms)
private _filterIcons = memoizeOne(
(
filter: string,
filteredItems: PickerComboBoxItem[],
allItems: PickerComboBoxItem[]
_allItems: PickerComboBoxItem[]
): PickerComboBoxItem[] => {
const normalizedFilter = filter.toLowerCase().replace(/\s+/g, "-");
const iconItems = allItems?.length ? allItems : filteredItems;
// The full list also holds the suggestions and their section titles
const iconItems = ICONS.length ? ICONS : filteredItems;
if (!normalizedFilter.length) {
return iconItems;
@@ -175,8 +220,7 @@ export class HaIconPicker extends LitElement {
protected firstUpdated() {
if (!ICONS_LOADED) {
loadIcons().then(() => {
this._getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
this.requestUpdate();
this._picker?.refreshItems();
});
}
}
+12 -2
View File
@@ -201,6 +201,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
@property({ attribute: "pin-block" })
public pinBlock?: "start" | "center" | "end" | "nearest";
@query(".list") private _list?: HaListBase;
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
@@ -453,6 +456,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this.value
)}
.pinIndex=${this.value ? this._getInitialSelectedIndex() : undefined}
.pinBlock=${this.pinBlock ?? "center"}
@mousedown=${this._preventBlur}
@focus=${this._focusList}
@blur=${this._resetSelectedItem}
@@ -685,6 +689,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
fireEvent(this, "index-selected", { index, item, newTab });
}
// Fuse indexes every row as text when the first one is a section title
private _searchableItems = memoizeOne((items: PickerComboBoxItem[]) =>
items.filter((item) => typeof item !== "string")
);
private _fuseIndex = memoizeOne(
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
@@ -708,10 +717,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return;
}
const index = this._fuseIndex(this._allItems, this.searchKeys);
const searchableItems = this._searchableItems(this._allItems);
const index = this._fuseIndex(searchableItems, this.searchKeys);
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
this._allItems,
searchableItems,
searchString,
(item) => item.id,
index
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["sitkamer", "woonkamer", "leefarea", "gesinskamer"],
"bedroom": ["slaapkamer", "gastekamer", "hoofslaapkamer"],
"kids_room": ["kinderkamer", "speelkamer", "babakamer"],
"kitchen": ["kombuis"],
"dining_room": ["eetkamer"],
"bathroom": ["badkamer", "stort"],
"toilet": ["toilet", "wc", "kleinhuisie"],
"office": ["kantoor", "studeerkamer", "tuiskantoor"],
"laundry_room": ["waskamer", "opwasplek"],
"hallway": ["gang", "portaal", "ingang"],
"stairs": ["trap"],
"garage": ["motorhuis", "garage", "afdak"],
"garden": ["tuin", "erf", "grasperk", "kweekhuis"],
"terrace": ["stoep", "balkon", "terras", "patio", "veranda"],
"basement": ["kelder"],
"attic": ["solder"],
"storage": ["stoorkamer", "pakkamer", "spens"],
"utility_room": ["tegniese kamer"],
"gym": ["gimnasium", "oefenkamer"],
"pool": ["swembad"],
"home_cinema": ["tuisteater"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["غرفة المعيشة", "الصالة", "صالة", "المجلس", "مجلس"],
"bedroom": ["غرفة النوم", "غرفة نوم", "غرفة الضيوف"],
"kids_room": ["غرفة الأطفال", "غرفة الاطفال", "غرفة الألعاب"],
"kitchen": ["المطبخ", "مطبخ"],
"dining_room": ["غرفة الطعام", "غرفة السفرة", "السفرة"],
"bathroom": ["الحمام", "حمام", "دش"],
"toilet": ["المرحاض", "دورة المياه", "wc"],
"office": ["المكتب", "مكتب", "غرفة المكتب"],
"laundry_room": ["غرفة الغسيل"],
"hallway": ["الممر", "ممر", "المدخل", "مدخل", "الردهة"],
"stairs": ["الدرج", "درج", "السلالم"],
"garage": ["المرآب", "الكراج", "كراج"],
"garden": ["الحديقة", "حديقة", "الفناء", "الحوش"],
"terrace": ["الشرفة", "شرفة", "التراس", "البلكونة", "السطح"],
"basement": ["القبو", "قبو", "السرداب"],
"attic": ["العلية", "السندرة"],
"storage": ["المخزن", "مخزن", "غرفة التخزين"],
"utility_room": ["غرفة الغلاية", "الغرفة الفنية"],
"gym": ["صالة الرياضة", "الجيم", "جيم"],
"pool": ["المسبح", "مسبح"],
"home_cinema": ["السينما المنزلية", "سينما منزلية"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["всекидневна", "хол", "дневна"],
"bedroom": ["спалня", "стая за гости"],
"kids_room": ["детска стая", "детска"],
"kitchen": ["кухня"],
"dining_room": ["трапезария"],
"bathroom": ["баня", "душ"],
"toilet": ["тоалетна", "wc"],
"office": ["офис", "кабинет"],
"laundry_room": ["перално помещение"],
"hallway": ["коридор", "антре", "входно антре"],
"stairs": ["стълби", "стълбище"],
"garage": ["гараж"],
"garden": ["градина", "двор", "морава", "оранжерия"],
"terrace": ["тераса", "балкон", "веранда"],
"basement": ["мазе", "сутерен"],
"attic": ["таван", "мансарда"],
"storage": ["килер", "склад", "барака"],
"utility_room": ["котелно", "техническо помещение"],
"gym": ["фитнес"],
"pool": ["басейн"],
"home_cinema": ["домашно кино"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["বসার ঘর", "বৈঠকখানা", "ড্রয়িং রুম"],
"bedroom": ["শোবার ঘর", "শয়নকক্ষ", "বেডরুম", "অতিথি কক্ষ"],
"kids_room": ["বাচ্চাদের ঘর", "শিশুদের ঘর"],
"kitchen": ["রান্নাঘর", "কিচেন"],
"dining_room": ["খাবার ঘর", "ডাইনিং রুম"],
"bathroom": ["স্নানঘর", "বাথরুম", "গোসলখানা"],
"toilet": ["শৌচাগার", "টয়লেট", "wc"],
"office": ["অফিস", "পড়ার ঘর"],
"laundry_room": ["লন্ড্রি"],
"hallway": ["বারান্দা", "করিডোর", "প্রবেশপথ"],
"stairs": ["সিঁড়ি"],
"garage": ["গ্যারেজ"],
"garden": ["বাগান", "উঠান", "লন"],
"terrace": ["ছাদ", "ব্যালকনি"],
"basement": ["বেসমেন্ট", "ভূগর্ভস্থ ঘর"],
"attic": ["চিলেকোঠা"],
"storage": ["ভাঁড়ার", "স্টোর রুম", "গুদাম"],
"utility_room": ["ইউটিলিটি রুম"],
"gym": ["জিম", "ব্যায়ামাগার"],
"pool": ["সুইমিং পুল"],
"home_cinema": ["হোম থিয়েটার"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["dnevni boravak", "dnevna soba"],
"bedroom": ["spavaća soba", "soba za goste"],
"kids_room": ["dječija soba", "igraonica"],
"kitchen": ["kuhinja"],
"dining_room": ["trpezarija"],
"bathroom": ["kupatilo", "tuš"],
"toilet": ["wc", "toalet"],
"office": ["kancelarija", "radna soba"],
"laundry_room": ["vešeraj"],
"hallway": ["hodnik", "predsoblje", "ulaz"],
"stairs": ["stepenice", "stepenište"],
"garage": ["garaža"],
"garden": ["bašta", "vrt", "dvorište", "travnjak", "plastenik"],
"terrace": ["terasa", "balkon"],
"basement": ["podrum"],
"attic": ["tavan", "potkrovlje"],
"storage": ["ostava", "špajz", "šupa"],
"utility_room": ["kotlovnica"],
"gym": ["teretana"],
"pool": ["bazen"],
"home_cinema": ["kućni kino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["sala destar", "saló", "sala"],
"bedroom": ["dormitori", "habitació", "habitació de convidats"],
"kids_room": ["habitació dels nens", "habitació infantil", "sala de jocs"],
"kitchen": ["cuina"],
"dining_room": ["menjador"],
"bathroom": ["bany", "cambra de bany", "dutxa"],
"toilet": ["lavabo", "vàter", "wc"],
"office": ["despatx", "oficina", "estudi"],
"laundry_room": ["safareig", "bugaderia"],
"hallway": ["passadís", "rebedor", "entrada", "vestíbul"],
"stairs": ["escala", "escales"],
"garage": ["garatge"],
"garden": ["jardí", "pati", "gespa", "hivernacle", "hort"],
"terrace": ["terrassa", "balcó", "porxo"],
"basement": ["soterrani", "celler"],
"attic": ["golfes", "àtic"],
"storage": ["traster", "rebost", "magatzem"],
"utility_room": ["sala tècnica", "sala de calderes"],
"gym": ["gimnàs"],
"pool": ["piscina"],
"home_cinema": ["cinema a casa", "sala de cinema"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["obývák", "obývací pokoj", "obývací místnost"],
"bedroom": ["ložnice", "pokoj pro hosty"],
"kids_room": ["dětský pokoj", "herna"],
"kitchen": ["kuchyně", "kuchyňský kout"],
"dining_room": ["jídelna"],
"bathroom": ["koupelna", "sprcha"],
"toilet": ["toaleta", "wc", "záchod"],
"office": ["pracovna", "kancelář"],
"laundry_room": ["prádelna"],
"hallway": ["chodba", "předsíň", "hala", "zádveří"],
"stairs": ["schody", "schodiště"],
"garage": ["garáž"],
"garden": ["zahrada", "trávník", "skleník"],
"terrace": ["terasa", "balkon", "veranda"],
"basement": ["sklep", "suterén"],
"attic": ["půda", "podkroví"],
"storage": ["sklad", "spíž", "komora", "kůlna"],
"utility_room": ["technická místnost", "kotelna"],
"gym": ["posilovna", "fitness"],
"pool": ["bazén"],
"home_cinema": ["domácí kino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["lolfa", "ystafell fyw", "parlwr"],
"bedroom": ["ystafell wely", "llofft"],
"kids_room": ["ystafell y plant", "meithrinfa"],
"kitchen": ["cegin"],
"dining_room": ["ystafell fwyta"],
"bathroom": ["ystafell ymolchi", "cawod"],
"toilet": ["toiled", "tŷ bach", "wc"],
"office": ["swyddfa", "stydi"],
"laundry_room": ["golchdy"],
"hallway": ["cyntedd", "coridor", "mynedfa"],
"stairs": ["grisiau"],
"garage": ["garej", "modurdy"],
"garden": ["gardd", "iard", "lawnt", "tŷ gwydr"],
"terrace": ["teras", "balconi", "patio"],
"basement": ["seler", "islawr"],
"attic": ["atig", "croglofft"],
"storage": ["storfa", "pantri", "sied"],
"utility_room": ["ystafell beiriannau"],
"gym": ["campfa"],
"pool": ["pwll nofio", "pwll"],
"home_cinema": ["sinema cartref"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["stue", "opholdsstue", "dagligstue", "alrum"],
"bedroom": ["soveværelse", "gæsteværelse", "sovekammer"],
"kids_room": ["børneværelse", "legeværelse"],
"kitchen": ["køkken", "køkkenalrum"],
"dining_room": ["spisestue"],
"bathroom": ["badeværelse", "bad", "bruserum"],
"toilet": ["toilet", "wc", "gæstetoilet"],
"office": ["kontor", "arbejdsværelse", "hjemmekontor"],
"laundry_room": ["bryggers", "vaskerum"],
"hallway": ["gang", "entré", "entre", "hall", "gangen"],
"stairs": ["trappe", "trappeopgang"],
"garage": ["garage", "carport"],
"garden": ["have", "græsplæne", "drivhus"],
"terrace": ["terrasse", "altan", "balkon", "veranda"],
"basement": ["kælder"],
"attic": ["loft", "loftsrum"],
"storage": ["depot", "skur", "viktualierum", "opbevaring"],
"utility_room": ["teknikrum", "fyrrum"],
"gym": ["træningsrum", "motionsrum"],
"pool": ["pool", "svømmebassin"],
"home_cinema": ["hjemmebiograf", "biografrum"]
}
+38
View File
@@ -0,0 +1,38 @@
{
"living_room": ["wohnzimmer", "stube", "wohnbereich", "wohnraum"],
"bedroom": ["schlafzimmer", "gästezimmer", "elternschlafzimmer"],
"kids_room": ["kinderzimmer", "babyzimmer", "spielzimmer"],
"kitchen": ["küche", "kochnische", "wohnküche"],
"dining_room": ["esszimmer", "essbereich"],
"bathroom": ["badezimmer", "bad", "dusche", "duschbad"],
"toilet": ["toilette", "wc", "gäste wc", "klo"],
"office": ["büro", "arbeitszimmer", "homeoffice", "home office"],
"laundry_room": ["waschküche", "waschraum", "hauswirtschaftsraum"],
"hallway": [
"flur",
"diele",
"gang",
"eingang",
"eingangsbereich",
"korridor"
],
"stairs": ["treppe", "treppenhaus"],
"garage": ["garage", "carport"],
"garden": ["garten", "rasen", "vorgarten", "gewächshaus"],
"terrace": ["terrasse", "balkon", "veranda", "wintergarten"],
"basement": ["keller", "untergeschoss"],
"attic": ["dachboden", "speicher", "dachgeschoss"],
"storage": [
"abstellraum",
"abstellkammer",
"lager",
"vorratskammer",
"speisekammer",
"schuppen",
"gartenhaus"
],
"utility_room": ["technikraum", "heizungsraum", "heizraum", "serverraum"],
"gym": ["fitnessraum", "fitnessstudio", "sportraum"],
"pool": ["pool", "schwimmbad", "schwimmbecken"],
"home_cinema": ["heimkino", "kinoraum", "fernsehzimmer"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["σαλόνι", "καθιστικό"],
"bedroom": ["υπνοδωμάτιο", "κρεβατοκάμαρα", "ξενώνας"],
"kids_room": ["παιδικό δωμάτιο", "παιδικό"],
"kitchen": ["κουζίνα"],
"dining_room": ["τραπεζαρία"],
"bathroom": ["μπάνιο", "λουτρό", "ντους"],
"toilet": ["τουαλέτα", "wc"],
"office": ["γραφείο"],
"laundry_room": ["πλυσταριό"],
"hallway": ["διάδρομος", "χολ", "είσοδος"],
"stairs": ["σκάλα", "σκάλες", "κλιμακοστάσιο"],
"garage": ["γκαράζ"],
"garden": ["κήπος", "αυλή", "γκαζόν", "θερμοκήπιο"],
"terrace": ["ταράτσα", "βεράντα", "μπαλκόνι"],
"basement": ["υπόγειο", "κελάρι"],
"attic": ["σοφίτα"],
"storage": ["αποθήκη", "αποθηκευτικός χώρος"],
"utility_room": ["λεβητοστάσιο", "μηχανοστάσιο"],
"gym": ["γυμναστήριο"],
"pool": ["πισίνα"],
"home_cinema": ["οικιακός κινηματογράφος"]
}
+70
View File
@@ -0,0 +1,70 @@
{
"living_room": [
"living room",
"lounge",
"family room",
"sitting room",
"den",
"living"
],
"bedroom": [
"bedroom",
"guest room",
"master bedroom",
"master suite",
"bed room"
],
"kids_room": [
"kids room",
"childrens room",
"nursery",
"playroom",
"baby room"
],
"kitchen": ["kitchen", "kitchenette"],
"dining_room": ["dining room", "dining area", "dining"],
"bathroom": ["bathroom", "bath", "shower room", "ensuite", "washroom"],
"toilet": ["toilet", "wc", "restroom", "powder room", "loo", "lavatory"],
"office": ["office", "study", "home office", "workspace", "workroom"],
"laundry_room": ["laundry", "laundry room"],
"hallway": [
"hallway",
"hall",
"corridor",
"entrance",
"entryway",
"foyer",
"mudroom"
],
"stairs": ["stairs", "staircase", "stairway", "stairwell", "landing"],
"garage": ["garage", "carport"],
"garden": ["garden", "yard", "backyard", "front yard", "lawn", "greenhouse"],
"terrace": ["terrace", "patio", "balcony", "deck", "porch", "veranda"],
"basement": ["basement", "cellar"],
"attic": ["attic", "loft"],
"storage": [
"storage",
"storage room",
"pantry",
"closet",
"shed",
"storeroom"
],
"utility_room": [
"utility room",
"boiler room",
"technical room",
"plant room",
"server room"
],
"gym": ["gym", "fitness room", "workout room", "home gym"],
"pool": ["pool", "swimming pool", "pool house"],
"home_cinema": [
"home cinema",
"home theater",
"home theatre",
"cinema room",
"media room",
"tv room"
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["salono", "loĝoĉambro", "loĝejo"],
"bedroom": ["dormoĉambro", "gastoĉambro"],
"kids_room": ["infanĉambro", "ludĉambro"],
"kitchen": ["kuirejo"],
"dining_room": ["manĝoĉambro", "manĝejo"],
"bathroom": ["banĉambro", "banejo", "duŝejo"],
"toilet": ["necesejo", "wc", "tualetejo"],
"office": ["oficejo", "laborĉambro", "studejo"],
"laundry_room": ["lavejo", "lavĉambro"],
"hallway": ["koridoro", "enirejo", "vestiblo", "halo"],
"stairs": ["ŝtuparo"],
"garage": ["garaĝo"],
"garden": ["ĝardeno", "korto", "gazono", "forcejo"],
"terrace": ["teraso", "balkono", "verando"],
"basement": ["kelo"],
"attic": ["subtegmento"],
"storage": ["provizejo", "deponejo", "stokejo"],
"utility_room": ["teknika ĉambro", "hejtejo"],
"gym": ["sportejo", "trejnejo"],
"pool": ["naĝejo"],
"home_cinema": ["hejma kinejo"]
}
+48
View File
@@ -0,0 +1,48 @@
{
"living_room": ["salón", "sala de estar", "sala", "living"],
"bedroom": [
"dormitorio",
"habitación",
"recámara",
"cuarto",
"alcoba",
"habitación de invitados"
],
"kids_room": [
"habitación infantil",
"cuarto de los niños",
"habitación de los niños",
"cuarto del bebé",
"sala de juegos"
],
"kitchen": ["cocina"],
"dining_room": ["comedor"],
"bathroom": ["baño", "cuarto de baño", "ducha"],
"toilet": ["aseo", "inodoro", "wc", "servicio", "retrete"],
"office": ["oficina", "despacho", "estudio"],
"laundry_room": ["lavadero", "lavandería", "cuarto de lavado"],
"hallway": [
"pasillo",
"recibidor",
"entrada",
"vestíbulo",
"hall",
"distribuidor"
],
"stairs": ["escalera", "escaleras"],
"garage": ["garaje", "cochera"],
"garden": ["jardín", "patio trasero", "césped", "invernadero", "huerto"],
"terrace": ["terraza", "balcón", "porche", "patio"],
"basement": ["sótano", "bodega"],
"attic": ["ático", "desván", "buhardilla"],
"storage": ["trastero", "almacén", "despensa", "armario", "cobertizo"],
"utility_room": [
"sala técnica",
"cuarto técnico",
"sala de calderas",
"cuarto de máquinas"
],
"gym": ["gimnasio", "sala de fitness"],
"pool": ["piscina", "alberca"],
"home_cinema": ["cine en casa", "sala de cine", "sala de tv"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["elutuba"],
"bedroom": ["magamistuba", "külalistetuba"],
"kids_room": ["lastetuba", "mängutuba"],
"kitchen": ["köök"],
"dining_room": ["söögituba"],
"bathroom": ["vannituba", "vannitoba", "dušš", "pesuruum"],
"toilet": ["tualett", "wc"],
"office": ["kabinet", "kontor", "tööruum"],
"laundry_room": ["pesuköök"],
"hallway": ["koridor", "esik", "eeskoda"],
"stairs": ["trepp", "trepikoda"],
"garage": ["garaaž"],
"garden": ["aed", "muru", "kasvuhoone"],
"terrace": ["terrass", "rõdu", "veranda"],
"basement": ["kelder"],
"attic": ["pööning"],
"storage": ["panipaik", "sahver", "kuur"],
"utility_room": ["tehnoruum", "katlaruum"],
"gym": ["jõusaal"],
"pool": ["bassein"],
"home_cinema": ["kodukino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["egongela", "saloia"],
"bedroom": ["logela", "gela"],
"kids_room": ["haurren gela", "jolas gela"],
"kitchen": ["sukaldea"],
"dining_room": ["jangela"],
"bathroom": ["bainugela", "dutxa"],
"toilet": ["komuna", "wc", "komun"],
"office": ["bulegoa", "ikasgela"],
"laundry_room": ["garbitegia"],
"hallway": ["korridorea", "sarrera", "ataria"],
"stairs": ["eskailerak", "eskailera"],
"garage": ["garajea"],
"garden": ["lorategia", "baratza", "belardia"],
"terrace": ["terraza", "balkoia"],
"basement": ["sotoa", "upategia"],
"attic": ["ganbara"],
"storage": ["trastelekua", "biltegia", "jakitegia"],
"utility_room": ["gela teknikoa"],
"gym": ["gimnasioa"],
"pool": ["igerilekua"],
"home_cinema": ["etxeko zinema"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["پذیرایی", "نشیمن", "سالن", "اتاق نشیمن"],
"bedroom": ["اتاق خواب", "اتاق مهمان"],
"kids_room": ["اتاق کودک", "اتاق بچه", "اتاق بازی"],
"kitchen": ["آشپزخانه"],
"dining_room": ["ناهارخوری", "اتاق غذاخوری"],
"bathroom": ["حمام", "دوش"],
"toilet": ["دستشویی", "توالت", "سرویس بهداشتی", "wc"],
"office": ["دفتر", "اتاق کار"],
"laundry_room": ["رختشویخانه"],
"hallway": ["راهرو", "ورودی", "هال"],
"stairs": ["پله", "راه پله"],
"garage": ["پارکینگ", "گاراژ"],
"garden": ["باغ", "حیاط", "باغچه", "گلخانه"],
"terrace": ["تراس", "بالکن", "ایوان"],
"basement": ["زیرزمین"],
"attic": ["زیرشیروانی"],
"storage": ["انباری", "انبار"],
"utility_room": ["موتورخانه", "اتاق فنی"],
"gym": ["باشگاه", "اتاق ورزش"],
"pool": ["استخر"],
"home_cinema": ["سینمای خانگی"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["olohuone", "oleskelutila", "tupa"],
"bedroom": ["makuuhuone", "vierashuone"],
"kids_room": ["lastenhuone", "leikkihuone"],
"kitchen": ["keittiö", "keittokomero"],
"dining_room": ["ruokailuhuone", "ruokailutila"],
"bathroom": ["kylpyhuone", "pesuhuone", "suihku"],
"toilet": ["wc", "vessa", "käymälä"],
"office": ["toimisto", "työhuone", "kotitoimisto"],
"laundry_room": ["kodinhoitohuone", "pyykkihuone"],
"hallway": ["eteinen", "käytävä", "aula"],
"stairs": ["portaat", "porraskäytävä"],
"garage": ["autotalli", "autokatos"],
"garden": ["puutarha", "piha", "nurmikko", "kasvihuone"],
"terrace": ["terassi", "parveke", "kuisti", "veranta"],
"basement": ["kellari"],
"attic": ["ullakko", "vintti"],
"storage": ["varasto", "vaatehuone", "ruokakomero", "vaja"],
"utility_room": ["tekninen tila", "lämmönjakohuone", "pannuhuone"],
"gym": ["kuntosali", "kuntohuone"],
"pool": ["uima allas", "allas"],
"home_cinema": ["kotiteatteri", "elokuvahuone"]
}
+53
View File
@@ -0,0 +1,53 @@
{
"living_room": [
"salon",
"séjour",
"salle de séjour",
"pièce de vie",
"living"
],
"bedroom": [
"chambre",
"chambre parentale",
"suite parentale",
"chambre damis"
],
"kids_room": [
"chambre denfant",
"chambre des enfants",
"chambre bébé",
"salle de jeux",
"nurserie"
],
"kitchen": ["cuisine", "kitchenette"],
"dining_room": ["salle à manger", "coin repas"],
"bathroom": ["salle de bain", "salle de bains", "salle deau", "douche"],
"toilet": ["toilettes", "wc", "toilette", "cabinet de toilette"],
"office": ["bureau", "cabinet de travail"],
"laundry_room": ["buanderie", "lingerie"],
"hallway": ["couloir", "entrée", "hall", "vestibule", "dégagement"],
"stairs": ["escalier", "escaliers", "cage descalier", "palier"],
"garage": ["garage", "carport"],
"garden": ["jardin", "pelouse", "potager", "cour", "serre"],
"terrace": ["terrasse", "balcon", "véranda", "patio", "loggia"],
"basement": ["sous sol", "cave"],
"attic": ["grenier", "combles", "mansarde"],
"storage": [
"rangement",
"débarras",
"cellier",
"placard",
"remise",
"abri de jardin",
"cabanon"
],
"utility_room": [
"local technique",
"chaufferie",
"salle technique",
"baie informatique"
],
"gym": ["salle de sport", "salle de fitness", "gym"],
"pool": ["piscine", "pool house"],
"home_cinema": ["home cinéma", "salle de cinéma", "salle télé", "salle tv"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["wenkeamer", "húskeamer"],
"bedroom": ["sliepkeamer", "logearkeamer"],
"kids_room": ["bernkeamer", "boarterskeamer"],
"kitchen": ["koken"],
"dining_room": ["itenskeamer"],
"bathroom": ["badkeamer", "dûs"],
"toilet": ["húske", "wc", "toilet"],
"office": ["kantoar", "wurkkeamer"],
"laundry_room": ["waskeamer", "bykoken"],
"hallway": ["gong", "hal", "yngong"],
"stairs": ["trep"],
"garage": ["garaazje"],
"garden": ["tún", "gers", "kas"],
"terrace": ["terras", "balkon", "veranda"],
"basement": ["kelder"],
"attic": ["souder", "solder"],
"storage": ["berging", "skuorre", "opslach"],
"utility_room": ["technyske romte"],
"gym": ["fitnessromte"],
"pool": ["swimbad"],
"home_cinema": ["thúsbioskoop"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["seomra suí", "seomra teaghlaigh", "parlús"],
"bedroom": ["seomra codlata", "seomra leapa"],
"kids_room": ["seomra na bpáistí", "seomra súgartha"],
"kitchen": ["cistin"],
"dining_room": ["seomra bia"],
"bathroom": ["seomra folctha", "cithfholcadh"],
"toilet": ["leithreas", "wc"],
"office": ["oifig", "seomra staidéir"],
"laundry_room": ["seomra níocháin"],
"hallway": ["halla", "pasáiste", "bealach isteach"],
"stairs": ["staighre"],
"garage": ["garáiste"],
"garden": ["gairdín", "clós", "faiche", "teach gloine"],
"terrace": ["ardán", "balcóin", "póirse"],
"basement": ["íoslach", "siléar"],
"attic": ["áiléar"],
"storage": ["stóras", "seomra stórais", "cúlteach"],
"utility_room": ["seomra fóntais"],
"gym": ["giomnáisiam"],
"pool": ["linn snámha"],
"home_cinema": ["pictiúrlann bhaile"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["sala de estar", "salón", "sala"],
"bedroom": ["dormitorio", "cuarto", "habitación"],
"kids_room": ["cuarto dos nenos", "cuarto infantil"],
"kitchen": ["cociña"],
"dining_room": ["comedor"],
"bathroom": ["baño", "cuarto de baño", "ducha"],
"toilet": ["aseo", "váter", "wc"],
"office": ["oficina", "despacho", "estudo"],
"laundry_room": ["lavandaría"],
"hallway": ["corredor", "entrada", "vestíbulo"],
"stairs": ["escaleira", "escaleiras"],
"garage": ["garaxe"],
"garden": ["xardín", "horta", "invernadoiro"],
"terrace": ["terraza", "balcón"],
"basement": ["soto", "adega"],
"attic": ["faiado"],
"storage": ["trasteiro", "despensa", "almacén"],
"utility_room": ["sala técnica"],
"gym": ["ximnasio"],
"pool": ["piscina"],
"home_cinema": ["cine na casa"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["stube", "wohnzimmer", "wohnstube"],
"bedroom": ["schlafzimmer", "schlofzimmer", "gästezimmer"],
"kids_room": ["chinderzimmer", "kinderzimmer"],
"kitchen": ["chuchi", "küche"],
"dining_room": ["esszimmer", "ässzimmer"],
"bathroom": ["bad", "badzimmer", "dusche"],
"toilet": ["wc", "toilette", "klo"],
"office": ["büro", "arbeitszimmer"],
"laundry_room": ["wöschchuchi", "waschküche"],
"hallway": ["gang", "flur", "iigang", "eingang"],
"stairs": ["stäge", "treppe"],
"garage": ["garage", "carport"],
"garden": ["garte", "garten", "rase", "gwächshuus"],
"terrace": ["terrasse", "balkon", "sitzplatz", "veranda"],
"basement": ["chäller", "keller"],
"attic": ["estrich", "dachstock"],
"storage": ["abstellruum", "abstellraum", "reduit", "lager", "schopf"],
"utility_room": ["heizigsruum", "technikruum"],
"gym": ["fitnessruum", "fitnessraum"],
"pool": ["pool", "schwimmbad"],
"home_cinema": ["heimkino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["סלון", "חדר מגורים", "חדר אורחים"],
"bedroom": ["חדר שינה", "חדר הורים"],
"kids_room": ["חדר ילדים", "חדר משחקים"],
"kitchen": ["מטבח"],
"dining_room": ["פינת אוכל", "חדר אוכל"],
"bathroom": ["חדר רחצה", "אמבטיה", "מקלחת"],
"toilet": ["שירותים", "wc"],
"office": ["משרד", "חדר עבודה"],
"laundry_room": ["חדר כביסה"],
"hallway": ["מסדרון", "כניסה", "פרוזדור"],
"stairs": ["מדרגות", "חדר מדרגות"],
"garage": ["חניה", "מוסך"],
"garden": ["גינה", "חצר", "דשא", "חממה"],
"terrace": ["מרפסת", "פטיו"],
"basement": ["מרתף"],
"attic": ["עליית גג"],
"storage": ["מחסן", "מזווה"],
"utility_room": ["חדר טכני", "חדר שרתים"],
"gym": ["חדר כושר"],
"pool": ["בריכה"],
"home_cinema": ["קולנוע ביתי"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["बैठक", "बैठक कक्ष", "लिविंग रूम", "ड्राइंग रूम"],
"bedroom": ["शयनकक्ष", "बेडरूम", "अतिथि कक्ष"],
"kids_room": ["बच्चों का कमरा", "खेल कक्ष"],
"kitchen": ["रसोई", "रसोईघर", "किचन"],
"dining_room": ["भोजन कक्ष", "डाइनिंग रूम"],
"bathroom": ["स्नानघर", "बाथरूम", "गुसलखाना"],
"toilet": ["शौचालय", "टॉयलेट", "wc"],
"office": ["कार्यालय", "दफ्तर", "ऑफिस", "अध्ययन कक्ष"],
"laundry_room": ["कपड़े धोने का कमरा", "लॉन्ड्री"],
"hallway": ["गलियारा", "प्रवेश", "बरामदा"],
"stairs": ["सीढ़ी", "सीढ़ियाँ"],
"garage": ["गैराज"],
"garden": ["बगीचा", "बाग", "आंगन", "लॉन"],
"terrace": ["छत", "बालकनी", "टैरेस"],
"basement": ["तहखाना", "बेसमेंट"],
"attic": ["अटारी"],
"storage": ["भंडार", "स्टोर रूम", "गोदाम"],
"utility_room": ["यूटिलिटी रूम"],
"gym": ["जिम", "व्यायामशाला"],
"pool": ["स्विमिंग पूल", "तरणताल"],
"home_cinema": ["होम थिएटर"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["dnevni boravak", "dnevna soba"],
"bedroom": ["spavaća soba", "soba za goste"],
"kids_room": ["dječja soba", "igraonica"],
"kitchen": ["kuhinja"],
"dining_room": ["blagovaonica"],
"bathroom": ["kupaonica", "tuš"],
"toilet": ["wc", "zahod", "toalet"],
"office": ["ured", "radna soba"],
"laundry_room": ["praonica"],
"hallway": ["hodnik", "predsoblje", "ulaz"],
"stairs": ["stepenice", "stubište"],
"garage": ["garaža"],
"garden": ["vrt", "dvorište", "travnjak", "staklenik"],
"terrace": ["terasa", "balkon", "veranda"],
"basement": ["podrum"],
"attic": ["tavan", "potkrovlje"],
"storage": ["spremište", "ostava", "šupa"],
"utility_room": ["kotlovnica", "tehnička prostorija"],
"gym": ["teretana"],
"pool": ["bazen"],
"home_cinema": ["kućno kino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["nappali"],
"bedroom": ["hálószoba", "háló", "vendégszoba"],
"kids_room": ["gyerekszoba", "játszószoba"],
"kitchen": ["konyha"],
"dining_room": ["étkező", "ebédlő"],
"bathroom": ["fürdőszoba", "fürdő", "zuhanyzó"],
"toilet": ["wc", "mosdó", "vécé"],
"office": ["iroda", "dolgozószoba"],
"laundry_room": ["mosókonyha"],
"hallway": ["folyosó", "előszoba", "hall", "közlekedő"],
"stairs": ["lépcső", "lépcsőház"],
"garage": ["garázs", "autóbeálló"],
"garden": ["kert", "udvar", "pázsit", "üvegház"],
"terrace": ["terasz", "erkély", "veranda"],
"basement": ["pince", "alagsor"],
"attic": ["padlás", "tetőtér"],
"storage": ["tároló", "kamra", "raktár", "gardrób"],
"utility_room": ["kazánház", "gépészeti helyiség"],
"gym": ["edzőterem", "konditerem"],
"pool": ["medence", "uszoda"],
"home_cinema": ["házimozi", "mozi szoba"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["հյուրասենյակ", "ընդունարան"],
"bedroom": ["ննջասենյակ"],
"kids_room": ["մանկական սենյակ", "մանկական"],
"kitchen": ["խոհանոց"],
"dining_room": ["ճաշասենյակ"],
"bathroom": ["լոգարան", "ցնցուղ"],
"toilet": ["զուգարան", "wc"],
"office": ["աշխատասենյակ", "գրասենյակ"],
"laundry_room": ["լվացքատուն"],
"hallway": ["միջանցք", "նախասենյակ", "մուտք"],
"stairs": ["աստիճաններ", "սանդուղք"],
"garage": ["ավտոտնակ"],
"garden": ["այգի", "բակ", "ջերմոց"],
"terrace": ["տեռաս", "պատշգամբ", "պատշգամբը"],
"basement": ["նկուղ"],
"attic": ["ձեղնահարկ"],
"storage": ["պահեստ", "մառան"],
"utility_room": ["տեխնիկական սենյակ", "կաթսայատուն"],
"gym": ["մարզասրահ"],
"pool": ["լողավազան"],
"home_cinema": ["տնային կինոթատրոն"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["ruang tamu", "ruang keluarga", "ruang tengah"],
"bedroom": ["kamar tidur", "kamar", "kamar tamu"],
"kids_room": ["kamar anak", "ruang bermain"],
"kitchen": ["dapur"],
"dining_room": ["ruang makan"],
"bathroom": ["kamar mandi"],
"toilet": ["toilet", "wc", "kakus"],
"office": ["kantor", "ruang kerja"],
"laundry_room": ["ruang cuci", "tempat cuci"],
"hallway": ["lorong", "koridor", "pintu masuk"],
"stairs": ["tangga"],
"garage": ["garasi", "carport"],
"garden": ["taman", "halaman", "kebun"],
"terrace": ["teras", "balkon", "beranda"],
"basement": ["ruang bawah tanah"],
"attic": ["loteng"],
"storage": ["gudang", "ruang penyimpanan"],
"utility_room": ["ruang teknis"],
"gym": ["ruang olahraga", "gym"],
"pool": ["kolam renang"],
"home_cinema": ["home teater", "ruang bioskop"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["stofa", "setustofa"],
"bedroom": ["svefnherbergi", "gestaherbergi"],
"kids_room": ["barnaherbergi", "leikherbergi"],
"kitchen": ["eldhús"],
"dining_room": ["borðstofa"],
"bathroom": ["baðherbergi", "bað", "sturta"],
"toilet": ["salerni", "klósett", "snyrting"],
"office": ["skrifstofa", "vinnuherbergi"],
"laundry_room": ["þvottahús"],
"hallway": ["gangur", "forstofa", "anddyri"],
"stairs": ["stigi", "stigagangur"],
"garage": ["bílskúr"],
"garden": ["garður", "gróðurhús"],
"terrace": ["verönd", "svalir", "pallur"],
"basement": ["kjallari"],
"attic": ["háaloft", "ris"],
"storage": ["geymsla", "búr"],
"utility_room": ["tæknirými"],
"gym": ["líkamsræktarherbergi"],
"pool": ["sundlaug", "laug"],
"home_cinema": ["heimabíó"]
}
+28
View File
@@ -0,0 +1,28 @@
{
"living_room": ["soggiorno", "salotto", "sala", "living"],
"bedroom": [
"camera da letto",
"camera",
"camera matrimoniale",
"camera degli ospiti"
],
"kids_room": ["cameretta", "camera dei bambini", "stanza dei giochi"],
"kitchen": ["cucina", "angolo cottura"],
"dining_room": ["sala da pranzo", "zona pranzo"],
"bathroom": ["bagno", "doccia", "stanza da bagno"],
"toilet": ["toilette", "wc", "gabinetto", "bagno di servizio"],
"office": ["ufficio", "studio", "studiolo"],
"laundry_room": ["lavanderia"],
"hallway": ["corridoio", "ingresso", "disimpegno", "atrio"],
"stairs": ["scala", "scale", "pianerottolo"],
"garage": ["garage", "box", "autorimessa"],
"garden": ["giardino", "prato", "orto", "serra", "cortile"],
"terrace": ["terrazzo", "terrazza", "balcone", "veranda", "portico"],
"basement": ["cantina", "seminterrato", "scantinato"],
"attic": ["soffitta", "sottotetto", "mansarda"],
"storage": ["ripostiglio", "magazzino", "dispensa", "deposito", "capanno"],
"utility_room": ["locale tecnico", "centrale termica", "locale caldaia"],
"gym": ["palestra"],
"pool": ["piscina"],
"home_cinema": ["home theater", "sala cinema", "sala tv"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["リビング", "居間", "リビングルーム", "茶の間"],
"bedroom": ["寝室", "ベッドルーム", "客間"],
"kids_room": ["子供部屋", "子ども部屋", "キッズルーム", "遊び部屋"],
"kitchen": ["キッチン", "台所"],
"dining_room": ["ダイニング", "食堂"],
"bathroom": ["浴室", "風呂", "お風呂", "バスルーム", "洗面所", "脱衣所"],
"toilet": ["トイレ", "お手洗い", "wc"],
"office": ["書斎", "オフィス", "仕事部屋", "事務所"],
"laundry_room": ["ランドリー", "洗濯室"],
"hallway": ["廊下", "玄関", "ホール"],
"stairs": ["階段"],
"garage": ["ガレージ", "車庫"],
"garden": ["庭", "ガーデン", "菜園", "温室"],
"terrace": ["テラス", "ベランダ", "バルコニー", "ウッドデッキ"],
"basement": ["地下室", "地下"],
"attic": ["屋根裏", "ロフト"],
"storage": ["物置", "納戸", "倉庫", "収納", "パントリー", "クローゼット"],
"utility_room": ["機械室", "ボイラー室", "サーバー室"],
"gym": ["ジム", "トレーニングルーム"],
"pool": ["プール"],
"home_cinema": ["ホームシアター", "シアタールーム"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["მისაღები", "მისაღები ოთახი", "სასტუმრო ოთახი"],
"bedroom": ["საძინებელი"],
"kids_room": ["საბავშვო", "საბავშვო ოთახი"],
"kitchen": ["სამზარეულო"],
"dining_room": ["სასადილო"],
"bathroom": ["აბაზანა", "საშხაპე"],
"toilet": ["ტუალეტი", "საპირფარეშო", "wc"],
"office": ["ოფისი", "კაბინეტი", "სამუშაო ოთახი"],
"laundry_room": ["სამრეცხაო"],
"hallway": ["დერეფანი", "შესასვლელი", "ჰოლი"],
"stairs": ["კიბე"],
"garage": ["ავტოფარეხი", "გარაჟი"],
"garden": ["ბაღი", "ეზო", "სათბური"],
"terrace": ["ტერასა", "აივანი", "ვერანდა"],
"basement": ["სარდაფი"],
"attic": ["სხვენი", "მანსარდა"],
"storage": ["საკუჭნაო", "საწყობი"],
"utility_room": ["ტექნიკური ოთახი", "საქვაბე"],
"gym": ["სპორტდარბაზი"],
"pool": ["აუზი"],
"home_cinema": ["სახლის კინოთეატრი"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["거실"],
"bedroom": ["침실", "안방", "손님방"],
"kids_room": ["아이방", "아기방", "놀이방", "어린이방"],
"kitchen": ["주방", "부엌"],
"dining_room": ["식당", "다이닝룸"],
"bathroom": ["욕실", "샤워실"],
"toilet": ["화장실"],
"office": ["서재", "사무실", "작업실", "공부방"],
"laundry_room": ["세탁실", "다용도실"],
"hallway": ["복도", "현관"],
"stairs": ["계단"],
"garage": ["차고"],
"garden": ["정원", "마당", "온실"],
"terrace": ["테라스", "베란다", "발코니"],
"basement": ["지하실", "지하"],
"attic": ["다락방", "다락"],
"storage": ["창고", "수납실", "드레스룸", "팬트리"],
"utility_room": ["보일러실", "기계실"],
"gym": ["헬스장", "운동실"],
"pool": ["수영장"],
"home_cinema": ["홈시어터"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["wunnzëmmer", "salon", "stuff"],
"bedroom": ["schlofzëmmer", "gaaschtzëmmer"],
"kids_room": ["kannerzëmmer", "spillzëmmer"],
"kitchen": ["kichen"],
"dining_room": ["iesszëmmer"],
"bathroom": ["buedzëmmer", "bued", "dusch"],
"toilet": ["toilette", "wc"],
"office": ["büro", "aarbechtszëmmer"],
"laundry_room": ["wäschkummer", "buanderie"],
"hallway": ["gank", "entrée", "flouer"],
"stairs": ["trap", "trapenhaus"],
"garage": ["garage"],
"garden": ["gaart", "wiss", "gewächshaus"],
"terrace": ["terrass", "balcon", "veranda"],
"basement": ["keller"],
"attic": ["späicher", "speicher"],
"storage": ["depot", "debarras", "späis"],
"utility_room": ["technikraum", "heizungskeller"],
"gym": ["fitnessraum"],
"pool": ["schwämm", "pool"],
"home_cinema": ["heemkino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["svetainė", "gyvenamasis kambarys"],
"bedroom": ["miegamasis", "svečių kambarys"],
"kids_room": ["vaikų kambarys", "žaidimų kambarys"],
"kitchen": ["virtuvė"],
"dining_room": ["valgomasis"],
"bathroom": ["vonios kambarys", "vonia", "dušas"],
"toilet": ["tualetas", "wc"],
"office": ["kabinetas", "biuras", "darbo kambarys"],
"laundry_room": ["skalbykla"],
"hallway": ["koridorius", "prieškambaris"],
"stairs": ["laiptai", "laiptinė"],
"garage": ["garažas"],
"garden": ["sodas", "kiemas", "veja", "šiltnamis"],
"terrace": ["terasa", "balkonas", "veranda"],
"basement": ["rūsys"],
"attic": ["palėpė", "mansarda"],
"storage": ["sandėlis", "sandėliukas"],
"utility_room": ["techninė patalpa", "katilinė"],
"gym": ["sporto salė"],
"pool": ["baseinas"],
"home_cinema": ["namų kinas"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["dzīvojamā istaba", "viesistaba"],
"bedroom": ["guļamistaba"],
"kids_room": ["bērnu istaba", "rotaļu istaba"],
"kitchen": ["virtuve"],
"dining_room": ["ēdamistaba"],
"bathroom": ["vannas istaba", "duša"],
"toilet": ["tualete", "wc"],
"office": ["birojs", "kabinets", "darba istaba"],
"laundry_room": ["veļas telpa"],
"hallway": ["gaitenis", "priekšnams", "koridors"],
"stairs": ["kāpnes"],
"garage": ["garāža"],
"garden": ["dārzs", "zāliens", "siltumnīca"],
"terrace": ["terase", "balkons", "veranda"],
"basement": ["pagrabs"],
"attic": ["bēniņi"],
"storage": ["noliktava", "pieliekamais"],
"utility_room": ["tehniskā telpa", "katlu telpa"],
"gym": ["sporta zāle"],
"pool": ["baseins"],
"home_cinema": ["mājas kinozāle"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["дневна соба", "дневен престој"],
"bedroom": ["спална соба", "соба за гости"],
"kids_room": ["детска соба"],
"kitchen": ["кујна"],
"dining_room": ["трпезарија"],
"bathroom": ["бања", "купатило", "туш"],
"toilet": ["тоалет", "wc"],
"office": ["канцеларија", "работна соба"],
"laundry_room": ["перална"],
"hallway": ["ходник", "предсобје", "влез"],
"stairs": ["скали"],
"garage": ["гаража"],
"garden": ["градина", "двор", "тревник"],
"terrace": ["тераса", "балкон"],
"basement": ["подрум"],
"attic": ["таван"],
"storage": ["остава", "магацин"],
"utility_room": ["котларница"],
"gym": ["теретана"],
"pool": ["базен"],
"home_cinema": ["домашно кино"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["സ്വീകരണമുറി", "ലിവിംഗ് റൂം", "ഹാൾ"],
"bedroom": ["കിടപ്പുമുറി", "ബെഡ്റൂം", "അതിഥി മുറി"],
"kids_room": ["കുട്ടികളുടെ മുറി"],
"kitchen": ["അടുക്കള"],
"dining_room": ["ഊണുമുറി", "ഡൈനിംഗ് റൂം"],
"bathroom": ["കുളിമുറി", "ബാത്ത്റൂം"],
"toilet": ["ശൗചാലയം", "ടോയ്ലറ്റ്", "wc"],
"office": ["ഓഫീസ്", "പഠനമുറി"],
"laundry_room": ["അലക്കുമുറി"],
"hallway": ["ഇടനാഴി", "പ്രവേശന കവാടം"],
"stairs": ["ഗോവണി", "പടികൾ"],
"garage": ["ഗാരേജ്", "കാർ പോർച്ച്"],
"garden": ["പൂന്തോട്ടം", "മുറ്റം", "തോട്ടം"],
"terrace": ["ടെറസ്", "ബാൽക്കണി", "വരാന്ത"],
"basement": ["നിലവറ"],
"attic": ["തട്ടിൻപുറം"],
"storage": ["സ്റ്റോർ റൂം", "സംഭരണമുറി"],
"utility_room": ["യൂട്ടിലിറ്റി റൂം"],
"gym": ["ജിം", "വ്യായാമമുറി"],
"pool": ["നീന്തൽക്കുളം"],
"home_cinema": ["ഹോം തിയേറ്റർ"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["ruang tamu", "ruang keluarga", "bilik tamu"],
"bedroom": ["bilik tidur", "bilik", "bilik tetamu"],
"kids_room": ["bilik kanak kanak", "bilik anak", "bilik permainan"],
"kitchen": ["dapur"],
"dining_room": ["ruang makan", "bilik makan"],
"bathroom": ["bilik mandi"],
"toilet": ["tandas", "toilet", "wc"],
"office": ["pejabat", "bilik kerja", "bilik belajar"],
"laundry_room": ["bilik dobi", "ruang dobi"],
"hallway": ["koridor", "laluan", "pintu masuk", "serambi masuk"],
"stairs": ["tangga"],
"garage": ["garaj", "porch kereta"],
"garden": ["taman", "laman", "halaman", "kebun"],
"terrace": ["teres", "balkoni", "beranda", "anjung"],
"basement": ["bilik bawah tanah", "ruang bawah tanah"],
"attic": ["loteng"],
"storage": ["stor", "bilik stor", "gudang"],
"utility_room": ["bilik utiliti", "bilik teknikal"],
"gym": ["gim", "bilik senaman"],
"pool": ["kolam renang"],
"home_cinema": ["teater rumah", "pawagam rumah"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["stue", "dagligstue", "tv stue"],
"bedroom": ["soverom", "gjesterom"],
"kids_room": ["barnerom", "lekerom"],
"kitchen": ["kjøkken"],
"dining_room": ["spisestue"],
"bathroom": ["bad", "baderom", "dusj"],
"toilet": ["toalett", "wc", "do"],
"office": ["kontor", "hjemmekontor", "arbeidsrom"],
"laundry_room": ["vaskerom", "vaskekjeller"],
"hallway": ["gang", "entré", "gangen", "hall"],
"stairs": ["trapp", "trappegang"],
"garage": ["garasje", "carport"],
"garden": ["hage", "plen", "drivhus"],
"terrace": ["terrasse", "balkong", "veranda", "platting"],
"basement": ["kjeller"],
"attic": ["loft"],
"storage": ["bod", "lager", "spiskammers"],
"utility_room": ["teknisk rom", "fyrrom"],
"gym": ["treningsrom", "gym"],
"pool": ["basseng"],
"home_cinema": ["hjemmekino", "kinorom"]
}
+28
View File
@@ -0,0 +1,28 @@
{
"living_room": ["woonkamer", "zitkamer", "huiskamer", "living"],
"bedroom": ["slaapkamer", "logeerkamer", "ouderslaapkamer"],
"kids_room": ["kinderkamer", "babykamer", "speelkamer"],
"kitchen": ["keuken"],
"dining_room": ["eetkamer", "eethoek"],
"bathroom": ["badkamer", "douche", "doucheruimte"],
"toilet": ["toilet", "wc"],
"office": ["kantoor", "werkkamer", "studeerkamer", "thuiskantoor"],
"laundry_room": ["wasruimte", "washok", "bijkeuken"],
"hallway": ["hal", "gang", "entree", "inkomhal"],
"stairs": ["trap", "trappenhuis", "overloop"],
"garage": ["garage", "carport"],
"garden": ["tuin", "achtertuin", "voortuin", "gazon", "kas"],
"terrace": ["terras", "balkon", "veranda", "serre"],
"basement": ["kelder", "souterrain"],
"attic": ["zolder", "vliering"],
"storage": ["berging", "opslag", "schuur", "tuinhuis", "voorraadkast"],
"utility_room": [
"technische ruimte",
"stookruimte",
"meterkast",
"serverruimte"
],
"gym": ["fitnessruimte", "sportruimte", "gym"],
"pool": ["zwembad", "pool"],
"home_cinema": ["thuisbioscoop", "bioscoopkamer", "tv kamer"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["stove", "daglegstove"],
"bedroom": ["soverom", "gjesterom"],
"kids_room": ["barnerom", "leikerom"],
"kitchen": ["kjøkken"],
"dining_room": ["spisestove"],
"bathroom": ["bad", "baderom", "dusj"],
"toilet": ["toalett", "wc", "do"],
"office": ["kontor", "heimekontor", "arbeidsrom"],
"laundry_room": ["vaskerom"],
"hallway": ["gang", "entré", "hall"],
"stairs": ["trapp"],
"garage": ["garasje"],
"garden": ["hage", "plen", "drivhus"],
"terrace": ["terrasse", "balkong", "veranda"],
"basement": ["kjellar"],
"attic": ["loft"],
"storage": ["bod", "lager"],
"utility_room": ["teknisk rom", "fyrrom"],
"gym": ["treningsrom"],
"pool": ["basseng"],
"home_cinema": ["heimekino"]
}
+28
View File
@@ -0,0 +1,28 @@
{
"living_room": ["salon", "pokój dzienny", "pokój gościnny"],
"bedroom": ["sypialnia"],
"kids_room": [
"pokój dziecięcy",
"pokój dziecka",
"pokój dzieci",
"pokój zabaw"
],
"kitchen": ["kuchnia", "aneks kuchenny"],
"dining_room": ["jadalnia"],
"bathroom": ["łazienka", "prysznic"],
"toilet": ["toaleta", "wc", "ubikacja"],
"office": ["biuro", "gabinet", "pracownia"],
"laundry_room": ["pralnia"],
"hallway": ["korytarz", "przedpokój", "hol", "wiatrołap", "sień"],
"stairs": ["schody", "klatka schodowa"],
"garage": ["garaż", "wiata"],
"garden": ["ogród", "ogródek", "trawnik", "szklarnia"],
"terrace": ["taras", "balkon", "weranda"],
"basement": ["piwnica", "suterena"],
"attic": ["strych", "poddasze"],
"storage": ["spiżarnia", "schowek", "magazyn", "komórka", "garderoba"],
"utility_room": ["kotłownia", "pomieszczenie techniczne", "serwerownia"],
"gym": ["siłownia"],
"pool": ["basen"],
"home_cinema": ["kino domowe", "sala kinowa"]
}
+28
View File
@@ -0,0 +1,28 @@
{
"living_room": ["sala de estar", "sala", "living"],
"bedroom": ["quarto", "suíte", "dormitório", "quarto de hóspedes"],
"kids_room": [
"quarto das crianças",
"quarto infantil",
"quarto do bebê",
"brinquedoteca"
],
"kitchen": ["cozinha"],
"dining_room": ["sala de jantar", "copa"],
"bathroom": ["banheiro", "chuveiro"],
"toilet": ["lavabo", "toalete", "vaso sanitário"],
"office": ["escritório", "home office"],
"laundry_room": ["lavanderia", "área de serviço"],
"hallway": ["corredor", "entrada", "hall", "hall de entrada"],
"stairs": ["escada", "escadas"],
"garage": ["garagem"],
"garden": ["jardim", "quintal", "gramado", "estufa", "horta"],
"terrace": ["terraço", "varanda", "sacada", "pátio", "área gourmet"],
"basement": ["porão", "subsolo"],
"attic": ["sótão"],
"storage": ["depósito", "despensa", "almoxarifado", "quartinho"],
"utility_room": ["casa de máquinas", "sala técnica"],
"gym": ["academia"],
"pool": ["piscina"],
"home_cinema": ["home theater", "sala de cinema", "sala de tv"]
}
+28
View File
@@ -0,0 +1,28 @@
{
"living_room": ["sala de estar", "sala", "salão"],
"bedroom": ["quarto", "quarto de dormir", "suíte", "quarto de hóspedes"],
"kids_room": [
"quarto das crianças",
"quarto infantil",
"quarto do bebé",
"sala de brincar"
],
"kitchen": ["cozinha", "kitchenette"],
"dining_room": ["sala de jantar"],
"bathroom": ["casa de banho", "banheira", "duche"],
"toilet": ["wc", "sanita", "lavabo"],
"office": ["escritório", "gabinete"],
"laundry_room": ["lavandaria"],
"hallway": ["corredor", "entrada", "hall", "vestíbulo"],
"stairs": ["escada", "escadas"],
"garage": ["garagem"],
"garden": ["jardim", "quintal", "relvado", "estufa", "horta"],
"terrace": ["terraço", "varanda", "alpendre", "pátio"],
"basement": ["cave", "cave de arrumos"],
"attic": ["sótão", "águas furtadas"],
"storage": ["arrecadação", "despensa", "armazém", "arrumos"],
"utility_room": ["sala técnica", "casa das máquinas"],
"gym": ["ginásio"],
"pool": ["piscina"],
"home_cinema": ["cinema em casa", "sala de cinema", "sala de tv"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["sufragerie", "living", "cameră de zi"],
"bedroom": ["dormitor", "dormitorul", "cameră de oaspeți"],
"kids_room": ["camera copiilor", "camera copilului", "cameră de joacă"],
"kitchen": ["bucătărie", "bucătăria"],
"dining_room": ["sala de mese", "dining"],
"bathroom": ["baie", "baia", "duș"],
"toilet": ["toaletă", "wc"],
"office": ["birou", "birou acasă"],
"laundry_room": ["spălătorie"],
"hallway": ["hol", "hol de intrare", "coridor", "intrare"],
"stairs": ["scări", "scara"],
"garage": ["garaj"],
"garden": ["grădină", "curte", "gazon", "seră"],
"terrace": ["terasă", "balcon", "verandă"],
"basement": ["subsol", "pivniță"],
"attic": ["pod", "mansardă"],
"storage": ["debara", "cămară", "depozit", "magazie"],
"utility_room": ["centrală termică", "cameră tehnică"],
"gym": ["sală de sport", "sala de fitness"],
"pool": ["piscină"],
"home_cinema": ["cinematograf acasă", "home cinema"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["гостиная", "зал", "общая комната"],
"bedroom": ["спальня", "гостевая"],
"kids_room": ["детская", "игровая"],
"kitchen": ["кухня"],
"dining_room": ["столовая"],
"bathroom": ["ванная", "ванная комната", "душевая", "санузел"],
"toilet": ["туалет", "уборная", "wc"],
"office": ["кабинет", "офис"],
"laundry_room": ["прачечная", "постирочная"],
"hallway": ["прихожая", "коридор", "холл", "тамбур"],
"stairs": ["лестница"],
"garage": ["гараж", "навес"],
"garden": ["сад", "двор", "огород", "газон", "теплица"],
"terrace": ["терраса", "балкон", "веранда", "лоджия"],
"basement": ["подвал", "погреб", "цокольный этаж"],
"attic": ["чердак", "мансарда"],
"storage": ["кладовая", "кладовка", "гардеробная", "сарай"],
"utility_room": ["котельная", "техническое помещение", "серверная"],
"gym": ["спортзал", "тренажерный зал"],
"pool": ["бассейн"],
"home_cinema": ["домашний кинотеатр", "кинозал"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["obývačka", "obývacia izba"],
"bedroom": ["spálňa", "izba pre hostí"],
"kids_room": ["detská izba", "herňa"],
"kitchen": ["kuchyňa"],
"dining_room": ["jedáleň"],
"bathroom": ["kúpeľňa", "sprcha"],
"toilet": ["toaleta", "wc", "záchod"],
"office": ["pracovňa", "kancelária"],
"laundry_room": ["práčovňa"],
"hallway": ["chodba", "predsieň", "hala"],
"stairs": ["schody", "schodisko"],
"garage": ["garáž"],
"garden": ["záhrada", "trávnik", "skleník"],
"terrace": ["terasa", "balkón", "veranda"],
"basement": ["pivnica", "suterén"],
"attic": ["povala", "podkrovie"],
"storage": ["sklad", "špajza", "komora"],
"utility_room": ["technická miestnosť", "kotolňa"],
"gym": ["posilňovňa", "fitness"],
"pool": ["bazén"],
"home_cinema": ["domáce kino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["dnevna soba", "dnevni prostor"],
"bedroom": ["spalnica", "soba za goste"],
"kids_room": ["otroška soba", "igralnica"],
"kitchen": ["kuhinja"],
"dining_room": ["jedilnica"],
"bathroom": ["kopalnica", "tuš"],
"toilet": ["stranišče", "wc"],
"office": ["pisarna", "delovna soba"],
"laundry_room": ["pralnica"],
"hallway": ["hodnik", "predsoba", "vhod"],
"stairs": ["stopnice", "stopnišče"],
"garage": ["garaža"],
"garden": ["vrt", "trata", "rastlinjak"],
"terrace": ["terasa", "balkon", "veranda"],
"basement": ["klet"],
"attic": ["podstrešje"],
"storage": ["shramba", "skladišče", "lopa"],
"utility_room": ["tehnični prostor", "kotlovnica"],
"gym": ["fitnes"],
"pool": ["bazen"],
"home_cinema": ["domači kino"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["dhoma e ndenjes", "sallon", "ndenjja"],
"bedroom": ["dhoma e gjumit", "dhoma e mysafirëve"],
"kids_room": ["dhoma e fëmijëve", "dhoma e lojërave"],
"kitchen": ["kuzhina", "kuzhinë"],
"dining_room": ["dhoma e ngrënies"],
"bathroom": ["banjo", "banja", "dush"],
"toilet": ["tualet", "wc"],
"office": ["zyra", "zyrë", "studio"],
"laundry_room": ["lavanderi"],
"hallway": ["korridor", "hyrje", "holl"],
"stairs": ["shkallë", "shkallët"],
"garage": ["garazh"],
"garden": ["kopsht", "oborr", "serrë"],
"terrace": ["tarracë", "ballkon", "verandë"],
"basement": ["bodrum", "qilar"],
"attic": ["papafingo"],
"storage": ["depo", "magazinë", "qilar ushqimesh"],
"utility_room": ["dhoma teknike"],
"gym": ["palestër"],
"pool": ["pishinë"],
"home_cinema": ["kinema shtëpiake"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["dnevna soba", "dnevni boravak"],
"bedroom": ["spavaća soba", "soba za goste"],
"kids_room": ["dečja soba", "igraonica"],
"kitchen": ["kuhinja"],
"dining_room": ["trpezarija"],
"bathroom": ["kupatilo", "tuš"],
"toilet": ["wc", "toalet"],
"office": ["kancelarija", "radna soba"],
"laundry_room": ["vešernica"],
"hallway": ["hodnik", "predsoblje", "ulaz"],
"stairs": ["stepenice", "stepenište"],
"garage": ["garaža"],
"garden": ["bašta", "dvorište", "travnjak", "staklenik"],
"terrace": ["terasa", "balkon"],
"basement": ["podrum"],
"attic": ["tavan", "potkrovlje"],
"storage": ["ostava", "špajz", "šupa"],
"utility_room": ["kotlarnica"],
"gym": ["teretana"],
"pool": ["bazen"],
"home_cinema": ["kućni bioskop"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["дневна соба", "дневни боравак"],
"bedroom": ["спаваћа соба", "соба за госте"],
"kids_room": ["дечја соба", "играоница"],
"kitchen": ["кухиња"],
"dining_room": ["трпезарија"],
"bathroom": ["купатило", "туш"],
"toilet": ["тоалет", "wc"],
"office": ["канцеларија", "радна соба"],
"laundry_room": ["вешерница"],
"hallway": ["ходник", "предсобље", "улаз"],
"stairs": ["степенице", "степениште"],
"garage": ["гаража"],
"garden": ["башта", "двориште", "травњак", "стакленик"],
"terrace": ["тераса", "балкон"],
"basement": ["подрум"],
"attic": ["таван", "поткровље"],
"storage": ["остава", "шпајз", "шупа"],
"utility_room": ["котларница"],
"gym": ["теретана"],
"pool": ["базен"],
"home_cinema": ["кућни биоскоп"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["vardagsrum", "allrum", "sällskapsrum"],
"bedroom": ["sovrum", "gästrum", "master bedroom"],
"kids_room": ["barnrum", "lekrum"],
"kitchen": ["kök"],
"dining_room": ["matsal", "matrum"],
"bathroom": ["badrum", "dusch", "duschrum"],
"toilet": ["toalett", "wc", "gästtoalett"],
"office": ["kontor", "arbetsrum", "hemmakontor"],
"laundry_room": ["tvättstuga", "tvättrum"],
"hallway": ["hall", "hallen", "korridor", "entré", "tambur"],
"stairs": ["trappa", "trapphus"],
"garage": ["garage", "carport"],
"garden": ["trädgård", "gräsmatta", "växthus", "tomt"],
"terrace": ["terrass", "altan", "balkong", "veranda", "uteplats"],
"basement": ["källare"],
"attic": ["vind", "vindsförråd"],
"storage": ["förråd", "skafferi", "klädkammare", "bod"],
"utility_room": ["pannrum", "teknikrum", "serverrum"],
"gym": ["gym", "träningsrum"],
"pool": ["pool", "simbassäng"],
"home_cinema": ["hemmabio", "biorum", "tv rum"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["வரவேற்பறை", "ஹால்", "லிவிங் ரூம்"],
"bedroom": ["படுக்கையறை", "விருந்தினர் அறை"],
"kids_room": ["குழந்தைகள் அறை"],
"kitchen": ["சமையலறை"],
"dining_room": ["சாப்பாட்டு அறை"],
"bathroom": ["குளியலறை"],
"toilet": ["கழிப்பறை", "wc"],
"office": ["அலுவலகம்", "படிப்பறை"],
"laundry_room": ["சலவை அறை"],
"hallway": ["நடைபாதை", "நுழைவாயில்"],
"stairs": ["படிக்கட்டு"],
"garage": ["கேரேஜ்", "வாகன நிறுத்தம்"],
"garden": ["தோட்டம்", "முற்றம்"],
"terrace": ["மொட்டை மாடி", "பால்கனி"],
"basement": ["அடித்தளம்"],
"attic": ["பரண்"],
"storage": ["சேமிப்பு அறை", "கிடங்கு"],
"utility_room": ["பயன்பாட்டு அறை"],
"gym": ["உடற்பயிற்சி அறை", "ஜிம்"],
"pool": ["நீச்சல் குளம்"],
"home_cinema": ["ஹோம் தியேட்டர்"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["హాల్", "లివింగ్ రూమ్", "కూర్చునే గది"],
"bedroom": ["పడకగది", "బెడ్ రూమ్", "అతిథి గది"],
"kids_room": ["పిల్లల గది"],
"kitchen": ["వంటగది", "కిచెన్"],
"dining_room": ["భోజనాల గది", "డైనింగ్ రూమ్"],
"bathroom": ["స్నానాల గది", "బాత్రూమ్"],
"toilet": ["మరుగుదొడ్డి", "టాయిలెట్", "wc"],
"office": ["ఆఫీసు", "కార్యాలయం", "చదువుకునే గది"],
"laundry_room": ["లాండ్రీ"],
"hallway": ["వరండా", "కారిడార్", "ప్రవేశ ద్వారం"],
"stairs": ["మెట్లు"],
"garage": ["గ్యారేజ్"],
"garden": ["తోట", "పెరడు"],
"terrace": ["డాబా", "బాల్కనీ", "టెర్రస్"],
"basement": ["నేలమాళిగ", "బేస్మెంట్"],
"attic": ["అటక"],
"storage": ["స్టోర్ రూమ్", "గోదాము"],
"utility_room": ["యుటిలిటీ రూమ్"],
"gym": ["వ్యాయామశాల", "జిమ్"],
"pool": ["ఈత కొలను", "స్విమ్మింగ్ పూల్"],
"home_cinema": ["హోమ్ థియేటర్"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["ห้องนั่งเล่น", "ห้องรับแขก"],
"bedroom": ["ห้องนอน", "ห้องนอนแขก"],
"kids_room": ["ห้องเด็ก", "ห้องนอนเด็ก", "ห้องเล่น"],
"kitchen": ["ห้องครัว", "ครัว"],
"dining_room": ["ห้องทานข้าว", "ห้องอาหาร"],
"bathroom": ["ห้องน้ำ", "ห้องอาบน้ำ"],
"toilet": ["ห้องส้วม", "สุขา"],
"office": ["ห้องทำงาน", "ออฟฟิศ"],
"laundry_room": ["ห้องซักผ้า"],
"hallway": ["ทางเดิน", "โถงทางเดิน", "ทางเข้า"],
"stairs": ["บันได"],
"garage": ["โรงรถ", "ที่จอดรถ"],
"garden": ["สวน", "สนามหญ้า", "ลานบ้าน"],
"terrace": ["ระเบียง", "ชาน", "เฉลียง"],
"basement": ["ชั้นใต้ดิน"],
"attic": ["ห้องใต้หลังคา"],
"storage": ["ห้องเก็บของ", "โกดัง"],
"utility_room": ["ห้องเครื่อง"],
"gym": ["ห้องออกกำลังกาย", "ยิม"],
"pool": ["สระว่ายน้ำ", "สระน้ำ"],
"home_cinema": ["โฮมเธียเตอร์", "ห้องดูหนัง"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["oturma odası", "salon"],
"bedroom": ["yatak odası", "misafir odası"],
"kids_room": ["çocuk odası", "oyun odası"],
"kitchen": ["mutfak"],
"dining_room": ["yemek odası"],
"bathroom": ["banyo", "duş"],
"toilet": ["tuvalet", "wc"],
"office": ["ofis", "çalışma odası"],
"laundry_room": ["çamaşır odası"],
"hallway": ["koridor", "hol", "antre", "giriş"],
"stairs": ["merdiven"],
"garage": ["garaj"],
"garden": ["bahçe", "çim", "sera"],
"terrace": ["teras", "balkon", "veranda"],
"basement": ["bodrum", "mahzen"],
"attic": ["çatı katı", "tavan arası"],
"storage": ["depo", "kiler"],
"utility_room": ["kazan dairesi", "teknik oda"],
"gym": ["spor salonu"],
"pool": ["havuz"],
"home_cinema": ["ev sineması"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["вітальня", "зала"],
"bedroom": ["спальня", "гостьова кімната"],
"kids_room": ["дитяча", "дитяча кімната", "ігрова"],
"kitchen": ["кухня"],
"dining_room": ["їдальня"],
"bathroom": ["ванна", "ванна кімната", "душова", "санвузол"],
"toilet": ["туалет", "вбиральня", "wc"],
"office": ["кабінет", "офіс"],
"laundry_room": ["пральня"],
"hallway": ["передпокій", "коридор", "хол", "тамбур"],
"stairs": ["сходи"],
"garage": ["гараж", "навіс"],
"garden": ["сад", "двір", "город", "газон", "теплиця"],
"terrace": ["тераса", "балкон", "веранда", "лоджія"],
"basement": ["підвал", "льох", "цокольний поверх"],
"attic": ["горище", "мансарда"],
"storage": ["комора", "гардеробна", "сарай"],
"utility_room": ["котельня", "технічне приміщення"],
"gym": ["спортзал", "тренажерний зал"],
"pool": ["басейн"],
"home_cinema": ["домашній кінотеатр", "кінозал"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["لاؤنج", "ڈرائنگ روم", "بیٹھک"],
"bedroom": ["سونے کا کمرہ", "بیڈروم", "مہمان خانہ"],
"kids_room": ["بچوں کا کمرہ"],
"kitchen": ["باورچی خانہ", "کچن"],
"dining_room": ["کھانے کا کمرہ", "ڈائننگ روم"],
"bathroom": ["غسل خانہ", "باتھ روم"],
"toilet": ["بیت الخلا", "ٹوائلٹ", "wc"],
"office": ["دفتر", "آفس", "اسٹڈی"],
"laundry_room": ["لانڈری"],
"hallway": ["راہداری", "داخلہ"],
"stairs": ["سیڑھیاں"],
"garage": ["گیراج", "پورچ"],
"garden": ["باغ", "باغیچہ", "صحن", "لان"],
"terrace": ["چھت", "بالکونی", "برآمدہ", "ٹیرس"],
"basement": ["تہ خانہ"],
"attic": ["اٹاری"],
"storage": ["اسٹور", "گودام"],
"utility_room": ["یوٹیلیٹی روم"],
"gym": ["جم"],
"pool": ["سوئمنگ پول", "تالاب"],
"home_cinema": ["ہوم تھیٹر"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["phòng khách"],
"bedroom": ["phòng ngủ", "phòng ngủ khách"],
"kids_room": ["phòng trẻ em", "phòng bé", "phòng con", "phòng chơi"],
"kitchen": ["bếp", "nhà bếp", "phòng bếp"],
"dining_room": ["phòng ăn"],
"bathroom": ["phòng tắm", "nhà tắm"],
"toilet": ["nhà vệ sinh", "toilet", "wc"],
"office": ["văn phòng", "phòng làm việc"],
"laundry_room": ["phòng giặt"],
"hallway": ["hành lang", "sảnh", "lối vào"],
"stairs": ["cầu thang"],
"garage": ["nhà để xe", "gara"],
"garden": ["vườn", "sân", "sân vườn"],
"terrace": ["sân thượng", "ban công", "hiên"],
"basement": ["tầng hầm"],
"attic": ["gác mái", "gác xép"],
"storage": ["kho", "phòng kho"],
"utility_room": ["phòng kỹ thuật"],
"gym": ["phòng tập", "phòng gym"],
"pool": ["hồ bơi", "bể bơi"],
"home_cinema": ["phòng chiếu phim", "rạp tại gia"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["客厅", "起居室"],
"bedroom": ["卧室", "主卧", "次卧", "客房", "睡房"],
"kids_room": ["儿童房", "婴儿房", "游戏室"],
"kitchen": ["厨房"],
"dining_room": ["餐厅", "饭厅"],
"bathroom": ["浴室", "卫生间", "洗手间", "淋浴间"],
"toilet": ["厕所"],
"office": ["书房", "办公室", "工作室"],
"laundry_room": ["洗衣房"],
"hallway": ["走廊", "门厅", "玄关", "过道"],
"stairs": ["楼梯"],
"garage": ["车库"],
"garden": ["花园", "院子", "庭院", "菜园", "温室"],
"terrace": ["露台", "阳台", "天台"],
"basement": ["地下室"],
"attic": ["阁楼"],
"storage": ["储藏室", "储物间", "仓库", "衣帽间"],
"utility_room": ["设备间", "锅炉房", "机房"],
"gym": ["健身房"],
"pool": ["泳池", "游泳池"],
"home_cinema": ["家庭影院", "影音室"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"living_room": ["客廳", "起居室"],
"bedroom": ["臥室", "主臥", "次臥", "客房", "睡房", "房間"],
"kids_room": ["兒童房", "嬰兒房", "遊戲室"],
"kitchen": ["廚房"],
"dining_room": ["餐廳", "飯廳"],
"bathroom": ["浴室", "衛浴", "淋浴間"],
"toilet": ["廁所", "洗手間"],
"office": ["書房", "辦公室", "工作室"],
"laundry_room": ["洗衣房", "洗衣間"],
"hallway": ["走廊", "玄關", "門廳", "走道"],
"stairs": ["樓梯"],
"garage": ["車庫"],
"garden": ["花園", "院子", "庭院", "溫室"],
"terrace": ["露台", "陽台", "天台"],
"basement": ["地下室"],
"attic": ["閣樓"],
"storage": ["儲藏室", "儲物間", "倉庫", "衣帽間"],
"utility_room": ["機房", "鍋爐房", "設備間"],
"gym": ["健身房"],
"pool": ["泳池", "游泳池"],
"home_cinema": ["家庭劇院", "影音室"]
}
+177
View File
@@ -0,0 +1,177 @@
export const ROOM_ICONS = {
living_room: [
"mdi:sofa",
"mdi:sofa-single",
"mdi:television",
"mdi:fireplace",
"mdi:floor-lamp",
],
bedroom: ["mdi:bed", "mdi:bed-king", "mdi:bed-double", "mdi:lamp"],
kids_room: ["mdi:teddy-bear", "mdi:toy-brick", "mdi:bunk-bed", "mdi:cradle"],
kitchen: [
"mdi:stove",
"mdi:fridge",
"mdi:countertop",
"mdi:chef-hat",
"mdi:pot-steam",
"mdi:coffee-maker",
],
dining_room: [
"mdi:silverware-fork-knife",
"mdi:table-chair",
"mdi:table-furniture",
"mdi:glass-wine",
],
bathroom: [
"mdi:shower",
"mdi:bathtub",
"mdi:shower-head",
"mdi:toothbrush",
"mdi:hair-dryer",
],
toilet: ["mdi:toilet", "mdi:paper-roll"],
office: [
"mdi:desk",
"mdi:laptop",
"mdi:monitor",
"mdi:chair-rolling",
"mdi:bookshelf",
],
laundry_room: [
"mdi:washing-machine",
"mdi:tumble-dryer",
"mdi:iron",
"mdi:hanger",
],
hallway: ["mdi:door", "mdi:door-open", "mdi:coat-rack", "mdi:key"],
stairs: ["mdi:stairs", "mdi:stairs-up", "mdi:stairs-down"],
garage: ["mdi:garage", "mdi:garage-variant", "mdi:car", "mdi:tools"],
garden: [
"mdi:flower",
"mdi:tree",
"mdi:grass",
"mdi:sprout",
"mdi:watering-can",
"mdi:greenhouse",
],
terrace: [
"mdi:balcony",
"mdi:grill",
"mdi:umbrella-outline",
"mdi:coach-lamp",
"mdi:patio-heater",
],
basement: [
"mdi:home-floor-negative-1",
"mdi:stairs-down",
"mdi:water-boiler",
],
attic: ["mdi:home-roof", "mdi:package-variant-closed"],
storage: ["mdi:archive", "mdi:package-variant-closed", "mdi:warehouse"],
utility_room: [
"mdi:water-boiler",
"mdi:heat-pump",
"mdi:server-network",
"mdi:flash",
],
gym: ["mdi:dumbbell", "mdi:weight-lifter", "mdi:yoga", "mdi:run"],
pool: ["mdi:pool", "mdi:swim"],
home_cinema: ["mdi:theater", "mdi:projector", "mdi:television"],
} as const satisfies Record<string, readonly string[]>;
export type RoomType = keyof typeof ROOM_ICONS;
type RoomKeywords = Partial<Record<RoomType, string[]>>;
export const ROOM_TYPES = Object.keys(ROOM_ICONS) as RoomType[];
export const isRoomType = (value: string): value is RoomType =>
(ROOM_TYPES as string[]).includes(value);
// Languages written without spaces between words
const NO_WORD_SPACING =
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}]/u;
const normalizeRoomText = (text: string): string =>
text
.normalize("NFD")
.replace(/\p{M}/gu, "")
.toLowerCase()
.replace(/['’]/g, "")
.replace(/[^\p{L}\p{N}]+/gu, " ")
.trim();
interface RoomMatch {
room: RoomType;
start: number;
end: number;
}
const findMatches = (name: string, keywords: RoomKeywords): RoomMatch[] => {
const text = ` ${normalizeRoomText(name)} `;
const candidates: RoomMatch[] = [];
for (const room of ROOM_TYPES) {
for (const keyword of keywords[room] ?? []) {
const phrase = normalizeRoomText(keyword);
if (!phrase) {
continue;
}
const needle = NO_WORD_SPACING.test(phrase) ? phrase : ` ${phrase} `;
const start = text.indexOf(needle);
if (start !== -1) {
candidates.push({ room, start, end: start + needle.length });
}
}
}
// The longest phrase wins, so "guest bedroom" is not also read as "bedroom"
candidates.sort((a, b) => b.end - b.start - (a.end - a.start));
const kept: RoomMatch[] = [];
for (const match of candidates) {
const overlaps = kept.some(
(other) => match.start < other.end - 1 && other.start < match.end - 1
);
if (!overlaps) {
kept.push(match);
}
}
return kept.sort((a, b) => a.start - b.start);
};
const matchRoomTypes = (
names: string[],
keywords: RoomKeywords
): RoomType[] => {
const rooms = new Set<RoomType>();
for (const name of names) {
for (const match of findMatches(name, keywords)) {
rooms.add(match.room);
}
}
return [...rooms];
};
const roomIconSuggestions = (rooms: RoomType[]): string[] => [
...new Set(rooms.flatMap((room) => ROOM_ICONS[room])),
];
const KEYWORDS: Record<string, Promise<RoomKeywords | undefined>> = {};
const loadRoomKeywords = (language: string) => {
KEYWORDS[language] ??= import(`./area_icon_keywords/${language}.json`)
.then((module) => module.default as RoomKeywords)
.catch(() => undefined);
return KEYWORDS[language];
};
export const getAreaIconSuggestions = async (
names: string[],
language: string
): Promise<string[]> => {
const keywords =
(await loadRoomKeywords(language)) ??
(await loadRoomKeywords(language.split("-")[0]));
if (!keywords) {
return [];
}
return roomIconSuggestions(matchRoomTypes(names, keywords));
};
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -29,6 +29,10 @@ import type {
AreaRegistryEntryMutableParams,
} from "../../../data/area/area_registry";
import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
import {
getAreaIconSuggestions,
ROOM_ICONS,
} from "../../../data/area_icon_suggestions";
import {
SENSOR_DEVICE_CLASS_HUMIDITY,
SENSOR_DEVICE_CLASS_TEMPERATURE,
@@ -96,6 +100,12 @@ class DialogAreaDetail
@state() private _submitting = false;
@state() private _suggestedIcons?: string[];
private _iconFollowsName = false;
private _iconSuggestionRequest = 0;
@state() private _open = false;
@state() private _suggestionInclude: MetadataSuggestionInclude = {
@@ -128,6 +138,7 @@ class DialogAreaDetail
this._temperatureEntity = null;
this._humidityEntity = null;
}
this._iconFollowsName = !this._params.entry;
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._currentState());
await this.updateComplete;
@@ -151,6 +162,34 @@ class DialogAreaDetail
return true;
}
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("_name") || changedProps.has("_aliases")) {
this._updateSuggestedIcons();
}
}
private async _updateSuggestedIcons() {
const request = ++this._iconSuggestionRequest;
const icons = await getAreaIconSuggestions(
[this._name ?? "", ...(this._aliases ?? [])],
this.hass.locale.language
);
if (request !== this._iconSuggestionRequest || !this._open) {
return;
}
this._suggestedIcons = icons;
if (this._iconFollowsName) {
const untouched = !this.isDirtyState;
this._icon = icons[0] ?? null;
if (untouched) {
this._initDirtyTracking({ type: "deep" }, this._currentState());
} else {
this._updateDirtyState(this._currentState());
}
}
}
private _entityName(entityId: string | null): string | null {
const stateObj = entityId ? this.hass.states[entityId] : undefined;
return stateObj ? this.hass.formatEntityName(stateObj) : null;
@@ -193,6 +232,7 @@ class DialogAreaDetail
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
.suggestedIcons=${this._suggestedIcons}
></ha-icon-picker>
<ha-floor-picker
@@ -300,6 +340,7 @@ class DialogAreaDetail
this._suggestionInclude = {
...this._suggestionInclude,
name: this._name.trim() === "",
room: !this._icon || this._iconFollowsName,
};
return generateMetadataSuggestionTask<{
@@ -349,6 +390,11 @@ class DialogAreaDetail
this._floor = processed.floor;
}
if (processed.room && (!this._icon || this._iconFollowsName)) {
this._icon = ROOM_ICONS[processed.room][0];
this._iconFollowsName = false;
}
this._updateDirtyState(this._currentState());
}
@@ -464,6 +510,7 @@ class DialogAreaDetail
private _iconChanged(ev) {
this._error = undefined;
this._icon = ev.detail.value;
this._iconFollowsName = false;
this._updateDirtyState(this._currentState());
}
@@ -1,5 +1,7 @@
import { dump } from "js-yaml";
import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task";
import type { RoomType } from "../../../data/area_icon_suggestions";
import { isRoomType, ROOM_TYPES } from "../../../data/area_icon_suggestions";
import type { HomeAssistant } from "../../../types";
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
import {
@@ -14,6 +16,7 @@ export interface MetadataSuggestionResult {
category?: string;
labels?: string[];
floor?: string;
room?: RoomType;
}
export type MetadataSuggestionDomain =
@@ -25,6 +28,7 @@ export interface MetadataSuggestionInclude {
categories?: boolean;
labels?: boolean;
floor?: boolean;
room?: boolean;
}
export const SUGGESTION_INCLUDE_DEFAULT: MetadataSuggestionInclude = {
@@ -86,6 +90,7 @@ export async function generateMetadataSuggestionTask<T>(
// providers reject a select/enum schema with an empty options list.
const includeCategories = categoryOptions.length > 0;
const includeFloor = floorOptions.length > 0;
const includeRoom = Boolean(include.room) && domain === "area";
const structure: AITaskStructure = {
...(include.name && {
@@ -139,6 +144,20 @@ export async function generateMetadataSuggestionTask<T>(
},
},
}),
...(includeRoom && {
room: {
description: `The type of room that best matches the ${domain}`,
required: false,
selector: {
select: {
options: ROOM_TYPES.map((room) => ({
value: room,
label: room.replace(/_/g, " "),
})),
},
},
},
}),
};
const requestedParts = [
@@ -147,6 +166,7 @@ export async function generateMetadataSuggestionTask<T>(
includeCategories ? "a category" : null,
include.labels ? "labels" : null,
includeFloor ? "a floor" : null,
includeRoom ? "a room type" : null,
].filter((entry): entry is string => entry !== null);
const categoryLabels: string[] = [
@@ -233,6 +253,10 @@ export async function processMetadataSuggestion(
const processed: MetadataSuggestionResult = {
name: include.name ? result.data.name : undefined,
description: include.description ? result.data.description : undefined,
room:
include.room && result.data.room && isRoomType(result.data.room)
? result.data.room
: undefined,
};
// Convert category name to ID
@@ -95,7 +95,6 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import { showVoiceAssistantSetupDialog } from "../../../dialogs/voice-assistant-setup/show-voice-assistant-setup-dialog";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
@@ -995,13 +994,7 @@ export class HaConfigDevicePage extends LitElement {
(batteryDomain === "binary_sensor" ||
!Number.isNaN(Number(battery.state)))
? html`
<button
class="battery"
aria-label=${`${this.hass.formatEntityName(
battery
)} ${this.hass.formatEntityState(battery)}`}
@click=${this._showBatteryMoreInfo}
>
<div class="battery">
${
batteryDomain === "sensor"
? this.hass.formatEntityState(battery)
@@ -1011,7 +1004,7 @@ export class HaConfigDevicePage extends LitElement {
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryChargingState}
></ha-battery-icon>
</button>
</div>
`
: ""
}
@@ -1394,15 +1387,6 @@ export class HaConfigDevicePage extends LitElement {
}
}
private _showBatteryMoreInfo() {
const batteryEntity = this._batteryEntity(
this._entities(this.deviceId, this._entityReg, this.hass.devices)
);
if (batteryEntity) {
showMoreInfoDialog(this, { entityId: batteryEntity.entity_id });
}
}
private _computeEntityName(
entity: EntityRegistryEntry,
devices: HomeAssistant["devices"]
@@ -1772,19 +1756,6 @@ export class HaConfigDevicePage extends LitElement {
align-items: center;
display: flex;
white-space: nowrap;
background: none;
border: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
cursor: pointer;
border-radius: var(--ha-border-radius-sm);
}
.battery:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
.column > *:not(:first-child) {
-4
View File
@@ -211,10 +211,6 @@ export const getMyRedirects = (): Redirects => ({
tools_states: {
redirect: "/config/tools/state",
},
tools_action_history: {
component: "logbook",
redirect: "/config/tools/action-history",
},
tools_actions: {
redirect: "/config/tools/action",
},
@@ -2,12 +2,12 @@ import { mdiDelete } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "@home-assistant/components/card";
import memoizeOne from "memoize-one";
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-card";
import "../../components/ha-icon-button";
import type { HaIconButton } from "../../components/ha-icon-button";
import "../../components/ha-settings-row";
@@ -37,79 +37,83 @@ class HaLongLivedTokens extends LitElement {
const accessTokens = this._accessTokens(this.refreshTokens);
return html`
<hac-card>
<h2 slot="header" class="header">
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.header"
)}
</h2>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.description"
<ha-card
.header=${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.header"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.description"
)}
<a
href="https://contributors.home-assistant.io/core/internals/auth-api/#make-authenticated-requests"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.learn_auth_requests"
)}
</a>
${
!accessTokens.length
? html`<p>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.empty_state"
)}
</p>`
: accessTokens.map(
(token) =>
html`<ha-settings-row two-line>
<span slot="heading">${token.client_name}</span>
<div slot="description">
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.created",
{
date: relativeTime(
new Date(token.created_at),
this.hass.locale
),
}
)}
</div>
${
this._hasBeenUsed(token)
? html`<div slot="description">
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.last_used",
{
date: relativeTime(
new Date(token.last_used_at),
this.hass.locale
),
location: token.last_used_ip,
}
)}
</div>`
: nothing
}
<ha-icon-button
.token=${token}
.disabled=${token.is_current}
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDelete}
@click=${this._deleteToken}
></ha-icon-button>
</ha-settings-row>`
)
}
<ha-button slot="footer" @click=${this._createToken}>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.create"
)}
</ha-button>
</hac-card>
<a
href="https://contributors.home-assistant.io/core/internals/auth-api/#make-authenticated-requests"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.learn_auth_requests"
)}
</a>
${
!accessTokens.length
? html`<p>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.empty_state"
)}
</p>`
: accessTokens.map(
(token) =>
html`<ha-settings-row two-line>
<span slot="heading">${token.client_name}</span>
<div slot="description">
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.created",
{
date: relativeTime(
new Date(token.created_at),
this.hass.locale
),
}
)}
</div>
${
this._hasBeenUsed(token)
? html`<div slot="description">
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.last_used",
{
date: relativeTime(
new Date(token.last_used_at),
this.hass.locale
),
location: token.last_used_ip,
}
)}
</div>`
: nothing
}
<ha-icon-button
.token=${token}
.disabled=${token.is_current}
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDelete}
@click=${this._deleteToken}
></ha-icon-button>
</ha-settings-row>`
)
}
</div>
<div class="card-actions">
<ha-button @click=${this._createToken}>
${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.create"
)}
</ha-button>
</div>
</ha-card>
`;
}
@@ -170,9 +174,6 @@ class HaLongLivedTokens extends LitElement {
return [
haStyle,
css`
hac-card::part(footer) {
justify-content: flex-end;
}
ha-settings-row {
padding: 0;
}
@@ -182,18 +183,9 @@ class HaLongLivedTokens extends LitElement {
ha-icon-button {
color: var(--primary-text-color);
}
.header {
margin: 0;
padding: var(--ha-space-5) var(--ha-space-4) var(--ha-space-4);
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
letter-spacing: -0.012em;
/* haStyle truncates h2 to one line, long titles should wrap */
white-space: normal;
overflow: visible;
.card-actions {
display: flex;
justify-content: flex-end;
}
`,
];
+4
View File
@@ -1105,6 +1105,10 @@
"failed_create_floor": "Failed to create floor.",
"unknown": "Unknown floor selected"
},
"icon_picker": {
"suggested": "Suggested",
"all_icons": "All icons"
},
"area-filter": {
"title": "Areas",
"no_areas": "No areas",
+1 -7
View File
@@ -8,8 +8,6 @@
// 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,
@@ -34,10 +32,6 @@
{
"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",
@@ -164,5 +158,5 @@
}
},
// Exclude e2e tests — they have their own tsconfig that adds node types.
"exclude": ["test/e2e", "packages/*/dist"]
"exclude": ["test/e2e"]
}