@droans/ha-components 0.1.0 → 0.2.0-b.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yaml +15 -1
- package/README.md +5 -2
- package/eslint.config.mjs +2 -0
- package/globals.d.ts +11 -0
- package/package.json +5 -2
- package/packages/ha-components/common/util/debounce.ts +31 -0
- package/packages/ha-components/common/util/promise-timeout.ts +35 -0
- package/packages/ha-components/common/util/render-status.ts +8 -0
- package/packages/ha-components/components/hac-control-select-menu.ts +280 -0
- package/packages/ha-components/components/hac-icon.ts +199 -0
- package/packages/{components/marquee-text.ts → ha-components/components/hac-marquee-text.ts} +2 -0
- package/packages/ha-components/components/hac-svg-icon.ts +69 -0
- package/packages/ha-components/data/custom_icons.ts +39 -0
- package/packages/ha-components/data/custom_iconsets.ts +14 -0
- package/packages/ha-components/data/iconsets.ts +107 -0
- package/packages/ha-components/resources/home-assistant-logo-svg.ts +2 -0
- package/packages/ha-components/resources/icon-metadata.ts +5 -0
- package/packages/ha-components/resources/iconMetadata.json +1 -0
- package/packages/ha-components/types.ts +10 -0
- package/packages/utils/decorators.ts +40 -0
- package/tsconfig.json +1 -1
|
@@ -7,12 +7,15 @@ on:
|
|
|
7
7
|
release:
|
|
8
8
|
types:
|
|
9
9
|
- published
|
|
10
|
+
branch:
|
|
11
|
+
- main
|
|
10
12
|
permissions:
|
|
11
13
|
id-token: write
|
|
12
14
|
contents: read
|
|
13
15
|
|
|
14
16
|
jobs:
|
|
15
17
|
publish:
|
|
18
|
+
if: contains(github.event.release.target_commitish, 'main')
|
|
16
19
|
runs-on: ubuntu-latest
|
|
17
20
|
steps:
|
|
18
21
|
- uses: actions/checkout@v4
|
|
@@ -21,4 +24,15 @@ jobs:
|
|
|
21
24
|
with:
|
|
22
25
|
node-version: '24'
|
|
23
26
|
registry-url: 'https://registry.npmjs.org'
|
|
24
|
-
- run: npm publish
|
|
27
|
+
- run: npm publish
|
|
28
|
+
publish-prerelease:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
if: contains(github.event.release.target_commitish, 'dev')
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- uses: actions/setup-node@v4
|
|
35
|
+
with:
|
|
36
|
+
node-version: '24'
|
|
37
|
+
registry-url: 'https://registry.npmjs.org'
|
|
38
|
+
- run: npm publish --tag beta
|
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# ha-components
|
|
2
2
|
|
|
3
|
-
A library of HA components. For reusability,
|
|
3
|
+
A library of HA components. For reusability, the components are only redefined if they have not yet been defined as a custom element.
|
|
4
4
|
|
|
5
5
|
Elements:
|
|
6
|
-
*
|
|
6
|
+
* ha-control-select-menu (`hac-control-select-menu`)
|
|
7
|
+
* ha-icon (`hac-icon`)
|
|
8
|
+
* ha-marquee-text (`hac-marquee-text`)
|
|
9
|
+
* ha-svg-icon (`hac-svg-icon`)
|
package/eslint.config.mjs
CHANGED
package/globals.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@droans/ha-components",
|
|
3
3
|
"description": "Home Assistant Built-In Components re-exported for reuse",
|
|
4
|
-
"repository": "https://github.com/droans/
|
|
4
|
+
"repository": "https://github.com/droans/ha-components.git",
|
|
5
5
|
"author": "Michael Carroll",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.2.0-b.3",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"home-assistant",
|
|
9
9
|
"homeassistant",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@eslint/js": "9.39.1",
|
|
21
|
+
"@material/mwc-select": "0.27.0",
|
|
21
22
|
"@rollup/plugin-json": "^6.0.0",
|
|
22
23
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
23
24
|
"@rollup/plugin-terser": "^0.4.0",
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
"eslint-config-prettier": "10.1.8",
|
|
29
30
|
"eslint-plugin-jest": "29.1.0",
|
|
30
31
|
"eslint-plugin-wc": "^3.0.1",
|
|
32
|
+
"idb-keyval": "6.2.2",
|
|
33
|
+
"memoize-one": "6.0.0",
|
|
31
34
|
"prettier": "^3.6.2",
|
|
32
35
|
"rollup": "^4.53.2",
|
|
33
36
|
"rollup-plugin-commonjs": "^10.1.0",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// From: https://davidwalsh.name/javascript-debounce-function
|
|
2
|
+
|
|
3
|
+
// Returns a function, that, as long as it continues to be invoked, will not
|
|
4
|
+
// be triggered. The function will be called after it stops being called for
|
|
5
|
+
// N milliseconds. If `immediate` is passed, trigger the function on the
|
|
6
|
+
// leading edge and on the trailing.
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
export const debounce = <T extends any[]>(
|
|
10
|
+
func: (...args: T) => void,
|
|
11
|
+
wait: number,
|
|
12
|
+
immediate = false
|
|
13
|
+
) => {
|
|
14
|
+
let timeout: number | undefined;
|
|
15
|
+
const debouncedFunc = (...args: T): void => {
|
|
16
|
+
const later = () => {
|
|
17
|
+
timeout = undefined;
|
|
18
|
+
func(...args);
|
|
19
|
+
};
|
|
20
|
+
const callNow = immediate && !timeout;
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
timeout = window.setTimeout(later, wait);
|
|
23
|
+
if (callNow) {
|
|
24
|
+
func(...args);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
debouncedFunc.cancel = () => {
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
};
|
|
30
|
+
return debouncedFunc;
|
|
31
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
declare global {
|
|
3
|
+
interface ErrorConstructor {
|
|
4
|
+
captureStackTrace(thisArg: any, func: any): void
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class TimeoutError extends Error {
|
|
9
|
+
public timeout: number;
|
|
10
|
+
|
|
11
|
+
constructor(timeout: number, ...params) {
|
|
12
|
+
super(...params);
|
|
13
|
+
|
|
14
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
15
|
+
if (Error.captureStackTrace) {
|
|
16
|
+
Error.captureStackTrace(this, TimeoutError);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.name = "TimeoutError";
|
|
20
|
+
// Custom debugging information
|
|
21
|
+
this.timeout = timeout;
|
|
22
|
+
this.message = `Timed out in ${timeout} ms.`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
|
|
27
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
reject(new TimeoutError(ms));
|
|
30
|
+
}, ms);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Returns a race between our timeout and the passed in promise
|
|
34
|
+
return Promise.race([promise, timeout]);
|
|
35
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
2
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
3
|
+
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
|
4
|
+
import { mdiMenuDown } from "@mdi/js";
|
|
5
|
+
import type { PropertyValues } from "lit";
|
|
6
|
+
import type { CSSResultGroup } from '@material/mwc-base/node_modules/@lit/reactive-element/css-tag';
|
|
7
|
+
import { css } from "@material/mwc-base/node_modules/lit";
|
|
8
|
+
import { html, nothing } from "lit";
|
|
9
|
+
import { property, query } from "lit/decorators";
|
|
10
|
+
import { classMap } from "lit/directives/class-map";
|
|
11
|
+
import { ifDefined } from "lit/directives/if-defined";
|
|
12
|
+
import { debounce } from "../common/util/debounce";
|
|
13
|
+
import { nextRender } from "../common/util/render-status";
|
|
14
|
+
import "./ha-icon";
|
|
15
|
+
import type { HaIcon } from "./hac-icon";
|
|
16
|
+
import "./ha-ripple";
|
|
17
|
+
import "./ha-svg-icon";
|
|
18
|
+
import type { HaSvgIcon } from "./hac-svg-icon";
|
|
19
|
+
import "./ha-menu";
|
|
20
|
+
import { customElementOverride } from "../../utils/decorators.js";
|
|
21
|
+
|
|
22
|
+
@customElementOverride("hac-control-select-menu")
|
|
23
|
+
export class HaControlSelectMenu extends SelectBase {
|
|
24
|
+
@query(".select") protected mdcRoot!: HTMLElement;
|
|
25
|
+
|
|
26
|
+
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
|
27
|
+
|
|
28
|
+
@property({ type: Boolean, attribute: "show-arrow" })
|
|
29
|
+
public showArrow = false;
|
|
30
|
+
|
|
31
|
+
@property({ type: Boolean, attribute: "hide-label" })
|
|
32
|
+
public hideLabel = false;
|
|
33
|
+
|
|
34
|
+
@property() public options;
|
|
35
|
+
|
|
36
|
+
protected updated(changedProps: PropertyValues) {
|
|
37
|
+
super.updated(changedProps);
|
|
38
|
+
if (changedProps.get("options")) {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
40
|
+
this.layoutOptions();
|
|
41
|
+
this.selectByValue(this.value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public override render() {
|
|
46
|
+
const classes = {
|
|
47
|
+
"select-disabled": this.disabled,
|
|
48
|
+
"select-required": this.required,
|
|
49
|
+
"select-invalid": !this.isUiValid,
|
|
50
|
+
"select-no-value": !this.selectedText,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
|
|
54
|
+
const labelAttribute =
|
|
55
|
+
this.label && this.hideLabel ? this.label : undefined;
|
|
56
|
+
|
|
57
|
+
return html`
|
|
58
|
+
<div class="select ${classMap(classes)}">
|
|
59
|
+
<input
|
|
60
|
+
class="formElement"
|
|
61
|
+
.name=${this.name}
|
|
62
|
+
.value=${this.value}
|
|
63
|
+
hidden
|
|
64
|
+
?disabled=${this.disabled}
|
|
65
|
+
?required=${this.required}
|
|
66
|
+
/>
|
|
67
|
+
<!-- @ts-ignore -->
|
|
68
|
+
<div
|
|
69
|
+
class="select-anchor"
|
|
70
|
+
aria-autocomplete="none"
|
|
71
|
+
role="combobox"
|
|
72
|
+
aria-expanded=${this.menuOpen}
|
|
73
|
+
aria-invalid=${!this.isUiValid}
|
|
74
|
+
aria-haspopup="listbox"
|
|
75
|
+
aria-labelledby=${ifDefined(labelledby)}
|
|
76
|
+
aria-label=${ifDefined(labelAttribute)}
|
|
77
|
+
aria-required=${this.required}
|
|
78
|
+
aria-controls="listbox"
|
|
79
|
+
@focus=${this.onFocus}
|
|
80
|
+
@blur=${this.onBlur}
|
|
81
|
+
@click=${this.onClick}
|
|
82
|
+
@keydown=${this.onKeydown}
|
|
83
|
+
>
|
|
84
|
+
${this._renderIcon()}
|
|
85
|
+
<div class="content">
|
|
86
|
+
${this.hideLabel
|
|
87
|
+
? nothing
|
|
88
|
+
: html`<p id="label" class="label">${this.label}</p>`}
|
|
89
|
+
${this.selectedText
|
|
90
|
+
? html`<p class="value">${this.selectedText}</p>`
|
|
91
|
+
: nothing}
|
|
92
|
+
</div>
|
|
93
|
+
${this._renderArrow()}
|
|
94
|
+
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
|
95
|
+
</div>
|
|
96
|
+
${this.renderMenu()}
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected override renderMenu() {
|
|
102
|
+
const classes = this.getMenuClasses();
|
|
103
|
+
return html`<ha-menu
|
|
104
|
+
innerRole="listbox"
|
|
105
|
+
wrapFocus
|
|
106
|
+
class=${classMap(classes)}
|
|
107
|
+
activatable
|
|
108
|
+
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
|
|
109
|
+
.open=${this.menuOpen}
|
|
110
|
+
.anchor=${this.anchorElement}
|
|
111
|
+
.fixed=${this.fixedMenuPosition}
|
|
112
|
+
@selected=${this.onSelected}
|
|
113
|
+
@opened=${this.onOpened}
|
|
114
|
+
@closed=${this.onClosed}
|
|
115
|
+
@items-updated=${this.onItemsUpdated}
|
|
116
|
+
@keydown=${this.handleTypeahead}
|
|
117
|
+
>
|
|
118
|
+
${this.renderMenuContent()}
|
|
119
|
+
</ha-menu>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private _renderArrow() {
|
|
123
|
+
if (!this.showArrow) return nothing;
|
|
124
|
+
|
|
125
|
+
return html`
|
|
126
|
+
<div class="icon">
|
|
127
|
+
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
|
128
|
+
</div>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _renderIcon() {
|
|
133
|
+
const index = this.mdcFoundation?.getSelectedIndex();
|
|
134
|
+
const items = this.menuElement?.items ?? [];
|
|
135
|
+
const item = index != null ? items[index] : undefined;
|
|
136
|
+
const defaultIcon = this.querySelector("[slot='icon']");
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
138
|
+
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
|
|
139
|
+
| HaSvgIcon
|
|
140
|
+
| HaIcon
|
|
141
|
+
| null;
|
|
142
|
+
|
|
143
|
+
if (!defaultIcon && !icon) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return html`
|
|
148
|
+
<div class="icon">
|
|
149
|
+
${icon && icon.localName === "ha-svg-icon" && "path" in icon
|
|
150
|
+
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
|
|
151
|
+
: icon && icon.localName === "ha-icon" && "icon" in icon
|
|
152
|
+
? html`<ha-icon .path=${icon.icon}></ha-icon>`
|
|
153
|
+
: html`<slot name="icon"></slot>`}
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
connectedCallback() {
|
|
159
|
+
super.connectedCallback();
|
|
160
|
+
window.addEventListener("translations-updated", this._translationsUpdated);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
disconnectedCallback() {
|
|
164
|
+
super.disconnectedCallback();
|
|
165
|
+
window.removeEventListener(
|
|
166
|
+
"translations-updated",
|
|
167
|
+
this._translationsUpdated
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private _translationsUpdated = debounce(async () => {
|
|
172
|
+
await nextRender();
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
174
|
+
this.layoutOptions();
|
|
175
|
+
}, 500);
|
|
176
|
+
|
|
177
|
+
static override styles: CSSResultGroup =
|
|
178
|
+
css`
|
|
179
|
+
:host {
|
|
180
|
+
display: inline-block;
|
|
181
|
+
--control-select-menu-focus-color: var(--secondary-text-color);
|
|
182
|
+
--control-select-menu-text-color: var(--primary-text-color);
|
|
183
|
+
--control-select-menu-background-color: var(--disabled-color);
|
|
184
|
+
--control-select-menu-background-opacity: 0.2;
|
|
185
|
+
--control-select-menu-border-radius: var(--ha-border-radius-lg);
|
|
186
|
+
--control-select-menu-height: 48px;
|
|
187
|
+
--control-select-menu-padding: 6px 10px;
|
|
188
|
+
--mdc-icon-size: 20px;
|
|
189
|
+
--ha-ripple-color: var(--secondary-text-color);
|
|
190
|
+
font-size: var(--ha-font-size-m);
|
|
191
|
+
line-height: 1.4;
|
|
192
|
+
width: auto;
|
|
193
|
+
color: var(--primary-text-color);
|
|
194
|
+
-webkit-tap-highlight-color: transparent;
|
|
195
|
+
}
|
|
196
|
+
.select-anchor {
|
|
197
|
+
height: var(--control-select-menu-height);
|
|
198
|
+
padding: var(--control-select-menu-padding);
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
position: relative;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: row;
|
|
204
|
+
align-items: center;
|
|
205
|
+
border-radius: var(--control-select-menu-border-radius);
|
|
206
|
+
box-sizing: border-box;
|
|
207
|
+
outline: none;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
background: none;
|
|
210
|
+
/* For safari border-radius overflow */
|
|
211
|
+
z-index: 0;
|
|
212
|
+
transition:
|
|
213
|
+
box-shadow 180ms ease-in-out,
|
|
214
|
+
color 180ms ease-in-out;
|
|
215
|
+
gap: 10px;
|
|
216
|
+
width: 100%;
|
|
217
|
+
user-select: none;
|
|
218
|
+
font-style: normal;
|
|
219
|
+
font-weight: var(--ha-font-weight-normal);
|
|
220
|
+
letter-spacing: 0.25px;
|
|
221
|
+
}
|
|
222
|
+
.content {
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: column;
|
|
225
|
+
align-items: flex-start;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
flex: 1;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.content p {
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
white-space: nowrap;
|
|
234
|
+
text-overflow: ellipsis;
|
|
235
|
+
min-width: 0;
|
|
236
|
+
width: 100%;
|
|
237
|
+
margin: auto;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.label {
|
|
241
|
+
font-size: 0.85em;
|
|
242
|
+
letter-spacing: 0.4px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.select-no-value .label {
|
|
246
|
+
font-size: inherit;
|
|
247
|
+
line-height: inherit;
|
|
248
|
+
letter-spacing: inherit;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.select-anchor:focus-visible {
|
|
252
|
+
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.select-anchor::before {
|
|
256
|
+
content: "";
|
|
257
|
+
position: absolute;
|
|
258
|
+
top: 0;
|
|
259
|
+
left: 0;
|
|
260
|
+
height: 100%;
|
|
261
|
+
width: 100%;
|
|
262
|
+
background-color: var(--control-select-menu-background-color);
|
|
263
|
+
transition:
|
|
264
|
+
background-color 180ms ease-in-out,
|
|
265
|
+
opacity 180ms ease-in-out;
|
|
266
|
+
opacity: var(--control-select-menu-background-opacity);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.select-disabled .select-anchor {
|
|
270
|
+
cursor: not-allowed;
|
|
271
|
+
color: var(--disabled-color);
|
|
272
|
+
}
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
declare global {
|
|
277
|
+
interface HTMLElementTagNameMap {
|
|
278
|
+
"ha-control-select-menu": HaControlSelectMenu;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
3
|
+
import type { PropertyValues } from "lit";
|
|
4
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
5
|
+
import { property, state } from "lit/decorators";
|
|
6
|
+
import { debounce } from "../common/util/debounce";
|
|
7
|
+
import type { CustomIcon } from "../data/custom_icons";
|
|
8
|
+
import { customIcons } from "../data/custom_icons";
|
|
9
|
+
import type { Chunks, Icons } from "../data/iconsets";
|
|
10
|
+
import {
|
|
11
|
+
MDI_PREFIXES,
|
|
12
|
+
findIconChunk,
|
|
13
|
+
getIcon,
|
|
14
|
+
writeCache,
|
|
15
|
+
} from "../data/iconsets";
|
|
16
|
+
import "./ha-svg-icon";
|
|
17
|
+
import { customElementOverride } from "../../utils/decorators.js";
|
|
18
|
+
|
|
19
|
+
type DeprecatedIcon = Record<
|
|
20
|
+
string,
|
|
21
|
+
{
|
|
22
|
+
removeIn: string;
|
|
23
|
+
newName?: string;
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
const mdiDeprecatedIcons: DeprecatedIcon = {};
|
|
28
|
+
|
|
29
|
+
const chunks: Chunks = {};
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
32
|
+
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
|
33
|
+
|
|
34
|
+
const cachedIcons: Record<string, string> = {};
|
|
35
|
+
|
|
36
|
+
@customElementOverride("hac-icon")
|
|
37
|
+
export class HaIcon extends LitElement {
|
|
38
|
+
@property() public icon?: string;
|
|
39
|
+
|
|
40
|
+
@state() private _path?: string;
|
|
41
|
+
|
|
42
|
+
@state() private _secondaryPath?: string;
|
|
43
|
+
|
|
44
|
+
@state() private _viewBox?: string;
|
|
45
|
+
|
|
46
|
+
@state() private _legacy = false;
|
|
47
|
+
|
|
48
|
+
public willUpdate(changedProps: PropertyValues) {
|
|
49
|
+
super.willUpdate(changedProps);
|
|
50
|
+
if (changedProps.has("icon")) {
|
|
51
|
+
this._path = undefined;
|
|
52
|
+
this._secondaryPath = undefined;
|
|
53
|
+
this._viewBox = undefined;
|
|
54
|
+
this._loadIcon();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected render() {
|
|
59
|
+
if (!this.icon) {
|
|
60
|
+
return nothing;
|
|
61
|
+
}
|
|
62
|
+
if (this._legacy) {
|
|
63
|
+
return html`<!-- @ts-ignore we don't provide the iron-icon element -->
|
|
64
|
+
<iron-icon .icon=${this.icon}></iron-icon>`;
|
|
65
|
+
}
|
|
66
|
+
return html`<ha-svg-icon
|
|
67
|
+
.path=${this._path}
|
|
68
|
+
.secondaryPath=${this._secondaryPath}
|
|
69
|
+
.viewBox=${this._viewBox}
|
|
70
|
+
></ha-svg-icon>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async _loadIcon() {
|
|
74
|
+
if (!this.icon) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const requestedIcon = this.icon;
|
|
78
|
+
const [iconPrefix, origIconName] = this.icon.split(":", 2);
|
|
79
|
+
|
|
80
|
+
let iconName = origIconName;
|
|
81
|
+
|
|
82
|
+
if (!iconPrefix || !iconName) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!MDI_PREFIXES.includes(iconPrefix)) {
|
|
87
|
+
const customIcon = customIcons[iconPrefix];
|
|
88
|
+
if (customIcon) {
|
|
89
|
+
if (customIcon && typeof customIcon.getIcon === "function") {
|
|
90
|
+
this._setCustomPath(customIcon.getIcon(iconName), requestedIcon);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this._legacy = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this._legacy = false;
|
|
99
|
+
|
|
100
|
+
if (iconName in mdiDeprecatedIcons) {
|
|
101
|
+
const deprecatedIcon = mdiDeprecatedIcons[iconName];
|
|
102
|
+
let message: string;
|
|
103
|
+
|
|
104
|
+
if (deprecatedIcon.newName) {
|
|
105
|
+
message = `Icon ${iconPrefix}:${iconName} was renamed to ${iconPrefix}:${deprecatedIcon.newName}, please change your config, it will be removed in version ${deprecatedIcon.removeIn}.`;
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
107
|
+
iconName = deprecatedIcon.newName!;
|
|
108
|
+
} else {
|
|
109
|
+
message = `Icon ${iconPrefix}:${iconName} was removed from MDI, please replace this icon with an other icon in your config, it will be removed in version ${deprecatedIcon.removeIn}.`;
|
|
110
|
+
}
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.warn(message);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (iconName in cachedIcons) {
|
|
116
|
+
this._path = cachedIcons[iconName];
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (iconName === "home-assistant") {
|
|
121
|
+
const icon = (await import("../resources/home-assistant-logo-svg"))
|
|
122
|
+
.mdiHomeAssistant;
|
|
123
|
+
|
|
124
|
+
if (this.icon === requestedIcon) {
|
|
125
|
+
this._path = icon;
|
|
126
|
+
}
|
|
127
|
+
cachedIcons[iconName] = icon;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let databaseIcon: string | undefined;
|
|
132
|
+
try {
|
|
133
|
+
databaseIcon = await getIcon(iconName);
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
135
|
+
} catch (_err) {
|
|
136
|
+
// Firefox in private mode doesn't support IDB
|
|
137
|
+
// iOS Safari sometimes doesn't open the DB
|
|
138
|
+
databaseIcon = undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (databaseIcon) {
|
|
142
|
+
if (this.icon === requestedIcon) {
|
|
143
|
+
this._path = databaseIcon;
|
|
144
|
+
}
|
|
145
|
+
cachedIcons[iconName] = databaseIcon;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const chunk = findIconChunk(iconName);
|
|
149
|
+
|
|
150
|
+
if (chunk in chunks) {
|
|
151
|
+
this._setPath(chunks[chunk], iconName, requestedIcon);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const iconPromise = fetch(`/static/mdi/${chunk}.json`).then((response) =>
|
|
156
|
+
response.json()
|
|
157
|
+
);
|
|
158
|
+
chunks[chunk] = iconPromise;
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
160
|
+
this._setPath(iconPromise, iconName, requestedIcon);
|
|
161
|
+
debouncedWriteCache();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async _setCustomPath(
|
|
165
|
+
promise: Promise<CustomIcon>,
|
|
166
|
+
requestedIcon: string
|
|
167
|
+
) {
|
|
168
|
+
const icon = await promise;
|
|
169
|
+
if (this.icon !== requestedIcon) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this._path = icon.path;
|
|
173
|
+
this._secondaryPath = icon.secondaryPath;
|
|
174
|
+
this._viewBox = icon.viewBox;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async _setPath(
|
|
178
|
+
promise: Promise<Icons>,
|
|
179
|
+
iconName: string,
|
|
180
|
+
requestedIcon: string
|
|
181
|
+
) {
|
|
182
|
+
const iconPack = await promise;
|
|
183
|
+
if (this.icon === requestedIcon) {
|
|
184
|
+
this._path = iconPack[iconName];
|
|
185
|
+
}
|
|
186
|
+
cachedIcons[iconName] = iconPack[iconName];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static styles = css`
|
|
190
|
+
:host {
|
|
191
|
+
fill: currentcolor;
|
|
192
|
+
}
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
declare global {
|
|
196
|
+
interface HTMLElementTagNameMap {
|
|
197
|
+
"ha-icon": HaIcon;
|
|
198
|
+
}
|
|
199
|
+
}
|
package/packages/{components/marquee-text.ts → ha-components/components/hac-marquee-text.ts}
RENAMED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
type PropertyValues,
|
|
7
7
|
} from "lit";
|
|
8
8
|
import { property, query } from "lit/decorators.js";
|
|
9
|
+
import { customElementOverride } from "../../utils/decorators.js";
|
|
9
10
|
|
|
11
|
+
@customElementOverride("hac-marquee-text")
|
|
10
12
|
export class MarqueeText extends LitElement {
|
|
11
13
|
@property({ type: Number }) speed = 15; // pixels per second
|
|
12
14
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SVGTemplateResult } from "lit";
|
|
2
|
+
import { css, LitElement, nothing, svg } from "lit";
|
|
3
|
+
import { customElement, property } from "lit/decorators";
|
|
4
|
+
|
|
5
|
+
@customElement("ha-svg-icon")
|
|
6
|
+
export class HaSvgIcon extends LitElement {
|
|
7
|
+
@property() public path?: string;
|
|
8
|
+
|
|
9
|
+
@property({ attribute: false }) public secondaryPath?: string;
|
|
10
|
+
|
|
11
|
+
@property({ attribute: false }) public viewBox?: string;
|
|
12
|
+
|
|
13
|
+
protected render(): SVGTemplateResult {
|
|
14
|
+
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
|
15
|
+
return svg`
|
|
16
|
+
<svg
|
|
17
|
+
viewBox=${this.viewBox || "0 0 24 24"}
|
|
18
|
+
preserveAspectRatio="xMidYMid meet"
|
|
19
|
+
focusable="false"
|
|
20
|
+
role="img"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
>
|
|
23
|
+
<g>
|
|
24
|
+
${
|
|
25
|
+
this.path
|
|
26
|
+
? svg`<path class="primary-path" d=${this.path}></path>`
|
|
27
|
+
: nothing
|
|
28
|
+
}
|
|
29
|
+
${
|
|
30
|
+
this.secondaryPath
|
|
31
|
+
? svg`<path class="secondary-path" d=${this.secondaryPath}></path>`
|
|
32
|
+
: nothing
|
|
33
|
+
}
|
|
34
|
+
</g>
|
|
35
|
+
</svg>`;
|
|
36
|
+
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static styles = css`
|
|
40
|
+
:host {
|
|
41
|
+
display: var(--ha-icon-display, inline-flex);
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
position: relative;
|
|
45
|
+
vertical-align: middle;
|
|
46
|
+
fill: var(--icon-primary-color, currentcolor);
|
|
47
|
+
width: var(--mdc-icon-size, 24px);
|
|
48
|
+
height: var(--mdc-icon-size, 24px);
|
|
49
|
+
}
|
|
50
|
+
svg {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
display: block;
|
|
55
|
+
}
|
|
56
|
+
path.primary-path {
|
|
57
|
+
opacity: var(--icon-primary-opactity, 1);
|
|
58
|
+
}
|
|
59
|
+
path.secondary-path {
|
|
60
|
+
fill: var(--icon-secondary-color, currentcolor);
|
|
61
|
+
opacity: var(--icon-secondary-opactity, 0.5);
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
declare global {
|
|
66
|
+
interface HTMLElementTagNameMap {
|
|
67
|
+
"ha-svg-icon": HaSvgIcon;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { customIconsets } from "./custom_iconsets";
|
|
3
|
+
|
|
4
|
+
export interface CustomIcon {
|
|
5
|
+
path: string;
|
|
6
|
+
secondaryPath?: string;
|
|
7
|
+
viewBox?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CustomIconListItem {
|
|
11
|
+
name: string;
|
|
12
|
+
keywords?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CustomIconHelpers {
|
|
16
|
+
getIcon: (name: string) => Promise<CustomIcon>;
|
|
17
|
+
getIconList?: () => Promise<CustomIconListItem[]>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CustomIconsWindow {
|
|
21
|
+
customIcons?: Record<string, CustomIconHelpers>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const customIconsWindow = window as CustomIconsWindow;
|
|
25
|
+
|
|
26
|
+
if (!("customIcons" in customIconsWindow)) {
|
|
27
|
+
customIconsWindow.customIcons = {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Proxy for backward compatibility with icon sets
|
|
31
|
+
export const customIcons = new Proxy(customIconsWindow.customIcons!, {
|
|
32
|
+
get: (obj, prop: string) =>
|
|
33
|
+
obj[prop] ??
|
|
34
|
+
(customIconsets[prop]
|
|
35
|
+
? {
|
|
36
|
+
getIcon: customIconsets[prop],
|
|
37
|
+
}
|
|
38
|
+
: undefined),
|
|
39
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CustomIcon } from "./custom_icons";
|
|
2
|
+
|
|
3
|
+
interface CustomIconsetsWindow {
|
|
4
|
+
customIconsets?: Record<string, (name: string) => Promise<CustomIcon>>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const customIconsetsWindow = window as CustomIconsetsWindow;
|
|
8
|
+
|
|
9
|
+
if (!("customIconsets" in customIconsetsWindow)) {
|
|
10
|
+
customIconsetsWindow.customIconsets = {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
14
|
+
export const customIconsets = customIconsetsWindow.customIconsets!;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
__SUPERVISOR__: boolean;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
import { clear, get, set, createStore, promisifyRequest } from "idb-keyval";
|
|
12
|
+
import memoizeOne from "memoize-one";
|
|
13
|
+
import { promiseTimeout } from "../common/util/promise-timeout";
|
|
14
|
+
import { iconMetadata } from "../resources/icon-metadata";
|
|
15
|
+
import type { IconMeta } from "../types";
|
|
16
|
+
|
|
17
|
+
export type Icons = Record<string, string>;
|
|
18
|
+
|
|
19
|
+
export type Chunks = Record<string, Promise<Icons>>;
|
|
20
|
+
|
|
21
|
+
const getStore = memoizeOne(async () => {
|
|
22
|
+
const iconStore = createStore("hass-icon-db", "mdi-icon-store");
|
|
23
|
+
|
|
24
|
+
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
|
|
25
|
+
if (!__SUPERVISOR__) {
|
|
26
|
+
const version = await get("_version", iconStore);
|
|
27
|
+
|
|
28
|
+
if (!version) {
|
|
29
|
+
set("_version", iconMetadata.version, iconStore);
|
|
30
|
+
} else if (version !== iconMetadata.version) {
|
|
31
|
+
await clear(iconStore);
|
|
32
|
+
set("_version", iconMetadata.version, iconStore);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return iconStore;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"];
|
|
40
|
+
|
|
41
|
+
let toRead: [
|
|
42
|
+
string,
|
|
43
|
+
(iconPath: string | undefined) => void,
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
(e: any) => void,
|
|
46
|
+
][] = [];
|
|
47
|
+
|
|
48
|
+
// Queue up as many icon fetches in 1 transaction
|
|
49
|
+
export const getIcon = (iconName: string) =>
|
|
50
|
+
new Promise<string | undefined>((resolve, reject) => {
|
|
51
|
+
toRead.push([iconName, resolve, reject]);
|
|
52
|
+
|
|
53
|
+
if (toRead.length > 1) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Start initializing the store, so it's ready when we need it
|
|
58
|
+
const iconStoreProm = getStore();
|
|
59
|
+
const readIcons = async () => {
|
|
60
|
+
const iconStore = await iconStoreProm;
|
|
61
|
+
iconStore("readonly", (store) => {
|
|
62
|
+
for (const [iconName_, resolve_, reject_] of toRead) {
|
|
63
|
+
promisifyRequest<string | undefined>(store.get(iconName_))
|
|
64
|
+
.then((icon) => resolve_(icon))
|
|
65
|
+
.catch((e) => reject_(e));
|
|
66
|
+
}
|
|
67
|
+
toRead = [];
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
promiseTimeout(1000, readIcons()).catch((e) => {
|
|
72
|
+
// Firefox in private mode doesn't support IDB
|
|
73
|
+
// Safari sometime doesn't open the DB so we time out
|
|
74
|
+
for (const [, , reject_] of toRead) {
|
|
75
|
+
reject_(e);
|
|
76
|
+
}
|
|
77
|
+
toRead = [];
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const findIconChunk = (icon: string): string => {
|
|
82
|
+
let lastChunk: IconMeta;
|
|
83
|
+
for (const chunk of iconMetadata.parts) {
|
|
84
|
+
if (chunk.start !== undefined && icon < chunk.start) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
lastChunk = chunk;
|
|
88
|
+
}
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
90
|
+
return lastChunk!.file;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const writeCache = async (chunks: Chunks) => {
|
|
94
|
+
const keys = Object.keys(chunks);
|
|
95
|
+
const iconsSets: Icons[] = await Promise.all(Object.values(chunks));
|
|
96
|
+
const iconStore = await getStore();
|
|
97
|
+
// We do a batch opening the store just once, for (considerable) performance
|
|
98
|
+
iconStore("readwrite", (store) => {
|
|
99
|
+
iconsSets.forEach((icons, idx) => {
|
|
100
|
+
Object.entries(icons).forEach(([name, path]) => {
|
|
101
|
+
store.put(path, name);
|
|
102
|
+
});
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
104
|
+
delete chunks[keys[idx]];
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const mdiHomeAssistant =
|
|
2
|
+
"m12.151 1.5882c-.3262 0-.6523.1291-.8996.3867l-8.3848 8.7354c-.0619.0644-.1223.1368-.1807.2154-.0588.0789-.1151.1638-.1688.2534-.2593.4325-.4552.9749-.5232 1.4555-.0026.018-.0076.0369-.0094.0548-.0121.0987-.0184.1944-.0184.2857v8.0124a1.2731 1.2731 0 001.2731 1.2731h7.8313l-3.4484-3.593a1.7399 1.7399 0 111.0803-1.125l2.6847 2.7972v-10.248a1.7399 1.7399 0 111.5276-0v7.187l2.6702-2.782a1.7399 1.7399 0 111.0566 1.1505l-3.7269 3.8831v2.7299h8.174a1.2471 1.2471 0 001.2471-1.2471v-8.0375c0-.0912-.0059-.1868-.0184-.2855-.0603-.4935-.2636-1.0617-.5326-1.5105-.0537-.0896-.1101-.1745-.1684-.253-.0588-.079-.1191-.1513-.181-.2158l-8.3848-8.7363c-.2473-.2577-.5735-.3866-.8995-.3864";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as iconMetadata_ from "./iconMetadata.json";
|
|
2
|
+
import type { IconMetaFile } from "../types";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
5
|
+
export const iconMetadata = (iconMetadata_ as any).default as IconMetaFile;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"7.4.47","parts":[{"file":"7a7139d465f1f41cb26ab851a17caa21a9331234"},{"start":"account-supervisor-circle-","file":"9561286c4c1021d46b9006596812178190a7cc1c"},{"start":"alpha-r-c","file":"eb466b7087fb2b4d23376ea9bc86693c45c500fa"},{"start":"arrow-decision-o","file":"4b3c01b7e0723b702940c5ac46fb9e555646972b"},{"start":"baby-f","file":"2611401d85450b95ab448ad1d02c1a432b409ed2"},{"start":"battery-hi","file":"89bcd31855b34cd9d31ac693fb073277e74f1f6a"},{"start":"blur-r","file":"373709cd5d7e688c2addc9a6c5d26c2d57c02c48"},{"start":"briefcase-account-","file":"a75956cf812ee90ee4f656274426aafac81e1053"},{"start":"calendar-question-","file":"3253f2529b5ebdd110b411917bacfacb5b7063e6"},{"start":"car-lig","file":"74566af3501ad6ae58ad13a8b6921b3cc2ef879d"},{"start":"cellphone-co","file":"7677f1cfb2dd4f5562a2aa6d3ae43a2e6997b21a"},{"start":"circle-slice-2","file":"70d08c50ec4522dd75d11338db57846588263ee2"},{"start":"cloud-co","file":"141d2bfa55ca4c83f4bae2812a5da59a84fec4ff"},{"start":"cog-s","file":"5a640365f8e47c609005d5e098e0e8104286d120"},{"start":"cookie-l","file":"dd85b8eb8581b176d3acf75d1bd82e61ca1ba2fc"},{"start":"currency-eur-","file":"15362279f4ebfc3620ae55f79d2830ad86d5213e"},{"start":"delete-o","file":"239434ab8df61237277d7599ebe066c55806c274"},{"start":"draw-","file":"5605918a592070803ba2ad05a5aba06263da0d70"},{"start":"emoticon-po","file":"a838cfcec34323946237a9f18e66945f55260f78"},{"start":"fan","file":"effd56103b37a8c7f332e22de8e4d67a69b70db7"},{"start":"file-question-","file":"b2424b50bd465ae192593f1c3d086c5eec893af8"},{"start":"flask-off-","file":"3b76295cde006a18f0301dd98eed8c57e1d5a425"},{"start":"food-s","file":"1c6941474cbeb1755faaaf5771440577f4f1f9c6"},{"start":"gamepad-u","file":"c6efe18db6bc9654ae3540c7dee83218a5450263"},{"start":"google-f","file":"df341afe6ad4437457cf188499cb8d2df8ac7b9e"},{"start":"head-c","file":"282121c9e45ed67f033edcc1eafd279334c00f46"},{"start":"home-pl","file":"27e8e38fc7adcacf2a210802f27d841b49c8c508"},{"start":"inbox-","file":"0f0316ec7b1b7f7ce3eaabce26c9ef619b5a1694"},{"start":"key-v","file":"ea33462be7b953ff1eafc5dac2d166b210685a60"},{"start":"leaf-circle-","file":"33db9bbd66ce48a2db3e987fdbd37fb0482145a4"},{"start":"lock-p","file":"b89e27ed39e9d10c44259362a4b57f3c579d3ec8"},{"start":"message-s","file":"7b5ab5a5cadbe06e3113ec148f044aa701eac53a"},{"start":"moti","file":"01024d78c248d36805b565e343dd98033cc3bcaf"},{"start":"newspaper-variant-o","file":"22a6ec4a4fdd0a7c0acaf805f6127b38723c9189"},{"start":"on","file":"c73d55b412f394e64632e2011a59aa05e5a1f50d"},{"start":"paw-ou","file":"3f669bf26d16752dc4a9ea349492df93a13dcfbf"},{"start":"pigg","file":"0c24edb27eb1c90b6e33fc05f34ef3118fa94256"},{"start":"printer-pos-sy","file":"41a55cda866f90b99a64395c3bb18c14983dcf0a"},{"start":"read","file":"c7ed91552a3a64c9be88c85e807404cf705b7edf"},{"start":"robot-vacuum-variant-o","file":"917d2a35d7268c0ea9ad9ecab2778060e19d90e0"},{"start":"sees","file":"6e82d9861d8fac30102bafa212021b819f303bdb"},{"start":"shoe-f","file":"e2fe7ce02b5472301418cc90a0e631f187b9f238"},{"start":"snowflake-m","file":"a28ba9f5309090c8b49a27ca20ff582a944f6e71"},{"start":"st","file":"7e92d03f095ec27e137b708b879dfd273bd735ab"},{"start":"su","file":"61c74913720f9de59a379bdca37f1d2f0dc1f9db"},{"start":"tag-plus-","file":"8f3184156a4f38549cf4c4fffba73a6a941166ae"},{"start":"timer-a","file":"baab470d11cfb3a3cd3b063ee6503a77d12a80d0"},{"start":"transit-d","file":"8561c0d9b1ac03fab360fd8fe9729c96e8693239"},{"start":"vector-arrange-b","file":"c9a3439257d4bab33d3355f1f2e11842e8171141"},{"start":"water-ou","file":"02dbccfb8ca35f39b99f5a085b095fc1275005a0"},{"start":"webc","file":"57bafd4b97341f4f2ac20a609d023719f23a619c"},{"start":"zip","file":"65ae094e8263236fa50486584a08c03497a38d93"}]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
2
|
+
type Constructor<T> = {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/prefer-function-type
|
|
4
|
+
new (...args: any[]): T;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type CustomElementClass = Omit<typeof HTMLElement, 'new'>;
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
10
|
+
export type CustomElementDecorator = {
|
|
11
|
+
// legacy
|
|
12
|
+
(cls: CustomElementClass): void;
|
|
13
|
+
|
|
14
|
+
// standard
|
|
15
|
+
(
|
|
16
|
+
target: CustomElementClass,
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
|
18
|
+
context: ClassDecoratorContext<Constructor<HTMLElement>>
|
|
19
|
+
): void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const customElementOverride =
|
|
23
|
+
(tagName: string): CustomElementDecorator =>
|
|
24
|
+
(
|
|
25
|
+
classOrTarget: CustomElementClass | Constructor<HTMLElement>,
|
|
26
|
+
context?: ClassDecoratorContext<Constructor<HTMLElement>>
|
|
27
|
+
) => {
|
|
28
|
+
if (context !== undefined) {
|
|
29
|
+
context.addInitializer(() => {
|
|
30
|
+
customElements.define(
|
|
31
|
+
tagName,
|
|
32
|
+
classOrTarget as CustomElementConstructor
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
if (!window.customElements.get(tagName)) {
|
|
37
|
+
customElements.define(tagName, classOrTarget as CustomElementConstructor);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
package/tsconfig.json
CHANGED