mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-26 11:27:45 +00:00
Compare commits
216 Commits
protocol-b
...
feature/po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca5550e02 | ||
|
|
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 | ||
|
|
c293cf56f6 | ||
|
|
2ec6f3615d | ||
|
|
d3b92059e5 | ||
|
|
72e69f6291 | ||
|
|
3bff97595f | ||
|
|
19b1d03cd1 | ||
|
|
f04557688c | ||
|
|
d6953ea1bc | ||
|
|
4501db18c7 | ||
|
|
c68bcc5b32 | ||
|
|
3753c7d313 | ||
|
|
4a2ef18375 | ||
|
|
051da41eec | ||
|
|
9b1a679f21 | ||
|
|
905a0f957c | ||
|
|
d9a687b79c | ||
|
|
3b56497134 | ||
|
|
ed0ec871ce | ||
|
|
29f5362182 | ||
|
|
5096bab26c | ||
|
|
ce2892cab9 | ||
|
|
57748c15de | ||
|
|
ecb6a33c86 | ||
|
|
d34921ff6d | ||
|
|
e6c9e81082 | ||
|
|
aa13c6fa53 | ||
|
|
d47738aa24 | ||
|
|
d473ee1084 | ||
|
|
0372ed932f | ||
|
|
2baa044db5 | ||
|
|
3d04046bcc | ||
|
|
8e860cb17d | ||
|
|
c41d7ff923 | ||
|
|
c22fc1021a | ||
|
|
6344233934 | ||
|
|
23441d593b | ||
|
|
8393ed5fd4 | ||
|
|
09afe9bb51 | ||
|
|
fb8d6062c5 | ||
|
|
f93ae58b83 | ||
|
|
7626b26b2d | ||
|
|
a1bf30e501 | ||
|
|
37a45d1729 | ||
|
|
6962a915a3 | ||
|
|
de3e2bcafa | ||
|
|
e732280b70 | ||
|
|
4fb3453f73 | ||
|
|
0f9cb9c13e | ||
|
|
7aa235c6af | ||
|
|
e8ddae8189 | ||
|
|
c26e59f19c | ||
|
|
83aa06cb18 | ||
|
|
9d3d0dac48 | ||
|
|
8da1154924 | ||
|
|
eb588075b8 | ||
|
|
bdeaf10d74 | ||
|
|
bec0d19fc9 | ||
|
|
325a7974c2 | ||
|
|
fab1fde6e3 | ||
|
|
0e9564e676 | ||
|
|
244eb75049 | ||
|
|
644bb016d6 | ||
|
|
02dbcf0946 | ||
|
|
f7df4d8a90 | ||
|
|
e00ced23ee | ||
|
|
f5cc2104ef | ||
|
|
161dd26b4d | ||
|
|
29cee99f10 | ||
|
|
47341e93fc | ||
|
|
cbae7d6e2f | ||
|
|
ebff35d17f | ||
|
|
aec4a06156 | ||
|
|
917f2b4434 | ||
|
|
79ec6b972e | ||
|
|
9e35befa99 | ||
|
|
75160d67d3 | ||
|
|
b145d09041 | ||
|
|
f3f7a1e46a | ||
|
|
091315d9a9 | ||
|
|
75b830cdf9 | ||
|
|
e4b8352832 | ||
|
|
4e193187f9 | ||
|
|
5394b3b8cf | ||
|
|
2ab867986a | ||
|
|
a1c3a6c662 | ||
|
|
11296adbd4 | ||
|
|
4e04f4284e | ||
|
|
a0cc0d9cca | ||
|
|
c925053bb8 | ||
|
|
22a7aa8f8e | ||
|
|
3a5f719a3e | ||
|
|
7b7182c147 | ||
|
|
0eb7229819 | ||
|
|
fcc6f1b5e9 |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.14
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
|
||||
|
||||
114
.github/copilot-instructions.md
vendored
114
.github/copilot-instructions.md
vendored
@@ -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,13 +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-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
### Dialog Component
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -266,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()`
|
||||
@@ -281,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:**
|
||||
|
||||
@@ -291,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)
|
||||
@@ -309,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
|
||||
@@ -394,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)
|
||||
@@ -559,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
|
||||
@@ -716,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)
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
|
||||
# ℹ️ 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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
|
||||
31
.github/workflows/restrict-task-creation.yml
vendored
31
.github/workflows/restrict-task-creation.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let changeFunction;
|
||||
let sidebarChangeCallback;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
changeFunction?.({
|
||||
sidebarChangeCallback?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -16,14 +14,11 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
@@ -48,4 +43,5 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,9 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
<ha-alert alert-type="info">
|
||||
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
|
||||
</ha-alert>
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
1
gallery/src/pages/brand/logo.ts
Normal file
1
gallery/src/pages/brand/logo.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-alert";
|
||||
@@ -215,7 +215,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
|
||||
@@ -394,7 +394,7 @@ 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>
|
||||
|
||||
@@ -410,7 +410,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"
|
||||
>
|
||||
@@ -447,7 +446,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<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>
|
||||
|
||||
3
gallery/src/pages/components/ha-dialog.markdown
Normal file
3
gallery/src/pages/components/ha-dialog.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Dialog (ha-dialog)
|
||||
---
|
||||
@@ -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>
|
||||
|
||||
@@ -514,6 +521,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)
|
||||
---
|
||||
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -223,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;
|
||||
}
|
||||
|
||||
47
package.json
47
package.json
@@ -29,7 +29,7 @@
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
@@ -37,22 +37,22 @@
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.0",
|
||||
"@formatjs/intl-displaynames": "7.2.0",
|
||||
"@formatjs/intl-durationformat": "0.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.0",
|
||||
"@formatjs/intl-listformat": "8.2.0",
|
||||
"@formatjs/intl-locale": "5.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.1",
|
||||
"@formatjs/intl-pluralrules": "6.2.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.1",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
"@formatjs/intl-listformat": "8.2.1",
|
||||
"@formatjs/intl-locale": "5.2.1",
|
||||
"@formatjs/intl-numberformat": "9.2.2",
|
||||
"@formatjs/intl-pluralrules": "6.2.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.2",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -68,10 +68,8 @@
|
||||
"@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-menu": "0.27.0",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-snackbar": "0.27.0",
|
||||
@@ -112,7 +110,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.1",
|
||||
"intl-messageformat": "11.1.2",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -133,7 +131,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -146,17 +144,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@html-eslint/eslint-plugin": "0.55.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.1",
|
||||
"@rspack/core": "1.7.4",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -192,14 +191,14 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.0",
|
||||
"glob": "13.0.1",
|
||||
"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": "27.4.0",
|
||||
"jsdom": "28.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -229,13 +228,13 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.2.0",
|
||||
"globals": "17.3.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.0"
|
||||
"node": "24.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ if [[ -n "$DEVCONTAINER" ]]; then
|
||||
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
||||
nvm uninstall "$nodeCurrent"
|
||||
fi
|
||||
|
||||
# install yarn if not already available
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
npm install -g corepack
|
||||
yes | yarn
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
|
||||
@@ -31,7 +31,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);
|
||||
|
||||
@@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
@@ -116,3 +116,6 @@ export const UNIT_F = "°F";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
|
||||
/** String to visually separate labels on UI */
|
||||
export const STRINGS_SEPARATOR_DOT = " · ";
|
||||
|
||||
@@ -3,13 +3,14 @@ import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValuePart } from "../../types";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberToParts,
|
||||
getNumberFormatOptions,
|
||||
isNumericFromAttributes,
|
||||
} from "../number/format_number";
|
||||
@@ -51,8 +52,36 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes: any,
|
||||
state: string
|
||||
): string => {
|
||||
const parts = computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
entityId,
|
||||
attributes,
|
||||
state
|
||||
);
|
||||
return parts.map((part) => part.value).join("");
|
||||
};
|
||||
|
||||
const computeStateToPartsFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string
|
||||
): ValuePart[] => {
|
||||
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||
return localize(`state.default.${state}`);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: localize(`state.default.${state}`),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
@@ -73,19 +102,27 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
DURATION_UNITS.includes(attributes.unit_of_measurement)
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
),
|
||||
},
|
||||
];
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
// state is monetary
|
||||
if (attributes.device_class === "monetary") {
|
||||
let parts: Record<string, string>[] = [];
|
||||
try {
|
||||
return formatNumber(state, locale, {
|
||||
parts = formatNumberToParts(state, locale, {
|
||||
style: "currency",
|
||||
currency: attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
@@ -98,8 +135,34 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
// default processing of numeric values
|
||||
const value = formatNumber(
|
||||
state,
|
||||
locale,
|
||||
@@ -114,10 +177,14 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes.unit_of_measurement;
|
||||
|
||||
if (unit) {
|
||||
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
|
||||
return [
|
||||
{ type: "value", value: value },
|
||||
{ type: "literal", value: blankBeforeUnit(unit, locale) },
|
||||
{ type: "unit", value: unit },
|
||||
];
|
||||
}
|
||||
|
||||
return value;
|
||||
return [{ type: "value", value: value }];
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
@@ -129,36 +196,51 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
const components = state.split(" ");
|
||||
if (components.length === 2) {
|
||||
// Date and time.
|
||||
return formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (components.length === 1) {
|
||||
if (state.includes("-")) {
|
||||
// Date only.
|
||||
return formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (state.includes(":")) {
|
||||
// Time only.
|
||||
const now = new Date();
|
||||
return formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
} catch (_e) {
|
||||
// Formatting methods may throw error if date parsing doesn't go well,
|
||||
// just return the state string in that case.
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,25 +264,58 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(state), locale, config);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(new Date(state), locale, config),
|
||||
},
|
||||
];
|
||||
} catch (_err) {
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value:
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const computeStateToParts = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): ValuePart[] => {
|
||||
const entity = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
return computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
@@ -52,7 +51,22 @@ export const formatNumber = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
): string =>
|
||||
formatNumberToParts(num, localeOptions, options)
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
|
||||
/**
|
||||
* Returns an array of objects containing the formatted number in parts
|
||||
* Similar to Intl.NumberFormat.prototype.formatToParts()
|
||||
*
|
||||
* Input params - same as for formatNumber()
|
||||
*/
|
||||
export const formatNumberToParts = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): any[] => {
|
||||
const locale = localeOptions
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
@@ -71,7 +85,7 @@ export const formatNumber = (
|
||||
return new Intl.NumberFormat(
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
).formatToParts(Number(num));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -86,15 +100,10 @@ export const formatNumber = (
|
||||
...options,
|
||||
useGrouping: false,
|
||||
})
|
||||
).format(Number(num));
|
||||
).formatToParts(Number(num));
|
||||
}
|
||||
|
||||
if (typeof num === "string") {
|
||||
return num;
|
||||
}
|
||||
return `${round(num, options?.maximumFractionDigits).toString()}${
|
||||
options?.style === "currency" ? ` ${options.currency}` : ""
|
||||
}`;
|
||||
return [{ type: "literal", value: num }];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
28
src/common/number/normalize-by-si-prefix.ts
Normal file
28
src/common/number/normalize-by-si-prefix.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -12,6 +12,10 @@ export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => string;
|
||||
export type FormatEntityStateToPartsFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => ValuePart[];
|
||||
export type FormatEntityAttributeValueFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
@@ -46,12 +50,13 @@ export const computeFormatFunctions = async (
|
||||
sensorNumericDeviceClasses: string[]
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityStateToParts: FormatEntityStateToPartsFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } =
|
||||
const { computeStateDisplay, computeStateToParts } =
|
||||
await import("../entity/compute_state_display");
|
||||
const {
|
||||
computeAttributeValueDisplay,
|
||||
@@ -70,6 +75,16 @@ export const computeFormatFunctions = async (
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityStateToParts: (stateObj, state) =>
|
||||
computeStateToParts(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
computeAttributeValueDisplay(
|
||||
localize,
|
||||
|
||||
@@ -18,8 +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 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";
|
||||
@@ -27,7 +30,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
|
||||
@@ -92,10 +94,18 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
|
||||
private _suspendResize = false;
|
||||
|
||||
private _layoutTransitionActive = false;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: () => {
|
||||
if (this.chart) {
|
||||
if (this._suspendResize) {
|
||||
this._shouldResizeChart = true;
|
||||
return;
|
||||
}
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
this._shouldResizeChart = true;
|
||||
} else {
|
||||
@@ -113,8 +123,11 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
private _pendingSetup = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
@@ -126,7 +139,13 @@ export class HaChartBase extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._setupChart();
|
||||
this._pendingSetup = true;
|
||||
afterNextRender(() => {
|
||||
if (this.isConnected && this._pendingSetup) {
|
||||
this._pendingSetup = false;
|
||||
this._setupChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._listeners.push(
|
||||
@@ -181,6 +200,26 @@ export class HaChartBase extends LitElement {
|
||||
() => window.removeEventListener("keyup", handleKeyUp)
|
||||
);
|
||||
}
|
||||
|
||||
const handleLayoutTransition: EventListener = (ev) => {
|
||||
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
|
||||
this._layoutTransitionActive = Boolean(event.detail?.active);
|
||||
this.toggleAttribute(
|
||||
"layout-transition-active",
|
||||
this._layoutTransitionActive
|
||||
);
|
||||
this._suspendResize = this._layoutTransitionActive;
|
||||
if (!this._suspendResize) {
|
||||
this._resizeChartIfNeeded();
|
||||
}
|
||||
};
|
||||
window.addEventListener("hass-layout-transition", handleLayoutTransition);
|
||||
this._listeners.push(() =>
|
||||
window.removeEventListener(
|
||||
"hass-layout-transition",
|
||||
handleLayoutTransition
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
@@ -988,19 +1027,29 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize({
|
||||
animation:
|
||||
this._reducedMotion ||
|
||||
typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
this._resizeChartIfNeeded();
|
||||
};
|
||||
|
||||
private _resizeChartIfNeeded() {
|
||||
if (!this.chart || !this._shouldResizeChart) {
|
||||
return;
|
||||
}
|
||||
if (this._suspendResize) {
|
||||
return;
|
||||
}
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
return;
|
||||
}
|
||||
this.chart.resize({
|
||||
animation:
|
||||
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
@@ -1022,11 +1071,18 @@ export class HaChartBase extends LitElement {
|
||||
display: block;
|
||||
position: relative;
|
||||
letter-spacing: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
:host([layout-transition-active]),
|
||||
:host([layout-transition-active]) .container,
|
||||
:host([layout-transition-active]) .chart-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.container.has-height {
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
@@ -1034,6 +1090,7 @@ export class HaChartBase extends LitElement {
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
overflow: visible;
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
@@ -1058,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,
|
||||
},
|
||||
};
|
||||
@@ -572,6 +575,7 @@ export class StatisticsChart extends LitElement {
|
||||
let firstSum: number | null | undefined = null;
|
||||
stats.forEach((stat) => {
|
||||
const startDate = new Date(stat.start);
|
||||
const endDate = new Date(stat.end);
|
||||
if (prevDate === startDate) {
|
||||
return;
|
||||
}
|
||||
@@ -601,10 +605,25 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
@@ -619,16 +638,6 @@ export class StatisticsChart extends LitElement {
|
||||
isFinite(currentValue) &&
|
||||
!this._hiddenStats.has(statistic_id)
|
||||
) {
|
||||
// First, close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
// Then push the current state at now
|
||||
statTypes.forEach((type, i) => {
|
||||
const val: (number | null)[] = [];
|
||||
|
||||
@@ -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,13 +32,20 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
@state() private _hiddenColumns?: string[];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: DataTableSettingsDialogParams) {
|
||||
this._params = params;
|
||||
this._columnOrder = 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 });
|
||||
}
|
||||
@@ -92,12 +102,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 +160,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>
|
||||
`;
|
||||
}
|
||||
@@ -282,21 +292,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";
|
||||
@@ -636,7 +637,7 @@ export class HaDataTable extends LitElement {
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${i !== 0
|
||||
? " · "
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: nothing}${column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]}`
|
||||
@@ -1086,9 +1087,12 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row.empty-row {
|
||||
height: var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
height: max(
|
||||
var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
),
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1192,6 +1196,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__cell--numeric {
|
||||
text-align: var(--float-end);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon {
|
||||
|
||||
@@ -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";
|
||||
@@ -11,10 +11,8 @@ import {
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
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";
|
||||
@@ -192,7 +190,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
private _automationChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
@@ -217,10 +215,4 @@ export abstract class HaDeviceAutomationPicker<
|
||||
delete value.metadata;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,16 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import {
|
||||
isUnavailableState,
|
||||
UNAVAILABLE,
|
||||
UNKNOWN,
|
||||
} from "../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -180,16 +171,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
default:
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
return isUnavailableState(entityState.state)
|
||||
? "—"
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
: this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "value"
|
||||
)?.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +224,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
return entityState.attributes.unit_of_measurement || null;
|
||||
return (
|
||||
this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "unit"
|
||||
)?.value || null
|
||||
);
|
||||
}
|
||||
|
||||
private _clearInterval() {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
@@ -35,15 +36,19 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog 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 +56,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 +78,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 +96,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 +116,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 +134,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 +186,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 +212,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 +220,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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -48,7 +48,6 @@ export class HaAnsiToHtml extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
pre.wrap {
|
||||
|
||||
@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
"ui.components.area-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
|
||||
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
@@ -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;
|
||||
&: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 {
|
||||
|
||||
@@ -2,14 +2,12 @@ import type { PropertyValueMap } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
import type { AssistPipeline } from "../data/assist_pipeline";
|
||||
import { listAssistPipelines } from "../data/assist_pipeline";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const PREFERRED = "preferred";
|
||||
const LAST_USED = "last_used";
|
||||
@@ -41,6 +39,31 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const value = this.value ?? this._default;
|
||||
const options: HaSelectOption[] = [
|
||||
{
|
||||
value: PREFERRED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.includeLastUsed) {
|
||||
options.unshift({
|
||||
value: LAST_USED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
|
||||
});
|
||||
}
|
||||
|
||||
options.push(
|
||||
...this._pipelines.map((pipeline) => ({
|
||||
value: pipeline.id,
|
||||
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
|
||||
}))
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -49,33 +72,8 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${this.includeLastUsed
|
||||
? html`
|
||||
<ha-list-item .value=${LAST_USED}>
|
||||
${this.hass!.localize(
|
||||
"ui.components.pipeline-picker.last_used"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: null}
|
||||
<ha-list-item .value=${PREFERRED}>
|
||||
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
})}
|
||||
</ha-list-item>
|
||||
${this._pipelines.map(
|
||||
(pipeline) =>
|
||||
html`<ha-list-item .value=${pipeline.id}>
|
||||
${pipeline.name}
|
||||
(${formatLanguageCode(pipeline.language, this.hass.locale)})
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -96,17 +94,17 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === this._default)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === this._default)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === this._default ? undefined : target.value;
|
||||
this.value = value === this._default ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -260,14 +259,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
.disabled=${this.disabled}
|
||||
name="amPm"
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
.name=${"amPm"}
|
||||
@selected=${this._valueChanged}
|
||||
@closed=${stopPropagation}
|
||||
.options=${["AM", "PM"]}
|
||||
>
|
||||
<ha-list-item value="AM">AM</ha-list-item>
|
||||
<ha-list-item value="PM">PM</ha-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper
|
||||
@@ -282,10 +277,12 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
textField.name === "amPm" ? textField.value : Number(textField.value);
|
||||
textField.name === "amPm"
|
||||
? (ev as HaSelectSelectEvent).detail.value
|
||||
: Number(textField.value);
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
minutes: this.minutes,
|
||||
@@ -366,16 +363,12 @@ export class HaBaseTimeInput extends LitElement {
|
||||
ha-textfield:last-child {
|
||||
--text-field-border-top-right-radius: var(--mdc-shape-medium);
|
||||
}
|
||||
ha-select {
|
||||
--mdc-shape-small: 0;
|
||||
width: 85px;
|
||||
}
|
||||
:host([clearable]) .mdc-select__anchor {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -2,12 +2,11 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
|
||||
import { fetchBlueprints } from "../data/blueprint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
@@ -55,20 +54,16 @@ class HaBluePrintPicker extends LitElement {
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._blueprintChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<ha-list-item .value=${blueprint.path}>
|
||||
${blueprint.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
.options=${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => ({
|
||||
value: blueprint.path,
|
||||
label: blueprint.name,
|
||||
})
|
||||
)}
|
||||
>
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -82,8 +77,8 @@ class HaBluePrintPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev) {
|
||||
const newValue = ev.target.value;
|
||||
private _blueprintChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
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"]);
|
||||
|
||||
@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 +59,117 @@ 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 }>) => {
|
||||
if (this._sliderInteractionActive) {
|
||||
ev.preventDefault();
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDrawer).drawer
|
||||
) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
this._escapePressed = false;
|
||||
};
|
||||
|
||||
private _handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
}
|
||||
};
|
||||
|
||||
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 +185,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,13 +213,30 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -174,6 +336,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;
|
||||
}
|
||||
@@ -199,6 +375,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
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))
|
||||
@@ -221,6 +398,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 +434,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 +476,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"]) {
|
||||
@@ -212,17 +212,17 @@ 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 {
|
||||
|
||||
@@ -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";
|
||||
@@ -310,6 +311,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();
|
||||
}
|
||||
@@ -802,100 +808,105 @@ 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) + 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(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: var(--ha-border-radius-square) !important;
|
||||
}
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: var(--ha-border-radius-square) !important;
|
||||
}
|
||||
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
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";
|
||||
@@ -224,7 +224,7 @@ export class HaColorPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
ev.stopPropagation();
|
||||
const selected = ev.detail.value;
|
||||
const normalized =
|
||||
|
||||
@@ -89,7 +89,7 @@ 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
|
||||
@@ -263,15 +263,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);
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { ConfigEntry, SubEntry } from "../data/config_entries";
|
||||
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
||||
@@ -14,9 +13,8 @@ import { fetchIntegrationManifest } from "../data/integration";
|
||||
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -73,37 +71,35 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._agents.map((agent) => ({
|
||||
value: agent.id,
|
||||
label: agent.name,
|
||||
disabled:
|
||||
agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0,
|
||||
}));
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize(
|
||||
"ui.components.conversation-agent-picker.none"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize(
|
||||
"ui.components.coversation-agent-picker.conversation_agent"
|
||||
"ui.components.conversation-agent-picker.conversation_agent"
|
||||
)}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize(
|
||||
"ui.components.coversation-agent-picker.none"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._agents.map(
|
||||
(agent) =>
|
||||
html`<ha-list-item
|
||||
.value=${agent.id}
|
||||
.disabled=${agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0}
|
||||
>
|
||||
${agent.name}
|
||||
</ha-list-item>`
|
||||
)}</ha-select
|
||||
.options=${options}
|
||||
></ha-select
|
||||
>${(this._subConfigEntry &&
|
||||
this._configEntry?.supported_subentry_types[
|
||||
this._subConfigEntry.subentry_type
|
||||
@@ -238,17 +234,17 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._agents!.find((agent) => agent.id === this.value)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -65,6 +65,10 @@ export class HaDateRangePicker extends LitElement {
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
public timePicker = false;
|
||||
|
||||
public open(): void {
|
||||
this._openPicker();
|
||||
}
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public minimal = false;
|
||||
@@ -306,6 +310,15 @@ export class HaDateRangePicker extends LitElement {
|
||||
return dateRangePicker.vueComponent.$children[0];
|
||||
}
|
||||
|
||||
private _openPicker() {
|
||||
if (!this._dateRangePicker.open) {
|
||||
const datePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker div.date-range-inputs"
|
||||
) as any;
|
||||
datePicker?.click();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputClick() {
|
||||
// close the date picker, so it will open again on the click event
|
||||
if (this._dateRangePicker.open) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { haStyleDialog } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-dialog";
|
||||
import "./ha-button";
|
||||
import "./ha-dialog-footer";
|
||||
import "./ha-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
export class HaDialogDatePicker extends LitElement {
|
||||
@@ -22,6 +23,8 @@ export class HaDialogDatePicker extends LitElement {
|
||||
|
||||
@state() private _params?: DatePickerDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _value?: string;
|
||||
|
||||
public async showDialog(params: DatePickerDialogParams): Promise<void> {
|
||||
@@ -30,9 +33,14 @@ export class HaDialogDatePicker extends LitElement {
|
||||
await nextRender();
|
||||
this._params = params;
|
||||
this._value = params.value;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -41,7 +49,13 @@ export class HaDialogDatePicker extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog open @closed=${this.closeDialog}>
|
||||
return html`<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="small"
|
||||
without-header
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<app-datepicker
|
||||
.value=${this._value}
|
||||
.min=${this._params.min}
|
||||
@@ -50,34 +64,39 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></app-datepicker>
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
dialogaction="cancel"
|
||||
class="cancel-btn"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
|
||||
<div class="bottom-actions">
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
@@ -110,9 +129,18 @@ export class HaDialogDatePicker extends LitElement {
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
.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;
|
||||
--app-datepicker-accent-color: var(--primary-color);
|
||||
--app-datepicker-bg-color: transparent;
|
||||
--app-datepicker-color: var(--primary-text-color);
|
||||
@@ -129,11 +157,6 @@ export class HaDialogDatePicker extends LitElement {
|
||||
app-datepicker::part(body) {
|
||||
direction: ltr;
|
||||
}
|
||||
@media all and (min-width: 450px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 300px;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
app-datepicker {
|
||||
width: 100%;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,184 +1,447 @@
|
||||
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 { fireEvent } from "../common/dom/fire_event";
|
||||
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 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>
|
||||
`;
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
|
||||
/**
|
||||
* 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-border-radius - Border radius of the dialog surface.
|
||||
* @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;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
|
||||
protected renderHeading() {
|
||||
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("open")) {
|
||||
this._open = this.open;
|
||||
}
|
||||
}
|
||||
|
||||
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 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 () => {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.contentElement.removeEventListener("scroll", this._onScroll);
|
||||
}
|
||||
|
||||
private _onScroll = () => {
|
||||
this._updateScrolledAttribute();
|
||||
};
|
||||
|
||||
private _updateScrolledAttribute() {
|
||||
if (!this.contentElement) return;
|
||||
this.toggleAttribute("scrolled", this.contentElement.scrollTop !== 0);
|
||||
private _handleAfterShow = () => {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
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);
|
||||
-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-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 .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;
|
||||
}
|
||||
@eventOptions({ passive: true })
|
||||
private _handleBodyScroll(ev: Event) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
.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 _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleHide(ev: CustomEvent<{ source: Element }>) {
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDialog).dialog
|
||||
) {
|
||||
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);
|
||||
--ha-dialog-surface-background: var(
|
||||
--card-background-color,
|
||||
var(--ha-color-surface-default)
|
||||
);
|
||||
--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 {
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
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;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,18 @@ import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-layout-transition": { active: boolean; reason?: string };
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-layout-transition": HASSDomEvent<
|
||||
HASSDomEvents["hass-layout-transition"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
const blockingElements = (document as any).$blockingElements;
|
||||
|
||||
@@ -15,6 +27,30 @@ export class HaDrawer extends DrawerBase {
|
||||
|
||||
private _rtlStyle?: HTMLElement;
|
||||
|
||||
private _sidebarTransitionActive = false;
|
||||
|
||||
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
|
||||
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = true;
|
||||
fireEvent(window, "hass-layout-transition", {
|
||||
active: true,
|
||||
reason: "sidebar",
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
|
||||
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = false;
|
||||
fireEvent(window, "hass-layout-transition", {
|
||||
active: false,
|
||||
reason: "sidebar",
|
||||
});
|
||||
};
|
||||
|
||||
protected createAdapter() {
|
||||
return {
|
||||
...super.createAdapter(),
|
||||
@@ -63,6 +99,38 @@ export class HaDrawer extends DrawerBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
private async _setupSwipe() {
|
||||
const hammer = await import("../resources/hammer");
|
||||
this._mc = new hammer.Manager(document, {
|
||||
@@ -90,6 +158,16 @@ export class HaDrawer extends DrawerBase {
|
||||
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: initial !important;
|
||||
transition-property: transform, width;
|
||||
transition-duration:
|
||||
var(--mdc-drawer-transition-duration, 0.2s),
|
||||
var(--ha-animation-duration-normal);
|
||||
transition-timing-function:
|
||||
var(
|
||||
--mdc-drawer-transition-timing-function,
|
||||
cubic-bezier(0.4, 0, 0.2, 1)
|
||||
),
|
||||
ease;
|
||||
}
|
||||
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
|
||||
z-index: 200;
|
||||
@@ -103,6 +181,15 @@ export class HaDrawer extends DrawerBase {
|
||||
direction: var(--direction);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-inline-start var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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
|
||||
@@ -37,6 +39,7 @@ export class HaDropdownItem extends DropdownItem {
|
||||
|
||||
#check {
|
||||
visibility: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
@@ -46,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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,8 +2,15 @@ import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdo
|
||||
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";
|
||||
|
||||
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
* @param T - The type of the value of the selected item.
|
||||
*/
|
||||
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
item: Omit<HaDropdownItem, "value"> & { value: T };
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
@@ -16,11 +23,68 @@ export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown")
|
||||
// @ts-ignore Allow to set an alternative anchor element
|
||||
export class HaDropdown extends Dropdown {
|
||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | HaIconButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | HaIconButton | 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;
|
||||
}
|
||||
|
||||
/** 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 | HaIconButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
// @ts-ignore fallback to default trigger slot if no anchorElement is set
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -152,7 +153,9 @@ class HaDurationInput extends LitElement {
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
private _durationChanged(
|
||||
ev: ValueChangedEvent<TimeChangedEvent | undefined>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -315,8 +315,12 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
ha-list {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: 4px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--ha-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -179,6 +182,9 @@ export class HaFilterDomains extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -199,6 +202,9 @@ export class HaFilterIntegrations extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,9 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.floor-picker.add_new_sugestion",
|
||||
"ui.components.floor-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -76,6 +76,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)) {
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -148,7 +148,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -434,6 +434,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;
|
||||
|
||||
@@ -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,48 @@ 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::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,68 @@ 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::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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.label-picker.add_new_sugestion",
|
||||
"ui.components.label-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -107,9 +107,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
|
||||
@state() _defaultLanguages: string[] = [];
|
||||
|
||||
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
|
||||
@@ -194,7 +191,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
import { Dialog } from "@material/web/dialog/internal/dialog";
|
||||
import { styles } from "@material/web/dialog/internal/dialog-styles";
|
||||
import {
|
||||
type DialogAnimation,
|
||||
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
} from "@material/web/dialog/internal/animations";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
// workaround to be able to overlay a dialog with another dialog
|
||||
Dialog.addInitializer(async (instance) => {
|
||||
await instance.updateComplete;
|
||||
|
||||
const dialogInstance = instance as HaMdDialog;
|
||||
|
||||
// @ts-expect-error dialog is private
|
||||
dialogInstance.dialog.prepend(dialogInstance.scrim);
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.inset = 0;
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.zIndex = 0;
|
||||
|
||||
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
|
||||
dialogInstance.getOpenAnimation = () => {
|
||||
const animations = getOpenAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
dialogInstance.getCloseAnimation = () => {
|
||||
const animations = getCloseAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||
|
||||
/**
|
||||
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||
*
|
||||
*/
|
||||
@customElement("ha-md-dialog")
|
||||
export class HaMdDialog extends Dialog {
|
||||
/**
|
||||
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||
*/
|
||||
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||
public disableCancelAction = false;
|
||||
|
||||
private _polyfillDialogRegistered = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("cancel", this._handleCancel);
|
||||
|
||||
if (typeof HTMLDialogElement !== "function") {
|
||||
this.addEventListener("open", this._handleOpen);
|
||||
|
||||
if (!DIALOG_POLYFILL) {
|
||||
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||
}
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent open in older browsers and wait for polyfill to load
|
||||
private async _handleOpen(openEvent: Event) {
|
||||
openEvent.preventDefault();
|
||||
|
||||
if (this._polyfillDialogRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polyfillDialogRegistered = true;
|
||||
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||
const dialog = this.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement;
|
||||
|
||||
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||
dialogPolyfill.default.registerDialog(dialog);
|
||||
this.removeEventListener("open", this._handleOpen);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
private async _loadPolyfillStylesheet(href) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => resolve();
|
||||
link.onerror = () =>
|
||||
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||
|
||||
this.shadowRoot?.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
private _handleCancel(closeEvent: Event) {
|
||||
if (this.disableCancelAction) {
|
||||
closeEvent.preventDefault();
|
||||
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
|
||||
if (this.animate !== undefined) {
|
||||
dialogElement?.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(-1deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
{
|
||||
transform: "rotate(1.5deg)",
|
||||
"animation-timing-function": "ease-out",
|
||||
},
|
||||
{
|
||||
transform: "rotate(0deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 200,
|
||||
iterations: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-dialog-container-color: var(--card-background-color);
|
||||
--md-dialog-headline-color: var(--primary-text-color);
|
||||
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||
--md-sys-color-scrim: #000000;
|
||||
|
||||
--md-dialog-headline-weight: var(--ha-font-weight-normal);
|
||||
--md-dialog-headline-size: var(--ha-font-size-xl);
|
||||
--md-dialog-supporting-text-size: var(--ha-font-size-m);
|
||||
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
|
||||
:host([type="alert"]) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, 0);
|
||||
margin-bottom: var(--safe-area-inset-bottom, 0);
|
||||
margin-left: var(--safe-area-inset-left, 0);
|
||||
margin-right: var(--safe-area-inset-right, 0);
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
slot[name="actions"]::slotted(*) {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow: var(--dialog-content-overflow, auto);
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; /* overlay navigation */
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// by default the dialog open/close animation will be from/to the top
|
||||
// but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
|
||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide up
|
||||
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade in
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide down
|
||||
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade out
|
||||
[{ opacity: "1" }, { opacity: "0" }],
|
||||
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const getMobileOpenFromBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||
};
|
||||
|
||||
export const getMobileCloseToBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-dialog": HaMdDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Divider } from "@material/web/divider/internal/divider";
|
||||
import { styles } from "@material/web/divider/internal/divider-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-divider")
|
||||
export class HaMdDivider extends Divider {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { MenuItemEl } from "@material/web/menu/internal/menuitem/menu-item";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-menu-item")
|
||||
export class HaMdMenuItem extends MenuItemEl {
|
||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-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);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-menu-item-label-text-font: Roboto, sans-serif;
|
||||
}
|
||||
:host(.warning) {
|
||||
--md-menu-item-label-text-color: var(--error-color);
|
||||
--md-menu-item-leading-icon-color: var(--error-color);
|
||||
}
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu-item": HaMdMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Menu } from "@material/web/menu/internal/menu";
|
||||
import { styles } from "@material/web/menu/internal/menu-styles";
|
||||
import type { CloseMenuEvent } from "@material/web/menu/menu";
|
||||
import {
|
||||
CloseReason,
|
||||
KeydownCloseKey,
|
||||
} from "@material/web/menu/internal/controllers/shared";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||
|
||||
@customElement("ha-md-menu")
|
||||
export class HaMdMenu extends Menu {
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("close-menu", this._handleCloseMenu);
|
||||
}
|
||||
|
||||
private _handleCloseMenu(ev: CloseMenuEvent) {
|
||||
if (
|
||||
ev.detail.reason.kind === CloseReason.KEYDOWN &&
|
||||
ev.detail.reason.key === KeydownCloseKey.ESCAPE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu": HaMdMenu;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"close-menu": CloseMenuEvent;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { FilledSelect } from "@material/web/select/internal/filled-select";
|
||||
import { styles as sharedStyles } from "@material/web/select/internal/shared-styles";
|
||||
import { styles } from "@material/web/select/internal/filled-select-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select")
|
||||
export class HaMdSelect extends FilledSelect {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
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-variant: var(--secondary-text-color);
|
||||
|
||||
--md-sys-color-surface-container-highest: var(--input-fill-color);
|
||||
--md-sys-color-on-surface: var(--input-ink-color);
|
||||
|
||||
--md-sys-color-surface-container: var(--input-fill-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-sys-color-secondary-container: var(--input-fill-color);
|
||||
--md-menu-container-color: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select": HaMdSelect;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
|
||||
import { styles } from "@material/mwc-menu/mwc-menu.css";
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "./ha-list";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MenuBase {
|
||||
protected get listElement() {
|
||||
if (!this.listElement_) {
|
||||
this.listElement_ = this.renderRoot.querySelector("ha-list");
|
||||
return this.listElement_;
|
||||
}
|
||||
|
||||
return this.listElement_;
|
||||
}
|
||||
|
||||
protected renderList() {
|
||||
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
|
||||
const classes = this.renderListClasses();
|
||||
|
||||
return html`<ha-list
|
||||
rootTabbable
|
||||
.innerAriaLabel=${this.innerAriaLabel}
|
||||
.innerRole=${this.innerRole}
|
||||
.multi=${this.multi}
|
||||
class=${classMap(classes)}
|
||||
.itemRoles=${itemRoles}
|
||||
.wrapFocus=${this.wrapFocus}
|
||||
.activatable=${this.activatable}
|
||||
@action=${this.onAction}
|
||||
>
|
||||
<slot></slot>
|
||||
</ha-list>`;
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-menu": HaMenu;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { SupervisorMounts } from "../data/supervisor/mounts";
|
||||
import {
|
||||
@@ -15,9 +14,9 @@ import {
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@@ -52,60 +51,54 @@ class HaMountPicker extends LitElement {
|
||||
if (!this._mounts) {
|
||||
return nothing;
|
||||
}
|
||||
const dataDiskOption = html`<ha-list-item
|
||||
graphic="icon"
|
||||
.value=${_BACKUP_DATA_DISK_}
|
||||
>
|
||||
<span>
|
||||
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup"}
|
||||
</span>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
|
||||
</ha-list-item>`;
|
||||
|
||||
const options: HaSelectOption[] = this._filterMounts(
|
||||
this._mounts,
|
||||
this.usage
|
||||
).map((mount) => ({
|
||||
value: mount.name,
|
||||
label: mount.name,
|
||||
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
|
||||
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
|
||||
}`,
|
||||
iconPath:
|
||||
mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
: mount.usage === SupervisorMountUsage.SHARE
|
||||
? mdiFolder
|
||||
: mdiBackupRestore,
|
||||
}));
|
||||
|
||||
if (this.usage === SupervisorMountUsage.BACKUP) {
|
||||
const dataDiskOption = {
|
||||
value: _BACKUP_DATA_DISK_,
|
||||
iconPath: mdiHarddisk,
|
||||
label:
|
||||
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup",
|
||||
};
|
||||
if (
|
||||
!this._mounts.default_backup_mount ||
|
||||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
|
||||
) {
|
||||
options.unshift(dataDiskOption);
|
||||
} else {
|
||||
options.push(dataDiskOption);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.mount-picker.mount")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
@selected=${this._mountChanged}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${this.usage === SupervisorMountUsage.BACKUP &&
|
||||
(!this._mounts.default_backup_mount ||
|
||||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
|
||||
? dataDiskOption
|
||||
: nothing}
|
||||
${this._filterMounts(this._mounts, this.usage).map(
|
||||
(mount) =>
|
||||
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
|
||||
<span>${mount.name}</span>
|
||||
<span slot="secondary"
|
||||
>${mount.server}${mount.port
|
||||
? `:${mount.port}`
|
||||
: nothing}${mount.type === SupervisorMountType.NFS
|
||||
? mount.path
|
||||
: `:${mount.share}`}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
: mount.usage === SupervisorMountUsage.SHARE
|
||||
? mdiFolder
|
||||
: mdiBackupRestore}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
)}
|
||||
${this.usage === SupervisorMountUsage.BACKUP &&
|
||||
this._mounts.default_backup_mount
|
||||
? dataDiskOption
|
||||
: nothing}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -153,16 +146,10 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
private _mountChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
private _mountChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HaSelect;
|
||||
const newValue = target.value;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
if (newValue !== this.value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { mdiDevices, mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -237,6 +237,22 @@ export class HaNavigationPicker extends LitElement {
|
||||
addGroup("views", views);
|
||||
addGroup("other_routes", otherRoutes);
|
||||
|
||||
if (
|
||||
searchString &&
|
||||
!this._navigationItems.some((navItem) => navItem.id === searchString)
|
||||
) {
|
||||
items.push({
|
||||
id: searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.add_custom_path"
|
||||
),
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
sorting_label: searchString,
|
||||
group: "other_routes",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -262,12 +278,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(
|
||||
this.hass!.connection,
|
||||
// path should be null to fetch default lovelace panel
|
||||
panel.url_path === "lovelace" ? null : panel.url_path,
|
||||
true
|
||||
)
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
|
||||
@@ -196,7 +196,7 @@ export class HaPasswordField 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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user