mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-17 20:20:24 +00:00
Compare commits
319
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba22a12a20 | ||
|
|
098b54f749 | ||
|
|
4c6a7091a6 | ||
|
|
322cb35526 | ||
|
|
c34f6bea2b | ||
|
|
41bf0652b0 | ||
|
|
23af40743b | ||
|
|
c4326b4f3a | ||
|
|
d248f5614f | ||
|
|
a4da7b26ea | ||
|
|
3c49cdf3c0 | ||
|
|
26af81d1a4 | ||
|
|
2a08f2d79b | ||
|
|
a5be02b743 | ||
|
|
4228871f00 | ||
|
|
9a7a8fd377 | ||
|
|
8b82882e15 | ||
|
|
2701015eda | ||
|
|
1991a9e493 | ||
|
|
2b72c54194 | ||
|
|
a7cb2fe7a7 | ||
|
|
51ea0c8201 | ||
|
|
ead7081bc6 | ||
|
|
ee982b1899 | ||
|
|
e8b100a39e | ||
|
|
50c361db62 | ||
|
|
e7a8d15a13 | ||
|
|
fbd0409837 | ||
|
|
a0d100611f | ||
|
|
a969bf1065 | ||
|
|
a153330610 | ||
|
|
bd2f1ca3a8 | ||
|
|
3263034416 | ||
|
|
82b28b547a | ||
|
|
61c2c750b4 | ||
|
|
117690ee70 | ||
|
|
e753de85eb | ||
|
|
a240019968 | ||
|
|
0bdf4b8777 | ||
|
|
6337828ed8 | ||
|
|
b8e5af652b | ||
|
|
e4ae29e8b5 | ||
|
|
08231dbbb0 | ||
|
|
0ca656933d | ||
|
|
b23cf8eba4 | ||
|
|
61b546415d | ||
|
|
4e1b709303 | ||
|
|
34e65b302d | ||
|
|
336d0e1b9d | ||
|
|
58d4cf8d84 | ||
|
|
d3453aff37 | ||
|
|
64ff2e414c | ||
|
|
2ca25c980f | ||
|
|
73d93bc601 | ||
|
|
5ca6a8aced | ||
|
|
7ff4993e0b | ||
|
|
4e6fbacccc | ||
|
|
2958d49e36 | ||
|
|
92289dc7ea | ||
|
|
f6c1a890e4 | ||
|
|
d06321ed43 | ||
|
|
3c3d8d9974 | ||
|
|
4f39fa482d | ||
|
|
5d0fe3236c | ||
|
|
b86142ae50 | ||
|
|
5d2f3ee5e8 | ||
|
|
e3f7c631a7 | ||
|
|
49f9d95853 | ||
|
|
db3d7701b5 | ||
|
|
3e55acf531 | ||
|
|
f102618d9d | ||
|
|
a3c02b511d | ||
|
|
74111d248e | ||
|
|
f8161b3505 | ||
|
|
6070c1907a | ||
|
|
ce5991582c | ||
|
|
d17217fc90 | ||
|
|
86b4bd0013 | ||
|
|
108ba3abd6 | ||
|
|
d38a2894c4 | ||
|
|
4c70376a62 | ||
|
|
8d69bd1401 | ||
|
|
5dfecd3693 | ||
|
|
efd51d2234 | ||
|
|
668299c16a | ||
|
|
5e155a4030 | ||
|
|
809fa10135 | ||
|
|
1cbc38f231 | ||
|
|
9ed39bb523 | ||
|
|
4e3d66cf40 | ||
|
|
2eaad79d1c | ||
|
|
afef7a2c0f | ||
|
|
18d5224002 | ||
|
|
dbffdfeaca | ||
|
|
0a4b7917ab | ||
|
|
e1524358d9 | ||
|
|
8774f9c3fc | ||
|
|
f9a9aeacab | ||
|
|
b798fee116 | ||
|
|
b25f731f0f | ||
|
|
26a7372c5e | ||
|
|
70d3409d62 | ||
|
|
0711ecddab | ||
|
|
bcfaa67eba | ||
|
|
1b60e6e04e | ||
|
|
a1a634f6dc | ||
|
|
55f48fbb56 | ||
|
|
ca4d66b94c | ||
|
|
51fd2eedd9 | ||
|
|
434a7c2e93 | ||
|
|
b849fecf0b | ||
|
|
3a48e1996f | ||
|
|
8299386737 | ||
|
|
5e58ff476f | ||
|
|
758d955053 | ||
|
|
1efd5d26f0 | ||
|
|
36979f10cc | ||
|
|
812c59fcb4 | ||
|
|
0c34165bcf | ||
|
|
8c2bfbe9ce | ||
|
|
8f721d74e2 | ||
|
|
63782e6ef3 | ||
|
|
eaad2295a9 | ||
|
|
e74eee3d34 | ||
|
|
cc39010839 | ||
|
|
7f97425214 | ||
|
|
8fac6e63de | ||
|
|
2ac8fe2b21 | ||
|
|
45ca1b2cdc | ||
|
|
0667f1e789 | ||
|
|
db49678ccb | ||
|
|
2ca7e9f71e | ||
|
|
8d883450a8 | ||
|
|
2c136e00f5 | ||
|
|
6f82478598 | ||
|
|
1093bd890f | ||
|
|
456c638750 | ||
|
|
60ca50deb4 | ||
|
|
2064ab4141 | ||
|
|
d34c42e587 | ||
|
|
5da7bf6fba | ||
|
|
f05ff58d27 | ||
|
|
7b0a381d93 | ||
|
|
8b38e6d170 | ||
|
|
6daf0eb469 | ||
|
|
6f8f849af3 | ||
|
|
cafe0f62c6 | ||
|
|
721cf46ce5 | ||
|
|
4e087760ab | ||
|
|
8fcfd4be84 | ||
|
|
b03680a8ab | ||
|
|
7ab0622bec | ||
|
|
c5aad44768 | ||
|
|
20ee7e5dc7 | ||
|
|
32fdcc708e | ||
|
|
7dd9b3308e | ||
|
|
71b870be15 | ||
|
|
f08c5fa03a | ||
|
|
fca408ae23 | ||
|
|
f3a814e38a | ||
|
|
7b0e4651c4 | ||
|
|
e5fb0e21ec | ||
|
|
beb4c3bf8a | ||
|
|
ad41f91c7b | ||
|
|
dc9c20f4ac | ||
|
|
776840a527 | ||
|
|
3568d8281a | ||
|
|
5491b6c023 | ||
|
|
e60d8f3ca4 | ||
|
|
aa0df190ed | ||
|
|
7552e91f24 | ||
|
|
0c61304023 | ||
|
|
c61bc718c2 | ||
|
|
2229e851be | ||
|
|
32d3b854ca | ||
|
|
f5dbb89e25 | ||
|
|
2ca47fddd3 | ||
|
|
ca21658968 | ||
|
|
d8c1fe7f4d | ||
|
|
a159a84228 | ||
|
|
5c95fa65dd | ||
|
|
20cde0ef70 | ||
|
|
b0d272bc3d | ||
|
|
d844c5b894 | ||
|
|
2229d42429 | ||
|
|
2ba0e77e73 | ||
|
|
e72facdec8 | ||
|
|
16bbd84962 | ||
|
|
010eee76c5 | ||
|
|
c138608445 | ||
|
|
9632251a36 | ||
|
|
17c3699707 | ||
|
|
681cbfdbd1 | ||
|
|
7c6e88ca3f | ||
|
|
8ba7ff1705 | ||
|
|
8727396e63 | ||
|
|
f03a573154 | ||
|
|
be8a7e0fa5 | ||
|
|
67e3eeb45e | ||
|
|
309e60fc4f | ||
|
|
8edfd4d5ad | ||
|
|
32f69c08a1 | ||
|
|
4d7d76c9aa | ||
|
|
9f10bc1371 | ||
|
|
93a0f37974 | ||
|
|
2ad264beaf | ||
|
|
2eec2ded13 | ||
|
|
eca535dd81 | ||
|
|
33ba3f20aa | ||
|
|
a45ef6e019 | ||
|
|
b256fc820d | ||
|
|
67fb7f61c2 | ||
|
|
1b3ea3d55d | ||
|
|
3f724aba4f | ||
|
|
7e225ed8a7 | ||
|
|
bd78800d8a | ||
|
|
13d40993ef | ||
|
|
1ad74d46d6 | ||
|
|
4a128c904e | ||
|
|
e414bab746 | ||
|
|
a354026780 | ||
|
|
24014116dc | ||
|
|
b07b604a20 | ||
|
|
81525a2b14 | ||
|
|
3b6babe0be | ||
|
|
432e8fc0d7 | ||
|
|
2e350d24f5 | ||
|
|
024e953c89 | ||
|
|
c29401a5a5 | ||
|
|
f913677dfe | ||
|
|
3b3f8f3343 | ||
|
|
03917a5e1c | ||
|
|
5e3002f739 | ||
|
|
da4395b8b3 | ||
|
|
16609053ac | ||
|
|
7beadb8108 | ||
|
|
8ca169fc23 | ||
|
|
6502c14fad | ||
|
|
3d4b10caab | ||
|
|
20bfe8b633 | ||
|
|
c6ecb5f217 | ||
|
|
938cc6a1a1 | ||
|
|
1d1e05dbdf | ||
|
|
a1b4923673 | ||
|
|
823a42f567 | ||
|
|
81275a96ff | ||
|
|
2e372b2f8a | ||
|
|
541cc7d10b | ||
|
|
678ee7e82a | ||
|
|
ee72f4818d | ||
|
|
ccbd9c1f24 | ||
|
|
84135a9424 | ||
|
|
4ebc334298 | ||
|
|
3c4c3e39e5 | ||
|
|
1267003b42 | ||
|
|
733359c869 | ||
|
|
50b6e07ae5 | ||
|
|
1b62a7cff8 | ||
|
|
d98e373f64 | ||
|
|
649516c9fa | ||
|
|
bbc4fb96b2 | ||
|
|
0ae639aeb0 | ||
|
|
0e7e41065e | ||
|
|
685843f112 | ||
|
|
5e1a99d94a | ||
|
|
d843349865 | ||
|
|
ec23164aa9 | ||
|
|
e74ef11101 | ||
|
|
a222f6a736 | ||
|
|
ef3dd16d45 | ||
|
|
5d4e1d205e | ||
|
|
1ee5ebbe75 | ||
|
|
59d705aa3d | ||
|
|
332e108dae | ||
|
|
3c15b29d0a | ||
|
|
130c708e23 | ||
|
|
588a14a8a7 | ||
|
|
a1ef6ad266 | ||
|
|
a6c1f87730 | ||
|
|
49252a3808 | ||
|
|
c7877fe38f | ||
|
|
e355a61d8f | ||
|
|
f2e19e51ce | ||
|
|
fd9ab8f561 | ||
|
|
faa1b3c98f | ||
|
|
acc4a84fc9 | ||
|
|
4d723dac37 | ||
|
|
f1d4d0ef98 | ||
|
|
88180a2708 | ||
|
|
258d87e3d5 | ||
|
|
55f22ba61a | ||
|
|
812f3ca8b9 | ||
|
|
7f880d11a0 | ||
|
|
6b2452c538 | ||
|
|
c2cbf8bd21 | ||
|
|
224bcece9c | ||
|
|
dc84b7698f | ||
|
|
bc22e6a9bd | ||
|
|
d44874783a | ||
|
|
8d1bb5c867 | ||
|
|
da1b528eee | ||
|
|
756138408a | ||
|
|
3c8f112565 | ||
|
|
2521f3dde4 | ||
|
|
56390aa01a | ||
|
|
9aac5b19da | ||
|
|
24afc3dc88 | ||
|
|
873c7b2947 | ||
|
|
648db4276b | ||
|
|
f86c3e7856 | ||
|
|
1d0251cc28 | ||
|
|
518cf87847 | ||
|
|
81a9216c44 | ||
|
|
f0e10e0058 | ||
|
|
5df8ea4f07 | ||
|
|
73f081f5cc | ||
|
|
f0d1db1da6 | ||
|
|
c658eb414b | ||
|
|
bac493b72b |
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: component-alert
|
||||
description: Show user feedback with ha-alert. Use when choosing alert types, properties, and accessible dynamic status messaging.
|
||||
---
|
||||
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
- Types: `error`, `warning`, `info`, `success`
|
||||
- Properties: `title`, `alert-type`, `dismissable`, `icon`, `action`, `rtl`
|
||||
- Content announced by screen readers when dynamically displayed
|
||||
|
||||
```html
|
||||
<ha-alert alert-type="error">Error message</ha-alert>
|
||||
<ha-alert alert-type="warning" title="Warning">Description</ha-alert>
|
||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-alert.markdown`
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: component-form
|
||||
description: Build schema-driven ha-form UIs. Use when defining HaFormSchema, wiring data/error/schema, and localized labels/helpers in forms.
|
||||
---
|
||||
|
||||
### Form Component (ha-form)
|
||||
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||
- Built-in validation with error display
|
||||
- Use `autofocus` attribute to automatically focus the first focusable element. If using the legacy `ha-dialog` dialogs, use `dialogInitialFocus`
|
||||
- Use `computeLabel`, `computeError`, `computeHelper` for translations
|
||||
|
||||
```typescript
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
.schema=${this._schema}
|
||||
.error=${this._errors}
|
||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-form.markdown`
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: component-tooltip
|
||||
description: Add contextual hover help with ha-tooltip. Use when integrating Home Assistant themed tooltips and checking canonical usage references.
|
||||
---
|
||||
|
||||
### Tooltip Component (ha-tooltip)
|
||||
|
||||
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- **Component definition**: `src/components/ha-tooltip.ts`
|
||||
- **Usage example**: `src/components/ha-label.ts`
|
||||
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: create-card
|
||||
description: Create Lovelace card implementations. Use when implementing LovelaceCard methods, config validation, card size behavior, and optional editor hooks.
|
||||
---
|
||||
|
||||
#### Creating a Lovelace Card
|
||||
|
||||
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
|
||||
|
||||
```typescript
|
||||
@customElement("hui-my-card")
|
||||
export class HuiMyCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _config?: MyCardConfig;
|
||||
|
||||
public setConfig(config: MyCardConfig): void {
|
||||
if (!config.entity) {
|
||||
throw new Error("Entity required");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 3; // Height in grid units
|
||||
}
|
||||
|
||||
// Optional: Editor for card configuration
|
||||
public static getConfigElement(): LovelaceCardEditor {
|
||||
return document.createElement("hui-my-card-editor");
|
||||
}
|
||||
|
||||
// Optional: Stub config for card picker
|
||||
public static getStubConfig(): object {
|
||||
return { entity: "" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Card Guidelines:**
|
||||
|
||||
- Cards are highly customizable for different households
|
||||
- Implement `LovelaceCard` interface with `setConfig()` and `getCardSize()`
|
||||
- Use proper error handling in `setConfig()`
|
||||
- Consider all possible states (loading, error, unavailable)
|
||||
- Support different entity types and states
|
||||
- Follow responsive design principles
|
||||
- Add configuration editor when needed
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
name: dialogs
|
||||
description: Build and review Home Assistant dialogs. Use when opening dialogs with the show-dialog event, implementing HassDialog lifecycle, and configuring dialog sizing and footer actions.
|
||||
---
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
```typescript
|
||||
fireEvent(this, "show-dialog", {
|
||||
dialogTag: "dialog-example",
|
||||
dialogImport: () => import("./dialog-example"),
|
||||
dialogParams: { title: "Example", data: someData },
|
||||
});
|
||||
```
|
||||
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
- Return `nothing` when no params (loading state)
|
||||
- Fire `dialog-closed` event in `_dialogClosed()` handler
|
||||
- Use `header-title` attribute for simple titles
|
||||
- Use `header-subtitle` attribute for simple subtitles
|
||||
- Use slots for custom content where the standard attributes are not enough
|
||||
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
|
||||
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
|
||||
|
||||
### Creating a Dialog
|
||||
|
||||
```typescript
|
||||
@customElement("dialog-my-feature")
|
||||
export class DialogMyFeature
|
||||
extends LitElement
|
||||
implements HassDialog<MyDialogParams>
|
||||
{
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _submit(): void {
|
||||
// Example submit handler: perform save logic, then close the dialog
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-subtitle=${this._params.subtitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p>Dialog content</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
```
|
||||
|
||||
**Dialog Sizing:**
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
||||
- Custom sizing is NOT recommended - use the standard width presets
|
||||
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
|
||||
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
|
||||
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
|
||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
### Dialog Design Guidelines
|
||||
|
||||
- Max width: 560px (Alert/confirmation: 320px fixed width)
|
||||
- Close X-icon on top left (all screen sizes)
|
||||
- Submit button grouped with cancel at bottom right
|
||||
- Keep button labels short: "Save", "Delete", "Enable"
|
||||
- Destructive actions use red warning button
|
||||
- Always use a title (best practice)
|
||||
- Strive for minimalism
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: keyboard-shortcuts
|
||||
description: Register safe keyboard shortcuts with ShortcutManager. Use when handling focused inputs, text selection, and non-latin keyboard fallback behavior.
|
||||
---
|
||||
|
||||
### Keyboard Shortcuts (ShortcutManager)
|
||||
|
||||
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Automatically blocks shortcuts when input fields are focused
|
||||
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
|
||||
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- **Class definition**: `src/common/keyboard/shortcuts.ts`
|
||||
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: mixin-subscribe-panel
|
||||
description: Use when implementing panel classes with SubscribeMixin and hassSubscribe() entity subscriptions.
|
||||
---
|
||||
|
||||
### Creating a Panel
|
||||
|
||||
```typescript
|
||||
@customElement("ha-panel-myfeature")
|
||||
export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
narrow!: boolean;
|
||||
|
||||
@property()
|
||||
route!: Route;
|
||||
|
||||
hassSubscribe() {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: style-tokens
|
||||
description: Apply Home Assistant CSS token styling. Use when choosing --ha-space-* spacing tokens, theme variables, responsive behavior, and RTL-safe component CSS.
|
||||
---
|
||||
|
||||
### Styling Guidelines
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Defined in `src/resources/theme/core.globals.ts`
|
||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||
- **Support RTL**: Ensure all layouts work in RTL languages
|
||||
|
||||
```typescript
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
padding: var(--ha-space-4);
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: view-transitions
|
||||
description: Implement View Transitions API patterns. Use when adding withViewTransition(), view-transition-name, fallback behavior, and transition constraints.
|
||||
---
|
||||
|
||||
### View Transitions
|
||||
|
||||
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
|
||||
|
||||
**Core Resources:**
|
||||
|
||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
|
||||
|
||||
**Implementation Guidelines:**
|
||||
|
||||
1. Always use `withViewTransition()` wrapper for automatic fallback
|
||||
2. Keep transitions simple (subtle crossfades and fades work best)
|
||||
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
|
||||
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
|
||||
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
|
||||
|
||||
**Default Root Transition:**
|
||||
|
||||
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
|
||||
|
||||
**Important Constraints:**
|
||||
|
||||
- Each `view-transition-name` must be unique at any given time
|
||||
- Only one view transition can run at a time
|
||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
||||
|
||||
**Current Usage & Planned Applications:**
|
||||
|
||||
- Launch screen fade out (implemented)
|
||||
- Automation sidebar transitions (planned - #27238)
|
||||
- More info dialog content changes (planned - #27672)
|
||||
- Toolbar navigation, ha-spinner transitions (planned)
|
||||
|
||||
**Specification & Documentation:**
|
||||
|
||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
||||
|
||||
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
|
||||
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
|
||||
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
|
||||
@@ -21,6 +21,14 @@
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If your PR includes visual changes, please add screenshots or a short video
|
||||
showing the before and after. This helps reviewers understand the impact of
|
||||
your changes.
|
||||
Note: Remove this section if this PR has no visual changes.
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
<!--
|
||||
What type of change does your PR introduce to the Home Assistant frontend?
|
||||
@@ -35,16 +43,6 @@
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Example configuration
|
||||
<!--
|
||||
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||
your PR.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
@@ -54,6 +52,8 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
@@ -61,18 +61,50 @@
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||
We're here to help! This is simply a reminder of what we are going to look
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
|
||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
Unfortunately, the number of incoming pull requests is higher than what our
|
||||
reviewers can review and merge so there is a long backlog of pull requests
|
||||
waiting for review. You can help here!
|
||||
|
||||
By reviewing another pull request, you will help raise the code quality of
|
||||
that pull request and the final review will be faster. This way the general
|
||||
pace of pull request reviews will go up and your wait time will go down.
|
||||
|
||||
When picking a pull request to review, try to choose one that hasn't yet
|
||||
been reviewed.
|
||||
|
||||
Thanks for helping out!
|
||||
-->
|
||||
|
||||
To help with the load of incoming pull requests:
|
||||
|
||||
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
|
||||
|
||||
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
|
||||
@@ -194,13 +194,13 @@ The View Transitions API creates smooth animations between DOM state changes. Wh
|
||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-duration-fast` (150ms), `--ha-animation-duration-normal` (250ms), `--ha-animation-duration-slow` (350ms) (all respect `prefers-reduced-motion`)
|
||||
|
||||
**Implementation Guidelines:**
|
||||
|
||||
1. Always use `withViewTransition()` wrapper for automatic fallback
|
||||
2. Keep transitions simple (subtle crossfades and fades work best)
|
||||
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
|
||||
3. Use `--ha-animation-duration-*` CSS variables for consistent timing (`fast`, `normal`, `slow`)
|
||||
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
|
||||
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
|
||||
|
||||
@@ -214,13 +214,6 @@ By default, `:root` receives `view-transition-name: root`, creating a full-page
|
||||
- Only one view transition can run at a time
|
||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
||||
|
||||
**Current Usage & Planned Applications:**
|
||||
|
||||
- Launch screen fade out (implemented)
|
||||
- Automation sidebar transitions (planned - #27238)
|
||||
- More info dialog content changes (planned - #27672)
|
||||
- Toolbar navigation, ha-spinner transitions (planned)
|
||||
|
||||
**Specification & Documentation:**
|
||||
|
||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
||||
@@ -246,12 +239,7 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
|
||||
## Component Library
|
||||
|
||||
### Dialog Components
|
||||
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
### Dialog Component
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -265,6 +253,7 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Use `ha-dialog` component
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
@@ -280,7 +269,6 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
||||
- Custom sizing is NOT recommended - use the standard width presets
|
||||
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
@@ -290,17 +278,9 @@ fireEvent(this, "show-dialog", {
|
||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
**Recent Examples:**
|
||||
|
||||
See these files for current patterns:
|
||||
|
||||
- `src/panels/config/repairs/dialog-repairs-issue.ts`
|
||||
- `src/dialogs/restart/dialog-restart.ts`
|
||||
- `src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts`
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-wa-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialogs.markdown`
|
||||
|
||||
### Form Component (ha-form)
|
||||
@@ -308,7 +288,6 @@ See these files for current patterns:
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||
- Built-in validation with error display
|
||||
- Use `dialogInitialFocus` in dialogs
|
||||
- Use `computeLabel`, `computeError`, `computeHelper` for translations
|
||||
|
||||
```typescript
|
||||
@@ -393,81 +372,6 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Dialog
|
||||
|
||||
```typescript
|
||||
@customElement("dialog-my-feature")
|
||||
export class DialogMyFeature
|
||||
extends LitElement
|
||||
implements HassDialog<MyDialogParams>
|
||||
{
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-subtitle=${this._params.subtitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p>Dialog content</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
```
|
||||
|
||||
### Dialog Design Guidelines
|
||||
|
||||
- Max width: 560px (Alert/confirmation: 320px fixed width)
|
||||
- Close X-icon on top left (all screen sizes)
|
||||
- Submit button grouped with cancel at bottom right
|
||||
- Keep button labels short: "Save", "Delete", "Enable"
|
||||
- Destructive actions use red warning button
|
||||
- Always use a title (best practice)
|
||||
- Strive for minimalism
|
||||
|
||||
#### Creating a Lovelace Card
|
||||
|
||||
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
|
||||
@@ -558,6 +462,10 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Pull Requests
|
||||
|
||||
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately. Do not omit, reorder, or rewrite the template sections. Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check. If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it.
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
@@ -715,9 +623,6 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
- [ ] Dialogs implement HassDialog interface
|
||||
- [ ] Dialog styling uses haStyleDialog
|
||||
- [ ] Dialog accessibility includes dialogInitialFocus
|
||||
- [ ] ha-alert used correctly for messages
|
||||
- [ ] ha-form uses proper schema structure
|
||||
- [ ] Components handle all states (loading, error, unavailable)
|
||||
|
||||
@@ -89,13 +89,13 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
@@ -5,9 +5,38 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
check-authorization:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
# Home Assistant frontend project guide
|
||||
|
||||
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
|
||||
|
||||
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
|
||||
|
||||
Specialized implementation guidance has been moved to `.agents/skills/`, including dialogs, styling tokens, view transitions, form, alert, tooltip, keyboard shortcuts, SubscribeMixin panel patterns, and card creation.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
- [Core Architecture](#core-architecture)
|
||||
- [Development Standards](#development-standards)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Text and Copy Guidelines](#text-and-copy-guidelines)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Review Guidelines](#review-guidelines)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
- `hui-` - Lovelace UI components
|
||||
- `dialog-` - Dialog components
|
||||
|
||||
### Import Patterns
|
||||
|
||||
```typescript
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-alert-dialog";
|
||||
```
|
||||
|
||||
## Core Architecture
|
||||
|
||||
The Home Assistant frontend is a modern web application that:
|
||||
|
||||
- Uses Web Components (custom elements) built with Lit framework
|
||||
- Is written entirely in TypeScript with strict type checking
|
||||
- Communicates with the backend via WebSocket API
|
||||
- Provides comprehensive theming and internationalization
|
||||
|
||||
## Development Standards
|
||||
|
||||
### Code Quality Requirements
|
||||
|
||||
**Linting and Formatting (Enforced by Tools)**
|
||||
|
||||
- ESLint config extends Airbnb, TypeScript strict, Lit, Web Components, Accessibility
|
||||
- Prettier with ES5 trailing commas enforced
|
||||
- No console statements (`no-console: "error"`) - use proper logging
|
||||
- Import organization: No unused imports, consistent type imports
|
||||
|
||||
**Naming Conventions**
|
||||
|
||||
- PascalCase for types and classes
|
||||
- camelCase for variables, methods
|
||||
- Private methods require leading underscore
|
||||
- Public methods forbid leading underscore
|
||||
|
||||
### TypeScript Usage
|
||||
|
||||
- **Always use strict TypeScript**: Enable all strict flags, avoid `any` types
|
||||
- **Proper type imports**: Use `import type` for type-only imports
|
||||
- **Define interfaces**: Create proper interfaces for data structures
|
||||
- **Type component properties**: All Lit properties must be properly typed
|
||||
- **No unused variables**: Prefix with `_` if intentionally unused
|
||||
- **Consistent imports**: Use `@typescript-eslint/consistent-type-imports`
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface EntityConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@property({ type: Object })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
// Bad
|
||||
@property()
|
||||
hass: any;
|
||||
```
|
||||
|
||||
### Web Components with Lit
|
||||
|
||||
- **Use Lit 3.x patterns**: Follow modern Lit practices
|
||||
- **Extend appropriate base classes**: Use `LitElement`, `SubscribeMixin`, or other mixins as needed
|
||||
- **Define custom element names**: Use `ha-` prefix for components
|
||||
|
||||
```typescript
|
||||
@customElement("ha-my-component")
|
||||
export class HaMyComponent extends LitElement {
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _config?: MyComponentConfig;
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>Content</div>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Guidelines
|
||||
|
||||
- **Use composition**: Prefer composition over inheritance
|
||||
- **Lazy load panels**: Heavy panels should be dynamically imported
|
||||
- **Optimize renders**: Use `@state()` for internal state, `@property()` for public API
|
||||
- **Handle loading states**: Always show appropriate loading indicators
|
||||
- **Support themes**: Use CSS custom properties from theme
|
||||
|
||||
### Data Management
|
||||
|
||||
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
|
||||
- **Cache appropriately**: Use collections and caching for frequently accessed data
|
||||
- **Handle errors gracefully**: All API calls should have error handling
|
||||
- **Update real-time**: Subscribe to state changes for live updates
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
try {
|
||||
const result = await fetchEntityRegistry(this.hass.connection);
|
||||
this._processResult(result);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: `Failed to load: ${err.message}`,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Best Practices
|
||||
|
||||
- **Code split**: Split code at the panel/dialog level
|
||||
- **Lazy load**: Use dynamic imports for heavy components
|
||||
- **Optimize bundle**: Keep initial bundle size minimal
|
||||
- **Use virtual scrolling**: For long lists, implement virtual scrolling
|
||||
- **Memoize computations**: Cache expensive calculations
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
- **Write tests**: Add tests for data processing and utilities
|
||||
- **Test with Vitest**: Use the established test framework
|
||||
- **Mock appropriately**: Mock WebSocket connections and API calls
|
||||
- **Test accessibility**: Ensure components are accessible
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Internationalization
|
||||
|
||||
- **Use localize**: Always use the localization system
|
||||
- **Add translation keys**: Add keys to src/translations/en.json
|
||||
- **Support placeholders**: Use proper placeholder syntax
|
||||
|
||||
```typescript
|
||||
this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
count: 5,
|
||||
});
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
- **ARIA labels**: Add appropriate ARIA labels
|
||||
- **Keyboard navigation**: Ensure all interactions work with keyboard
|
||||
- **Screen reader support**: Test with screen readers
|
||||
- **Color contrast**: Meet WCAG AA standards
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Setup and Commands
|
||||
|
||||
1. **Setup**: `script/setup` - Install dependencies
|
||||
2. **Develop**: `script/develop` - Development server
|
||||
3. **Lint**: `yarn lint` - Run all linting before committing
|
||||
4. **Test**: `yarn test` - Add and run tests
|
||||
5. **Build**: `script/build_frontend` - Test production build
|
||||
|
||||
### Common Pitfalls to Avoid
|
||||
|
||||
- Don't use `querySelector` - Use refs or component properties
|
||||
- Don't manipulate DOM directly - Let Lit handle rendering
|
||||
- Don't use global styles - Scope styles to components
|
||||
- Don't block the main thread - Use web workers for heavy computation
|
||||
- Don't ignore TypeScript errors - Fix all type issues
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- Sanitize HTML - Never use `unsafeHTML` with user content
|
||||
- Validate inputs - Always validate user inputs
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
|
||||
**Delete vs Remove** (Based on gallery/src/pages/Text/remove-delete-add-create.markdown)
|
||||
|
||||
- **Use "Remove"** for actions that can be restored or reapplied:
|
||||
- Removing a user's permission
|
||||
- Removing a user from a group
|
||||
- Removing links between items
|
||||
- Removing a widget from dashboard
|
||||
- Removing an item from a cart
|
||||
- **Use "Delete"** for permanent, non-recoverable actions:
|
||||
- Deleting a field
|
||||
- Deleting a value in a field
|
||||
- Deleting a task
|
||||
- Deleting a group
|
||||
- Deleting a permission
|
||||
- Deleting a calendar event
|
||||
|
||||
**Create vs Add** (Create pairs with Delete, Add pairs with Remove)
|
||||
|
||||
- **Use "Add"** for already-existing items:
|
||||
- Adding a permission to a user
|
||||
- Adding a user to a group
|
||||
- Adding links between items
|
||||
- Adding a widget to dashboard
|
||||
- Adding an item to a cart
|
||||
- **Use "Create"** for something made from scratch:
|
||||
- Creating a new field
|
||||
- Creating a new task
|
||||
- Creating a new group
|
||||
- Creating a new permission
|
||||
- Creating a new calendar event
|
||||
|
||||
#### Writing Style (Consistent with Home Assistant Documentation)
|
||||
|
||||
- **Use American English**: Standard spelling and terminology
|
||||
- **Friendly, informational tone**: Be inspiring, personal, comforting, engaging
|
||||
- **Address users directly**: Use "you" and "your"
|
||||
- **Be inclusive**: Objective, non-discriminatory language
|
||||
- **Be concise**: Use clear, direct language
|
||||
- **Be consistent**: Follow established terminology patterns
|
||||
- **Use active voice**: "Delete the automation" not "The automation should be deleted"
|
||||
- **Avoid jargon**: Use terms familiar to home automation users
|
||||
|
||||
#### Language Standards
|
||||
|
||||
- **Always use "Home Assistant"** in full, never "HA" or "HASS"
|
||||
- **Avoid abbreviations**: Spell out terms when possible
|
||||
- **Use sentence case everywhere**: Titles, headings, buttons, labels, UI elements
|
||||
- ✅ "Create new automation"
|
||||
- ❌ "Create New Automation"
|
||||
- ✅ "Device settings"
|
||||
- ❌ "Device Settings"
|
||||
- **Oxford comma**: Use in lists (item 1, item 2, and item 3)
|
||||
- **Replace Latin terms**: Use "like" instead of "e.g.", "for example" instead of "i.e."
|
||||
- **Avoid CAPS for emphasis**: Use bold or italics instead
|
||||
- **Write for all skill levels**: Both technical and non-technical users
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
#### Translation Considerations
|
||||
|
||||
- **Add translation keys**: All user-facing text must be translatable
|
||||
- **Use placeholders**: Support dynamic content in translations
|
||||
- **Keep context**: Provide enough context for translators
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
name: automation.alias,
|
||||
});
|
||||
|
||||
// Bad - hardcoded text
|
||||
("Are you sure you want to delete this automation?");
|
||||
```
|
||||
|
||||
### Common Review Issues (From PR Analysis)
|
||||
|
||||
#### User Experience and Accessibility
|
||||
|
||||
- **Form validation**: Always provide proper field labels and validation feedback
|
||||
- **Form accessibility**: Prevent password managers from incorrectly identifying fields
|
||||
- **Loading states**: Show clear progress indicators during async operations
|
||||
- **Error handling**: Display meaningful error messages when operations fail
|
||||
- **Mobile responsiveness**: Ensure components work well on small screens
|
||||
- **Hit targets**: Make clickable areas large enough for touch interaction
|
||||
- **Visual feedback**: Provide clear indication of interactive states
|
||||
|
||||
#### Dialog and Modal Patterns
|
||||
|
||||
- **Dialog width constraints**: Respect minimum and maximum width requirements
|
||||
- **Interview progress**: Show clear progress for multi-step operations
|
||||
- **State persistence**: Handle dialog state properly during background operations
|
||||
- **Cancel behavior**: Ensure cancel/close buttons work consistently
|
||||
- **Form prefilling**: Use smart defaults but allow user override
|
||||
|
||||
#### Component Design Patterns
|
||||
|
||||
- **Terminology consistency**: Use "Join"/"Apply" instead of "Group" when appropriate
|
||||
- **Visual hierarchy**: Ensure proper font sizes and spacing ratios
|
||||
- **Grid alignment**: Components should align to the design grid system
|
||||
- **Badge placement**: Position badges and indicators consistently
|
||||
- **Color theming**: Respect theme variables and design system colors
|
||||
|
||||
#### Code Quality Issues
|
||||
|
||||
- **Null checking**: Always check if entities exist before accessing properties
|
||||
- **TypeScript safety**: Handle potentially undefined array/object access
|
||||
- **Import organization**: Remove unused imports and use proper type imports
|
||||
- **Event handling**: Properly subscribe and unsubscribe from events
|
||||
- **Memory leaks**: Clean up subscriptions and event listeners
|
||||
|
||||
#### Configuration and Props
|
||||
|
||||
- **Optional parameters**: Make configuration fields optional when sensible
|
||||
- **Smart defaults**: Provide reasonable default values
|
||||
- **Future extensibility**: Design APIs that can be extended later
|
||||
- **Validation**: Validate configuration before applying changes
|
||||
|
||||
## Review Guidelines
|
||||
|
||||
### Core Requirements Checklist
|
||||
|
||||
- [ ] TypeScript strict mode passes (`yarn lint:types`)
|
||||
- [ ] No ESLint errors or warnings (`yarn lint:eslint`)
|
||||
- [ ] Prettier formatting applied (`yarn lint:prettier`)
|
||||
- [ ] Lit analyzer passes (`yarn lint:lit`)
|
||||
- [ ] Component follows Lit best practices
|
||||
- [ ] Proper error handling implemented
|
||||
- [ ] Loading states handled
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Theme variables used
|
||||
- [ ] Translations added
|
||||
- [ ] Accessible to screen readers
|
||||
- [ ] Tests added (where applicable)
|
||||
- [ ] No console statements (use proper logging)
|
||||
- [ ] Unused imports removed
|
||||
- [ ] Proper naming conventions
|
||||
|
||||
### Text and Copy Checklist
|
||||
|
||||
- [ ] Follows terminology guidelines (Delete vs Remove, Create vs Add)
|
||||
- [ ] Localization keys added for all user-facing text
|
||||
- [ ] Uses "Home Assistant" (never "HA" or "HASS")
|
||||
- [ ] Sentence case for ALL text (titles, headings, buttons, labels)
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
- [ ] Dialogs implement HassDialog interface
|
||||
- [ ] Dialog styling uses haStyleDialog
|
||||
- [ ] Dialog accessibility
|
||||
- [ ] ha-alert used correctly for messages
|
||||
- [ ] ha-form uses proper schema structure
|
||||
- [ ] Components handle all states (loading, error, unavailable)
|
||||
- [ ] Entity existence checked before property access
|
||||
- [ ] Event subscriptions properly cleaned up
|
||||
+22
-34
@@ -14,40 +14,28 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
flow_from: [
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
flow_to: [
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
power: [
|
||||
{ stat_rate: "sensor.power_grid" },
|
||||
{ stat_rate: "sensor.power_grid_return" },
|
||||
],
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -192,5 +193,13 @@ export default tseslint.config(
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
},
|
||||
rules: {
|
||||
"html/no-invalid-attr-value": "error",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ type DialogType =
|
||||
| "basic"
|
||||
| "basic-subtitle-below"
|
||||
| "basic-subtitle-above"
|
||||
| "allow-mode-change"
|
||||
| "form"
|
||||
| "form-block-mode"
|
||||
| "actions"
|
||||
| "large"
|
||||
| "small";
|
||||
@@ -69,8 +69,8 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<ha-button @click=${this._handleOpenDialog("form")}
|
||||
>Adaptive dialog with form</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
|
||||
>Adaptive dialog with form (block mode change)</ha-button
|
||||
<ha-button @click=${this._handleOpenDialog("allow-mode-change")}
|
||||
>Adaptive dialog with allow mode change</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("actions")}
|
||||
>Adaptive dialog with actions</ha-button
|
||||
@@ -164,27 +164,15 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "form-block-mode"}
|
||||
header-title="Adaptive dialog with form (block mode change)"
|
||||
header-subtitle="This form will not reset when the viewport size changes"
|
||||
block-mode-change
|
||||
.allowModeChange=${this._openDialog === "allow-mode-change"}
|
||||
header-title="Adaptive dialog with allow mode change"
|
||||
header-subtitle="Resize the window while this dialog is open"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="primaryAction"
|
||||
variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
<div>
|
||||
This dialog can switch between dialog mode and bottom sheet mode
|
||||
while open.
|
||||
</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
@@ -215,7 +203,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<li>
|
||||
<strong>Dialog mode:</strong> Used on larger screens (width >
|
||||
870px and height > 500px). Renders as a centered dialog using
|
||||
<code>ha-wa-dialog</code>.
|
||||
<code>ha-dialog</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bottom sheet mode:</strong> Used on mobile devices and
|
||||
@@ -225,10 +213,9 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The mode is determined automatically and updates when the window is
|
||||
resized. To prevent mode changes after the initial mount (useful for
|
||||
preventing form resets), use the <code>block-mode-change</code>
|
||||
attribute.
|
||||
By default, the mode is determined at mount time and then stays fixed
|
||||
while the dialog is open. To allow switching modes while the viewport
|
||||
changes, use the <code>allow-mode-change</code> attribute.
|
||||
</p>
|
||||
|
||||
<h3>Width</h3>
|
||||
@@ -394,15 +381,15 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<p>
|
||||
If you don't need responsive behavior, use
|
||||
<code>ha-wa-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-bottom-sheet</code> for mobile-only sheets.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Use the <code>block-mode-change</code> attribute when you want to
|
||||
prevent the dialog from switching modes after it's opened. This is
|
||||
especially useful for forms, as it prevents form data from being lost
|
||||
when users resize their browser window.
|
||||
Use the <code>allow-mode-change</code> attribute when you want the
|
||||
dialog to switch between modes as the viewport changes after opening.
|
||||
For forms, you can keep the default behavior to avoid resetting fields
|
||||
on resize.
|
||||
</p>
|
||||
|
||||
<h3>Example usage</h3>
|
||||
@@ -410,7 +397,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
width="medium"
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
>
|
||||
@@ -427,27 +413,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<p>Example with <code>block-mode-change</code> for forms:</p>
|
||||
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
header-title="Edit configuration"
|
||||
block-mode-change
|
||||
>
|
||||
<ha-form .schema=\${schema} .data=\${data}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Save</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
<p>
|
||||
This component combines <code>ha-wa-dialog</code> and
|
||||
This component combines <code>ha-dialog</code> and
|
||||
<code>ha-bottom-sheet</code> with automatic mode switching based on
|
||||
screen size.
|
||||
</p>
|
||||
@@ -521,12 +490,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>block-mode-change</code></td>
|
||||
<td><code>allow-mode-change</code></td>
|
||||
<td>
|
||||
When set, the mode is determined at mount time based on the
|
||||
current screen size, but subsequent mode changes are blocked.
|
||||
Useful for preventing forms from resetting when the viewport
|
||||
size changes.
|
||||
When set, the dialog can switch between modes as the viewport
|
||||
size changes while it is open.
|
||||
</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
@@ -548,6 +515,14 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<td><code>--ha-dialog-surface-background</code></td>
|
||||
<td>Dialog/sheet background color.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-surface-backdrop-filter</code></td>
|
||||
<td>Dialog/sheet surface backdrop filter.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-box-shadow</code></td>
|
||||
<td>Dialog surface box shadow (dialog mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-border-radius</code></td>
|
||||
<td>Border radius of the dialog surface (dialog mode only).</td>
|
||||
@@ -560,6 +535,34 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<td><code>--ha-dialog-hide-duration</code></td>
|
||||
<td>Hide animation duration (dialog mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-scrim-backdrop-filter</code></td>
|
||||
<td>Dialog/sheet scrim backdrop filter.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-backdrop-filter</code></td>
|
||||
<td>Dialog/sheet scrim backdrop filter (legacy fallback).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--mdc-dialog-scrim-color</code></td>
|
||||
<td>Dialog/sheet scrim color (legacy compatibility).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-bottom-sheet-surface-background</code></td>
|
||||
<td>Bottom sheet background color (sheet mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-bottom-sheet-surface-backdrop-filter</code></td>
|
||||
<td>Bottom sheet surface backdrop filter (sheet mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-bottom-sheet-scrim-backdrop-filter</code></td>
|
||||
<td>Bottom sheet scrim backdrop filter (sheet mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-bottom-sheet-scrim-color</code></td>
|
||||
<td>Bottom sheet scrim color (sheet mode only).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Dialog (ha-dialog)
|
||||
---
|
||||
+53
-30
@@ -6,7 +6,7 @@ import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dialog-footer";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-wa-dialog";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
@@ -22,14 +22,14 @@ type DialogType =
|
||||
| "form"
|
||||
| "actions";
|
||||
|
||||
@customElement("demo-components-ha-wa-dialog")
|
||||
export class DemoHaWaDialog extends LitElement {
|
||||
@customElement("demo-components-ha-dialog")
|
||||
export class DemoHaDialog extends LitElement {
|
||||
@state() private _openDialog: DialogType = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Dialog <code><ha-wa-dialog></code></h1>
|
||||
<h1>Dialog <code><ha-dialog></code></h1>
|
||||
|
||||
<p class="subtitle">Dialog component built with WebAwesome.</p>
|
||||
|
||||
@@ -53,24 +53,24 @@ export class DemoHaWaDialog extends LitElement {
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-wa-dialog
|
||||
<ha-dialog
|
||||
.open=${this._openDialog === "basic"}
|
||||
header-title="Basic dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
<ha-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-below"}
|
||||
header-title="Basic dialog with subtitle"
|
||||
header-subtitle="This is a basic dialog with a subtitle below"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
<ha-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-above"}
|
||||
header-title="Dialog with subtitle above"
|
||||
header-subtitle="This is a basic dialog with a subtitle above"
|
||||
@@ -78,9 +78,9 @@ export class DemoHaWaDialog extends LitElement {
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
<ha-dialog
|
||||
.open=${this._openDialog === "form"}
|
||||
header-title="Dialog with form"
|
||||
header-subtitle="This is a dialog with a form and a footer"
|
||||
@@ -91,17 +91,18 @@ export class DemoHaWaDialog extends LitElement {
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
Cancel
|
||||
</ha-button>
|
||||
<ha-button data-dialog="close" slot="primaryAction">
|
||||
Submit
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
<ha-dialog
|
||||
.open=${this._openDialog === "actions"}
|
||||
header-title="Dialog with actions"
|
||||
header-subtitle="This is a dialog with header actions"
|
||||
@@ -113,7 +114,7 @@ export class DemoHaWaDialog extends LitElement {
|
||||
</div>
|
||||
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
|
||||
<h2>Design</h2>
|
||||
|
||||
@@ -228,19 +229,19 @@ export class DemoHaWaDialog extends LitElement {
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
<th>Variant to use</th>
|
||||
<th>Appearance to use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>secondaryAction</code></td>
|
||||
<td>The secondary action button(s).</td>
|
||||
<td><code>plain</code></td>
|
||||
<td><code>appearance="plain"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>primaryAction</code></td>
|
||||
<td>The primary action button(s).</td>
|
||||
<td><code>accent</code></td>
|
||||
<td>Default (no appearance attribute)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -249,7 +250,7 @@ export class DemoHaWaDialog extends LitElement {
|
||||
|
||||
<h3>Example Usage</h3>
|
||||
|
||||
<pre><code><ha-wa-dialog
|
||||
<pre><code><ha-dialog
|
||||
open
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
@@ -261,12 +262,18 @@ export class DemoHaWaDialog extends LitElement {
|
||||
</div>
|
||||
<div>Dialog content</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button data-dialog="close" slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Submit</ha-button>
|
||||
Cancel
|
||||
</ha-button>
|
||||
<ha-button data-dialog="close" slot="primaryAction">
|
||||
Submit
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog></code></pre>
|
||||
</ha-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
@@ -373,13 +380,29 @@ export class DemoHaWaDialog extends LitElement {
|
||||
<td><code>--ha-dialog-surface-background</code></td>
|
||||
<td>Dialog background color.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-surface-backdrop-filter</code></td>
|
||||
<td>Backdrop filter applied to the dialog surface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-box-shadow</code></td>
|
||||
<td>Dialog surface box shadow.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-border-radius</code></td>
|
||||
<td>Border radius of the dialog surface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-z-index</code></td>
|
||||
<td>Z-index for the dialog.</td>
|
||||
<td><code>--ha-dialog-scrim-backdrop-filter</code></td>
|
||||
<td>Backdrop filter applied to the dialog scrim.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-backdrop-filter</code></td>
|
||||
<td>Legacy fallback for the dialog scrim backdrop filter.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--mdc-dialog-scrim-color</code></td>
|
||||
<td>Dialog scrim color (legacy compatibility).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-surface-margin-top</code></td>
|
||||
@@ -514,6 +537,6 @@ export class DemoHaWaDialog extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-wa-dialog": DemoHaWaDialog;
|
||||
"demo-components-ha-dialog": DemoHaDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Dialog (ha-wa-dialog)
|
||||
---
|
||||
@@ -222,6 +222,9 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
ha-language-picker {
|
||||
min-width: 200px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
|
||||
+24
-24
@@ -30,14 +30,14 @@
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@codemirror/view": "6.39.15",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-datetimeformat": "7.2.2",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.3",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -68,7 +68,6 @@
|
||||
"@material/mwc-fab": "0.27.0",
|
||||
"@material/mwc-floating-label": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-icon-button": "0.27.0",
|
||||
"@material/mwc-linear-progress": "0.27.0",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
@@ -84,7 +83,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -93,7 +92,7 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.8",
|
||||
"barcode-detector": "3.1.0",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.48.0",
|
||||
@@ -107,7 +106,7 @@
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
@@ -119,7 +118,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"marked": "17.0.3",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -149,13 +148,14 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.10",
|
||||
"@html-eslint/eslint-plugin": "0.57.1",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/core": "1.7.6",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -172,7 +172,7 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
@@ -180,27 +180,27 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint": "9.39.3",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.3.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.1",
|
||||
"glob": "13.0.6",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.0.0",
|
||||
"jsdom": "28.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lint-staged": "16.3.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -210,12 +210,12 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.7",
|
||||
"tar": "7.5.9",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"typescript-eslint": "8.56.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -235,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import "./ha-auth-form";
|
||||
import type { HaAuthForm } from "./ha-auth-form";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -52,6 +53,8 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@query("ha-auth-form") private _form?: HaAuthForm;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -179,7 +182,7 @@ export class HaAuthFlow extends LitElement {
|
||||
<div class="action">
|
||||
<ha-button
|
||||
@click=${this._handleSubmit}
|
||||
.disabled=${this._submitting}
|
||||
.loading=${this._submitting}
|
||||
>
|
||||
${this.step.type === "form"
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
@@ -370,6 +373,11 @@ export class HaAuthFlow extends LitElement {
|
||||
this._providerChanged(this.authProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._form?.reportValidity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
|
||||
const postData = { ...this._stepData, client_id: this.clientId };
|
||||
|
||||
@@ -12,6 +12,10 @@ export class HaAuthFormString extends HaFormString {
|
||||
return this;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
@@ -31,7 +35,7 @@ export class HaAuthFormString extends HaFormString {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -210,3 +210,39 @@ const formatDateWeekdayShortMem = memoizeOne(
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10
|
||||
export const formatDateWeekdayVeryShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) =>
|
||||
formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayVeryShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10, 2021
|
||||
export const formatDateWeekdayShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -133,33 +133,34 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
),
|
||||
});
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
// fallback to default numeric formatting below
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
if (parts.length) {
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
}
|
||||
}
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
// default processing of numeric values
|
||||
|
||||
@@ -38,6 +38,18 @@ export const getEntityContext = (
|
||||
return getEntityEntryContext(entry, entities, devices, areas, floors);
|
||||
};
|
||||
|
||||
export const getEntityAreaId = (
|
||||
entityId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[entityId];
|
||||
if (!entry) return undefined;
|
||||
const deviceId = entry.device_id;
|
||||
const device = deviceId ? devices[deviceId] : undefined;
|
||||
return entry.area_id || device?.area_id || undefined;
|
||||
};
|
||||
|
||||
export const getEntityEntryContext = (
|
||||
entry:
|
||||
| EntityRegistryDisplayEntry
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const SI_PREFIX_MULTIPLIERS: Record<string, number> = {
|
||||
T: 1e12,
|
||||
G: 1e9,
|
||||
M: 1e6,
|
||||
k: 1e3,
|
||||
m: 1e-3,
|
||||
"\u00B5": 1e-6, // µ (micro sign)
|
||||
"\u03BC": 1e-6, // μ (greek small letter mu)
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ).
|
||||
* Only applies when the unit is longer than 1 character and starts with a
|
||||
* recognized prefix, avoiding false positives on standalone units like "m" (meters).
|
||||
*/
|
||||
export const normalizeValueBySIPrefix = (
|
||||
value: number,
|
||||
unit: string | undefined
|
||||
): number => {
|
||||
if (!unit || unit.length <= 1) {
|
||||
return value;
|
||||
}
|
||||
const prefix = unit[0];
|
||||
if (prefix in SI_PREFIX_MULTIPLIERS) {
|
||||
return value * SI_PREFIX_MULTIPLIERS[prefix];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -1,3 +1,24 @@
|
||||
import { deepActiveElement } from "../dom/deep-active-element";
|
||||
|
||||
const getClipboardFallbackRoot = (): HTMLElement => {
|
||||
const activeElement = deepActiveElement();
|
||||
if (activeElement instanceof HTMLElement) {
|
||||
let root: Node = activeElement.getRootNode();
|
||||
let host: HTMLElement | null = null;
|
||||
|
||||
while (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
|
||||
host = root.host;
|
||||
root = root.host.getRootNode();
|
||||
}
|
||||
|
||||
if (host) {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
return document.body;
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
@@ -8,10 +29,15 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = rootEl ?? document.body;
|
||||
const root = rootEl || getClipboardFallbackRoot();
|
||||
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
el.setAttribute("readonly", "");
|
||||
el.style.position = "fixed";
|
||||
el.style.top = "0";
|
||||
el.style.left = "0";
|
||||
el.style.opacity = "0";
|
||||
root.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
|
||||
@@ -18,9 +18,11 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -28,8 +30,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
|
||||
@@ -1115,13 +1115,13 @@ export class HaChartBase extends LitElement {
|
||||
.chart-controls ::slotted(ha-icon-button) {
|
||||
background: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--mdc-icon-button-size: 32px;
|
||||
--ha-icon-button-size: 32px;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
.chart-controls.small ha-icon-button,
|
||||
.chart-controls.small ::slotted(ha-icon-button) {
|
||||
--mdc-icon-button-size: 22px;
|
||||
--ha-icon-button-size: 22px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.chart-controls ha-icon-button.inactive,
|
||||
|
||||
@@ -306,7 +306,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
visualMap: this._visualMap,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -255,7 +255,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
right: rtl ? labelWidth : 1,
|
||||
},
|
||||
tooltip: {
|
||||
appendTo: document.body,
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -335,7 +335,10 @@ export class StatisticsChart extends LitElement {
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import { stateColorProperties } from "../../common/entity/state_color";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { computeCssValue } from "../../resources/css-variables";
|
||||
|
||||
@@ -32,6 +33,22 @@ function computeTimelineStateColor(
|
||||
return computeCssValue("--history-unknown-color", computedStyles);
|
||||
}
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
// Zone states for person/device_tracker don't have specific CSS color variables,
|
||||
// so they all fall back to the same --state-person-active-color.
|
||||
// Only use a custom CSS variable if explicitly defined (e.g. --state-person-kitchen-color),
|
||||
// otherwise return undefined to get unique colors from the generic color handler.
|
||||
if (
|
||||
(domain === "person" || domain === "device_tracker") &&
|
||||
!((FIXED_DOMAIN_STATES[domain] || []) as readonly string[]).includes(state)
|
||||
) {
|
||||
return computeCssValue(
|
||||
`--state-${domain}-${slugify(state, "_")}-color`,
|
||||
computedStyles
|
||||
);
|
||||
}
|
||||
|
||||
const properties = stateColorProperties(stateObj, state);
|
||||
|
||||
if (!properties) {
|
||||
@@ -41,8 +58,6 @@ function computeTimelineStateColor(
|
||||
const rgb = computeCssValue(properties, computedStyles);
|
||||
|
||||
if (!rgb) return undefined;
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
|
||||
if (!shade) {
|
||||
return rgb;
|
||||
|
||||
@@ -9,10 +9,13 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import { createCloseHeading } from "../ha-dialog";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-sortable";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-dialog";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableColumnData,
|
||||
@@ -29,17 +32,49 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
@state() private _hiddenColumns?: string[];
|
||||
|
||||
private _lastFixedKeys: string[] = [];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: DataTableSettingsDialogParams) {
|
||||
this._params = params;
|
||||
this._columnOrder = params.columnOrder;
|
||||
this._columnOrder = this._preserveLastFixed(params.columnOrder);
|
||||
this._hiddenColumns = params.hiddenColumns;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _lastFixedCount(): number {
|
||||
const lastFixedKeys = Object.keys(this._params!.columns).filter(
|
||||
(col) => this._params!.columns[col].lastFixed
|
||||
);
|
||||
if (lastFixedKeys.length) {
|
||||
this._lastFixedKeys = lastFixedKeys;
|
||||
}
|
||||
return lastFixedKeys.length;
|
||||
}
|
||||
|
||||
private _preserveLastFixed(columnOrder) {
|
||||
let strippedColumnOrder;
|
||||
const lastFixedCount = this._lastFixedCount();
|
||||
if (lastFixedCount && columnOrder) {
|
||||
strippedColumnOrder = [...columnOrder];
|
||||
strippedColumnOrder.splice(
|
||||
columnOrder.length - lastFixedCount,
|
||||
lastFixedCount
|
||||
);
|
||||
}
|
||||
return strippedColumnOrder;
|
||||
}
|
||||
|
||||
private _sortedColumns = memoizeOne(
|
||||
(
|
||||
columns: DataTableColumnContainer,
|
||||
@@ -47,7 +82,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
hiddenColumns: string[] | undefined
|
||||
) =>
|
||||
Object.keys(columns)
|
||||
.filter((col) => !columns[col].hidden)
|
||||
.filter((col) => !columns[col].hidden && !columns[col].lastFixed)
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder?.indexOf(a) ?? -1;
|
||||
const orderB = columnOrder?.indexOf(b) ?? -1;
|
||||
@@ -92,12 +127,10 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
localize("ui.components.data-table.settings.header")
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${localize("ui.components.data-table.settings.header")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-sortable
|
||||
@item-moved=${this._columnMoved}
|
||||
@@ -152,15 +185,17 @@ export class DialogDataTableSettings extends LitElement {
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-sortable>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._reset}
|
||||
>${localize("ui.components.data-table.settings.restore")}</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${localize("ui.components.data-table.settings.done")}
|
||||
</ha-button>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._reset}
|
||||
>${localize("ui.components.data-table.settings.restore")}</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${localize("ui.components.data-table.settings.done")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -185,7 +220,8 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
this._columnOrder = columnOrder;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
const reportedOrder = columnOrder.concat(this._lastFixedKeys);
|
||||
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
private _toggle(ev) {
|
||||
@@ -266,7 +302,8 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
this._hiddenColumns = hidden;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
const reportedOrder = this._columnOrder.concat(this._lastFixedKeys);
|
||||
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
private _reset() {
|
||||
@@ -282,21 +319,9 @@ export class DialogDataTableSettings extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
--dialog-z-index: 10;
|
||||
--dialog-content-padding: 0 8px;
|
||||
}
|
||||
@media all and (max-width: 451px) {
|
||||
ha-dialog {
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: 250px;
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-4xl)
|
||||
var(--ha-border-radius-4xl) var(--ha-border-radius-square)
|
||||
var(--ha-border-radius-square);
|
||||
--mdc-dialog-min-height: calc(100% - 250px);
|
||||
--mdc-dialog-max-height: calc(100% - 250px);
|
||||
}
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 12px;
|
||||
overflow: visible;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
@@ -20,7 +21,6 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -86,6 +86,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
flex?: number;
|
||||
forceLTR?: boolean;
|
||||
hidden?: boolean;
|
||||
lastFixed?: boolean;
|
||||
}
|
||||
|
||||
export type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
|
||||
@@ -117,8 +118,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public clickable = false;
|
||||
|
||||
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
* @type {TemplateResult}
|
||||
@@ -135,9 +134,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
|
||||
@property({ attribute: false }) public groupColumn?: string;
|
||||
@@ -359,6 +355,11 @@ export class HaDataTable extends LitElement {
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder!.indexOf(a);
|
||||
const orderB = columnOrder!.indexOf(b);
|
||||
const fixedA = Boolean(columns[a].lastFixed);
|
||||
const fixedB = Boolean(columns[b].lastFixed);
|
||||
if (fixedA !== fixedB) {
|
||||
return fixedA ? 1 : -1;
|
||||
}
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
@@ -394,7 +395,6 @@ export class HaDataTable extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
@@ -428,9 +428,9 @@ export class HaDataTable extends LitElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
.indeterminate=${!!this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length &&
|
||||
.checked=${!!this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
@@ -517,7 +517,6 @@ export class HaDataTable extends LitElement {
|
||||
this._filteredData,
|
||||
localize,
|
||||
this.appendRow,
|
||||
this.hasFab,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups,
|
||||
@@ -714,14 +713,13 @@ export class HaDataTable extends LitElement {
|
||||
data: DataTableRowData[],
|
||||
localize: LocalizeFunc,
|
||||
appendRow,
|
||||
hasFab: boolean,
|
||||
groupColumn: string | undefined,
|
||||
groupOrder: string[] | undefined,
|
||||
collapsedGroups: string[],
|
||||
sortColumn: string | undefined,
|
||||
sortDirection: SortingDirection
|
||||
) => {
|
||||
if (appendRow || hasFab || groupColumn) {
|
||||
if (appendRow || groupColumn) {
|
||||
let items = [...data];
|
||||
|
||||
if (groupColumn) {
|
||||
@@ -811,13 +809,11 @@ export class HaDataTable extends LitElement {
|
||||
items.push({ append: true, selectable: false, content: appendRow });
|
||||
}
|
||||
|
||||
if (hasFab) {
|
||||
items.push({ empty: true });
|
||||
}
|
||||
items.push({ empty: true });
|
||||
|
||||
return items;
|
||||
}
|
||||
return data;
|
||||
return [...data, { empty: true }];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -869,7 +865,6 @@ export class HaDataTable extends LitElement {
|
||||
this._filteredData,
|
||||
this.localizeFunc || this.hass.localize,
|
||||
this.appendRow,
|
||||
this.hasFab,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups,
|
||||
@@ -1089,7 +1084,7 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__row.empty-row {
|
||||
height: var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
@@ -217,10 +215,4 @@ export abstract class HaDeviceAutomationPicker<
|
||||
delete value.metadata;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@@ -94,12 +95,19 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const items = this._getItems();
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
@@ -164,7 +164,7 @@ export class HaEntityToggle extends LitElement {
|
||||
min-width: 38px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
||||
import { mdiChartLine, mdiHelpCircleOutline, mdiShape } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
@@ -163,7 +163,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
icon_path: mdiHelpCircle,
|
||||
icon_path: mdiHelpCircleOutline,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { addBrandsAuth } from "../../util/brands-url";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
@@ -137,6 +138,7 @@ export class StateBadge extends LitElement {
|
||||
let imageUrl =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
imageUrl = addBrandsAuth(imageUrl);
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-wa-dialog";
|
||||
import type { DialogWidth } from "./ha-wa-dialog";
|
||||
import "./ha-dialog";
|
||||
import type { DialogWidth } from "./ha-dialog";
|
||||
|
||||
type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
|
||||
@@ -18,10 +18,11 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A responsive dialog component that automatically switches between a full dialog (ha-wa-dialog)
|
||||
* A responsive dialog component that automatically switches between a full dialog (ha-dialog)
|
||||
* and a bottom sheet (ha-bottom-sheet) based on screen size. Uses dialog mode on larger screens
|
||||
* (>870px width and >500px height) and bottom sheet mode on smaller screens or mobile devices.
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
@@ -30,20 +31,33 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* @slot footer - Dialog/sheet footer content.
|
||||
*
|
||||
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
|
||||
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog/sheet backdrop filter.
|
||||
* @cssprop --dialog-box-shadow - Dialog box shadow (dialog mode only).
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
|
||||
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
|
||||
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog/sheet scrim backdrop filter.
|
||||
* @cssprop --dialog-backdrop-filter - Dialog/sheet scrim backdrop filter (legacy).
|
||||
* @cssprop --mdc-dialog-scrim-color - Dialog/sheet scrim color (legacy).
|
||||
* @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color (sheet mode only).
|
||||
* @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet backdrop filter (sheet mode only).
|
||||
* @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter (sheet mode only).
|
||||
* @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color (sheet mode only).
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog/sheet open state.
|
||||
* @attr {("alert"|"standard")} type - Dialog type (dialog mode only). Defaults to "standard".
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog mode only). Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing by clicking the scrim/overlay.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} block-mode-change - When set, the mode is determined at mount time based on the current screen size, but subsequent mode changes are blocked. Useful for preventing forms from resetting when the viewport size changes.
|
||||
* @attr {boolean} flexcontent - Makes the content body a flex container.
|
||||
* @attr {boolean} without-header - Hides the default header.
|
||||
* @attr {boolean} allow-mode-change - When set, the component can switch between dialog and bottom-sheet modes as the viewport changes.
|
||||
*
|
||||
* @event opened - Fired when the dialog/sheet is shown (dialog mode only).
|
||||
* @event opened - Fired when the dialog/sheet is shown.
|
||||
* @event closed - Fired after the dialog/sheet is hidden.
|
||||
* @event after-show - Fired after show animation completes (dialog mode only).
|
||||
* @event after-show - Fired after show animation completes.
|
||||
*
|
||||
* @remarks
|
||||
* **Responsive Behavior:**
|
||||
@@ -51,9 +65,9 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* Dialog mode is used for screens wider than 870px and taller than 500px.
|
||||
* Bottom sheet mode is used for mobile devices and smaller screens.
|
||||
*
|
||||
* When `block-mode-change` is set, the mode is determined once at mount time based on the initial
|
||||
* screen size. Subsequent viewport size changes will not trigger mode switches, which is useful
|
||||
* for preventing form resets or other state loss when users resize their browser window.
|
||||
* By default, the mode is determined once at mount time and is then kept stable to avoid state
|
||||
* loss (like form resets) during viewport changes. Set `allow-mode-change` to opt into live
|
||||
* mode switching while the dialog is open.
|
||||
*
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when opened, add the `autofocus` attribute to it.
|
||||
@@ -73,9 +87,15 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: "alert" | "standard" = "standard";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
|
||||
@@ -85,12 +105,15 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, attribute: "block-mode-change" })
|
||||
public blockModeChange = false;
|
||||
@property({ type: Boolean, attribute: "allow-mode-change" })
|
||||
public allowModeChange = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@state() private _mode: DialogSheetMode = "dialog";
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
@@ -102,7 +125,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
this._unsubMediaQuery = listenMediaQuery(
|
||||
"(max-width: 870px), (max-height: 500px)",
|
||||
(matches) => {
|
||||
if (!this._modeSet || !this.blockModeChange) {
|
||||
if (!this._modeSet || this.allowModeChange) {
|
||||
this._mode = matches ? "bottom-sheet" : "dialog";
|
||||
this._modeSet = true;
|
||||
}
|
||||
@@ -120,33 +143,50 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
render() {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
<ha-bottom-sheet
|
||||
.ariaLabelledBy=${this.ariaLabelledBy ||
|
||||
(this.headerTitle !== undefined ? "ha-dialog-title" : undefined)}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.flexContent=${this.flexContent}
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
>
|
||||
${!this.withoutHeader
|
||||
? html`<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
? html`
|
||||
<slot name="header" slot="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ??
|
||||
"Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle"
|
||||
>${this.headerSubtitle}</span
|
||||
>`
|
||||
: html`<slot
|
||||
name="headerSubtitle"
|
||||
slot="subtitle"
|
||||
></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>`
|
||||
`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
@@ -155,16 +195,18 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.type=${this.type}
|
||||
.width=${this.width}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
.ariaLabelledBy=${this.ariaLabelledBy}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.headerTitle=${this.headerTitle}
|
||||
.headerSubtitle=${this.headerSubtitle}
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
flexcontent
|
||||
.flexContent=${this.flexContent}
|
||||
.withoutHeader=${this.withoutHeader}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
@@ -179,7 +221,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -187,10 +229,17 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
return [
|
||||
css`
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
--ha-bottom-sheet-surface-background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--ha-bottom-sheet-padding: 0 var(--safe-area-inset-right)
|
||||
var(--safe-area-inset-bottom) var(--safe-area-inset-left);
|
||||
--ha-bottom-sheet-content-padding: var(
|
||||
--dialog-content-padding,
|
||||
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -135,7 +135,7 @@ class HaAlert extends LitElement {
|
||||
}
|
||||
.action ha-icon-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
.issue-type.info > .icon {
|
||||
color: var(--info-color);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-switch";
|
||||
import "./ha-tooltip";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
@@ -33,105 +33,80 @@ export class HaAnalytics extends LitElement {
|
||||
const baseEnabled = !loading && this.analytics!.preferences.base;
|
||||
|
||||
return html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="base">
|
||||
${this.localize(
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="base">
|
||||
${this.localize(
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.description`
|
||||
)}
|
||||
</span>
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!baseEnabled}
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) => html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for=${preference}>
|
||||
${this.localize(
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for=${preference}>
|
||||
${this.localize(
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.description`
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<ha-switch
|
||||
.id="switch-${preference}"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip
|
||||
.for="switch-${preference}"
|
||||
placement="right"
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.id="switch-${preference}"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
></ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip .for="switch-${preference}" placement="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="diagnostics">
|
||||
${this.localize(
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="diagnostics">
|
||||
${this.localize(
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.description`
|
||||
)}
|
||||
</span>
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
this.shadowRoot!.querySelectorAll("*[data-for]").forEach((el) => {
|
||||
const forEl = (el as HTMLElement).dataset.for;
|
||||
delete (el as HTMLElement).dataset.for;
|
||||
|
||||
el.addEventListener("click", () => {
|
||||
const toFocus = this.shadowRoot!.querySelector(
|
||||
`*[name=${forEl}]`
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (toFocus) {
|
||||
toFocus.focus();
|
||||
toFocus.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClick(ev: Event) {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const preference = (target as any).preference;
|
||||
@@ -164,13 +139,10 @@ export class HaAnalytics extends LitElement {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
span[slot="description"] {
|
||||
cursor: pointer;
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
+160
-146
@@ -1,8 +1,9 @@
|
||||
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
runAssistPipeline,
|
||||
@@ -114,7 +115,7 @@ export class HaAssistChat extends LitElement {
|
||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||
|
||||
return html`
|
||||
<div class="messages">
|
||||
<div class="messages ha-scrollbar">
|
||||
${controlHA
|
||||
? nothing
|
||||
: html`
|
||||
@@ -585,154 +586,167 @@ export class HaAssistChat extends LitElement {
|
||||
return progress;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-alert {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.messages {
|
||||
flex: 1;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 12px 16px;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: 24px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(
|
||||
--chat-background-color-hass,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-alert {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.messages {
|
||||
flex: 1 1 400px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-4);
|
||||
}
|
||||
.input {
|
||||
padding: var(--ha-space-1) var(--ha-space-4) var(--ha-space-6);
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: var(--ha-space-6);
|
||||
margin: var(--ha-space-2) 0;
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
.message.user {
|
||||
margin-left: var(--ha-space-6);
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(
|
||||
--chat-background-color-user,
|
||||
var(--primary-color)
|
||||
);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.hass {
|
||||
margin-right: var(--ha-space-6);
|
||||
margin-inline-end: var(--ha-space-6);
|
||||
margin-inline-start: initial;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(
|
||||
--chat-background-color-hass,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
}
|
||||
.double-bounce1,
|
||||
.double-bounce2 {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||
animation: sk-bounce 2s infinite ease-in-out;
|
||||
}
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
}
|
||||
ha-markdown:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
}
|
||||
.double-bounce1,
|
||||
.double-bounce2 {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||
animation: sk-bounce 2s infinite ease-in-out;
|
||||
}
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.listening-icon {
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
.listening-icon {
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
.listening-icon[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.listening-icon[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
--mdc-icon-size: 16px;
|
||||
right: 5px;
|
||||
inset-inline-end: 5px;
|
||||
inset-inline-start: initial;
|
||||
top: 0px;
|
||||
}
|
||||
`;
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
--mdc-icon-size: 16px;
|
||||
right: 5px;
|
||||
inset-inline-end: 5px;
|
||||
inset-inline-start: initial;
|
||||
top: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-select";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -133,6 +133,17 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@queryAll("ha-textfield") private _inputs?: HaTextField[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.label
|
||||
@@ -368,7 +379,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
}
|
||||
ha-icon-button {
|
||||
position: relative;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -1,21 +1,73 @@
|
||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import type WaDrawer from "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
|
||||
const SWIPE_LOCKED_COMPONENTS = new Set([
|
||||
"ha-control-slider",
|
||||
"ha-slider",
|
||||
"ha-control-switch",
|
||||
"ha-control-circular-slider",
|
||||
"ha-hs-color-picker",
|
||||
"ha-map",
|
||||
"ha-more-info-control-select-container",
|
||||
"ha-filter-chip",
|
||||
]);
|
||||
|
||||
const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
|
||||
|
||||
/**
|
||||
* Home Assistant bottom sheet component.
|
||||
*
|
||||
* @element ha-bottom-sheet
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @cssprop --ha-bottom-sheet-height - Preferred height of the bottom sheet.
|
||||
* @cssprop --ha-bottom-sheet-max-height - Maximum height of the bottom sheet.
|
||||
* @cssprop --ha-bottom-sheet-max-width - Maximum width of the bottom sheet.
|
||||
* @cssprop --ha-bottom-sheet-border-radius - Top border radius of the bottom sheet.
|
||||
* @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color.
|
||||
* @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet surface backdrop filter.
|
||||
* @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter.
|
||||
* @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color.
|
||||
*
|
||||
* @cssprop --ha-dialog-surface-background - Bottom sheet background color fallback.
|
||||
* @cssprop --ha-dialog-surface-backdrop-filter - Bottom sheet surface backdrop filter fallback.
|
||||
* @cssprop --ha-dialog-scrim-backdrop-filter - Bottom sheet scrim backdrop filter fallback.
|
||||
* @cssprop --dialog-backdrop-filter - Bottom sheet scrim backdrop filter legacy fallback.
|
||||
* @cssprop --mdc-dialog-scrim-color - Bottom sheet scrim color legacy fallback.
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean }) public open = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
|
||||
@state() private _sliderInteractionActive = false;
|
||||
|
||||
@query("#drawer") private _drawer!: HTMLElement;
|
||||
|
||||
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||
@@ -28,14 +80,127 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _isDragging = false;
|
||||
|
||||
private _handleAfterHide(afterHideEvent: Event) {
|
||||
afterHideEvent.stopPropagation();
|
||||
this.open = false;
|
||||
const ev = new Event("closed", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
private _escapePressed = false;
|
||||
|
||||
private _handleShow = async () => {
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
const element = this.renderRoot.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-bottom-sheet-autofocus";
|
||||
}
|
||||
this.hass.auth.external?.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(
|
||||
this.renderRoot.querySelector("[autofocus]") as HTMLElement | null
|
||||
)?.focus();
|
||||
});
|
||||
this.dispatchEvent(ev);
|
||||
};
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleSliderInteractionStart = () => {
|
||||
this._sliderInteractionActive = true;
|
||||
};
|
||||
|
||||
private _handleSliderInteractionStop = () => {
|
||||
this._sliderInteractionActive = false;
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (this._sliderInteractionActive) {
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this.open = false;
|
||||
this._drawerOpen = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
private _handleHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
// Ignore bubbled wa-hide events from nested drawers (e.g., picker bottom sheet)
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceIsDrawer = ev.detail.source === (ev.target as WaDrawer).drawer;
|
||||
|
||||
if (this._sliderInteractionActive) {
|
||||
ev.preventDefault();
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
this._escapePressed = false;
|
||||
return;
|
||||
}
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDrawer) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
};
|
||||
|
||||
private _handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
if (this.preventScrimClose) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as WaDrawer).open = false;
|
||||
}
|
||||
};
|
||||
|
||||
private _handleCloseAction = (ev: Event) => {
|
||||
const shouldClose = ev
|
||||
.composedPath()
|
||||
.some(
|
||||
(node) =>
|
||||
node instanceof HTMLElement &&
|
||||
(node.getAttribute("data-dialog") === "close" ||
|
||||
node.getAttribute("data-drawer") === "close")
|
||||
);
|
||||
|
||||
if (shouldClose) {
|
||||
this._drawerOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
"slider-interaction-start",
|
||||
this._handleSliderInteractionStart,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"slider-interaction-stop",
|
||||
this._handleSliderInteractionStop,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
@@ -51,10 +216,21 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
id="drawer"
|
||||
placement="bottom"
|
||||
.open=${this._drawerOpen}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
.ariaLabelledby=${this.ariaLabelledBy}
|
||||
.ariaDescribedby=${this.ariaDescribedBy}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
@click=${this._handleCloseAction}
|
||||
without-header
|
||||
@touchstart=${this._handleTouchStart}
|
||||
>
|
||||
<div class="handle-wrapper" aria-hidden="true">
|
||||
<div class="handle"></div>
|
||||
</div>
|
||||
<slot name="header"></slot>
|
||||
<div class="content-wrapper">
|
||||
<div id="body" class="body ha-scrollbar">
|
||||
@@ -68,17 +244,37 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Check if any element inside drawer in the composed path has scrollTop > 0
|
||||
for (const path of ev.composedPath()) {
|
||||
const el = path as HTMLElement;
|
||||
if (el === this._drawer) {
|
||||
if (this.preventScrimClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = ev.composedPath();
|
||||
|
||||
for (const target of path) {
|
||||
if (target === this._drawer) {
|
||||
break;
|
||||
}
|
||||
if (el.scrollTop > 0) {
|
||||
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// Check if any element inside drawer in the composed path has scrollTop > 0 (list)
|
||||
target.scrollTop > 0 ||
|
||||
// Check if the element is a swipe locked component or has a swipe locked class
|
||||
SWIPE_LOCKED_COMPONENTS.has(target.localName) ||
|
||||
Array.from(target.classList).some((cls) =>
|
||||
SWIPE_LOCKED_CLASSES.has(cls)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop propagation so parent bottom sheets don't also start tracking
|
||||
// this gesture (same pattern as _handleKeyDown for Escape)
|
||||
ev.stopPropagation();
|
||||
this._startResizing(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
@@ -174,6 +370,20 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(
|
||||
"slider-interaction-start",
|
||||
this._handleSliderInteractionStart,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.removeEventListener(
|
||||
"slider-interaction-stop",
|
||||
this._handleSliderInteractionStop,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this._unregisterResizeHandlers();
|
||||
this._isDragging = false;
|
||||
}
|
||||
@@ -196,9 +406,42 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
transform: var(--dialog-transform);
|
||||
transition: var(--dialog-transition);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-drawer {
|
||||
--wa-color-surface-raised: transparent;
|
||||
--spacing: 0;
|
||||
--size: var(--ha-bottom-sheet-height, auto);
|
||||
--show-duration: 1ms;
|
||||
--hide-duration: 1ms;
|
||||
}
|
||||
wa-drawer::part(dialog) {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
wa-drawer::part(dialog)::backdrop {
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-bottom-sheet-scrim-backdrop-filter,
|
||||
var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-bottom-sheet-scrim-backdrop-filter,
|
||||
var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
)
|
||||
);
|
||||
background-color: var(
|
||||
--ha-bottom-sheet-scrim-color,
|
||||
var(--mdc-dialog-scrim-color, none)
|
||||
);
|
||||
}
|
||||
wa-drawer::part(body) {
|
||||
max-width: var(--ha-bottom-sheet-max-width);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
border-top-left-radius: var(
|
||||
--ha-bottom-sheet-border-radius,
|
||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||
@@ -209,7 +452,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
);
|
||||
background-color: var(
|
||||
--ha-bottom-sheet-surface-background,
|
||||
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
|
||||
var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
)
|
||||
);
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-bottom-sheet-surface-backdrop-filter,
|
||||
var(--ha-dialog-surface-backdrop-filter, none)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-bottom-sheet-surface-backdrop-filter,
|
||||
var(--ha-dialog-surface-backdrop-filter, none)
|
||||
);
|
||||
padding: var(
|
||||
--ha-bottom-sheet-padding,
|
||||
@@ -221,6 +475,35 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host([prevent-scrim-close]) .handle-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 16px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
height: 4px;
|
||||
background: var(--ha-bottom-sheet-handle-color, var(--divider-color));
|
||||
width: 40px;
|
||||
}
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -228,16 +511,24 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.body {
|
||||
padding: var(--ha-bottom-sheet-content-padding, 0);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(
|
||||
--ha-bottom-sheet-padding,
|
||||
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
|
||||
var(--safe-area-inset-left)
|
||||
--ha-bottom-sheet-content-padding,
|
||||
var(
|
||||
--ha-bottom-sheet-padding,
|
||||
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
|
||||
var(--safe-area-inset-left)
|
||||
)
|
||||
);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
slot[name="footer"] {
|
||||
display: block;
|
||||
@@ -262,6 +553,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"slider-interaction-start": undefined;
|
||||
"slider-interaction-stop": undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"slider-interaction-start": HASSDomEvent<
|
||||
HASSDomEvents["slider-interaction-start"]
|
||||
>;
|
||||
"slider-interaction-stop": HASSDomEvent<
|
||||
HASSDomEvents["slider-interaction-stop"]
|
||||
>;
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class HaButton extends Button {
|
||||
Button.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-padding-inline: 16px;
|
||||
--wa-form-control-padding-inline: var(--ha-space-4);
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-form-control-border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
@@ -68,7 +68,7 @@ export class HaButton extends Button {
|
||||
var(--button-height, 32px)
|
||||
);
|
||||
font-size: var(--wa-font-size-s, var(--ha-font-size-m));
|
||||
--wa-form-control-padding-inline: 12px;
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([variant="brand"]) {
|
||||
@@ -84,6 +84,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-primary-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-primary-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="neutral"]) {
|
||||
@@ -99,6 +102,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-neutral-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-neutral-normal-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
@@ -114,6 +120,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-success-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-success-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="warning"]) {
|
||||
@@ -129,6 +138,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-warning-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-warning-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) {
|
||||
@@ -144,6 +156,9 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-danger-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-danger-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([appearance~="plain"]) .button {
|
||||
@@ -187,6 +202,10 @@ export class HaButton extends Button {
|
||||
background-color: var(--ha-color-fill-disabled-normal-resting);
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
}
|
||||
:host([appearance~="plain"])
|
||||
.button:not(.disabled):not(.loading):active {
|
||||
background-color: var(--button-color-fill-quiet-active);
|
||||
}
|
||||
|
||||
:host([appearance~="accent"]) .button {
|
||||
background-color: var(
|
||||
@@ -212,21 +231,21 @@ export class HaButton extends Button {
|
||||
}
|
||||
|
||||
slot[name="start"]::slotted(*) {
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-end: var(--ha-space-1);
|
||||
}
|
||||
slot[name="end"]::slotted(*) {
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.button.has-start {
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
.button.has-end {
|
||||
padding-inline-end: 8px;
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
overflow: var(--ha-button-label-overflow, hidden);
|
||||
text-overflow: ellipsis;
|
||||
padding: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { STATE_RUNNING } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -58,12 +59,22 @@ export class HaCameraStream extends LitElement {
|
||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
const entityChanged =
|
||||
changedProps.has("stateObj") &&
|
||||
this.stateObj &&
|
||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||
this.stateObj.entity_id
|
||||
) {
|
||||
this.stateObj.entity_id;
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const backendStarted =
|
||||
changedProps.has("hass") &&
|
||||
this.hass &&
|
||||
this.stateObj &&
|
||||
oldHass &&
|
||||
this.hass.config.state === STATE_RUNNING &&
|
||||
oldHass.config?.state !== STATE_RUNNING;
|
||||
|
||||
if (entityChanged || backendStarted) {
|
||||
this._getCapabilities();
|
||||
this._getPosterUrl();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
mdiUndo,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, ReactiveElement, render } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -28,6 +28,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { getEntityContext } from "../common/entity/context/get_entity_context";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-code-editor-completion-items";
|
||||
@@ -83,6 +84,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "in-dialog" })
|
||||
public inDialog = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "has-toolbar" })
|
||||
public hasToolbar = true;
|
||||
|
||||
@@ -131,6 +135,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
// Force update on reconnection so editor is recreated
|
||||
if (this.hasUpdated) {
|
||||
this.requestUpdate();
|
||||
@@ -149,6 +154,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
fireEvent(this, "dialog-set-fullscreen", false);
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
@@ -215,6 +221,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (changedProps.has("error")) {
|
||||
this.classList.toggle("error-state", this.error);
|
||||
}
|
||||
if (changedProps.has("inDialog")) {
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
}
|
||||
if (changedProps.has("_isFullscreen")) {
|
||||
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||
this._updateToolbarButtons();
|
||||
@@ -310,6 +319,11 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
});
|
||||
this._canCopy = this._value?.length > 0;
|
||||
|
||||
const cmScroller = this.codemirror.dom.querySelector(".cm-scroller");
|
||||
if (cmScroller) {
|
||||
cmScroller.classList.add("ha-scrollbar");
|
||||
}
|
||||
|
||||
// Update the toolbar. Creating it if required
|
||||
this._updateToolbar();
|
||||
}
|
||||
@@ -428,10 +442,19 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _updateFullscreenState(
|
||||
fullscreen: boolean = this._isFullscreen
|
||||
): boolean {
|
||||
const previousFullscreen = this._isFullscreen;
|
||||
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
|
||||
// Update the current fullscreen state based on selected value. If fullscreen
|
||||
// is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
|
||||
this._isFullscreen =
|
||||
fullscreen && !this.disableFullscreen && this.hasToolbar;
|
||||
|
||||
if (previousFullscreen !== this._isFullscreen) {
|
||||
fireEvent(this, "dialog-set-fullscreen", this._isFullscreen);
|
||||
}
|
||||
|
||||
// Return whether successfully in requested state
|
||||
return this._isFullscreen === fullscreen;
|
||||
}
|
||||
@@ -802,100 +825,116 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return [];
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--code-editor-toolbar-height: 28px;
|
||||
}
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--code-editor-toolbar-height: 28px;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
}
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
}
|
||||
:host(.hasToolbar) .cm-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-content {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
:host(.error-state) .cm-content {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-content {
|
||||
border: none;
|
||||
border-top: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
:host(.hasToolbar) .cm-content {
|
||||
border: none;
|
||||
border-top: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-focused .cm-content {
|
||||
border-top: 2px solid var(--primary-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
:host(.hasToolbar) .cm-focused .cm-content {
|
||||
border-top: 2px solid var(--primary-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: calc(var(--header-height, 56px) + 8px) !important;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 6;
|
||||
border-radius: var(--ha-border-radius-lg) !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
background-color: var(
|
||||
--code-editor-background-color,
|
||||
var(--card-background-color)
|
||||
) !important;
|
||||
margin: 0 !important;
|
||||
padding-top: var(--safe-area-inset-top) !important;
|
||||
padding-left: var(--safe-area-inset-left) !important;
|
||||
padding-right: var(--safe-area-inset-right) !important;
|
||||
padding-bottom: var(--safe-area-inset-bottom) !important;
|
||||
box-sizing: border-box !important;
|
||||
display: block !important;
|
||||
}
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: calc(var(--header-height, 56px) + var(--ha-space-2)) !important;
|
||||
left: var(--ha-space-2) !important;
|
||||
right: var(--ha-space-2) !important;
|
||||
bottom: var(--ha-space-2) !important;
|
||||
z-index: 6;
|
||||
border-radius: var(--ha-border-radius-lg) !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
background-color: var(
|
||||
--code-editor-background-color,
|
||||
var(--card-background-color)
|
||||
) !important;
|
||||
margin: 0 !important;
|
||||
padding-top: var(--safe-area-inset-top) !important;
|
||||
padding-left: var(--safe-area-inset-left) !important;
|
||||
padding-right: var(--safe-area-inset-right) !important;
|
||||
padding-bottom: var(--safe-area-inset-bottom) !important;
|
||||
box-sizing: border-box !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
:host(.in-dialog.fullscreen) {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: var(--ha-border-radius-square) !important;
|
||||
}
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: var(--ha-border-radius-square) !important;
|
||||
}
|
||||
|
||||
.code-editor-toolbar {
|
||||
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
|
||||
--icon-button-toolbar-color: var(
|
||||
--code-editor-gutter-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
.code-editor-toolbar {
|
||||
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
|
||||
--icon-button-toolbar-color: var(
|
||||
--code-editor-gutter-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -33,6 +33,7 @@ export class HaControlButton extends LitElement {
|
||||
--control-button-background-color: var(--disabled-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
--control-button-border-radius: var(--ha-border-radius-md);
|
||||
--control-button-font-weight: var(--ha-font-weight-medium);
|
||||
--control-button-padding: 8px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-ripple-color: var(--secondary-text-color);
|
||||
@@ -59,7 +60,7 @@ export class HaControlButton extends LitElement {
|
||||
box-sizing: border-box;
|
||||
line-height: inherit;
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
font-weight: var(--control-button-font-weight);
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-attribute-icon";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon";
|
||||
@@ -16,17 +14,10 @@ export interface SelectOption {
|
||||
value: string;
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
attributeIcon?: {
|
||||
stateObj: HassEntity;
|
||||
attribute: string;
|
||||
attributeValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("ha-control-select-menu")
|
||||
export class HaControlSelectMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-arrow" })
|
||||
public showArrow = false;
|
||||
|
||||
@@ -47,6 +38,9 @@ export class HaControlSelectMenu extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public options: SelectOption[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing;
|
||||
|
||||
@query("button") private _triggerButton!: HTMLButtonElement;
|
||||
|
||||
public override render() {
|
||||
@@ -89,19 +83,13 @@ export class HaControlSelectMenu extends LitElement {
|
||||
private _renderOption = (option: SelectOption) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${option.value}
|
||||
class=${this.value === option.value ? "selected" : ""}
|
||||
.selected=${this.value === option.value}
|
||||
>${option.iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
|
||||
: option.icon
|
||||
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
|
||||
: option.attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${option.attributeIcon.stateObj}
|
||||
.attribute=${option.attributeIcon.attribute}
|
||||
.attributeValue=${option.attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
: this.renderIcon
|
||||
? html`<span slot="icon">${this.renderIcon(option.value)}</span>`
|
||||
: nothing}
|
||||
${option.label}</ha-dropdown-item
|
||||
>`;
|
||||
@@ -119,24 +107,20 @@ export class HaControlSelectMenu extends LitElement {
|
||||
}
|
||||
|
||||
private _renderIcon() {
|
||||
const { iconPath, icon, attributeIcon } =
|
||||
this.getValueObject(this.options, this.value) ?? {};
|
||||
const value = this.getValueObject(this.options, this.value);
|
||||
const defaultIcon = this.querySelector("[slot='icon']");
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
|
||||
: icon
|
||||
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
|
||||
: attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${attributeIcon.stateObj}
|
||||
.attribute=${attributeIcon.attribute}
|
||||
.attributeValue=${attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
${value?.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${value.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: value?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${value.icon}></ha-icon>`
|
||||
: this.renderIcon && this.value
|
||||
? this.renderIcon(this.value)
|
||||
: defaultIcon
|
||||
? html`<slot name="icon"></slot>`
|
||||
: nothing}
|
||||
@@ -172,12 +156,12 @@ export class HaControlSelectMenu extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1.4;
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.select-anchor {
|
||||
border: none;
|
||||
text-align: left;
|
||||
color: var(--primary-text-color);
|
||||
height: var(--control-select-menu-height);
|
||||
padding: var(--control-select-menu-padding);
|
||||
overflow: hidden;
|
||||
@@ -263,15 +247,6 @@ export class HaControlSelectMenu extends LitElement {
|
||||
cursor: not-allowed;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--control-select-menu-width);
|
||||
|
||||
@@ -95,7 +95,7 @@ export class HaCopyTextfield extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -9,6 +9,7 @@ import { TimeZone } from "../data/translation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
|
||||
|
||||
@@ -52,6 +53,12 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label}
|
||||
|
||||
@@ -93,6 +93,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
| "center"
|
||||
| "inline";
|
||||
|
||||
@state() private _calcedVerticalOpeningDirection?: "up" | "down";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this.hasUpdated && this.ranges === undefined) ||
|
||||
@@ -134,7 +136,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
opening-direction=${ifDefined(
|
||||
this.openingDirection || this._calcedOpeningDirection
|
||||
)}
|
||||
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
|
||||
opens-vertical=${ifDefined(
|
||||
this.verticalOpeningDirection || this._calcedVerticalOpeningDirection
|
||||
)}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
language=${this.hass.locale.language}
|
||||
@change=${this._handleChange}
|
||||
@@ -328,17 +332,24 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
private _handleClick() {
|
||||
// calculate opening direction if not set
|
||||
if (!this._dateRangePicker.open && !this.openingDirection) {
|
||||
const datePickerPosition = this.getBoundingClientRect().x;
|
||||
let opens: "right" | "left" | "center" | "inline";
|
||||
if (datePickerPosition > (2 * window.innerWidth) / 3) {
|
||||
opens = "left";
|
||||
} else if (datePickerPosition < window.innerWidth / 3) {
|
||||
opens = "right";
|
||||
} else {
|
||||
opens = "center";
|
||||
if (!this._dateRangePicker.open) {
|
||||
if (!this.openingDirection) {
|
||||
const datePickerPosition = this.getBoundingClientRect().x;
|
||||
let opens: "right" | "left" | "center" | "inline";
|
||||
if (datePickerPosition > (2 * window.innerWidth) / 3) {
|
||||
opens = "left";
|
||||
} else if (datePickerPosition < window.innerWidth / 3) {
|
||||
opens = "right";
|
||||
} else {
|
||||
opens = "center";
|
||||
}
|
||||
this._calcedOpeningDirection = opens;
|
||||
}
|
||||
if (!this.verticalOpeningDirection) {
|
||||
const rect = this.getBoundingClientRect();
|
||||
this._calcedVerticalOpeningDirection =
|
||||
rect.top > window.innerHeight / 2 ? "up" : "down";
|
||||
}
|
||||
this._calcedOpeningDirection = opens;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { HomeAssistant } from "../types";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-button";
|
||||
import "./ha-dialog-footer";
|
||||
import "./ha-wa-dialog";
|
||||
import "./ha-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
export class HaDialogDatePicker extends LitElement {
|
||||
@@ -49,7 +49,7 @@ export class HaDialogDatePicker extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-wa-dialog
|
||||
return html`<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="small"
|
||||
@@ -64,7 +64,8 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></app-datepicker>
|
||||
<ha-dialog-footer slot="footer">
|
||||
|
||||
<div class="bottom-actions">
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -82,6 +83,9 @@ export class HaDialogDatePicker extends LitElement {
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@@ -93,7 +97,7 @@ export class HaDialogDatePicker extends LitElement {
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>`;
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -123,9 +127,17 @@ export class HaDialogDatePicker extends LitElement {
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
app-datepicker {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { customElement } from "lit/decorators";
|
||||
*
|
||||
* @summary
|
||||
* A simple footer container for dialog actions,
|
||||
* typically used as the `footer` slot in `ha-wa-dialog`.
|
||||
* typically used as the `footer` slot in `ha-dialog`.
|
||||
*
|
||||
* @slot primaryAction - Primary action button(s), aligned to the end.
|
||||
* @slot secondaryAction - Secondary action button(s), placed before the primary action.
|
||||
|
||||
+499
-165
@@ -1,193 +1,527 @@
|
||||
import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base";
|
||||
import { styles } from "@material/mwc-dialog/mwc-dialog.css";
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant | undefined,
|
||||
title: string | TemplateResult
|
||||
) => html`
|
||||
<div class="header_title">
|
||||
<ha-icon-button
|
||||
.label=${hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
></ha-icon-button>
|
||||
<span>${title}</span>
|
||||
</div>
|
||||
`;
|
||||
type DialogHideEvent = CustomEvent<{ source?: Element }>;
|
||||
|
||||
/**
|
||||
* Home Assistant dialog component
|
||||
*
|
||||
* @element ha-dialog
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A stylable dialog built using the `wa-dialog` component, providing a standardized header (ha-dialog-header),
|
||||
* body, and footer (preferably using `ha-dialog-footer`) with slots
|
||||
*
|
||||
* You can open and close the dialog declaratively by using the `data-dialog="close"` attribute.
|
||||
* @see https://webawesome.com/docs/components/dialog/#opening-and-closing-dialogs-declaratively
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog content body.
|
||||
* @slot footer - Dialog footer content.
|
||||
*
|
||||
* @csspart dialog - The dialog surface.
|
||||
* @csspart header - The header container.
|
||||
* @csspart body - The scrollable body container.
|
||||
* @csspart footer - The footer container.
|
||||
*
|
||||
* @cssprop --dialog-content-padding - Padding for the dialog content sections.
|
||||
* @cssprop --ha-dialog-show-duration - Show animation duration.
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog backdrop filter.
|
||||
* @cssprop --dialog-box-shadow - Dialog box shadow.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog scrim backdrop filter.
|
||||
* @cssprop --dialog-backdrop-filter - Dialog scrim backdrop filter (legacy).
|
||||
* @cssprop --mdc-dialog-scrim-color - Dialog scrim color (legacy).
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
|
||||
*
|
||||
* @event opened - Fired when the dialog is shown.
|
||||
* @event closed - Fired after the dialog is hidden.
|
||||
*
|
||||
* @remarks
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when the dialog opens, add the `autofocus` attribute to it.
|
||||
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
|
||||
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/27143
|
||||
*/
|
||||
@customElement("ha-dialog")
|
||||
export class HaDialog extends DialogBase {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
public scrollToPos(x: number, y: number) {
|
||||
this.contentElement?.scrollTo(x, y);
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: "alert" | "standard" = "standard";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
|
||||
@property({ attribute: "header-subtitle" })
|
||||
public headerSubtitle?: string;
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@query(".body") public bodyContainer!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
"dialog-set-fullscreen",
|
||||
this._handleFullscreenChanged as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
protected renderHeading() {
|
||||
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
super.firstUpdated();
|
||||
this.suppressDefaultPressSelector = [
|
||||
this.suppressDefaultPressSelector,
|
||||
SUPPRESS_DEFAULT_PRESS_SELECTOR,
|
||||
].join(", ");
|
||||
this._updateScrolledAttribute();
|
||||
this.contentElement?.addEventListener("scroll", this._onScroll, {
|
||||
passive: true,
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("open")) {
|
||||
this._open = this.open;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-dialog
|
||||
.open=${this._open}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
without-header
|
||||
aria-labelledby=${ifDefined(
|
||||
this.ariaLabelledBy ||
|
||||
(this.headerTitle !== undefined ? "ha-dialog-title" : undefined)
|
||||
)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
${!this.withoutHeader
|
||||
? html` <slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>`
|
||||
: nothing}
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleShow = async (ev: Event) => {
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
this._open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-dialog-autofocus";
|
||||
}
|
||||
this.hass?.auth.external?.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAfterShow = (ev: Event) => {
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: DialogHideEvent) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
this._setFullscreen(false);
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
this.removeEventListener(
|
||||
"dialog-set-fullscreen",
|
||||
this._handleFullscreenChanged as EventListener
|
||||
);
|
||||
this._setFullscreen(false);
|
||||
super.disconnectedCallback();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _handleFullscreenChanged(ev: HASSDomEvent<boolean>): void {
|
||||
if (!this._open) {
|
||||
this._setFullscreen(ev.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
withViewTransition(() => {
|
||||
this._setFullscreen(ev.detail);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.contentElement.removeEventListener("scroll", this._onScroll);
|
||||
private _setFullscreen(fullscreen: boolean): void {
|
||||
this.toggleAttribute("fullscreen", fullscreen);
|
||||
}
|
||||
|
||||
private _onScroll = () => {
|
||||
this._updateScrolledAttribute();
|
||||
};
|
||||
|
||||
private _updateScrolledAttribute() {
|
||||
if (!this.contentElement) return;
|
||||
this.toggleAttribute("scrolled", this.contentElement.scrollTop !== 0);
|
||||
@eventOptions({ passive: true })
|
||||
private _handleBodyScroll(ev: Event) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host([scrolled]) ::slotted(ha-dialog-header) {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(
|
||||
--dialog-scroll-divider-color,
|
||||
var(--divider-color)
|
||||
);
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__scrim {
|
||||
background-color: var(--mdc-dialog-scrim-color, none);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(2) {
|
||||
flex: var(--primary-action-button-flex, unset);
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
padding: var(--dialog-container-padding, 0);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0 var(--ha-space-4);
|
||||
}
|
||||
.mdc-dialog__title:has(span) {
|
||||
padding: var(--ha-space-3) var(--ha-space-3) 0;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
content: unset;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__content {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
margin-top: var(--dialog-surface-margin-top);
|
||||
min-width: var(--mdc-dialog-min-width, auto);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
padding: var(--dialog-surface-padding, 0);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
if (this.preventScrimClose) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as WaDialog).open = false;
|
||||
}
|
||||
}
|
||||
|
||||
.header_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header_title span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding-left: var(--ha-space-1);
|
||||
padding-right: var(--ha-space-1);
|
||||
margin-right: var(--ha-space-3);
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header_button {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: calc(var(--ha-space-3) * -1);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dialog-actions {
|
||||
inset-inline-start: initial !important;
|
||||
inset-inline-end: 0 !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
private _handleHide(ev: DialogHideEvent) {
|
||||
const sourceIsDialog = ev.detail?.source === (ev.target as WaDialog).dialog;
|
||||
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDialog) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
wa-dialog {
|
||||
--full-width: var(
|
||||
--ha-dialog-width-full,
|
||||
min(95vw, var(--safe-width))
|
||||
);
|
||||
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
|
||||
--spacing: var(--dialog-content-padding, var(--ha-space-6));
|
||||
--show-duration: var(--ha-dialog-show-duration, 200ms);
|
||||
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
|
||||
--wa-color-surface-raised: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--wa-panel-border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
max-width: var(--ha-dialog-max-width, var(--safe-width));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-dialog {
|
||||
--show-duration: 0ms;
|
||||
--hide-duration: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
:host([width="small"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog,
|
||||
:host([fullscreen]) wa-dialog {
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
:host([fullscreen]) wa-dialog::part(dialog) {
|
||||
min-height: var(--safe-height);
|
||||
max-height: var(--safe-height);
|
||||
margin-top: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
:host([fullscreen]) .content-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([fullscreen]) .body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-surface-backdrop-filter,
|
||||
none
|
||||
);
|
||||
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
|
||||
color: var(--primary-text-color);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
--ha-dialog-max-height,
|
||||
calc(var(--safe-height) - var(--ha-space-20))
|
||||
);
|
||||
min-height: var(--ha-dialog-min-height);
|
||||
margin-top: var(--dialog-surface-margin-top, auto);
|
||||
/* Used to offset the dialog from the safe areas when space is limited */
|
||||
transform: translate(
|
||||
calc(
|
||||
var(--safe-area-offset-left, 0px) - var(
|
||||
--safe-area-offset-right,
|
||||
0px
|
||||
)
|
||||
),
|
||||
calc(
|
||||
var(--safe-area-offset-top, 0px) - var(
|
||||
--safe-area-offset-bottom,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog)::backdrop {
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
background-color: var(--mdc-dialog-scrim-color, none);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
}
|
||||
|
||||
:host([type="standard"]) wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
:host([type="standard"]) wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
||||
font-size: var(
|
||||
--ha-dialog-header-title-font-size,
|
||||
var(--ha-font-size-2xl)
|
||||
);
|
||||
line-height: var(
|
||||
--ha-dialog-header-title-line-height,
|
||||
var(--ha-line-height-condensed)
|
||||
);
|
||||
font-weight: var(
|
||||
--ha-dialog-header-title-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--ha-space-3);
|
||||
}
|
||||
|
||||
wa-dialog::part(body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(
|
||||
--dialog-content-padding,
|
||||
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
|
||||
);
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
wa-dialog::part(footer) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog": HaDialog;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"dialog-set-fullscreen": boolean;
|
||||
opened: undefined;
|
||||
"after-show": undefined;
|
||||
closed: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-divider")
|
||||
export class HaMdDivider extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
role=${ifDefined(this.label ? "separator" : undefined)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
>
|
||||
<span class="line"></span>
|
||||
${this.label
|
||||
? html`
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="line"></span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
width: var(--ha-divider-width, 100%);
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.label {
|
||||
padding: var(--ha-divider-label-padding, 0 16px);
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
background-color: var(--divider-color);
|
||||
height: var(--ha-divider-line-height, 1px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -186,9 +186,11 @@ export class HaDrawer extends DrawerBase {
|
||||
padding-inline-start var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* Use 1ms instead of "none" so the transitionend event still fires.
|
||||
The MDC drawer foundation relies on it to complete the close cycle. */
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-it
|
||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
/**
|
||||
@@ -17,6 +17,8 @@ import "./ha-svg-icon";
|
||||
*/
|
||||
@customElement("ha-dropdown-item")
|
||||
export class HaDropdownItem extends DropdownItem {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
protected renderCheckboxIcon() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
@@ -47,6 +49,16 @@ export class HaDropdownItem extends DropdownItem {
|
||||
:host([variant="danger"]) #icon ::slotted(*) {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
|
||||
:host([selected]) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
:host([selected]:hover) {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
@@ -29,16 +29,25 @@ export class HaDropdown extends Dropdown {
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
public get anchorElement(): HTMLButtonElement | HaIconButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
return this.popup?.anchor as HTMLButtonElement | HaIconButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
public set anchorElement(
|
||||
element: HTMLButtonElement | HaIconButton | undefined
|
||||
) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (this.popup.anchor && this.popup.anchor.localName === "ha-icon-button") {
|
||||
// @ts-ignore
|
||||
(this.popup.anchor as HaIconButton).selected = false;
|
||||
}
|
||||
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
@@ -46,7 +55,7 @@ export class HaDropdown extends Dropdown {
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
private override getTrigger(): HTMLButtonElement | HaIconButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
@@ -54,6 +63,28 @@ export class HaDropdown extends Dropdown {
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override async showMenu() {
|
||||
// @ts-ignore
|
||||
await super.showMenu();
|
||||
const triggerElement = this.getTrigger();
|
||||
if (triggerElement && triggerElement.localName === "ha-icon-button") {
|
||||
(triggerElement as HaIconButton).selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override async hideMenu() {
|
||||
const triggerElement = this.getTrigger();
|
||||
if (triggerElement && triggerElement.localName === "ha-icon-button") {
|
||||
(triggerElement as HaIconButton).selected = false;
|
||||
}
|
||||
// @ts-ignore
|
||||
await super.hideMenu();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-base-time-input";
|
||||
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -19,7 +19,7 @@ export interface HaDurationData {
|
||||
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
|
||||
|
||||
@customElement("ha-duration-input")
|
||||
class HaDurationInput extends LitElement {
|
||||
export class HaDurationInput extends LitElement {
|
||||
@property({ attribute: false }) public data?: HaDurationData;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -37,10 +37,24 @@ class HaDurationInput extends LitElement {
|
||||
@property({ attribute: "allow-negative", type: Boolean })
|
||||
public allowNegative = false;
|
||||
|
||||
@property({ attribute: "enable-second", type: Boolean })
|
||||
public enableSecond = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
|
||||
|
||||
private _toggleNegative = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="row">
|
||||
@@ -65,7 +79,7 @@ class HaDurationInput extends LitElement {
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enable-second
|
||||
.enableSecond=${this.enableSecond}
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
@@ -162,9 +176,9 @@ class HaDurationInput extends LitElement {
|
||||
if (value) {
|
||||
value.hours ||= 0;
|
||||
value.minutes ||= 0;
|
||||
value.seconds ||= 0;
|
||||
|
||||
if ("days" in value) value.days ||= 0;
|
||||
if ("seconds" in value) value.seconds ||= 0;
|
||||
if ("milliseconds" in value) value.milliseconds ||= 0;
|
||||
|
||||
if (this.allowNegative) {
|
||||
@@ -183,8 +197,11 @@ class HaDurationInput extends LitElement {
|
||||
value.milliseconds %= 1000;
|
||||
}
|
||||
|
||||
if (value.seconds > 59) {
|
||||
value.minutes += Math.floor(value.seconds / 60);
|
||||
if (!this.enableSecond && !value.seconds) {
|
||||
// @ts-ignore
|
||||
delete value.seconds;
|
||||
} else if (this.enableSecond && value.seconds > 59) {
|
||||
value.minutes = (value.minutes ?? 0) + Math.floor(value.seconds / 60);
|
||||
value.seconds %= 60;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class HaFab extends FabBase {
|
||||
}
|
||||
:disabled {
|
||||
--mdc-theme-secondary: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -317,7 +317,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
ha-button {
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
--mdc-icon-button-size: 24px;
|
||||
--ha-icon-button-size: 24px;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
width: 100%;
|
||||
|
||||
@@ -26,12 +26,12 @@ import { showCategoryRegistryDetailDialog } from "../panels/config/category/show
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
|
||||
@customElement("ha-filter-categories")
|
||||
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
@@ -317,7 +317,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
|
||||
@@ -122,7 +122,10 @@ export class HaFilterDevices extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,10 @@ export class HaFilterDomains extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,10 @@ export class HaFilterEntities extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,10 @@ export class HaFilterIntegrations extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - (49 + 48 + 32)}px`;
|
||||
`${this.clientHeight - (49 + 48 + 32 + 4)}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
// 48px - height of ha-list-item
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import "../ha-expansion-panel";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
@@ -35,6 +36,12 @@ export class HaFormExpandable extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@query("ha-form", true) private _form?: HaForm;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._form?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormFloatData,
|
||||
HaFormFloatSchema,
|
||||
} from "./types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-form-float")
|
||||
export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
@@ -25,12 +25,15 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield") private _input?: HaTextField;
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -76,6 +79,11 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow user to start typing a negative zero
|
||||
if (rawValue === "-0") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawValue !== "") {
|
||||
value = parseFloat(rawValue);
|
||||
if (isNaN(value)) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import "./ha-form";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormGridSchema,
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormGridSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-form-grid")
|
||||
export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
@@ -33,9 +34,22 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
@queryAll("ha-form", true) private _forms?: HaForm[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const forms = this._forms ?? [];
|
||||
let valid = true;
|
||||
for (const form of forms) {
|
||||
if (!form.reportValidity()) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-slider";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type {
|
||||
@@ -13,7 +14,6 @@ import type {
|
||||
HaFormIntegerData,
|
||||
HaFormIntegerSchema,
|
||||
} from "./types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@@ -29,24 +29,39 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield ha-slider") private _input?:
|
||||
@query("ha-textfield, ha-slider", true) private _input?:
|
||||
| HaTextField
|
||||
| HTMLInputElement;
|
||||
|
||||
private _lastValue?: HaFormIntegerData;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const showSlider = this._showSlider();
|
||||
if (showSlider && this.schema.required && isNaN(Number(this.data))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!showSlider) {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
private _showSlider(): boolean {
|
||||
return (
|
||||
this.schema.valueMin !== undefined &&
|
||||
this.schema.valueMax !== undefined &&
|
||||
this.schema.valueMax - this.schema.valueMin < 256
|
||||
) {
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._showSlider()) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
|
||||
@@ -44,6 +44,13 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
this._dropdown?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (!this.schema.required || (this.data && this.data.length > 0)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options = Array.isArray(this.schema.options)
|
||||
? this.schema.options
|
||||
|
||||
@@ -8,16 +8,17 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@@ -53,6 +54,11 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const form = this.renderRoot.querySelector<HaForm>("ha-form");
|
||||
return form ? form.reportValidity() : true;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("data")) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationInput } from "../ha-duration-input";
|
||||
import type { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-positive_time_period_dict")
|
||||
@@ -14,12 +15,15 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-time-input", true) private _input?: HTMLElement;
|
||||
@query("ha-duration-input", true) private _input?: HaDurationInput;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormSelectData,
|
||||
HaFormSelectSchema,
|
||||
} from "./types";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@@ -41,6 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
})
|
||||
);
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (!this.schema.required || this.data) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-selector-select
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
@@ -11,10 +15,6 @@ import type {
|
||||
HaFormStringData,
|
||||
HaFormStringSchema,
|
||||
} from "./types";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@@ -37,12 +37,15 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@state() protected unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield") private _input?: HaTextField;
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public focus(): void {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -148,7 +151,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, ReactiveElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -24,7 +24,7 @@ const LOAD_ELEMENTS = {
|
||||
};
|
||||
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : undefined;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
@@ -76,22 +76,64 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const root = this.renderRoot.querySelector(".root");
|
||||
if (!root) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
for (const child of root.children) {
|
||||
if (child.tagName !== "HA-ALERT") {
|
||||
if (child instanceof ReactiveElement) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await child.updateComplete;
|
||||
}
|
||||
(child as HTMLElement).focus();
|
||||
break;
|
||||
|
||||
const elements = [...root.children].filter(
|
||||
(child) => child.localName !== "ha-alert"
|
||||
) as (HTMLElement & { reportValidity?: () => boolean })[];
|
||||
|
||||
let isValid = true;
|
||||
let firstInvalidElement: HTMLElement | undefined;
|
||||
|
||||
this.schema.forEach((item, index) => {
|
||||
const element = elements[index];
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elementValid = true;
|
||||
|
||||
if (
|
||||
"reportValidity" in element &&
|
||||
typeof element.reportValidity === "function"
|
||||
) {
|
||||
elementValid = element.reportValidity();
|
||||
} else if (
|
||||
item.required &&
|
||||
!(
|
||||
"type" in item && ["boolean", "constant"].includes(item.type ?? "")
|
||||
) &&
|
||||
!(
|
||||
"selector" in item &&
|
||||
("boolean" in item.selector || "constant" in item.selector)
|
||||
)
|
||||
) {
|
||||
const value = getValue(this.data, item);
|
||||
elementValid = value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
|
||||
if (!elementValid) {
|
||||
isValid = false;
|
||||
if (!firstInvalidElement) {
|
||||
firstInvalidElement = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (firstInvalidElement) {
|
||||
firstInvalidElement.focus?.();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
@@ -105,11 +147,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
|
||||
@@ -53,7 +53,7 @@ export class HaFormfield extends FormfieldBase {
|
||||
:host(:not([alignEnd])) ::slotted(ha-switch) {
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
margin-inline-start: inline;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.mdc-form-field {
|
||||
align-items: var(--ha-formfield-align-items, center);
|
||||
|
||||
@@ -194,7 +194,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.image=${this.image}
|
||||
.label=${label}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
.required=${this.required}
|
||||
@@ -434,6 +433,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
--wa-space-l: 0;
|
||||
}
|
||||
|
||||
wa-popover::part(dialog)::backdrop {
|
||||
background: none;
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: max(var(--body-width), 250px);
|
||||
max-width: var(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import "./ha-icon-button";
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@@ -245,7 +245,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
}
|
||||
.reset {
|
||||
grid-area: reset;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class HaBadge extends LitElement {
|
||||
font-weight: var(--ha-heading-badge-font-weight, 400);
|
||||
line-height: var(--ha-heading-badge-line-height, 20px);
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
--ha-icon-display: block;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -25,7 +25,7 @@ export class HaHelpTooltip extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-svg-icon id="svg-icon" .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
<ha-svg-icon id="svg-icon" .path=${mdiHelpCircleOutline}></ha-svg-icon>
|
||||
<ha-tooltip for="svg-icon" .placement=${this.position}>
|
||||
${this.label}
|
||||
</ha-tooltip>
|
||||
|
||||
@@ -14,6 +14,14 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
|
||||
|
||||
@@ -23,6 +31,10 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ export class HaIconButtonNext extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight;
|
||||
|
||||
@@ -23,6 +31,10 @@ export class HaIconButtonNext extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ export class HaIconButtonPrev extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
|
||||
|
||||
@@ -23,6 +31,10 @@ export class HaIconButtonPrev extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaIconButton } from "./ha-icon-button";
|
||||
@@ -6,41 +7,51 @@ import { HaIconButton } from "./ha-icon-button";
|
||||
export class HaIconButtonToggle extends HaIconButton {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
mwc-icon-button {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) mwc-icon-button::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) mwc-icon-button {
|
||||
color: var(--primary-background-color);
|
||||
}
|
||||
:host([selected]:not([disabled])) mwc-icon-button::before {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
static styles: CSSResultGroup = [
|
||||
HaIconButton.styles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-button::part(base) {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
ha-button::part(base)::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) ha-button::part(base)::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) ha-button::after {
|
||||
opacity: 0;
|
||||
}
|
||||
:host([selected]) ha-button::part(base) {
|
||||
color: var(--primary-background-color);
|
||||
background-color: unset;
|
||||
}
|
||||
:host([selected]:not([disabled])) ha-button::part(base)::before {
|
||||
opacity: 1;
|
||||
}
|
||||
::slotted(*) {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -109,7 +109,7 @@ export class HaIconButtonToolbar extends LitElement {
|
||||
|
||||
.icon-toolbar-button {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--ha-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--mdc-icon-size: var(--icon-button-toolbar-icon);
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import type { IconButton } from "@material/mwc-icon-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-icon-button")
|
||||
@@ -19,15 +18,19 @@ export class HaIconButton extends LitElement {
|
||||
// These should always be set as properties, not attributes,
|
||||
// so that only the <button> element gets the attribute
|
||||
@property({ type: String, attribute: "aria-haspopup" })
|
||||
override ariaHasPopup!: IconButton["ariaHasPopup"];
|
||||
ariaHasPopup!: "false" | "true" | "menu" | "listbox" | "tree" | "grid";
|
||||
|
||||
@property({ attribute: "hide-title", type: Boolean }) hideTitle = false;
|
||||
|
||||
@query("mwc-icon-button", true) private _button?: IconButton;
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
public override focus() {
|
||||
this._button?.focus();
|
||||
}
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
@@ -36,30 +39,69 @@ export class HaIconButton extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-icon-button
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
title=${ifDefined(this.hideTitle ? undefined : this.label)}
|
||||
aria-haspopup=${ifDefined(this.ariaHasPopup)}
|
||||
.disabled=${this.disabled}
|
||||
.iconTag=${this.path ? "ha-svg-icon" : "span"}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
>
|
||||
${this.path
|
||||
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
|
||||
: html`<slot></slot>`}
|
||||
</mwc-icon-button>
|
||||
: html`<span><slot></slot></span>`}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
--ha-button-height: var(--ha-icon-button-size, 48px);
|
||||
}
|
||||
:host([disabled]) {
|
||||
ha-button {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
--wa-form-control-padding-inline: var(
|
||||
--ha-icon-button-padding-inline,
|
||||
--ha-space-2
|
||||
);
|
||||
--wa-color-on-normal: currentColor;
|
||||
--wa-color-fill-quiet: transparent;
|
||||
--ha-button-label-overflow: visible;
|
||||
}
|
||||
ha-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-theme-on-primary: currentColor;
|
||||
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
|
||||
ha-button::part(base) {
|
||||
width: var(--wa-form-control-height);
|
||||
aspect-ratio: 1;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
ha-button::part(label) {
|
||||
display: flex;
|
||||
}
|
||||
:host([selected]) ha-button::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host(:hover:not([disabled])) ha-button::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -191,7 +191,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -84,13 +84,11 @@ export class HaMarkdown extends LitElement {
|
||||
ha-markdown-element > :is(ol, ul) {
|
||||
padding-inline-start: var(--markdown-list-indent, revert);
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
}
|
||||
li:has(input[type="checkbox"]) > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
@@ -137,10 +135,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { SelectOptionEl } from "@material/web/select/internal/selectoption/select-option";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select-option")
|
||||
export class HaMdSelectOption extends SelectOptionEl {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select-option": HaMdSelectOption;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user