mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 03:07:21 +00:00
Compare commits
23 Commits
add-automa
...
20251103.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561122f03d | ||
|
|
95311be034 | ||
|
|
1eda44a102 | ||
|
|
d76781eb91 | ||
|
|
82d44e051f | ||
|
|
fdc9f5a3b7 | ||
|
|
ee6c82aba9 | ||
|
|
11d3f5c2ba | ||
|
|
feb68ce373 | ||
|
|
7f9a9de157 | ||
|
|
8e1b6a3d3b | ||
|
|
6e6e5a53e2 | ||
|
|
0408734ec5 | ||
|
|
317519fc08 | ||
|
|
843d79eab4 | ||
|
|
165a757f06 | ||
|
|
ea8b730142 | ||
|
|
e88c97d625 | ||
|
|
7560988b76 | ||
|
|
eecd8077b6 | ||
|
|
cbab5c3f7b | ||
|
|
a5d27c8bb8 | ||
|
|
a6a340b5db |
183
.github/copilot-instructions.md
vendored
183
.github/copilot-instructions.md
vendored
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
|
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
|
||||||
|
|
||||||
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Quick Reference](#quick-reference)
|
- [Quick Reference](#quick-reference)
|
||||||
@@ -153,10 +151,6 @@ try {
|
|||||||
### Styling Guidelines
|
### Styling Guidelines
|
||||||
|
|
||||||
- **Use CSS custom properties**: Leverage the theme system
|
- **Use CSS custom properties**: Leverage the theme system
|
||||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
|
||||||
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
|
||||||
- Defined in `src/resources/theme/core.globals.ts`
|
|
||||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
|
||||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||||
- **Follow Material Design**: Use Material Web Components where appropriate
|
- **Follow Material Design**: Use Material Web Components where appropriate
|
||||||
- **Support RTL**: Ensure all layouts work in RTL languages
|
- **Support RTL**: Ensure all layouts work in RTL languages
|
||||||
@@ -165,68 +159,21 @@ try {
|
|||||||
static get styles() {
|
static get styles() {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
padding: var(--ha-space-4);
|
--spacing: 16px;
|
||||||
|
padding: var(--spacing);
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
background-color: var(--card-background-color);
|
background-color: var(--card-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
gap: var(--ha-space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
:host {
|
:host {
|
||||||
padding: var(--ha-space-2);
|
--spacing: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### View Transitions
|
|
||||||
|
|
||||||
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
|
|
||||||
|
|
||||||
**Core Resources:**
|
|
||||||
|
|
||||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
|
||||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
|
||||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
|
||||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
|
|
||||||
|
|
||||||
**Implementation Guidelines:**
|
|
||||||
|
|
||||||
1. Always use `withViewTransition()` wrapper for automatic fallback
|
|
||||||
2. Keep transitions simple (subtle crossfades and fades work best)
|
|
||||||
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
|
|
||||||
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
|
|
||||||
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
|
|
||||||
|
|
||||||
**Default Root Transition:**
|
|
||||||
|
|
||||||
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
|
|
||||||
|
|
||||||
**Important Constraints:**
|
|
||||||
|
|
||||||
- Each `view-transition-name` must be unique at any given time
|
|
||||||
- Only one view transition can run at a time
|
|
||||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
|
||||||
|
|
||||||
**Current Usage & Planned Applications:**
|
|
||||||
|
|
||||||
- Launch screen fade out (implemented)
|
|
||||||
- Automation sidebar transitions (planned - #27238)
|
|
||||||
- More info dialog content changes (planned - #27672)
|
|
||||||
- Toolbar navigation, ha-spinner transitions (planned)
|
|
||||||
|
|
||||||
**Specification & Documentation:**
|
|
||||||
|
|
||||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
|
||||||
|
|
||||||
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
|
|
||||||
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
|
|
||||||
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
|
|
||||||
|
|
||||||
### Performance Best Practices
|
### Performance Best Practices
|
||||||
|
|
||||||
- **Code split**: Split code at the panel/dialog level
|
- **Code split**: Split code at the panel/dialog level
|
||||||
@@ -248,9 +195,8 @@ For browser support, API details, and current specifications, refer to these aut
|
|||||||
|
|
||||||
**Available Dialog Types:**
|
**Available Dialog Types:**
|
||||||
|
|
||||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
- `ha-md-dialog` - Preferred for new code (Material Design 3)
|
||||||
- `ha-md-dialog` - Material Design 3 dialog component
|
- `ha-dialog` - Legacy component still widely used
|
||||||
- `ha-dialog` - Legacy component (still widely used)
|
|
||||||
|
|
||||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||||
|
|
||||||
@@ -265,45 +211,15 @@ fireEvent(this, "show-dialog", {
|
|||||||
**Dialog Implementation Requirements:**
|
**Dialog Implementation Requirements:**
|
||||||
|
|
||||||
- Implement `HassDialog<T>` interface
|
- Implement `HassDialog<T>` interface
|
||||||
- Use `@state() private _open = false` to control dialog visibility
|
- Use `createCloseHeading()` for standard headers
|
||||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
- Import `haStyleDialog` for consistent styling
|
||||||
- Return `nothing` when no params (loading state)
|
- Return `nothing` when no params (loading state)
|
||||||
- Fire `dialog-closed` event in `_dialogClosed()` handler
|
- Fire `dialog-closed` event when closing
|
||||||
- Use `header-title` attribute for simple titles
|
- Add `dialogInitialFocus` for accessibility
|
||||||
- Use `header-subtitle` attribute for simple subtitles
|
|
||||||
- Use slots for custom content where the standard attributes are not enough
|
|
||||||
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
|
|
||||||
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
|
|
||||||
|
|
||||||
**Dialog Sizing:**
|
````
|
||||||
|
|
||||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
|
||||||
- Custom sizing is NOT recommended - use the standard width presets
|
|
||||||
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
|
|
||||||
|
|
||||||
**Button Appearance Guidelines:**
|
|
||||||
|
|
||||||
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
|
|
||||||
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
|
|
||||||
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
|
|
||||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
|
||||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
|
||||||
|
|
||||||
**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-dialogs.markdown`
|
|
||||||
|
|
||||||
### Form Component (ha-form)
|
### Form Component (ha-form)
|
||||||
|
|
||||||
- Schema-driven using `HaFormSchema[]`
|
- Schema-driven using `HaFormSchema[]`
|
||||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||||
- Built-in validation with error display
|
- Built-in validation with error display
|
||||||
@@ -319,11 +235,7 @@ See these files for current patterns:
|
|||||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
```
|
````
|
||||||
|
|
||||||
**Gallery Documentation:**
|
|
||||||
|
|
||||||
- `gallery/src/pages/components/ha-form.markdown`
|
|
||||||
|
|
||||||
### Alert Component (ha-alert)
|
### Alert Component (ha-alert)
|
||||||
|
|
||||||
@@ -337,35 +249,6 @@ See these files for current patterns:
|
|||||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Gallery Documentation:**
|
|
||||||
|
|
||||||
- `gallery/src/pages/components/ha-alert.markdown`
|
|
||||||
|
|
||||||
### Keyboard Shortcuts (ShortcutManager)
|
|
||||||
|
|
||||||
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
|
|
||||||
- Automatically blocks shortcuts when input fields are focused
|
|
||||||
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
|
|
||||||
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
- **Class definition**: `src/common/keyboard/shortcuts.ts`
|
|
||||||
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
|
|
||||||
|
|
||||||
### Tooltip Component (ha-tooltip)
|
|
||||||
|
|
||||||
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
- **Component definition**: `src/components/ha-tooltip.ts`
|
|
||||||
- **Usage example**: `src/components/ha-label.ts`
|
|
||||||
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
|
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
### Creating a Panel
|
### Creating a Panel
|
||||||
@@ -406,19 +289,11 @@ export class DialogMyFeature
|
|||||||
@state()
|
@state()
|
||||||
private _params?: MyDialogParams;
|
private _params?: MyDialogParams;
|
||||||
|
|
||||||
@state()
|
|
||||||
private _open = false;
|
|
||||||
|
|
||||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._open = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dialogClosed(): void {
|
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
@@ -429,27 +304,23 @@ export class DialogMyFeature
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-wa-dialog
|
<ha-dialog
|
||||||
.hass=${this.hass}
|
open
|
||||||
.open=${this._open}
|
@closed=${this.closeDialog}
|
||||||
header-title=${this._params.title}
|
.heading=${createCloseHeading(this.hass, this._params.title)}
|
||||||
header-subtitle=${this._params.subtitle}
|
|
||||||
@closed=${this._dialogClosed}
|
|
||||||
>
|
>
|
||||||
<p>Dialog content</p>
|
<!-- Dialog content -->
|
||||||
<ha-dialog-footer slot="footer">
|
<ha-button
|
||||||
<ha-button
|
appearance="plain"
|
||||||
slot="secondaryAction"
|
@click=${this.closeDialog}
|
||||||
appearance="plain"
|
slot="secondaryAction"
|
||||||
@click=${this.closeDialog}
|
>
|
||||||
>
|
${this.hass.localize("ui.common.cancel")}
|
||||||
${this.hass.localize("ui.common.cancel")}
|
</ha-button>
|
||||||
</ha-button>
|
<ha-button @click=${this._submit} slot="primaryAction">
|
||||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
${this.hass.localize("ui.common.save")}
|
||||||
${this.hass.localize("ui.common.save")}
|
</ha-button>
|
||||||
</ha-button>
|
</ha-dialog>
|
||||||
</ha-dialog-footer>
|
|
||||||
</ha-wa-dialog>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@@ -36,14 +36,14 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||||
|
|||||||
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
|
|||||||
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
|
|||||||
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||||
|
|||||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
- name: Send bundle stats and build information to RelativeCI
|
||||||
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0
|
uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0
|
||||||
with:
|
with:
|
||||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|||||||
12
.github/workflows/release.yaml
vendored
12
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.whl
|
dist/*.whl
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
- name: Tar folder
|
- name: Tar folder
|
||||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||||
- name: Upload release asset
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
@@ -137,6 +137,6 @@ jobs:
|
|||||||
- name: Tar folder
|
- name: Tar folder
|
||||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||||
- name: Upload release asset
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
||||||
|
|||||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
|||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.11.0.cjs
|
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
|
|||||||
module.exports.ignorePackages = () => [];
|
module.exports.ignorePackages = () => [];
|
||||||
|
|
||||||
// Files from NPM packages that we should replace with empty file
|
// Files from NPM packages that we should replace with empty file
|
||||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
module.exports.emptyPackages = ({ isHassioBuild }) =>
|
||||||
[
|
[
|
||||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||||
(isHassioBuild || isLandingPageBuild) &&
|
isHassioBuild &&
|
||||||
require.resolve(
|
require.resolve(
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||||
),
|
),
|
||||||
(isHassioBuild || isLandingPageBuild) &&
|
isHassioBuild &&
|
||||||
require.resolve(
|
require.resolve(
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||||
),
|
),
|
||||||
@@ -337,7 +337,6 @@ module.exports.config = {
|
|||||||
publicPath: publicPath(latestBuild),
|
publicPath: publicPath(latestBuild),
|
||||||
isProdBuild,
|
isProdBuild,
|
||||||
latestBuild,
|
latestBuild,
|
||||||
isLandingPageBuild: true,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const createRspackConfig = ({
|
|||||||
isStatsBuild,
|
isStatsBuild,
|
||||||
isTestBuild,
|
isTestBuild,
|
||||||
isHassioBuild,
|
isHassioBuild,
|
||||||
isLandingPageBuild,
|
|
||||||
dontHash,
|
dontHash,
|
||||||
}) => {
|
}) => {
|
||||||
if (!dontHash) {
|
if (!dontHash) {
|
||||||
@@ -169,9 +168,7 @@ const createRspackConfig = ({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new rspack.NormalModuleReplacementPlugin(
|
new rspack.NormalModuleReplacementPlugin(
|
||||||
new RegExp(
|
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
|
||||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
|
||||||
),
|
|
||||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||||
),
|
),
|
||||||
!isProdBuild && new LogStartCompilePlugin(),
|
!isProdBuild && new LogStartCompilePlugin(),
|
||||||
@@ -260,6 +257,7 @@ const createRspackConfig = ({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
experiments: {
|
experiments: {
|
||||||
|
layers: true,
|
||||||
outputModule: true,
|
outputModule: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-yaml-editor";
|
import "../../../../src/components/ha-yaml-editor";
|
||||||
import type { LegacyTrigger } from "../../../../src/data/automation";
|
import type { Trigger } from "../../../../src/data/automation";
|
||||||
import { describeTrigger } from "../../../../src/data/automation_i18n";
|
import { describeTrigger } from "../../../../src/data/automation_i18n";
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
@@ -66,7 +66,7 @@ const triggers = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialTrigger: LegacyTrigger = {
|
const initialTrigger: Trigger = {
|
||||||
trigger: "state",
|
trigger: "state",
|
||||||
entity_id: "light.kitchen",
|
entity_id: "light.kitchen",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
title: Dropdown
|
|
||||||
---
|
|
||||||
|
|
||||||
# Dropdown `<ha-dropdown>`
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdown` supports composed menu items (`<ha-dropdown-item>`) for icons, submenus, checkboxes, disabled entries, and destructive variants. Use composition with `slot="trigger"` to control the trigger button and use `<ha-dropdown-item>` for rich item content.
|
|
||||||
|
|
||||||
### Example usage (composition)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ha-dropdown>
|
|
||||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon .path="mdiContentCut" slot="icon"></ha-svg-icon>
|
|
||||||
Cut
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon .path="mdiContentCopy" slot="icon"></ha-svg-icon>
|
|
||||||
Copy
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item disabled>
|
|
||||||
<ha-svg-icon .path="mdiContentPaste" slot="icon"></ha-svg-icon>
|
|
||||||
Paste
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
Show images
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-all-images"
|
|
||||||
>Show all images</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-thumbnails"
|
|
||||||
>Show thumbnails</ha-dropdown-item
|
|
||||||
>
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item type="checkbox" checked>Emoji shortcuts</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item type="checkbox" checked>Word wrap</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item variant="danger">
|
|
||||||
<ha-svg-icon .path="mdiDelete" slot="icon"></ha-svg-icon>
|
|
||||||
Delete
|
|
||||||
</ha-dropdown-item>
|
|
||||||
</ha-dropdown>
|
|
||||||
```
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
This component is based on the webawesome dropdown component.
|
|
||||||
Check the [webawesome documentation](https://webawesome.com/docs/components/dropdown/) for more details.
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import "@home-assistant/webawesome/dist/components/button/button";
|
|
||||||
import "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
|
||||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
|
||||||
import "@home-assistant/webawesome/dist/components/popup/popup";
|
|
||||||
import {
|
|
||||||
mdiContentCopy,
|
|
||||||
mdiContentCut,
|
|
||||||
mdiContentPaste,
|
|
||||||
mdiDelete,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import type { TemplateResult } from "lit";
|
|
||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
|
||||||
import "../../../../src/components/ha-button";
|
|
||||||
import "../../../../src/components/ha-card";
|
|
||||||
import "../../../../src/components/ha-dropdown";
|
|
||||||
import "../../../../src/components/ha-dropdown-item";
|
|
||||||
import "../../../../src/components/ha-icon-button";
|
|
||||||
import "../../../../src/components/ha-svg-icon";
|
|
||||||
|
|
||||||
@customElement("demo-components-ha-dropdown")
|
|
||||||
export class DemoHaDropdown extends LitElement {
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
${["light", "dark"].map(
|
|
||||||
(mode) => html`
|
|
||||||
<div class=${mode}>
|
|
||||||
<ha-card header="ha-button in ${mode}">
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-dropdown>
|
|
||||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiContentCut}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
Cut
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiContentCopy}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
Copy
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item disabled>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiContentPaste}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
Paste
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item>
|
|
||||||
Show images
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-all-images"
|
|
||||||
>Show All Images</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-thumbnails"
|
|
||||||
>Show Thumbnails</ha-dropdown-item
|
|
||||||
>
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item type="checkbox" checked
|
|
||||||
>Emoji Shortcuts</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item type="checkbox" checked
|
|
||||||
>Word Wrap</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item variant="danger">
|
|
||||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
|
||||||
Delete
|
|
||||||
</ha-dropdown-item>
|
|
||||||
</ha-dropdown>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
firstUpdated(changedProps) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
applyThemesOnElement(
|
|
||||||
this.shadowRoot!.querySelector(".dark"),
|
|
||||||
{
|
|
||||||
default_theme: "default",
|
|
||||||
default_dark_theme: "default",
|
|
||||||
themes: {},
|
|
||||||
darkMode: true,
|
|
||||||
theme: "default",
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.dark,
|
|
||||||
.light {
|
|
||||||
display: block;
|
|
||||||
background-color: var(--primary-background-color);
|
|
||||||
padding: 0 50px;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
padding: unset;
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
margin: 24px auto;
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
.card-content div {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"demo-components-ha-dropdown": DemoHaDropdown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
import "@material/mwc-linear-progress";
|
import "@material/mwc-linear-progress";
|
||||||
import { mdiOpenInNew } from "@mdi/js";
|
import { type PropertyValues, css, html, nothing } from "lit";
|
||||||
import { css, html, nothing, type PropertyValues } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
|
||||||
import "../../src/components/ha-alert";
|
import "../../src/components/ha-alert";
|
||||||
import "../../src/components/ha-button";
|
|
||||||
import "../../src/components/ha-fade-in";
|
import "../../src/components/ha-fade-in";
|
||||||
import "../../src/components/ha-spinner";
|
import "../../src/components/ha-spinner";
|
||||||
import "../../src/components/ha-svg-icon";
|
|
||||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
|
||||||
import "../../src/onboarding/onboarding-welcome-links";
|
|
||||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
|
||||||
import { haStyle } from "../../src/resources/styles";
|
import { haStyle } from "../../src/resources/styles";
|
||||||
import "./components/landing-page-logs";
|
import "../../src/onboarding/onboarding-welcome-links";
|
||||||
import "./components/landing-page-network";
|
import "./components/landing-page-network";
|
||||||
|
import "./components/landing-page-logs";
|
||||||
|
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||||
|
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||||
|
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||||
|
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||||
import {
|
import {
|
||||||
getSupervisorNetworkInfo,
|
getSupervisorNetworkInfo,
|
||||||
pingSupervisor,
|
pingSupervisor,
|
||||||
type NetworkInfo,
|
type NetworkInfo,
|
||||||
} from "./data/supervisor";
|
} from "./data/supervisor";
|
||||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
|
||||||
|
|
||||||
export const ASSUME_CORE_START_SECONDS = 60;
|
export const ASSUME_CORE_START_SECONDS = 60;
|
||||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||||
@@ -97,21 +94,16 @@ class HaLandingPage extends LandingPageBaseElement {
|
|||||||
<ha-language-picker
|
<ha-language-picker
|
||||||
.value=${this.language}
|
.value=${this.language}
|
||||||
.label=${""}
|
.label=${""}
|
||||||
button-style
|
|
||||||
native-name
|
native-name
|
||||||
@value-changed=${this._languageChanged}
|
@value-changed=${this._languageChanged}
|
||||||
inline-arrow
|
inline-arrow
|
||||||
></ha-language-picker>
|
></ha-language-picker>
|
||||||
<ha-button
|
<a
|
||||||
appearance="plain"
|
|
||||||
variant="neutral"
|
|
||||||
href="https://www.home-assistant.io/getting-started/onboarding/"
|
href="https://www.home-assistant.io/getting-started/onboarding/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
|
>${this.localize("ui.panel.page-onboarding.help")}</a
|
||||||
>
|
>
|
||||||
${this.localize("ui.panel.page-onboarding.help")}
|
|
||||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -226,8 +218,26 @@ class HaLandingPage extends LandingPageBaseElement {
|
|||||||
ha-alert p {
|
ha-alert p {
|
||||||
text-align: unset;
|
text-align: unset;
|
||||||
}
|
}
|
||||||
.footer ha-svg-icon {
|
ha-language-picker {
|
||||||
--mdc-icon-size: var(--ha-space-5);
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
border-radius: var(--ha-border-radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
--ha-select-height: 40px;
|
||||||
|
--mdc-select-fill-color: none;
|
||||||
|
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||||
|
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||||
|
--mdc-select-idle-line-color: transparent;
|
||||||
|
--mdc-select-hover-line-color: transparent;
|
||||||
|
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||||
|
--mdc-shape-small: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-inline-end: 16px;
|
||||||
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
ha-fade-in {
|
ha-fade-in {
|
||||||
min-height: calc(100vh - 64px - 88px);
|
min-height: calc(100vh - 64px - 88px);
|
||||||
|
|||||||
49
package.json
49
package.json
@@ -52,7 +52,7 @@
|
|||||||
"@fullcalendar/list": "6.1.19",
|
"@fullcalendar/list": "6.1.19",
|
||||||
"@fullcalendar/luxon3": "6.1.19",
|
"@fullcalendar/luxon3": "6.1.19",
|
||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@home-assistant/webawesome": "3.0.0-ha.0",
|
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
|
||||||
"@lezer/highlight": "1.2.3",
|
"@lezer/highlight": "1.2.3",
|
||||||
"@lit-labs/motion": "1.0.9",
|
"@lit-labs/motion": "1.0.9",
|
||||||
"@lit-labs/observers": "2.0.6",
|
"@lit-labs/observers": "2.0.6",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "2.4.1",
|
"@material/web": "2.4.0",
|
||||||
"@mdi/js": "7.4.47",
|
"@mdi/js": "7.4.47",
|
||||||
"@mdi/svg": "7.4.47",
|
"@mdi/svg": "7.4.47",
|
||||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.9.1",
|
"@tsparticles/engine": "3.9.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
"@vaadin/combo-box": "24.9.5",
|
"@vaadin/combo-box": "24.9.2",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.9.5",
|
"@vaadin/vaadin-themable-mixin": "24.9.2",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
@@ -111,18 +111,18 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "1.6.14",
|
"hls.js": "1.6.13",
|
||||||
"home-assistant-js-websocket": "9.6.0",
|
"home-assistant-js-websocket": "9.5.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.18",
|
"intl-messageformat": "10.7.18",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||||
"leaflet.markercluster": "1.5.3",
|
"leaflet.markercluster": "1.5.3",
|
||||||
"lit": "3.3.1",
|
"lit": "3.3.1",
|
||||||
"lit-html": "3.3.1",
|
"lit-html": "3.3.1",
|
||||||
"luxon": "3.7.2",
|
"luxon": "3.7.2",
|
||||||
"marked": "17.0.0",
|
"marked": "16.4.1",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "4.0.3",
|
"node-vibrant": "4.0.3",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@@ -152,13 +152,13 @@
|
|||||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||||
"@babel/plugin-transform-runtime": "7.28.5",
|
"@babel/plugin-transform-runtime": "7.28.5",
|
||||||
"@babel/preset-env": "7.28.5",
|
"@babel/preset-env": "7.28.5",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
"@bundle-stats/plugin-webpack-filter": "4.21.5",
|
||||||
"@lokalise/node-api": "15.3.1",
|
"@lokalise/node-api": "15.3.1",
|
||||||
"@octokit/auth-oauth-device": "8.0.3",
|
"@octokit/auth-oauth-device": "8.0.2",
|
||||||
"@octokit/plugin-retry": "8.0.3",
|
"@octokit/plugin-retry": "8.0.2",
|
||||||
"@octokit/rest": "22.0.1",
|
"@octokit/rest": "22.0.0",
|
||||||
"@rsdoctor/rspack-plugin": "1.3.8",
|
"@rsdoctor/rspack-plugin": "1.3.4",
|
||||||
"@rspack/core": "1.6.1",
|
"@rspack/core": "1.5.8",
|
||||||
"@rspack/dev-server": "1.1.4",
|
"@rspack/dev-server": "1.1.4",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.22",
|
"@types/chromecast-caf-receiver": "6.0.22",
|
||||||
@@ -178,12 +178,12 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@vitest/coverage-v8": "4.0.8",
|
"@vitest/coverage-v8": "4.0.3",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.1",
|
"del": "8.0.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.38.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
@@ -194,14 +194,14 @@
|
|||||||
"eslint-plugin-wc": "3.0.2",
|
"eslint-plugin-wc": "3.0.2",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "11.3.2",
|
||||||
"glob": "12.0.0",
|
"glob": "11.0.3",
|
||||||
"gulp": "5.0.1",
|
"gulp": "5.0.1",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
"gulp-rename": "2.1.0",
|
"gulp-rename": "2.1.0",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "27.1.0",
|
"jsdom": "27.0.1",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "16.2.6",
|
"lint-staged": "16.2.6",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
@@ -213,13 +213,13 @@
|
|||||||
"rspack-manifest-plugin": "5.1.0",
|
"rspack-manifest-plugin": "5.1.0",
|
||||||
"serve": "14.2.5",
|
"serve": "14.2.5",
|
||||||
"sinon": "21.0.0",
|
"sinon": "21.0.0",
|
||||||
"tar": "7.5.2",
|
"tar": "7.5.1",
|
||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.46.3",
|
"typescript-eslint": "8.46.2",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "4.0.8",
|
"vitest": "4.0.3",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
@@ -231,12 +231,11 @@
|
|||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "2.1.1",
|
"@lit/reactive-element": "2.1.1",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
"globals": "16.5.0",
|
"globals": "16.4.0",
|
||||||
"tslib": "2.8.1",
|
"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",
|
"@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.11.0",
|
"packageManager": "yarn@4.10.3",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.21.1"
|
"node": "22.21.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20251029.0"
|
version = "20251103.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
license-files = ["LICENSE*"]
|
license-files = ["LICENSE*"]
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import { genClientId } from "home-assistant-js-websocket";
|
import { genClientId } from "home-assistant-js-websocket";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { keyed } from "lit/directives/keyed";
|
import { keyed } from "lit/directives/keyed";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/ha-alert";
|
import "../components/ha-alert";
|
||||||
import "../components/ha-button";
|
import "../components/ha-button";
|
||||||
@@ -59,8 +59,7 @@ export class HaAuthFlow extends LitElement {
|
|||||||
willUpdate(changedProps: PropertyValues) {
|
willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
if (!this.hasUpdated && this.clientId === genClientId()) {
|
if (!this.hasUpdated) {
|
||||||
// Preselect store token when logging in to own instance
|
|
||||||
this._storeToken = this.initStoreToken;
|
this._storeToken = this.initStoreToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,9 +117,6 @@ export class HaAuthFlow extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
.action ha-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<form>${this._renderForm()}</form>
|
<form>${this._renderForm()}</form>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable lit/prefer-static-styles */
|
/* eslint-disable lit/prefer-static-styles */
|
||||||
import { mdiOpenInNew } from "@mdi/js";
|
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@@ -7,8 +6,6 @@ import punycode from "punycode";
|
|||||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||||
import "../components/ha-alert";
|
import "../components/ha-alert";
|
||||||
import "../components/ha-button";
|
|
||||||
import "../components/ha-svg-icon";
|
|
||||||
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
|
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
|
||||||
import { fetchAuthProviders } from "../data/auth";
|
import { fetchAuthProviders } from "../data/auth";
|
||||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||||
@@ -136,8 +133,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.footer ha-svg-icon {
|
ha-language-picker {
|
||||||
--mdc-icon-size: var(--ha-space-5);
|
width: 200px;
|
||||||
|
border-radius: var(--ha-border-radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
--ha-select-height: 40px;
|
||||||
|
--mdc-select-fill-color: none;
|
||||||
|
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||||
|
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||||
|
--mdc-select-idle-line-color: transparent;
|
||||||
|
--mdc-select-hover-line-color: transparent;
|
||||||
|
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||||
|
--mdc-shape-small: 0;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-inline-end: 16px;
|
||||||
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--ha-font-size-3xl);
|
font-size: var(--ha-font-size-3xl);
|
||||||
@@ -191,21 +205,16 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
<ha-language-picker
|
<ha-language-picker
|
||||||
.value=${this.language}
|
.value=${this.language}
|
||||||
.label=${""}
|
.label=${""}
|
||||||
button-style
|
|
||||||
native-name
|
native-name
|
||||||
@value-changed=${this._languageChanged}
|
@value-changed=${this._languageChanged}
|
||||||
inline-arrow
|
inline-arrow
|
||||||
></ha-language-picker>
|
></ha-language-picker>
|
||||||
<ha-button
|
<a
|
||||||
appearance="plain"
|
|
||||||
variant="neutral"
|
|
||||||
href="https://www.home-assistant.io/docs/authentication/"
|
href="https://www.home-assistant.io/docs/authentication/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
|
>${this.localize("ui.panel.page-authorize.help")}</a
|
||||||
>
|
>
|
||||||
${this.localize("ui.panel.page-authorize.help")}
|
|
||||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
|
||||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
|
||||||
|
|
||||||
export interface AreasFloorHierarchy {
|
|
||||||
floors: {
|
|
||||||
id: string;
|
|
||||||
areas: string[];
|
|
||||||
}[];
|
|
||||||
areas: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAreasFloorHierarchy = (
|
|
||||||
floors: FloorRegistryEntry[],
|
|
||||||
areas: AreaRegistryEntry[]
|
|
||||||
): AreasFloorHierarchy => {
|
|
||||||
const floorAreas = new Map<string, string[]>();
|
|
||||||
const unassignedAreas: string[] = [];
|
|
||||||
|
|
||||||
for (const area of areas) {
|
|
||||||
if (area.floor_id) {
|
|
||||||
if (!floorAreas.has(area.floor_id)) {
|
|
||||||
floorAreas.set(area.floor_id, []);
|
|
||||||
}
|
|
||||||
floorAreas.get(area.floor_id)!.push(area.area_id);
|
|
||||||
} else {
|
|
||||||
unassignedAreas.push(area.area_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hierarchy: AreasFloorHierarchy = {
|
|
||||||
floors: floors.map((floor) => ({
|
|
||||||
id: floor.floor_id,
|
|
||||||
areas: floorAreas.get(floor.floor_id) || [],
|
|
||||||
})),
|
|
||||||
areas: unassignedAreas,
|
|
||||||
};
|
|
||||||
|
|
||||||
return hierarchy;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAreasOrder = (hierarchy: AreasFloorHierarchy): string[] => {
|
|
||||||
const order: string[] = [];
|
|
||||||
|
|
||||||
for (const floor of hierarchy.floors) {
|
|
||||||
order.push(...floor.areas);
|
|
||||||
}
|
|
||||||
order.push(...hierarchy.areas);
|
|
||||||
|
|
||||||
return order;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFloorOrder = (hierarchy: AreasFloorHierarchy): string[] =>
|
|
||||||
hierarchy.floors.map((floor) => floor.id);
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import type {
|
|
||||||
Condition,
|
|
||||||
TimeCondition,
|
|
||||||
} from "../../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract media queries from conditions recursively
|
|
||||||
*/
|
|
||||||
export function extractMediaQueries(conditions: Condition[]): string[] {
|
|
||||||
return conditions.reduce<string[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractMediaQueries(c.conditions));
|
|
||||||
}
|
|
||||||
if (c.condition === "screen" && c.media_query) {
|
|
||||||
array.push(c.media_query);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract time conditions from conditions recursively
|
|
||||||
*/
|
|
||||||
export function extractTimeConditions(
|
|
||||||
conditions: Condition[]
|
|
||||||
): TimeCondition[] {
|
|
||||||
return conditions.reduce<TimeCondition[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractTimeConditions(c.conditions));
|
|
||||||
}
|
|
||||||
if (c.condition === "time") {
|
|
||||||
array.push(c);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { listenMediaQuery } from "../dom/media_query";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
import { extractMediaQueries, extractTimeConditions } from "./extract";
|
|
||||||
import { calculateNextTimeUpdate } from "./time-calculator";
|
|
||||||
|
|
||||||
/** Maximum delay for setTimeout (2^31 - 1 milliseconds, ~24.8 days)
|
|
||||||
* Values exceeding this will overflow and execute immediately
|
|
||||||
*
|
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value
|
|
||||||
*/
|
|
||||||
const MAX_TIMEOUT_DELAY = 2147483647;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to setup media query listeners for conditional visibility
|
|
||||||
*/
|
|
||||||
export function setupMediaQueryListeners(
|
|
||||||
conditions: Condition[],
|
|
||||||
hass: HomeAssistant,
|
|
||||||
addListener: (unsub: () => void) => void,
|
|
||||||
onUpdate: (conditionsMet: boolean) => void
|
|
||||||
): void {
|
|
||||||
const mediaQueries = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
if (mediaQueries.length === 0) return;
|
|
||||||
|
|
||||||
// Optimization for single media query
|
|
||||||
const hasOnlyMediaQuery =
|
|
||||||
conditions.length === 1 &&
|
|
||||||
conditions[0].condition === "screen" &&
|
|
||||||
!!conditions[0].media_query;
|
|
||||||
|
|
||||||
mediaQueries.forEach((mediaQuery) => {
|
|
||||||
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
|
||||||
if (hasOnlyMediaQuery) {
|
|
||||||
onUpdate(matches);
|
|
||||||
} else {
|
|
||||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
|
||||||
onUpdate(conditionsMet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addListener(unsub);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to setup time-based listeners for conditional visibility
|
|
||||||
*/
|
|
||||||
export function setupTimeListeners(
|
|
||||||
conditions: Condition[],
|
|
||||||
hass: HomeAssistant,
|
|
||||||
addListener: (unsub: () => void) => void,
|
|
||||||
onUpdate: (conditionsMet: boolean) => void
|
|
||||||
): void {
|
|
||||||
const timeConditions = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
if (timeConditions.length === 0) return;
|
|
||||||
|
|
||||||
timeConditions.forEach((timeCondition) => {
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
|
|
||||||
const scheduleUpdate = () => {
|
|
||||||
const delay = calculateNextTimeUpdate(hass, timeCondition);
|
|
||||||
|
|
||||||
if (delay === undefined) return;
|
|
||||||
|
|
||||||
// Cap delay to prevent setTimeout overflow
|
|
||||||
const cappedDelay = Math.min(delay, MAX_TIMEOUT_DELAY);
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
if (delay <= MAX_TIMEOUT_DELAY) {
|
|
||||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
|
||||||
onUpdate(conditionsMet);
|
|
||||||
}
|
|
||||||
scheduleUpdate();
|
|
||||||
}, cappedDelay);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register cleanup function once, outside of scheduleUpdate
|
|
||||||
addListener(() => {
|
|
||||||
if (timeoutId !== undefined) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
scheduleUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { TZDate } from "@date-fns/tz";
|
|
||||||
import {
|
|
||||||
startOfDay,
|
|
||||||
addDays,
|
|
||||||
addMinutes,
|
|
||||||
differenceInMilliseconds,
|
|
||||||
} from "date-fns";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { TimeZone } from "../../data/translation";
|
|
||||||
import { parseTimeString } from "../datetime/check_time";
|
|
||||||
import type { TimeCondition } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate milliseconds until next time boundary for a time condition
|
|
||||||
* @param hass Home Assistant object
|
|
||||||
* @param timeCondition Time condition to calculate next update for
|
|
||||||
* @returns Milliseconds until next boundary, or undefined if no boundaries
|
|
||||||
*/
|
|
||||||
export function calculateNextTimeUpdate(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
|
||||||
): number | undefined {
|
|
||||||
const timezone =
|
|
||||||
hass.locale.time_zone === TimeZone.server
|
|
||||||
? hass.config.time_zone
|
|
||||||
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
|
|
||||||
const now = new TZDate(new Date(), timezone);
|
|
||||||
const updates: Date[] = [];
|
|
||||||
|
|
||||||
// Calculate next occurrence of after time
|
|
||||||
if (after) {
|
|
||||||
let afterDate = parseTimeString(after, timezone);
|
|
||||||
if (afterDate <= now) {
|
|
||||||
// If time has passed today, schedule for tomorrow
|
|
||||||
afterDate = addDays(afterDate, 1);
|
|
||||||
}
|
|
||||||
updates.push(afterDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate next occurrence of before time
|
|
||||||
if (before) {
|
|
||||||
let beforeDate = parseTimeString(before, timezone);
|
|
||||||
if (beforeDate <= now) {
|
|
||||||
// If time has passed today, schedule for tomorrow
|
|
||||||
beforeDate = addDays(beforeDate, 1);
|
|
||||||
}
|
|
||||||
updates.push(beforeDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If weekdays are specified, check for midnight (weekday transition)
|
|
||||||
if (weekdays && weekdays.length > 0 && weekdays.length < 7) {
|
|
||||||
// Calculate next midnight using startOfDay + addDays
|
|
||||||
const tomorrow = addDays(now, 1);
|
|
||||||
const midnight = startOfDay(tomorrow);
|
|
||||||
updates.push(midnight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the soonest update time
|
|
||||||
const nextUpdate = updates.reduce((soonest, current) =>
|
|
||||||
current < soonest ? current : soonest
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add 1 minute buffer to ensure we're past the boundary
|
|
||||||
const updateWithBuffer = addMinutes(nextUpdate, 1);
|
|
||||||
|
|
||||||
// Calculate difference in milliseconds
|
|
||||||
return differenceInMilliseconds(updateWithBuffer, now);
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import { TZDate } from "@date-fns/tz";
|
|
||||||
import { isBefore, isAfter, isWithinInterval } from "date-fns";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { TimeZone } from "../../data/translation";
|
|
||||||
import { WEEKDAY_MAP } from "./weekday";
|
|
||||||
import type { TimeCondition } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a time string format and value ranges without creating Date objects
|
|
||||||
* @param timeString Time string to validate (HH:MM or HH:MM:SS)
|
|
||||||
* @returns true if valid, false otherwise
|
|
||||||
*/
|
|
||||||
export function isValidTimeString(timeString: string): boolean {
|
|
||||||
// Reject empty strings
|
|
||||||
if (!timeString || timeString.trim() === "") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = timeString.split(":");
|
|
||||||
|
|
||||||
if (parts.length < 2 || parts.length > 3) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure each part contains only digits (and optional leading zeros)
|
|
||||||
// This prevents "8:00 AM" from passing validation
|
|
||||||
if (!parts.every((part) => /^\d+$/.test(part))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = parseInt(parts[0], 10);
|
|
||||||
const minutes = parseInt(parts[1], 10);
|
|
||||||
const seconds = parts.length === 3 ? parseInt(parts[2], 10) : 0;
|
|
||||||
|
|
||||||
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
hours >= 0 &&
|
|
||||||
hours <= 23 &&
|
|
||||||
minutes >= 0 &&
|
|
||||||
minutes <= 59 &&
|
|
||||||
seconds >= 0 &&
|
|
||||||
seconds <= 59
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a time string (HH:MM or HH:MM:SS) and set it on today's date in the given timezone
|
|
||||||
*
|
|
||||||
* Note: This function assumes the time string has already been validated by
|
|
||||||
* isValidTimeString() at configuration time. It does not re-validate at runtime
|
|
||||||
* for consistency with other condition types (screen, user, location, etc.)
|
|
||||||
*
|
|
||||||
* @param timeString The time string to parse (must be pre-validated)
|
|
||||||
* @param timezone The timezone to use
|
|
||||||
* @returns The Date object
|
|
||||||
*/
|
|
||||||
export const parseTimeString = (timeString: string, timezone: string): Date => {
|
|
||||||
const parts = timeString.split(":");
|
|
||||||
const hours = parseInt(parts[0], 10);
|
|
||||||
const minutes = parseInt(parts[1], 10);
|
|
||||||
const seconds = parts.length === 3 ? parseInt(parts[2], 10) : 0;
|
|
||||||
|
|
||||||
const now = new TZDate(new Date(), timezone);
|
|
||||||
const dateWithTime = new TZDate(
|
|
||||||
now.getFullYear(),
|
|
||||||
now.getMonth(),
|
|
||||||
now.getDate(),
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
0,
|
|
||||||
timezone
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Date(dateWithTime.getTime());
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current time matches the time condition (after/before/weekday)
|
|
||||||
* @param hass Home Assistant object
|
|
||||||
* @param timeCondition Time condition to check
|
|
||||||
* @returns true if current time matches the condition
|
|
||||||
*/
|
|
||||||
export const checkTimeInRange = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
|
||||||
): boolean => {
|
|
||||||
const timezone =
|
|
||||||
hass.locale.time_zone === TimeZone.server
|
|
||||||
? hass.config.time_zone
|
|
||||||
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
|
|
||||||
const now = new TZDate(new Date(), timezone);
|
|
||||||
|
|
||||||
// Check weekday condition
|
|
||||||
if (weekdays && weekdays.length > 0) {
|
|
||||||
const currentWeekday = WEEKDAY_MAP[now.getDay()];
|
|
||||||
if (!weekdays.includes(currentWeekday)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check time conditions
|
|
||||||
if (!after && !before) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const afterDate = after ? parseTimeString(after, timezone) : undefined;
|
|
||||||
const beforeDate = before ? parseTimeString(before, timezone) : undefined;
|
|
||||||
|
|
||||||
if (afterDate && beforeDate) {
|
|
||||||
if (isBefore(beforeDate, afterDate)) {
|
|
||||||
// Crosses midnight (e.g., 22:00 to 06:00)
|
|
||||||
return !isBefore(now, afterDate) || !isAfter(now, beforeDate);
|
|
||||||
}
|
|
||||||
return isWithinInterval(now, { start: afterDate, end: beforeDate });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (afterDate) {
|
|
||||||
return !isBefore(now, afterDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeDate) {
|
|
||||||
return !isAfter(now, beforeDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
import { getWeekStartByLocale } from "weekstart";
|
import { getWeekStartByLocale } from "weekstart";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { FirstWeekday } from "../../data/translation";
|
import { FirstWeekday } from "../../data/translation";
|
||||||
import { WEEKDAYS_LONG, type WeekdayIndex } from "./weekday";
|
|
||||||
|
export const weekdays = [
|
||||||
|
"sunday",
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
|
||||||
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
||||||
if (locale.first_weekday === FirstWeekday.language) {
|
if (locale.first_weekday === FirstWeekday.language) {
|
||||||
@@ -12,12 +23,12 @@ export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
|||||||
}
|
}
|
||||||
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
||||||
}
|
}
|
||||||
return WEEKDAYS_LONG.includes(locale.first_weekday)
|
return weekdays.includes(locale.first_weekday)
|
||||||
? (WEEKDAYS_LONG.indexOf(locale.first_weekday) as WeekdayIndex)
|
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||||
: 1;
|
: 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const firstWeekday = (locale: FrontendLocaleData) => {
|
export const firstWeekday = (locale: FrontendLocaleData) => {
|
||||||
const index = firstWeekdayIndex(locale);
|
const index = firstWeekdayIndex(locale);
|
||||||
return WEEKDAYS_LONG[index];
|
return weekdays[index];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
|
|
||||||
export type WeekdayShort =
|
|
||||||
| "sun"
|
|
||||||
| "mon"
|
|
||||||
| "tue"
|
|
||||||
| "wed"
|
|
||||||
| "thu"
|
|
||||||
| "fri"
|
|
||||||
| "sat";
|
|
||||||
|
|
||||||
export type WeekdayLong =
|
|
||||||
| "sunday"
|
|
||||||
| "monday"
|
|
||||||
| "tuesday"
|
|
||||||
| "wednesday"
|
|
||||||
| "thursday"
|
|
||||||
| "friday"
|
|
||||||
| "saturday";
|
|
||||||
|
|
||||||
export const WEEKDAYS_SHORT = [
|
|
||||||
"sun",
|
|
||||||
"mon",
|
|
||||||
"tue",
|
|
||||||
"wed",
|
|
||||||
"thu",
|
|
||||||
"fri",
|
|
||||||
"sat",
|
|
||||||
] as const satisfies readonly WeekdayShort[];
|
|
||||||
|
|
||||||
export const WEEKDAYS_LONG = [
|
|
||||||
"sunday",
|
|
||||||
"monday",
|
|
||||||
"tuesday",
|
|
||||||
"wednesday",
|
|
||||||
"thursday",
|
|
||||||
"friday",
|
|
||||||
"saturday",
|
|
||||||
] as const satisfies readonly WeekdayLong[];
|
|
||||||
|
|
||||||
export const WEEKDAY_MAP = {
|
|
||||||
0: "sun",
|
|
||||||
1: "mon",
|
|
||||||
2: "tue",
|
|
||||||
3: "wed",
|
|
||||||
4: "thu",
|
|
||||||
5: "fri",
|
|
||||||
6: "sat",
|
|
||||||
} as const satisfies Record<WeekdayIndex, WeekdayShort>;
|
|
||||||
|
|
||||||
export const WEEKDAY_SHORT_TO_LONG = {
|
|
||||||
sun: "sunday",
|
|
||||||
mon: "monday",
|
|
||||||
tue: "tuesday",
|
|
||||||
wed: "wednesday",
|
|
||||||
thu: "thursday",
|
|
||||||
fri: "friday",
|
|
||||||
sat: "saturday",
|
|
||||||
} as const satisfies Record<WeekdayShort, WeekdayLong>;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ThemeVars } from "../../data/ws-themes";
|
import type { ThemeVars } from "../../data/ws-themes";
|
||||||
import { darkColorVariables } from "../../resources/theme/color";
|
import { darkColorVariables } from "../../resources/theme/color";
|
||||||
import { darkSemanticVariables } from "../../resources/theme/semantic.globals";
|
|
||||||
import { derivedStyles } from "../../resources/theme/theme";
|
import { derivedStyles } from "../../resources/theme/theme";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import {
|
import {
|
||||||
@@ -53,7 +52,7 @@ export const applyThemesOnElement = (
|
|||||||
|
|
||||||
if (themeToApply && darkMode) {
|
if (themeToApply && darkMode) {
|
||||||
cacheKey = `${cacheKey}__dark`;
|
cacheKey = `${cacheKey}__dark`;
|
||||||
themeRules = { ...darkSemanticVariables, ...darkColorVariables };
|
themeRules = { ...darkColorVariables };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeToApply === "default") {
|
if (themeToApply === "default") {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic";
|
|||||||
export interface EntityFilter {
|
export interface EntityFilter {
|
||||||
domain?: string | string[];
|
domain?: string | string[];
|
||||||
device_class?: string | string[];
|
device_class?: string | string[];
|
||||||
device?: string | null | (string | null)[];
|
device?: string | string[];
|
||||||
area?: string | null | (string | null)[];
|
area?: string | string[];
|
||||||
floor?: string | null | (string | null)[];
|
floor?: string | string[];
|
||||||
label?: string | string[];
|
label?: string | string[];
|
||||||
entity_category?: EntityCategory | EntityCategory[];
|
entity_category?: EntityCategory | EntityCategory[];
|
||||||
hidden_platform?: string | string[];
|
hidden_platform?: string | string[];
|
||||||
@@ -19,18 +19,6 @@ export interface EntityFilter {
|
|||||||
|
|
||||||
export type EntityFilterFunc = (entityId: string) => boolean;
|
export type EntityFilterFunc = (entityId: string) => boolean;
|
||||||
|
|
||||||
const normalizeFilterArray = <T>(
|
|
||||||
value: T | null | T[] | (T | null)[] | undefined
|
|
||||||
): Set<T | null> | undefined => {
|
|
||||||
if (value === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (value === null) {
|
|
||||||
return new Set([null]);
|
|
||||||
}
|
|
||||||
return new Set(ensureArray(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateEntityFilter = (
|
export const generateEntityFilter = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
filter: EntityFilter
|
filter: EntityFilter
|
||||||
@@ -41,9 +29,11 @@ export const generateEntityFilter = (
|
|||||||
const deviceClasses = filter.device_class
|
const deviceClasses = filter.device_class
|
||||||
? new Set(ensureArray(filter.device_class))
|
? new Set(ensureArray(filter.device_class))
|
||||||
: undefined;
|
: undefined;
|
||||||
const floors = normalizeFilterArray(filter.floor);
|
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
|
||||||
const areas = normalizeFilterArray(filter.area);
|
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
|
||||||
const devices = normalizeFilterArray(filter.device);
|
const devices = filter.device
|
||||||
|
? new Set(ensureArray(filter.device))
|
||||||
|
: undefined;
|
||||||
const entityCategories = filter.entity_category
|
const entityCategories = filter.entity_category
|
||||||
? new Set(ensureArray(filter.entity_category))
|
? new Set(ensureArray(filter.entity_category))
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -83,20 +73,23 @@ export const generateEntityFilter = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (floors) {
|
if (floors) {
|
||||||
const floorId = floor?.floor_id ?? null;
|
if (!floor || !floors.has(floor.floor_id)) {
|
||||||
if (!floors.has(floorId)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (areas) {
|
if (areas) {
|
||||||
const areaId = area?.area_id ?? null;
|
if (!area) {
|
||||||
if (!areas.has(areaId)) {
|
return false;
|
||||||
|
}
|
||||||
|
if (!areas.has(area.area_id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (devices) {
|
if (devices) {
|
||||||
const deviceId = device?.id ?? null;
|
if (!device) {
|
||||||
if (!devices.has(deviceId)) {
|
return false;
|
||||||
|
}
|
||||||
|
if (!devices.has(device.id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { tinykeys } from "tinykeys";
|
|
||||||
import { canOverrideAlphanumericInput } from "../dom/can-override-input";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function to handle a keyboard shortcut.
|
|
||||||
*/
|
|
||||||
export type ShortcutHandler = (event: KeyboardEvent) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for a keyboard shortcut.
|
|
||||||
*/
|
|
||||||
export interface ShortcutConfig {
|
|
||||||
handler: ShortcutHandler;
|
|
||||||
/**
|
|
||||||
* If true, allows shortcuts even when text is selected.
|
|
||||||
* Default is false to avoid interrupting copy/paste.
|
|
||||||
*/
|
|
||||||
allowWhenTextSelected?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register keyboard shortcuts using tinykeys.
|
|
||||||
* Automatically blocks shortcuts in input fields and during text selection.
|
|
||||||
*/
|
|
||||||
function registerShortcuts(
|
|
||||||
shortcuts: Record<string, ShortcutConfig>
|
|
||||||
): () => void {
|
|
||||||
const wrappedShortcuts: Record<string, ShortcutHandler> = {};
|
|
||||||
|
|
||||||
Object.entries(shortcuts).forEach(([key, config]) => {
|
|
||||||
wrappedShortcuts[key] = (event: KeyboardEvent) => {
|
|
||||||
if (!canOverrideAlphanumericInput(event.composedPath())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
config.handler(event);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return tinykeys(window, wrappedShortcuts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages keyboard shortcuts registration and cleanup.
|
|
||||||
*/
|
|
||||||
export class ShortcutManager {
|
|
||||||
private _disposer?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register keyboard shortcuts.
|
|
||||||
* Uses tinykeys syntax: https://github.com/jamiebuilds/tinykeys#usage
|
|
||||||
*/
|
|
||||||
public add(shortcuts: Record<string, ShortcutConfig>) {
|
|
||||||
this._disposer?.();
|
|
||||||
this._disposer = registerShortcuts(shortcuts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all registered shortcuts.
|
|
||||||
*/
|
|
||||||
public remove() {
|
|
||||||
this._disposer?.();
|
|
||||||
this._disposer = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Parses a CSS duration string (e.g., "300ms", "3s") and returns the duration in milliseconds.
|
|
||||||
*
|
|
||||||
* @param duration - A CSS duration string (e.g., "300ms", "3s", "0.5s")
|
|
||||||
* @returns The duration in milliseconds, or 0 if the input is invalid
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* parseAnimationDuration("300ms") // Returns 300
|
|
||||||
* parseAnimationDuration("3s") // Returns 3000
|
|
||||||
* parseAnimationDuration("0.5s") // Returns 500
|
|
||||||
* parseAnimationDuration("invalid") // Returns 0
|
|
||||||
*/
|
|
||||||
export const parseAnimationDuration = (duration: string): number => {
|
|
||||||
const trimmed = duration.trim();
|
|
||||||
|
|
||||||
let value: number;
|
|
||||||
let multiplier: number;
|
|
||||||
|
|
||||||
if (trimmed.endsWith("ms")) {
|
|
||||||
value = parseFloat(trimmed.slice(0, -2));
|
|
||||||
multiplier = 1;
|
|
||||||
} else if (trimmed.endsWith("s")) {
|
|
||||||
value = parseFloat(trimmed.slice(0, -1));
|
|
||||||
multiplier = 1000;
|
|
||||||
} else {
|
|
||||||
// No recognized unit, try parsing as number (assume ms)
|
|
||||||
value = parseFloat(trimmed);
|
|
||||||
multiplier = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFinite(value) || value < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value * multiplier;
|
|
||||||
};
|
|
||||||
@@ -119,8 +119,8 @@ type Thresholds = Record<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export const DEFAULT_THRESHOLDS: Thresholds = {
|
export const DEFAULT_THRESHOLDS: Thresholds = {
|
||||||
second: 59, // seconds to minute
|
second: 45, // seconds to minute
|
||||||
minute: 59, // minutes to hour
|
minute: 45, // minutes to hour
|
||||||
hour: 22, // hour to day
|
hour: 22, // hour to day
|
||||||
day: 5, // day to week
|
day: 5, // day to week
|
||||||
week: 4, // week to months
|
week: 4, // week to months
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
|
||||||
*
|
|
||||||
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
|
||||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Synchronous callback
|
|
||||||
* withViewTransition(() => {
|
|
||||||
* this.large = !this.large;
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Async callback
|
|
||||||
* await withViewTransition(async () => {
|
|
||||||
* await this.updateData();
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const withViewTransition = (
|
|
||||||
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
|
||||||
): Promise<void> => {
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
return document.startViewTransition(() => callback(true)).finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Execute callback directly without transition
|
|
||||||
const result = callback(false);
|
|
||||||
return result instanceof Promise ? result : Promise.resolve();
|
|
||||||
};
|
|
||||||
@@ -6,8 +6,7 @@ export function downSampleLineData<
|
|||||||
data: T[] | undefined,
|
data: T[] | undefined,
|
||||||
maxDetails: number,
|
maxDetails: number,
|
||||||
minX?: number,
|
minX?: number,
|
||||||
maxX?: number,
|
maxX?: number
|
||||||
useMean = false
|
|
||||||
): T[] {
|
): T[] {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return [];
|
return [];
|
||||||
@@ -18,13 +17,15 @@ export function downSampleLineData<
|
|||||||
const min = minX ?? getPointData(data[0]!)[0];
|
const min = minX ?? getPointData(data[0]!)[0];
|
||||||
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
||||||
const step = Math.ceil((max - min) / Math.floor(maxDetails));
|
const step = Math.ceil((max - min) / Math.floor(maxDetails));
|
||||||
|
|
||||||
// Group points into frames
|
|
||||||
const frames = new Map<
|
const frames = new Map<
|
||||||
number,
|
number,
|
||||||
{ point: (typeof data)[number]; x: number; y: number }[]
|
{
|
||||||
|
min: { point: (typeof data)[number]; x: number; y: number };
|
||||||
|
max: { point: (typeof data)[number]; x: number; y: number };
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
// Group points into frames
|
||||||
for (const point of data) {
|
for (const point of data) {
|
||||||
const pointData = getPointData(point);
|
const pointData = getPointData(point);
|
||||||
if (!Array.isArray(pointData)) continue;
|
if (!Array.isArray(pointData)) continue;
|
||||||
@@ -35,53 +36,28 @@ export function downSampleLineData<
|
|||||||
const frameIndex = Math.floor((x - min) / step);
|
const frameIndex = Math.floor((x - min) / step);
|
||||||
const frame = frames.get(frameIndex);
|
const frame = frames.get(frameIndex);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
frames.set(frameIndex, [{ point, x, y }]);
|
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
|
||||||
} else {
|
} else {
|
||||||
frame.push({ point, x, y });
|
if (frame.min.y > y) {
|
||||||
|
frame.min = { point, x, y };
|
||||||
|
}
|
||||||
|
if (frame.max.y < y) {
|
||||||
|
frame.max = { point, x, y };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert frames back to points
|
// Convert frames back to points
|
||||||
const result: T[] = [];
|
const result: T[] = [];
|
||||||
|
for (const [_i, frame] of frames) {
|
||||||
if (useMean) {
|
// Use min/max points to preserve visual accuracy
|
||||||
// Use mean values for each frame
|
// The order of the data must be preserved so max may be before min
|
||||||
for (const [_i, framePoints] of frames) {
|
if (frame.min.x > frame.max.x) {
|
||||||
const sumY = framePoints.reduce((acc, p) => acc + p.y, 0);
|
result.push(frame.max.point);
|
||||||
const meanY = sumY / framePoints.length;
|
|
||||||
const sumX = framePoints.reduce((acc, p) => acc + p.x, 0);
|
|
||||||
const meanX = sumX / framePoints.length;
|
|
||||||
|
|
||||||
const firstPoint = framePoints[0].point;
|
|
||||||
const pointData = getPointData(firstPoint);
|
|
||||||
const meanPoint = (
|
|
||||||
Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] }
|
|
||||||
) as T;
|
|
||||||
result.push(meanPoint);
|
|
||||||
}
|
}
|
||||||
} else {
|
result.push(frame.min.point);
|
||||||
// Use min/max values for each frame
|
if (frame.min.x < frame.max.x) {
|
||||||
for (const [_i, framePoints] of frames) {
|
result.push(frame.max.point);
|
||||||
let minPoint = framePoints[0];
|
|
||||||
let maxPoint = framePoints[0];
|
|
||||||
|
|
||||||
for (const p of framePoints) {
|
|
||||||
if (p.y < minPoint.y) {
|
|
||||||
minPoint = p;
|
|
||||||
}
|
|
||||||
if (p.y > maxPoint.y) {
|
|
||||||
maxPoint = p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The order of the data must be preserved so max may be before min
|
|
||||||
if (minPoint.x > maxPoint.x) {
|
|
||||||
result.push(maxPoint.point);
|
|
||||||
}
|
|
||||||
result.push(minPoint.point);
|
|
||||||
if (minPoint.x < maxPoint.x) {
|
|
||||||
result.push(maxPoint.point);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
|||||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||||
const DOUBLE_TAP_TIME = 300;
|
const DOUBLE_TAP_TIME = 300;
|
||||||
|
const RESIZE_ANIMATION_DURATION = 250;
|
||||||
|
|
||||||
export type CustomLegendOption = ECOption["legend"] & {
|
export type CustomLegendOption = ECOption["legend"] & {
|
||||||
type: "custom";
|
type: "custom";
|
||||||
@@ -90,8 +91,6 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
private _shouldResizeChart = false;
|
private _shouldResizeChart = false;
|
||||||
|
|
||||||
private _resizeAnimationDuration?: number;
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
private _resizeController = new ResizeController(this, {
|
private _resizeController = new ResizeController(this, {
|
||||||
callback: () => {
|
callback: () => {
|
||||||
@@ -215,7 +214,6 @@ export class HaChartBase extends LitElement {
|
|||||||
) {
|
) {
|
||||||
// custom legend changes may require a resize to layout properly
|
// custom legend changes may require a resize to layout properly
|
||||||
this._shouldResizeChart = true;
|
this._shouldResizeChart = true;
|
||||||
this._resizeAnimationDuration = 250;
|
|
||||||
}
|
}
|
||||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||||
@@ -427,7 +425,6 @@ export class HaChartBase extends LitElement {
|
|||||||
...axis.axisPointer?.handle,
|
...axis.axisPointer?.handle,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
label: { show: false },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: axis
|
: axis
|
||||||
@@ -597,15 +594,10 @@ export class HaChartBase extends LitElement {
|
|||||||
aria: { show: true },
|
aria: { show: true },
|
||||||
dataZoom: this._getDataZoomConfig(),
|
dataZoom: this._getDataZoomConfig(),
|
||||||
toolbox: {
|
toolbox: {
|
||||||
top: Number.MAX_SAFE_INTEGER,
|
top: Infinity,
|
||||||
left: Number.MAX_SAFE_INTEGER,
|
left: Infinity,
|
||||||
feature: {
|
feature: {
|
||||||
dataZoom: {
|
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
|
||||||
show: true,
|
|
||||||
yAxisIndex: false,
|
|
||||||
filterMode: "none",
|
|
||||||
showTitle: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
iconStyle: { opacity: 0 },
|
iconStyle: { opacity: 0 },
|
||||||
},
|
},
|
||||||
@@ -633,10 +625,6 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _createTheme(style: CSSStyleDeclaration) {
|
private _createTheme(style: CSSStyleDeclaration) {
|
||||||
const textBorderColor =
|
|
||||||
style.getPropertyValue("--ha-card-background") ||
|
|
||||||
style.getPropertyValue("--card-background-color");
|
|
||||||
const textBorderWidth = 2;
|
|
||||||
return {
|
return {
|
||||||
color: getAllGraphColors(style),
|
color: getAllGraphColors(style),
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
@@ -660,22 +648,22 @@ export class HaChartBase extends LitElement {
|
|||||||
graph: {
|
graph: {
|
||||||
label: {
|
label: {
|
||||||
color: style.getPropertyValue("--primary-text-color"),
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
textBorderColor,
|
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||||
textBorderWidth,
|
textBorderWidth: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pie: {
|
pie: {
|
||||||
label: {
|
label: {
|
||||||
color: style.getPropertyValue("--primary-text-color"),
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
textBorderColor,
|
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||||
textBorderWidth,
|
textBorderWidth: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sankey: {
|
sankey: {
|
||||||
label: {
|
label: {
|
||||||
color: style.getPropertyValue("--primary-text-color"),
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
textBorderColor,
|
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||||
textBorderWidth,
|
textBorderWidth: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
categoryAxis: {
|
categoryAxis: {
|
||||||
@@ -989,14 +977,11 @@ export class HaChartBase extends LitElement {
|
|||||||
private _handleChartRenderFinished = () => {
|
private _handleChartRenderFinished = () => {
|
||||||
if (this._shouldResizeChart) {
|
if (this._shouldResizeChart) {
|
||||||
this.chart?.resize({
|
this.chart?.resize({
|
||||||
animation:
|
animation: this._reducedMotion
|
||||||
this._reducedMotion ||
|
? undefined
|
||||||
typeof this._resizeAnimationDuration !== "number"
|
: { duration: RESIZE_ANIMATION_DURATION },
|
||||||
? undefined
|
|
||||||
: { duration: this._resizeAnimationDuration },
|
|
||||||
});
|
});
|
||||||
this._shouldResizeChart = false;
|
this._shouldResizeChart = false;
|
||||||
this._resizeAnimationDuration = undefined;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import type { EChartsType } from "echarts/core";
|
|||||||
import type { GraphSeriesOption } from "echarts/charts";
|
import type { GraphSeriesOption } from "echarts/charts";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import type {
|
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
||||||
CallbackDataParams,
|
|
||||||
TopLevelFormatterParams,
|
|
||||||
} from "echarts/types/dist/shared";
|
|
||||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||||
@@ -19,7 +16,6 @@ import { deepEqual } from "../../common/util/deep-equal";
|
|||||||
export interface NetworkNode {
|
export interface NetworkNode {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
context?: string;
|
|
||||||
category?: number;
|
category?: number;
|
||||||
value?: number;
|
value?: number;
|
||||||
symbolSize?: number;
|
symbolSize?: number;
|
||||||
@@ -188,30 +184,10 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
|||||||
layout: physicsEnabled ? "force" : "none",
|
layout: physicsEnabled ? "force" : "none",
|
||||||
draggable: true,
|
draggable: true,
|
||||||
roam: true,
|
roam: true,
|
||||||
roamTrigger: "global",
|
|
||||||
selectedMode: "single",
|
selectedMode: "single",
|
||||||
label: {
|
label: {
|
||||||
show: showLabels,
|
show: showLabels,
|
||||||
position: "right",
|
position: "right",
|
||||||
formatter: (params: CallbackDataParams) => {
|
|
||||||
const node = params.data as NetworkNode;
|
|
||||||
if (node.context) {
|
|
||||||
return `{primary|${node.name ?? ""}}\n{secondary|${node.context}}`;
|
|
||||||
}
|
|
||||||
return node.name ?? "";
|
|
||||||
},
|
|
||||||
rich: {
|
|
||||||
primary: {
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: getComputedStyle(document.body).getPropertyValue(
|
|
||||||
"--secondary-text-color"
|
|
||||||
),
|
|
||||||
lineHeight: 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: isMobile ? "none" : "adjacency",
|
focus: isMobile ? "none" : "adjacency",
|
||||||
@@ -249,7 +225,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
|||||||
({
|
({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
context: node.context,
|
|
||||||
category: node.category,
|
category: node.category,
|
||||||
value: node.value,
|
value: node.value,
|
||||||
symbolSize: node.symbolSize || 30,
|
symbolSize: node.symbolSize || 30,
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
|
|||||||
var(--rgb-primary-text-color),
|
var(--rgb-primary-text-color),
|
||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
--_label-text-font: var(--ha-font-family-body);
|
|
||||||
border-radius: var(--ha-border-radius-md);
|
border-radius: var(--ha-border-radius-md);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ class HaDataTableLabels extends LitElement {
|
|||||||
@click=${clickAction ? this._labelClicked : undefined}
|
@click=${clickAction ? this._labelClicked : undefined}
|
||||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||||
style=${color ? `--color: ${color}` : ""}
|
style=${color ? `--color: ${color}` : ""}
|
||||||
.description=${label.description}
|
|
||||||
>
|
>
|
||||||
${label?.icon
|
${label?.icon
|
||||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||||
|
|||||||
@@ -298,18 +298,6 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (properties.has("data")) {
|
if (properties.has("data")) {
|
||||||
// Clean up checked rows that no longer exist in the data
|
|
||||||
if (this._checkedRows.length) {
|
|
||||||
const validIds = new Set(this.data.map((row) => String(row[this.id])));
|
|
||||||
const validCheckedRows = this._checkedRows.filter((id) =>
|
|
||||||
validIds.has(id)
|
|
||||||
);
|
|
||||||
if (validCheckedRows.length !== this._checkedRows.length) {
|
|
||||||
this._checkedRows = validCheckedRows;
|
|
||||||
this._checkedRowsChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._checkableRowsCount = this.data.filter(
|
this._checkableRowsCount = this.data.filter(
|
||||||
(row) => row.selectable !== false
|
(row) => row.selectable !== false
|
||||||
).length;
|
).length;
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ export class HaDevicePicker extends LitElement {
|
|||||||
const placeholder =
|
const placeholder =
|
||||||
this.placeholder ??
|
this.placeholder ??
|
||||||
this.hass.localize("ui.components.device-picker.placeholder");
|
this.hass.localize("ui.components.device-picker.placeholder");
|
||||||
|
const notFoundLabel = this.hass.localize(
|
||||||
|
"ui.components.device-picker.no_match"
|
||||||
|
);
|
||||||
|
|
||||||
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
||||||
|
|
||||||
@@ -206,10 +209,7 @@ export class HaDevicePicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize(
|
|
||||||
"ui.components.device-picker.no_devices"
|
|
||||||
)}
|
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
@@ -233,11 +233,6 @@ export class HaDevicePicker extends LitElement {
|
|||||||
this.value = value;
|
this.value = value;
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.device-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -269,6 +269,9 @@ export class HaEntityPicker extends LitElement {
|
|||||||
const placeholder =
|
const placeholder =
|
||||||
this.placeholder ??
|
this.placeholder ??
|
||||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
||||||
|
const notFoundLabel = this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.no_match"
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
@@ -279,7 +282,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${notFoundLabel}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.addButton ? undefined : this.value}
|
.value=${this.addButton ? undefined : this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
@@ -353,11 +356,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.entity.entity-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import "../ha-combo-box-item";
|
|||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
|
import "../ha-input-helper-text";
|
||||||
import type {
|
import type {
|
||||||
PickerComboBoxItem,
|
PickerComboBoxItem,
|
||||||
PickerComboBoxSearchFn,
|
PickerComboBoxSearchFn,
|
||||||
@@ -270,6 +271,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
||||||
|
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||||
output.push({
|
output.push({
|
||||||
@@ -277,6 +279,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
statistic_id: id,
|
statistic_id: id,
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
|
a11y_label: a11yLabel,
|
||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
@@ -455,6 +458,9 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
const placeholder =
|
const placeholder =
|
||||||
this.placeholder ??
|
this.placeholder ??
|
||||||
this.hass.localize("ui.components.statistic-picker.placeholder");
|
this.hass.localize("ui.components.statistic-picker.placeholder");
|
||||||
|
const notFoundLabel = this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.no_match"
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
@@ -462,10 +468,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_statistics"
|
|
||||||
)}
|
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
@@ -474,7 +477,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.helper=${this.helper}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
@@ -519,11 +521,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._picker?.open();
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.statistic-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ export class HaAreaPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
@@ -369,16 +367,14 @@ export class HaAreaPicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this.hass.localize(
|
||||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
"ui.components.area-picker.no_match"
|
||||||
.disabled=${this.disabled}
|
)}
|
||||||
.required=${this.required}
|
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.addButtonLabel=${this.addButtonLabel}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
@@ -426,11 +422,6 @@ export class HaAreaPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.area-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import type { HomeAssistant } from "../types";
|
|||||||
import { AudioRecorder } from "../util/audio-recorder";
|
import { AudioRecorder } from "../util/audio-recorder";
|
||||||
import { documentationUrl } from "../util/documentation-url";
|
import { documentationUrl } from "../util/documentation-url";
|
||||||
import "./ha-alert";
|
import "./ha-alert";
|
||||||
import "./ha-markdown";
|
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
import type { HaTextField } from "./ha-textfield";
|
import type { HaTextField } from "./ha-textfield";
|
||||||
|
|
||||||
@@ -41,11 +40,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
|
|
||||||
@query("#message-input") private _messageInput!: HaTextField;
|
@query("#message-input") private _messageInput!: HaTextField;
|
||||||
|
|
||||||
@query(".message:last-child")
|
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||||
private _lastChatMessage!: LitElement;
|
|
||||||
|
|
||||||
@query(".message:last-child img:last-of-type")
|
|
||||||
private _lastChatMessageImage: HTMLImageElement | undefined;
|
|
||||||
|
|
||||||
@state() private _conversation: AssistMessage[] = [];
|
@state() private _conversation: AssistMessage[] = [];
|
||||||
|
|
||||||
@@ -97,7 +92,10 @@ export class HaAssistChat extends LitElement {
|
|||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._audioRecorder?.close();
|
this._audioRecorder?.close();
|
||||||
|
this._audioRecorder = undefined;
|
||||||
this._unloadAudio();
|
this._unloadAudio();
|
||||||
|
this._conversation = [];
|
||||||
|
this._conversationId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@@ -114,7 +112,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="messages">
|
<div class="messages" id="scroll-container">
|
||||||
${controlHA
|
${controlHA
|
||||||
? nothing
|
? nothing
|
||||||
: html`
|
: html`
|
||||||
@@ -126,18 +124,11 @@ export class HaAssistChat extends LitElement {
|
|||||||
`}
|
`}
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
${this._conversation!.map(
|
${this._conversation!.map(
|
||||||
|
// New lines matter for messages
|
||||||
|
// prettier-ignore
|
||||||
(message) => html`
|
(message) => html`
|
||||||
<ha-markdown
|
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
||||||
class="message ${classMap({
|
`
|
||||||
error: !!message.error,
|
|
||||||
[message.who]: true,
|
|
||||||
})}"
|
|
||||||
breaks
|
|
||||||
cache
|
|
||||||
.content=${message.text}
|
|
||||||
>
|
|
||||||
</ha-markdown>
|
|
||||||
`
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="input" slot="primaryAction">
|
<div class="input" slot="primaryAction">
|
||||||
@@ -198,28 +189,12 @@ export class HaAssistChat extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _scrollMessagesBottom() {
|
private _scrollMessagesBottom() {
|
||||||
const lastChatMessage = this._lastChatMessage;
|
const scrollContainer = this._scrollContainer;
|
||||||
if (!lastChatMessage.hasUpdated) {
|
if (!scrollContainer) {
|
||||||
await lastChatMessage.updateComplete;
|
return;
|
||||||
}
|
|
||||||
if (
|
|
||||||
this._lastChatMessageImage &&
|
|
||||||
!this._lastChatMessageImage.naturalHeight
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await this._lastChatMessageImage.decode();
|
|
||||||
} catch (err: any) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn("Failed to decode image:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const isLastMessageFullyVisible =
|
|
||||||
lastChatMessage.getBoundingClientRect().y <
|
|
||||||
this.getBoundingClientRect().top + 24;
|
|
||||||
if (!isLastMessageFullyVisible) {
|
|
||||||
lastChatMessage.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
}
|
}
|
||||||
|
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleKeyUp(ev: KeyboardEvent) {
|
private _handleKeyUp(ev: KeyboardEvent) {
|
||||||
@@ -611,31 +586,42 @@ export class HaAssistChat extends LitElement {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.message {
|
.message {
|
||||||
|
white-space: pre-line;
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: var(--ha-font-size-l);
|
||||||
clear: both;
|
clear: both;
|
||||||
max-width: -webkit-fill-available;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
scroll-margin-top: 24px;
|
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: var(--ha-border-radius-xl);
|
border-radius: var(--ha-border-radius-xl);
|
||||||
}
|
}
|
||||||
|
.message:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
.message {
|
.message {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: var(--ha-font-size-l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.message p:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.message.user {
|
.message.user {
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
margin-inline-start: 24px;
|
margin-inline-start: 24px;
|
||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
text-align: right;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
--markdown-link-color: var(--text-primary-color);
|
|
||||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.hass {
|
.message.hass {
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
margin-inline-end: 24px;
|
margin-inline-end: 24px;
|
||||||
@@ -650,21 +636,20 @@ export class HaAssistChat extends LitElement {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message.user a {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.hass a {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
.message.error {
|
.message.error {
|
||||||
background-color: var(--error-color);
|
background-color: var(--error-color);
|
||||||
color: var(--text-primary-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);
|
|
||||||
&:not(:has(ha-markdown-element)) {
|
|
||||||
min-height: 1lh;
|
|
||||||
min-width: 1lh;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bouncer {
|
.bouncer {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import {
|
|
||||||
mdiAmpersand,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCodeBraces,
|
|
||||||
mdiDevices,
|
|
||||||
mdiGateOr,
|
|
||||||
mdiIdentifier,
|
|
||||||
mdiMapMarkerRadius,
|
|
||||||
mdiNotEqualVariant,
|
|
||||||
mdiNumeric,
|
|
||||||
mdiStateMachine,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { until } from "lit/directives/until";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-svg-icon";
|
|
||||||
|
|
||||||
export const CONDITION_ICONS = {
|
|
||||||
device: mdiDevices,
|
|
||||||
and: mdiAmpersand,
|
|
||||||
or: mdiGateOr,
|
|
||||||
not: mdiNotEqualVariant,
|
|
||||||
state: mdiStateMachine,
|
|
||||||
numeric_state: mdiNumeric,
|
|
||||||
sun: mdiWeatherSunny,
|
|
||||||
template: mdiCodeBraces,
|
|
||||||
time: mdiClockOutline,
|
|
||||||
trigger: mdiIdentifier,
|
|
||||||
zone: mdiMapMarkerRadius,
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-condition-icon")
|
|
||||||
export class HaConditionIcon extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public condition?: string;
|
|
||||||
|
|
||||||
@property() public icon?: string;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (this.icon) {
|
|
||||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.condition) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.hass) {
|
|
||||||
return this._renderFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = conditionIcon(this.hass, this.condition).then((icn) => {
|
|
||||||
if (icn) {
|
|
||||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
|
||||||
}
|
|
||||||
return this._renderFallback();
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`${until(icon)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderFallback() {
|
|
||||||
const domain = computeDomain(this.condition!);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${CONDITION_ICONS[this.condition!] ||
|
|
||||||
FALLBACK_DOMAIN_ICONS[domain]}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-condition-icon": HaConditionIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -94,12 +94,6 @@ export class HaDateInput extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _keyDown(ev: KeyboardEvent) {
|
private _keyDown(ev: KeyboardEvent) {
|
||||||
if (["Space", "Enter"].includes(ev.code)) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._openDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.canClear) {
|
if (!this.canClear) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,15 +75,11 @@ export class HaDialogHeader extends LitElement {
|
|||||||
font-size: var(--ha-font-size-xl);
|
font-size: var(--ha-font-size-xl);
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: var(--ha-line-height-condensed);
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: var(--ha-font-weight-medium);
|
||||||
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
|
||||||
}
|
}
|
||||||
.header-subtitle {
|
.header-subtitle {
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: var(--ha-font-size-m);
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: var(--ha-line-height-normal);
|
||||||
color: var(
|
color: var(--secondary-text-color);
|
||||||
--ha-dialog-header-subtitle-color,
|
|
||||||
var(--secondary-text-color)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||||
.header-bar {
|
.header-bar {
|
||||||
|
|||||||
@@ -90,8 +90,7 @@ export class HaDialog extends DialogBase {
|
|||||||
}
|
}
|
||||||
.mdc-dialog__actions {
|
.mdc-dialog__actions {
|
||||||
justify-content: var(--justify-action-buttons, flex-end);
|
justify-content: var(--justify-action-buttons, flex-end);
|
||||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
padding: 12px 16px 16px 16px;
|
||||||
var(--ha-space-4);
|
|
||||||
}
|
}
|
||||||
.mdc-dialog__actions span:nth-child(1) {
|
.mdc-dialog__actions span:nth-child(1) {
|
||||||
flex: var(--secondary-action-button-flex, unset);
|
flex: var(--secondary-action-button-flex, unset);
|
||||||
@@ -101,24 +100,22 @@ export class HaDialog extends DialogBase {
|
|||||||
}
|
}
|
||||||
.mdc-dialog__container {
|
.mdc-dialog__container {
|
||||||
align-items: var(--vertical-align-dialog, center);
|
align-items: var(--vertical-align-dialog, center);
|
||||||
padding: var(--dialog-container-padding, var(--ha-space-0));
|
|
||||||
}
|
}
|
||||||
.mdc-dialog__title {
|
.mdc-dialog__title {
|
||||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
|
padding: 16px 16px 0 16px;
|
||||||
var(--ha-space-4);
|
|
||||||
}
|
}
|
||||||
.mdc-dialog__title:has(span) {
|
.mdc-dialog__title:has(span) {
|
||||||
padding: var(--ha-space-3) var(--ha-space-3) var(--ha-space-0);
|
padding: 12px 12px 0;
|
||||||
}
|
}
|
||||||
.mdc-dialog__title::before {
|
.mdc-dialog__title::before {
|
||||||
content: unset;
|
content: unset;
|
||||||
}
|
}
|
||||||
.mdc-dialog .mdc-dialog__content {
|
.mdc-dialog .mdc-dialog__content {
|
||||||
position: var(--dialog-content-position, relative);
|
position: var(--dialog-content-position, relative);
|
||||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
padding: var(--dialog-content-padding, 24px);
|
||||||
}
|
}
|
||||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||||
padding-bottom: var(--dialog-content-padding, var(--ha-space-6));
|
padding-bottom: var(--dialog-content-padding, 24px);
|
||||||
}
|
}
|
||||||
.mdc-dialog .mdc-dialog__surface {
|
.mdc-dialog .mdc-dialog__surface {
|
||||||
position: var(--dialog-surface-position, relative);
|
position: var(--dialog-surface-position, relative);
|
||||||
@@ -136,7 +133,7 @@ export class HaDialog extends DialogBase {
|
|||||||
--ha-dialog-surface-background,
|
--ha-dialog-surface-background,
|
||||||
var(--mdc-theme-surface, #fff)
|
var(--mdc-theme-surface, #fff)
|
||||||
);
|
);
|
||||||
padding: var(--dialog-surface-padding, var(--ha-space-0));
|
padding: var(--dialog-surface-padding);
|
||||||
}
|
}
|
||||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -153,22 +150,22 @@ export class HaDialog extends DialogBase {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: block;
|
display: block;
|
||||||
padding-left: var(--ha-space-1);
|
padding-left: 4px;
|
||||||
padding-right: var(--ha-space-1);
|
padding-right: 4px;
|
||||||
margin-right: var(--ha-space-3);
|
margin-right: 12px;
|
||||||
margin-inline-end: var(--ha-space-3);
|
margin-inline-end: 12px;
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
.header_button {
|
.header_button {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
inset-inline-end: calc(var(--ha-space-3) * -1);
|
inset-inline-end: -12px;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
inset-inline-start: initial !important;
|
inset-inline-start: initial !important;
|
||||||
inset-inline-end: var(--ha-space-0) !important;
|
inset-inline-end: 0px !important;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
|
|
||||||
import { css, type CSSResultGroup } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Home Assistant dropdown item component
|
|
||||||
*
|
|
||||||
* @element ha-dropdown-item
|
|
||||||
* @extends {DropdownItem}
|
|
||||||
*
|
|
||||||
* @summary
|
|
||||||
* A stylable dropdown item component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown item.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@customElement("ha-dropdown-item")
|
|
||||||
export class HaDropdownItem extends DropdownItem {
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
DropdownItem.styles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
min-height: var(--ha-space-10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#icon ::slotted(*) {
|
|
||||||
color: var(--ha-color-on-neutral-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([variant="danger"]) #icon ::slotted(*) {
|
|
||||||
color: var(--ha-color-on-danger-quiet);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-dropdown-item": HaDropdownItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
|
||||||
import { css, type CSSResultGroup } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Home Assistant dropdown component
|
|
||||||
*
|
|
||||||
* @element ha-dropdown
|
|
||||||
* @extends {Dropdown}
|
|
||||||
*
|
|
||||||
* @summary
|
|
||||||
* A stylable dropdown component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@customElement("ha-dropdown")
|
|
||||||
export class HaDropdown extends Dropdown {
|
|
||||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
|
||||||
|
|
||||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
Dropdown.styles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
font-size: var(--ha-dropdown-font-size, var(--ha-font-size-m));
|
|
||||||
--wa-color-surface-raised: var(
|
|
||||||
--card-background-color,
|
|
||||||
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu {
|
|
||||||
padding: var(--ha-space-1);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-dropdown": HaDropdown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -209,7 +209,6 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
::slotted([slot="header"]) {
|
::slotted([slot="header"]) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -109,10 +109,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
.selected=${(this.value || []).includes(label.label_id)}
|
.selected=${(this.value || []).includes(label.label_id)}
|
||||||
hasMeta
|
hasMeta
|
||||||
>
|
>
|
||||||
<ha-label
|
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||||
style=${color ? `--color: ${color}` : ""}
|
|
||||||
.description=${label.description}
|
|
||||||
>
|
|
||||||
${label.icon
|
${label.icon
|
||||||
? html`<ha-icon
|
? html`<ha-icon
|
||||||
slot="icon"
|
slot="icon"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
mdiHomeFloor3,
|
mdiHomeFloor3,
|
||||||
mdiHomeFloorNegative1,
|
mdiHomeFloorNegative1,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
|
|||||||
|
|
||||||
@customElement("ha-floor-icon")
|
@customElement("ha-floor-icon")
|
||||||
export class HaFloorIcon extends LitElement {
|
export class HaFloorIcon extends LitElement {
|
||||||
@property({ attribute: false }) public floor?: Pick<
|
@property({ attribute: false }) public floor!: Pick<
|
||||||
FloorRegistryEntry,
|
FloorRegistryEntry,
|
||||||
"icon" | "level"
|
"icon" | "level"
|
||||||
>;
|
>;
|
||||||
@@ -56,9 +56,6 @@ export class HaFloorIcon extends LitElement {
|
|||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.floor) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
if (this.floor.icon) {
|
if (this.floor.icon) {
|
||||||
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
|
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,9 +383,8 @@ export class HaFloorPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this.hass.localize(
|
||||||
.emptyLabel=${this.hass.localize(
|
"ui.components.floor-picker.no_match"
|
||||||
"ui.components.floor-picker.no_floors"
|
|
||||||
)}
|
)}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@@ -445,11 +444,6 @@ export class HaFloorPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.floor-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { HomeAssistant } from "../types";
|
|||||||
import "./ha-bottom-sheet";
|
import "./ha-bottom-sheet";
|
||||||
import "./ha-button";
|
import "./ha-button";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-icon-button";
|
||||||
import "./ha-input-helper-text";
|
import "./ha-input-helper-text";
|
||||||
import "./ha-picker-combo-box";
|
import "./ha-picker-combo-box";
|
||||||
import type {
|
import type {
|
||||||
@@ -25,6 +26,9 @@ import "./ha-svg-icon";
|
|||||||
export class HaGenericPicker extends LitElement {
|
export class HaGenericPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
@@ -46,11 +50,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false, type: Array })
|
||||||
public getItems?: (
|
public getItems?: () => PickerComboBoxItem[];
|
||||||
searchString?: string,
|
|
||||||
section?: string
|
|
||||||
) => (PickerComboBoxItem | string)[];
|
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||||
@@ -64,11 +65,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: "not-found-label", type: String })
|
||||||
public notFoundLabel?: string | ((search: string) => string);
|
public notFoundLabel?: string;
|
||||||
|
|
||||||
@property({ attribute: "empty-label" })
|
|
||||||
public emptyLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "popover-placement" })
|
@property({ attribute: "popover-placement" })
|
||||||
public popoverPlacement:
|
public popoverPlacement:
|
||||||
@@ -88,25 +86,6 @@ export class HaGenericPicker extends LitElement {
|
|||||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||||
|
|
||||||
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
|
|
||||||
@property({ attribute: false }) public sections?: (
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
| "separator"
|
|
||||||
)[];
|
|
||||||
|
|
||||||
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
|
|
||||||
firstIndex: number;
|
|
||||||
lastIndex: number;
|
|
||||||
firstItem: PickerComboBoxItem | string;
|
|
||||||
secondItem: PickerComboBoxItem | string;
|
|
||||||
itemsCount: number;
|
|
||||||
}) => string | undefined;
|
|
||||||
|
|
||||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
|
||||||
|
|
||||||
@query(".container") private _containerElement?: HTMLDivElement;
|
@query(".container") private _containerElement?: HTMLDivElement;
|
||||||
|
|
||||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||||
@@ -119,11 +98,6 @@ export class HaGenericPicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _openedNarrow = false;
|
@state() private _openedNarrow = false;
|
||||||
|
|
||||||
static shadowRootOptions = {
|
|
||||||
...LitElement.shadowRootOptions,
|
|
||||||
delegatesFocus: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
private _narrow = false;
|
private _narrow = false;
|
||||||
|
|
||||||
// helper to set new value after closing picker, to avoid flicker
|
// helper to set new value after closing picker, to avoid flicker
|
||||||
@@ -216,19 +190,16 @@ export class HaGenericPicker extends LitElement {
|
|||||||
<ha-picker-combo-box
|
<ha-picker-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.label=${this.searchLabel}
|
.label=${this.searchLabel ??
|
||||||
|
(this.hass?.localize("ui.common.search") || "Search")}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
.rowRenderer=${this.rowRenderer}
|
.rowRenderer=${this.rowRenderer}
|
||||||
.notFoundLabel=${this.notFoundLabel}
|
.notFoundLabel=${this.notFoundLabel}
|
||||||
.emptyLabel=${this.emptyLabel}
|
|
||||||
.getItems=${this.getItems}
|
.getItems=${this.getItems}
|
||||||
.getAdditionalItems=${this.getAdditionalItems}
|
.getAdditionalItems=${this.getAdditionalItems}
|
||||||
.searchFn=${this.searchFn}
|
.searchFn=${this.searchFn}
|
||||||
.mode=${dialogMode ? "dialog" : "popover"}
|
.mode=${dialogMode ? "dialog" : "popover"}
|
||||||
.sections=${this.sections}
|
|
||||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
|
||||||
.selectedSection=${this.selectedSection}
|
|
||||||
></ha-picker-combo-box>
|
></ha-picker-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ class HaHLSPlayer extends LitElement {
|
|||||||
private static streamCount = 0;
|
private static streamCount = 0;
|
||||||
|
|
||||||
private _handleVisibilityChange = () => {
|
private _handleVisibilityChange = () => {
|
||||||
if (document.pictureInPictureElement) {
|
|
||||||
// video is playing in picture-in-picture mode, don't do anything
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
this._cleanUp();
|
this._cleanUp();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
|
|||||||
.path=${item.path}
|
.path=${item.path}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
${item.label}
|
${item.label}
|
||||||
</ha-md-menu-item>`
|
</ha-md-menu-item> `
|
||||||
)}
|
)}
|
||||||
</ha-md-button-menu>`
|
</ha-md-button-menu>`
|
||||||
: html`
|
: html`
|
||||||
@@ -103,7 +103,6 @@ export class HaIconOverflowMenu extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
cursor: initial;
|
|
||||||
}
|
}
|
||||||
div[role="separator"] {
|
div[role="separator"] {
|
||||||
border-right: 1px solid var(--divider-color);
|
border-right: 1px solid var(--divider-color);
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export interface DisplayItem {
|
|||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
disableSorting?: boolean;
|
disableSorting?: boolean;
|
||||||
disableHiding?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisplayValue {
|
export interface DisplayValue {
|
||||||
@@ -102,7 +101,6 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
icon,
|
icon,
|
||||||
iconPath,
|
iconPath,
|
||||||
disableSorting,
|
disableSorting,
|
||||||
disableHiding,
|
|
||||||
} = item;
|
} = item;
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
@@ -157,21 +155,18 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
${!isVisible || !disableHiding
|
<ha-icon-button
|
||||||
? html`<ha-icon-button
|
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
slot="end"
|
||||||
slot="end"
|
.label=${this.hass.localize(
|
||||||
.label=${this.hass.localize(
|
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
{
|
||||||
{
|
label: label,
|
||||||
label: label,
|
}
|
||||||
}
|
)}
|
||||||
)}
|
.value=${value}
|
||||||
.value=${value}
|
@click=${this._toggle}
|
||||||
@click=${this._toggle}
|
></ha-icon-button>
|
||||||
.disabled=${disableHiding || false}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
|
||||||
${isVisible && !disableSorting
|
${isVisible && !disableSorting
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
|
|||||||
@@ -154,10 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._getLabelsMemoized(
|
return this._getLabelsMemoized(
|
||||||
this.hass.states,
|
this.hass,
|
||||||
this.hass.areas,
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this._labels,
|
this._labels,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
@@ -227,9 +224,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this.hass.localize(
|
||||||
.emptyLabel=${this.hass.localize(
|
"ui.components.label-picker.no_match"
|
||||||
"ui.components.label-picker.no_labels"
|
|
||||||
)}
|
)}
|
||||||
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
@@ -292,11 +288,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.label-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,32 +1,17 @@
|
|||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { uid } from "../common/util/uid";
|
|
||||||
import "./ha-tooltip";
|
|
||||||
|
|
||||||
@customElement("ha-label")
|
@customElement("ha-label")
|
||||||
class HaLabel extends LitElement {
|
class HaLabel extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true }) dense = false;
|
@property({ type: Boolean, reflect: true }) dense = false;
|
||||||
|
|
||||||
@property({ attribute: "description" })
|
|
||||||
public description?: string;
|
|
||||||
|
|
||||||
private _elementId = "label-" + uid();
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-tooltip
|
<span class="content">
|
||||||
.for=${this._elementId}
|
<slot name="icon"></slot>
|
||||||
.disabled=${!this.description?.trim()}
|
<slot></slot>
|
||||||
>
|
</span>
|
||||||
${this.description}
|
|
||||||
</ha-tooltip>
|
|
||||||
<div class="container" .id=${this._elementId}>
|
|
||||||
<span class="content">
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +36,9 @@ class HaLabel extends LitElement {
|
|||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: var(--ha-font-weight-medium);
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: var(--ha-line-height-condensed);
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
|
vertical-align: middle;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
border-radius: var(--ha-border-radius-xl);
|
border-radius: var(--ha-border-radius-xl);
|
||||||
color: var(--ha-label-text-color);
|
color: var(--ha-label-text-color);
|
||||||
--mdc-icon-size: 12px;
|
--mdc-icon-size: 12px;
|
||||||
@@ -79,23 +66,14 @@ class HaLabel extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([dense]) {
|
:host([dense]) {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: var(--ha-border-radius-md);
|
|
||||||
}
|
|
||||||
:host([dense]) .container {
|
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
border-radius: var(--ha-border-radius-md);
|
||||||
}
|
}
|
||||||
:host([dense]) ::slotted([slot="icon"]) {
|
:host([dense]) ::slotted([slot="icon"]) {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import "./chips/ha-input-chip";
|
|||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-label-picker";
|
import "./ha-label-picker";
|
||||||
import type { HaLabelPicker } from "./ha-label-picker";
|
import type { HaLabelPicker } from "./ha-label-picker";
|
||||||
import "./ha-tooltip";
|
|
||||||
|
|
||||||
@customElement("ha-labels-picker")
|
@customElement("ha-labels-picker")
|
||||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||||
@@ -143,17 +142,9 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
|||||||
const color = label?.color
|
const color = label?.color
|
||||||
? computeCssColor(label.color)
|
? computeCssColor(label.color)
|
||||||
: undefined;
|
: undefined;
|
||||||
const elementId = "label-" + label.label_id;
|
|
||||||
return html`
|
return html`
|
||||||
<ha-tooltip
|
|
||||||
.for=${elementId}
|
|
||||||
.disabled=${!label?.description?.trim()}
|
|
||||||
>
|
|
||||||
${label?.description}
|
|
||||||
</ha-tooltip>
|
|
||||||
<ha-input-chip
|
<ha-input-chip
|
||||||
.item=${label}
|
.item=${label}
|
||||||
.id=${elementId}
|
|
||||||
@remove=${this._removeItem}
|
@remove=${this._removeItem}
|
||||||
@click=${this._openDetail}
|
@click=${this._openDetail}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { mdiMenuDown } from "@mdi/js";
|
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { formatLanguageCode } from "../common/language/format_language";
|
import { formatLanguageCode } from "../common/language/format_language";
|
||||||
@@ -9,10 +8,10 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
|||||||
import type { FrontendLocaleData } from "../data/translation";
|
import type { FrontendLocaleData } from "../data/translation";
|
||||||
import { translationMetadata } from "../resources/translations-metadata";
|
import { translationMetadata } from "../resources/translations-metadata";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import "./ha-button";
|
|
||||||
import "./ha-generic-picker";
|
import "./ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
import "./ha-list-item";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
|
import "./ha-select";
|
||||||
|
|
||||||
export const getLanguageOptions = (
|
export const getLanguageOptions = (
|
||||||
languages: string[],
|
languages: string[],
|
||||||
@@ -76,9 +75,6 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
@property({ attribute: "native-name", type: Boolean })
|
@property({ attribute: "native-name", type: Boolean })
|
||||||
public nativeName = false;
|
public nativeName = false;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "button-style" })
|
|
||||||
public buttonStyle = false;
|
|
||||||
|
|
||||||
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
||||||
|
|
||||||
@property({ attribute: "inline-arrow", type: Boolean })
|
@property({ attribute: "inline-arrow", type: Boolean })
|
||||||
@@ -86,8 +82,6 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
@state() _defaultLanguages: string[] = [];
|
@state() _defaultLanguages: string[] = [];
|
||||||
|
|
||||||
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this._computeDefaultLanguageOptions();
|
this._computeDefaultLanguageOptions();
|
||||||
@@ -107,13 +101,12 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
this.hass?.locale
|
this.hass?.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
private _getLanguageName = (lang?: string) =>
|
private _valueRenderer = (value) => {
|
||||||
this._getItems().find((language) => language.id === lang)?.primary;
|
const language = this._getItems().find(
|
||||||
|
(lang) => lang.id === value
|
||||||
private _valueRenderer = (value) =>
|
)?.primary;
|
||||||
html`<span slot="headline"
|
return html`<span slot="headline">${language ?? value}</span> `;
|
||||||
>${this._getLanguageName(value) ?? value}</span
|
};
|
||||||
> `;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const value =
|
const value =
|
||||||
@@ -125,10 +118,9 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
popover-placement="bottom-end"
|
popover-placement="bottom-end"
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this.hass?.localize(
|
||||||
.emptyLabel=${this.hass?.localize(
|
"ui.components.language-picker.no_match"
|
||||||
"ui.components.language-picker.no_languages"
|
)}
|
||||||
) || "No languages available"}
|
|
||||||
.placeholder=${this.label ??
|
.placeholder=${this.label ??
|
||||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||||
"Language")}
|
"Language")}
|
||||||
@@ -138,28 +130,10 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
@value-changed=${this._changed}
|
@value-changed=${this._changed}
|
||||||
hide-clear-icon
|
hide-clear-icon
|
||||||
>
|
></ha-generic-picker>
|
||||||
${this.buttonStyle
|
|
||||||
? html`<ha-button
|
|
||||||
slot="field"
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
@click=${this._openPicker}
|
|
||||||
appearance="plain"
|
|
||||||
variant="neutral"
|
|
||||||
>
|
|
||||||
${this._getLanguageName(value)}
|
|
||||||
<ha-svg-icon slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
|
||||||
</ha-button>`
|
|
||||||
: nothing}
|
|
||||||
</ha-generic-picker>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openPicker(ev: Event) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.genericPicker.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-generic-picker {
|
ha-generic-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -173,15 +147,6 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
this.value = ev.detail.value;
|
this.value = ev.detail.value;
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) => {
|
|
||||||
const term = html`<b>‘${search}’</b>`;
|
|
||||||
return this.hass
|
|
||||||
? this.hass.localize("ui.components.language-picker.no_match", {
|
|
||||||
term,
|
|
||||||
})
|
|
||||||
: html`No languages found for ${term}`;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { ReactiveElement, render, html } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
// eslint-disable-next-line import/extensions
|
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
||||||
import hash from "object-hash";
|
import hash from "object-hash";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { renderMarkdown } from "../resources/render-markdown";
|
import { renderMarkdown } from "../resources/render-markdown";
|
||||||
import { CacheManager } from "../util/cache-manager";
|
import { CacheManager } from "../util/cache-manager";
|
||||||
|
|
||||||
const h = (template: ReturnType<typeof unsafeHTML>) => html`${template}`;
|
|
||||||
|
|
||||||
const markdownCache = new CacheManager<string>(1000);
|
const markdownCache = new CacheManager<string>(1000);
|
||||||
|
|
||||||
const _gitHubMarkdownAlerts = {
|
const _gitHubMarkdownAlerts = {
|
||||||
@@ -52,26 +48,18 @@ class HaMarkdownElement extends ReactiveElement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPromise: ReturnType<typeof this._render> = Promise.resolve();
|
|
||||||
|
|
||||||
protected update(changedProps) {
|
protected update(changedProps) {
|
||||||
super.update(changedProps);
|
super.update(changedProps);
|
||||||
if (this.content !== undefined) {
|
if (this.content !== undefined) {
|
||||||
this._renderPromise = this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getUpdateComplete(): Promise<boolean> {
|
|
||||||
await super.getUpdateComplete();
|
|
||||||
await this._renderPromise;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||||
if (!this.innerHTML && this.cache) {
|
if (!this.innerHTML && this.cache) {
|
||||||
const key = this._computeCacheKey();
|
const key = this._computeCacheKey();
|
||||||
if (markdownCache.has(key)) {
|
if (markdownCache.has(key)) {
|
||||||
render(markdownCache.get(key)!, this.renderRoot);
|
this.innerHTML = markdownCache.get(key)!;
|
||||||
this._resize();
|
this._resize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +75,7 @@ class HaMarkdownElement extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _render() {
|
private async _render() {
|
||||||
const elements = await renderMarkdown(
|
this.innerHTML = await renderMarkdown(
|
||||||
String(this.content),
|
String(this.content),
|
||||||
{
|
{
|
||||||
breaks: this.breaks,
|
breaks: this.breaks,
|
||||||
@@ -99,11 +87,6 @@ class HaMarkdownElement extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
render(
|
|
||||||
elements.map((e) => h(unsafeHTML(e))),
|
|
||||||
this.renderRoot
|
|
||||||
);
|
|
||||||
|
|
||||||
this._resize();
|
this._resize();
|
||||||
|
|
||||||
const walker = document.createTreeWalker(
|
const walker = document.createTreeWalker(
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import {
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
css,
|
import { customElement, property } from "lit/decorators";
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
nothing,
|
|
||||||
type ReactiveElement,
|
|
||||||
type CSSResultGroup,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, query } from "lit/decorators";
|
|
||||||
import "./ha-markdown-element";
|
import "./ha-markdown-element";
|
||||||
|
|
||||||
@customElement("ha-markdown")
|
@customElement("ha-markdown")
|
||||||
@@ -25,14 +18,6 @@ export class HaMarkdown extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public cache = false;
|
@property({ type: Boolean }) public cache = false;
|
||||||
|
|
||||||
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
|
|
||||||
|
|
||||||
protected async getUpdateComplete() {
|
|
||||||
const result = await super.getUpdateComplete();
|
|
||||||
await this._markdownElement.updateComplete;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.content) {
|
if (!this.content) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -68,46 +53,19 @@ export class HaMarkdown extends LitElement {
|
|||||||
margin: var(--ha-space-1) 0;
|
margin: var(--ha-space-1) 0;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: var(--markdown-link-color, var(--primary-color));
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
background-color: rgba(10, 10, 10, 0.15);
|
|
||||||
border-radius: var(--markdown-image-border-radius);
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-height: 2lh;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
text-indent: 4px;
|
|
||||||
transition: height 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
p:first-child > img:first-child {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
p:first-child > img:last-child {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
ol,
|
|
||||||
ul {
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
&:has(input[type="checkbox"]) {
|
|
||||||
list-style: none;
|
|
||||||
& > input[type="checkbox"] {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
background-color: var(--markdown-svg-background-color, none);
|
|
||||||
color: var(--markdown-svg-color, none);
|
|
||||||
}
|
}
|
||||||
code,
|
code,
|
||||||
pre {
|
pre {
|
||||||
background-color: var(--markdown-code-background-color, none);
|
background-color: var(--markdown-code-background-color, none);
|
||||||
border-radius: var(--ha-border-radius-sm);
|
border-radius: var(--ha-border-radius-sm);
|
||||||
color: var(--markdown-code-text-color, inherit);
|
}
|
||||||
|
svg {
|
||||||
|
background-color: var(--markdown-svg-background-color, none);
|
||||||
|
color: var(--markdown-svg-color, none);
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: var(--ha-font-size-s);
|
||||||
@@ -139,24 +97,6 @@ export class HaMarkdown extends LitElement {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
margin: var(--ha-space-4) 0;
|
margin: var(--ha-space-4) 0;
|
||||||
}
|
}
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border: 1px solid var(--markdown-table-border-color, transparent);
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
border-left: 4px solid var(--divider-color);
|
|
||||||
margin-inline: 0;
|
|
||||||
padding-inline: 1em;
|
|
||||||
}
|
|
||||||
` as CSSResultGroup;
|
` as CSSResultGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,10 +175,10 @@ export class HaMdDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin-top: var(--safe-area-inset-top, var(--ha-space-0));
|
padding-top: var(--safe-area-inset-top);
|
||||||
margin-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
padding-bottom: var(--safe-area-inset-bottom);
|
||||||
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
padding-left: var(--safe-area-inset-left);
|
||||||
margin-right: var(--safe-area-inset-right, var(--ha-space-0));
|
padding-right: var(--safe-area-inset-right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ export class HaMdDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slot[name="actions"]::slotted(*) {
|
slot[name="actions"]::slotted(*) {
|
||||||
padding: var(--ha-space-4);
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroller {
|
.scroller {
|
||||||
@@ -195,7 +195,7 @@ export class HaMdDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slot[name="content"]::slotted(*) {
|
slot[name="content"]::slotted(*) {
|
||||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
padding: var(--dialog-content-padding, 24px);
|
||||||
}
|
}
|
||||||
.scrim {
|
.scrim {
|
||||||
z-index: 10; /* overlay navigation */
|
z-index: 10; /* overlay navigation */
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
|
import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
|
||||||
import { styles } from "@material/web/list/internal/listitem/list-item-styles";
|
import { styles } from "@material/web/list/internal/listitem/list-item-styles";
|
||||||
import { css, html, nothing, type TemplateResult } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import "./ha-ripple";
|
|
||||||
|
|
||||||
export const haMdListStyles = [
|
export const haMdListStyles = [
|
||||||
styles,
|
styles,
|
||||||
@@ -26,18 +25,6 @@ export const haMdListStyles = [
|
|||||||
@customElement("ha-md-list-item")
|
@customElement("ha-md-list-item")
|
||||||
export class HaMdListItem extends ListItemEl {
|
export class HaMdListItem extends ListItemEl {
|
||||||
static override styles = haMdListStyles;
|
static override styles = haMdListStyles;
|
||||||
|
|
||||||
protected renderRipple(): TemplateResult | typeof nothing {
|
|
||||||
if (this.type === "text") {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`<ha-ripple
|
|
||||||
part="ripple"
|
|
||||||
for="item"
|
|
||||||
?disabled=${this.disabled && this.type !== "link"}
|
|
||||||
></ha-ripple>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ export class HaMdMenuItem extends MenuItemEl {
|
|||||||
::slotted([slot="headline"]) {
|
::slotted([slot="headline"]) {
|
||||||
text-wrap: nowrap;
|
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);
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
|||||||
import { titleCase } from "../common/string/title-case";
|
import { titleCase } from "../common/string/title-case";
|
||||||
import { fetchConfig } from "../data/lovelace/config/types";
|
import { fetchConfig } from "../data/lovelace/config/types";
|
||||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
|
||||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||||
import "./ha-combo-box";
|
import "./ha-combo-box";
|
||||||
import type { HaComboBox } from "./ha-combo-box";
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
@@ -43,8 +42,13 @@ const createViewNavigationItem = (
|
|||||||
|
|
||||||
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||||
path: `/${panel.url_path}`,
|
path: `/${panel.url_path}`,
|
||||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
icon: panel.icon ?? "mdi:view-dashboard",
|
||||||
title: getPanelTitle(hass, panel) || "",
|
title:
|
||||||
|
panel.url_path === hass.defaultPanel
|
||||||
|
? hass.localize("panel.states")
|
||||||
|
: hass.localize(`panel.${panel.title}`) ||
|
||||||
|
panel.title ||
|
||||||
|
(panel.url_path ? titleCase(panel.url_path) : ""),
|
||||||
});
|
});
|
||||||
|
|
||||||
@customElement("ha-navigation-picker")
|
@customElement("ha-navigation-picker")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
|
import { mdiMagnify } from "@mdi/js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -14,12 +14,11 @@ import memoizeOne from "memoize-one";
|
|||||||
import { tinykeys } from "tinykeys";
|
import { tinykeys } from "tinykeys";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import { HaFuse } from "../resources/fuse";
|
import { HaFuse } from "../resources/fuse";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./chips/ha-chip-set";
|
|
||||||
import "./chips/ha-filter-chip";
|
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
@@ -28,18 +27,28 @@ import type { HaTextField } from "./ha-textfield";
|
|||||||
export interface PickerComboBoxItem {
|
export interface PickerComboBoxItem {
|
||||||
id: string;
|
id: string;
|
||||||
primary: string;
|
primary: string;
|
||||||
|
a11y_label?: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
search_labels?: string[];
|
search_labels?: string[];
|
||||||
sorting_label?: string;
|
sorting_label?: string;
|
||||||
icon_path?: string;
|
icon_path?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
|
||||||
|
// Hack to force empty label to always display empty value by default in the search field
|
||||||
|
export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
|
||||||
|
a11y_label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
|
||||||
|
|
||||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||||
item
|
item
|
||||||
) => html`
|
) => html`
|
||||||
<ha-combo-box-item type="button" compact>
|
<ha-combo-box-item
|
||||||
|
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
|
||||||
|
compact
|
||||||
|
>
|
||||||
${item.icon
|
${item.icon
|
||||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||||
: item.icon_path
|
: item.icon_path
|
||||||
@@ -78,11 +87,8 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
@state() private _listScrolled = false;
|
@state() private _listScrolled = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false, type: Array })
|
||||||
public getItems?: (
|
public getItems?: () => PickerComboBoxItem[];
|
||||||
searchString?: string,
|
|
||||||
section?: string
|
|
||||||
) => (PickerComboBoxItem | string)[];
|
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||||
@@ -90,45 +96,21 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: "not-found-label", type: String })
|
||||||
public notFoundLabel?: string | ((search: string) => string);
|
public notFoundLabel?: string;
|
||||||
|
|
||||||
@property({ attribute: "empty-label" })
|
|
||||||
public emptyLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
|
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
|
||||||
|
|
||||||
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
|
|
||||||
@property({ attribute: false }) public sections?: (
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
| "separator"
|
|
||||||
)[];
|
|
||||||
|
|
||||||
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
|
|
||||||
firstIndex: number;
|
|
||||||
lastIndex: number;
|
|
||||||
firstItem: PickerComboBoxItem | string;
|
|
||||||
secondItem: PickerComboBoxItem | string;
|
|
||||||
itemsCount: number;
|
|
||||||
}) => string | undefined;
|
|
||||||
|
|
||||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
|
||||||
|
|
||||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||||
|
|
||||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||||
|
|
||||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
@state() private _items: PickerComboBoxItemWithLabel[] = [];
|
||||||
|
|
||||||
@state() private _sectionTitle?: string;
|
private _allItems: PickerComboBoxItemWithLabel[] = [];
|
||||||
|
|
||||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
|
||||||
|
|
||||||
private _selectedItemIndex = -1;
|
private _selectedItemIndex = -1;
|
||||||
|
|
||||||
@@ -139,8 +121,6 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
private _removeKeyboardShortcuts?: () => void;
|
private _removeKeyboardShortcuts?: () => void;
|
||||||
|
|
||||||
private _search = "";
|
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._registerKeyboardShortcuts();
|
this._registerKeyboardShortcuts();
|
||||||
}
|
}
|
||||||
@@ -165,142 +145,74 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
"Search"}
|
"Search"}
|
||||||
@input=${this._filterChanged}
|
@input=${this._filterChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${this._renderSectionButtons()}
|
|
||||||
${this.sections?.length
|
|
||||||
? html`
|
|
||||||
<div class="section-title-wrapper">
|
|
||||||
<div
|
|
||||||
class="section-title ${!this.selectedSection &&
|
|
||||||
this._sectionTitle
|
|
||||||
? "show"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
${this._sectionTitle}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
.keyFunction=${this._keyFunction}
|
@scroll=${this._onScrollList}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
scroller
|
scroller
|
||||||
.items=${this._items}
|
.items=${this._items}
|
||||||
.renderItem=${this._renderItem}
|
.renderItem=${this._renderItem}
|
||||||
style="min-height: 36px;"
|
style="min-height: 36px;"
|
||||||
class=${this._listScrolled ? "scrolled" : ""}
|
class=${this._listScrolled ? "scrolled" : ""}
|
||||||
@scroll=${this._onScrollList}
|
|
||||||
@focus=${this._focusList}
|
@focus=${this._focusList}
|
||||||
@visibilityChanged=${this._visibilityChanged}
|
|
||||||
>
|
>
|
||||||
</lit-virtualizer>`;
|
</lit-virtualizer> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderSectionButtons() {
|
private _defaultNotFoundItem = memoizeOne(
|
||||||
if (!this.sections || this.sections.length === 0) {
|
(
|
||||||
return nothing;
|
label: this["notFoundLabel"],
|
||||||
}
|
localize?: LocalizeFunc
|
||||||
|
): PickerComboBoxItemWithLabel => ({
|
||||||
|
id: NO_MATCHING_ITEMS_FOUND_ID,
|
||||||
|
primary:
|
||||||
|
label ||
|
||||||
|
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||||
|
"No matching items found",
|
||||||
|
icon_path: mdiMagnify,
|
||||||
|
a11y_label:
|
||||||
|
label ||
|
||||||
|
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||||
|
"No matching items found",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
private _getAdditionalItems = (searchString?: string) => {
|
||||||
<ha-chip-set class="sections">
|
const items = this.getAdditionalItems?.(searchString) || [];
|
||||||
${this.sections.map((section) =>
|
|
||||||
section === "separator"
|
|
||||||
? html`<div class="separator"></div>`
|
|
||||||
: html`<ha-filter-chip
|
|
||||||
@click=${this._toggleSection}
|
|
||||||
.section-id=${section.id}
|
|
||||||
.selected=${this.selectedSection === section.id}
|
|
||||||
.label=${section.label}
|
|
||||||
>
|
|
||||||
</ha-filter-chip>`
|
|
||||||
)}
|
|
||||||
</ha-chip-set>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@eventOptions({ passive: true })
|
return items.map<PickerComboBoxItemWithLabel>((item) => ({
|
||||||
private _visibilityChanged(ev) {
|
...item,
|
||||||
if (
|
a11y_label: item.a11y_label || item.primary,
|
||||||
this._virtualizerElement &&
|
}));
|
||||||
this.sectionTitleFunction &&
|
};
|
||||||
this.sections?.length
|
|
||||||
) {
|
|
||||||
const firstItem = this._virtualizerElement.items[ev.first];
|
|
||||||
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
|
||||||
this._sectionTitle = this.sectionTitleFunction({
|
|
||||||
firstIndex: ev.first,
|
|
||||||
lastIndex: ev.last,
|
|
||||||
firstItem: firstItem as PickerComboBoxItem | string,
|
|
||||||
secondItem: secondItem as PickerComboBoxItem | string,
|
|
||||||
itemsCount: this._virtualizerElement.items.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getAdditionalItems = (searchString?: string) =>
|
private _getItems = (): PickerComboBoxItemWithLabel[] => {
|
||||||
this.getAdditionalItems?.(searchString) || [];
|
const items = this.getItems ? this.getItems() : [];
|
||||||
|
|
||||||
private _getItems = () => {
|
const sortedItems = items
|
||||||
let items = [
|
.map<PickerComboBoxItemWithLabel>((item) => ({
|
||||||
...(this.getItems
|
...item,
|
||||||
? this.getItems(this._search, this.selectedSection)
|
a11y_label: item.a11y_label || item.primary,
|
||||||
: []),
|
}))
|
||||||
];
|
.sort((entityA, entityB) =>
|
||||||
|
|
||||||
if (!this.sections?.length) {
|
|
||||||
items = items.sort((entityA, entityB) =>
|
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
(entityA as PickerComboBoxItem).sorting_label!,
|
entityA.sorting_label!,
|
||||||
(entityB as PickerComboBoxItem).sorting_label!,
|
entityB.sorting_label!,
|
||||||
this.hass?.locale.language ?? navigator.language
|
this.hass?.locale.language ?? navigator.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (!items.length) {
|
if (!sortedItems.length) {
|
||||||
items.push(NO_ITEMS_AVAILABLE_ID);
|
sortedItems.push(
|
||||||
|
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalItems = this._getAdditionalItems();
|
const additionalItems = this._getAdditionalItems();
|
||||||
items.push(...additionalItems);
|
sortedItems.push(...additionalItems);
|
||||||
|
return sortedItems;
|
||||||
if (this.mode === "dialog") {
|
|
||||||
items.push("padding"); // padding for safe area inset
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||||
if (item === "padding") {
|
|
||||||
return html`<div class="bottom-padding"></div>`;
|
|
||||||
}
|
|
||||||
if (item === NO_ITEMS_AVAILABLE_ID) {
|
|
||||||
return html`
|
|
||||||
<div class="combo-box-row">
|
|
||||||
<ha-combo-box-item type="text" compact>
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${this._search ? mdiMagnify : mdiMinusBoxOutline}
|
|
||||||
></ha-svg-icon>
|
|
||||||
<span slot="headline"
|
|
||||||
>${this._search
|
|
||||||
? typeof this.notFoundLabel === "function"
|
|
||||||
? this.notFoundLabel(this._search)
|
|
||||||
: this.notFoundLabel ||
|
|
||||||
this.hass?.localize("ui.components.combo-box.no_match") ||
|
|
||||||
"No matching items found"
|
|
||||||
: this.emptyLabel ||
|
|
||||||
this.hass?.localize("ui.components.combo-box.no_items") ||
|
|
||||||
"No items available"}</span
|
|
||||||
>
|
|
||||||
</ha-combo-box-item>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
if (typeof item === "string") {
|
|
||||||
return html`<div class="title">${item}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
||||||
return html`<div
|
return html`<div
|
||||||
id=${`list-item-${index}`}
|
id=${`list-item-${index}`}
|
||||||
@@ -309,7 +221,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
.index=${index}
|
.index=${index}
|
||||||
@click=${this._valueSelected}
|
@click=${this._valueSelected}
|
||||||
>
|
>
|
||||||
${renderer(item, index)}
|
${item.id === NO_MATCHING_ITEMS_FOUND_ID
|
||||||
|
? DEFAULT_ROW_RENDERER(item, index)
|
||||||
|
: renderer(item, index)}
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,6 +242,10 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const value = (ev.currentTarget as any).value as string;
|
const value = (ev.currentTarget as any).value as string;
|
||||||
const newValue = value?.trim();
|
const newValue = value?.trim();
|
||||||
|
|
||||||
|
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: newValue });
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,83 +256,51 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
private _filterChanged = (ev: Event) => {
|
private _filterChanged = (ev: Event) => {
|
||||||
const textfield = ev.target as HaTextField;
|
const textfield = ev.target as HaTextField;
|
||||||
const searchString = textfield.value.trim();
|
const searchString = textfield.value.trim();
|
||||||
this._search = searchString;
|
|
||||||
|
|
||||||
if (this.sections?.length) {
|
if (!searchString) {
|
||||||
this._items = this._getItems();
|
this._items = this._allItems;
|
||||||
} else {
|
return;
|
||||||
if (!searchString) {
|
|
||||||
this._items = this._allItems;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
|
||||||
const fuse = new HaFuse(
|
|
||||||
this._allItems as PickerComboBoxItem[],
|
|
||||||
{
|
|
||||||
shouldSort: false,
|
|
||||||
minMatchCharLength: Math.min(searchString.length, 2),
|
|
||||||
},
|
|
||||||
index
|
|
||||||
);
|
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(searchString);
|
|
||||||
let filteredItems = [...this._allItems];
|
|
||||||
|
|
||||||
if (results) {
|
|
||||||
const items: (PickerComboBoxItem | string)[] = results.map(
|
|
||||||
(result) => result.item
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalItems = this._getAdditionalItems();
|
|
||||||
items.push(...additionalItems);
|
|
||||||
|
|
||||||
filteredItems = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.searchFn) {
|
|
||||||
filteredItems = this.searchFn(
|
|
||||||
searchString,
|
|
||||||
filteredItems as PickerComboBoxItem[],
|
|
||||||
this._allItems as PickerComboBoxItem[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._items = filteredItems as PickerComboBoxItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const index = this._fuseIndex(this._allItems);
|
||||||
|
const fuse = new HaFuse(
|
||||||
|
this._allItems,
|
||||||
|
{
|
||||||
|
shouldSort: false,
|
||||||
|
minMatchCharLength: Math.min(searchString.length, 2),
|
||||||
|
},
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = fuse.multiTermsSearch(searchString);
|
||||||
|
let filteredItems = this._allItems as PickerComboBoxItem[];
|
||||||
|
if (results) {
|
||||||
|
const items = results.map((result) => result.item);
|
||||||
|
if (items.length === 0) {
|
||||||
|
items.push(
|
||||||
|
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const additionalItems = this._getAdditionalItems(searchString);
|
||||||
|
items.push(...additionalItems);
|
||||||
|
filteredItems = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchFn) {
|
||||||
|
filteredItems = this.searchFn(
|
||||||
|
searchString,
|
||||||
|
filteredItems,
|
||||||
|
this._allItems
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._items = filteredItems as PickerComboBoxItemWithLabel[];
|
||||||
this._selectedItemIndex = -1;
|
this._selectedItemIndex = -1;
|
||||||
if (this._virtualizerElement) {
|
if (this._virtualizerElement) {
|
||||||
this._virtualizerElement.scrollTo(0, 0);
|
this._virtualizerElement.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private _toggleSection(ev: Event) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._resetSelectedItem();
|
|
||||||
this._sectionTitle = undefined;
|
|
||||||
const section = (ev.target as HTMLElement)["section-id"] as string;
|
|
||||||
if (!section) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.selectedSection === section) {
|
|
||||||
this.selectedSection = undefined;
|
|
||||||
} else {
|
|
||||||
this.selectedSection = section;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._items = this._getItems();
|
|
||||||
|
|
||||||
// Reset scroll position when filter changes
|
|
||||||
if (this._virtualizerElement) {
|
|
||||||
this._virtualizerElement.scrollToIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _registerKeyboardShortcuts() {
|
private _registerKeyboardShortcuts() {
|
||||||
this._removeKeyboardShortcuts = tinykeys(this, {
|
this._removeKeyboardShortcuts = tinykeys(this, {
|
||||||
ArrowUp: this._selectPreviousItem,
|
ArrowUp: this._selectPreviousItem,
|
||||||
@@ -458,7 +344,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof items[nextIndex] === "string") {
|
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
// Skip titles, padding and empty search
|
// Skip titles, padding and empty search
|
||||||
if (nextIndex === maxItems) {
|
if (nextIndex === maxItems) {
|
||||||
return;
|
return;
|
||||||
@@ -487,7 +373,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof items[nextIndex] === "string") {
|
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
// Skip titles, padding and empty search
|
// Skip titles, padding and empty search
|
||||||
if (nextIndex === 0) {
|
if (nextIndex === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -509,6 +395,13 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
const nextIndex = 0;
|
const nextIndex = 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||||
|
NO_MATCHING_ITEMS_FOUND_ID
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||||
this._selectedItemIndex = nextIndex + 1;
|
this._selectedItemIndex = nextIndex + 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -526,6 +419,13 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||||
|
NO_MATCHING_ITEMS_FOUND_ID
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||||
this._selectedItemIndex = nextIndex - 1;
|
this._selectedItemIndex = nextIndex - 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -553,7 +453,10 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||||
|
|
||||||
if (this._virtualizerElement?.items.length === 1) {
|
if (
|
||||||
|
this._virtualizerElement?.items.length === 1 &&
|
||||||
|
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
|
||||||
|
) {
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: firstItem.id,
|
value: firstItem.id,
|
||||||
});
|
});
|
||||||
@@ -569,7 +472,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const item = this._virtualizerElement?.items[
|
const item = this._virtualizerElement?.items[
|
||||||
this._selectedItemIndex
|
this._selectedItemIndex
|
||||||
] as PickerComboBoxItem;
|
] as PickerComboBoxItem;
|
||||||
if (item) {
|
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
fireEvent(this, "value-changed", { value: item.id });
|
fireEvent(this, "value-changed", { value: item.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -581,9 +484,6 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
this._selectedItemIndex = -1;
|
this._selectedItemIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
|
||||||
typeof item === "string" ? item : item.id;
|
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
css`
|
css`
|
||||||
@@ -658,80 +558,6 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
background-color: var(--ha-color-fill-neutral-normal-hover);
|
background-color: var(--ha-color-fill-neutral-normal-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sections {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: var(--ha-space-2);
|
|
||||||
padding: var(--ha-space-3) var(--ha-space-3);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([mode="dialog"]) .sections {
|
|
||||||
padding: var(--ha-space-3) var(--ha-space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sections ha-filter-chip {
|
|
||||||
flex-shrink: 0;
|
|
||||||
--md-filter-chip-selected-container-color: var(
|
|
||||||
--ha-color-fill-primary-normal-hover
|
|
||||||
);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sections .separator {
|
|
||||||
height: var(--ha-space-8);
|
|
||||||
width: 0;
|
|
||||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title,
|
|
||||||
.title {
|
|
||||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
|
||||||
padding: var(--ha-space-1) var(--ha-space-2);
|
|
||||||
font-weight: var(--ha-font-weight-bold);
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
min-height: var(--ha-space-6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([mode="dialog"]) .title {
|
|
||||||
padding: var(--ha-space-1) var(--ha-space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([mode="dialog"]) ha-textfield {
|
|
||||||
padding: 0 var(--ha-space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title-wrapper {
|
|
||||||
height: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
width: calc(100% - var(--ha-space-8));
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title.show {
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-search {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--ha-space-3);
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
|
|
||||||
@customElement("ha-section-title")
|
|
||||||
class HaSectionTitle extends LitElement {
|
|
||||||
protected render() {
|
|
||||||
return html`<slot></slot>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
|
||||||
padding: var(--ha-space-1) var(--ha-space-2);
|
|
||||||
font-weight: var(--ha-font-weight-bold);
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
min-height: var(--ha-space-6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-section-title": HaSectionTitle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { StateSelector } from "../../data/selector";
|
import type { StateSelector } from "../../data/selector";
|
||||||
import { extractFromTarget } from "../../data/target";
|
|
||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../entity/ha-entity-state-picker";
|
import "../entity/ha-entity-state-picker";
|
||||||
@@ -27,29 +25,15 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
@property({ attribute: false }) public context?: {
|
@property({ attribute: false }) public context?: {
|
||||||
filter_attribute?: string;
|
filter_attribute?: string;
|
||||||
filter_entity?: string | string[];
|
filter_entity?: string | string[];
|
||||||
filter_target?: HassServiceTarget;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@state() private _entityIds?: string | string[];
|
|
||||||
|
|
||||||
willUpdate(changedProps) {
|
|
||||||
if (changedProps.has("selector") || changedProps.has("context")) {
|
|
||||||
this._resolveEntityIds(
|
|
||||||
this.selector.state?.entity_id,
|
|
||||||
this.context?.filter_entity,
|
|
||||||
this.context?.filter_target
|
|
||||||
).then((entityIds) => {
|
|
||||||
this._entityIds = entityIds;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.selector.state?.multiple) {
|
if (this.selector.state?.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-entity-states-picker
|
<ha-entity-states-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this._entityIds}
|
.entityId=${this.selector.state?.entity_id ||
|
||||||
|
this.context?.filter_entity}
|
||||||
.attribute=${this.selector.state?.attribute ||
|
.attribute=${this.selector.state?.attribute ||
|
||||||
this.context?.filter_attribute}
|
this.context?.filter_attribute}
|
||||||
.extraOptions=${this.selector.state?.extra_options}
|
.extraOptions=${this.selector.state?.extra_options}
|
||||||
@@ -66,7 +50,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
return html`
|
return html`
|
||||||
<ha-entity-state-picker
|
<ha-entity-state-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this._entityIds}
|
.entityId=${this.selector.state?.entity_id ||
|
||||||
|
this.context?.filter_entity}
|
||||||
.attribute=${this.selector.state?.attribute ||
|
.attribute=${this.selector.state?.attribute ||
|
||||||
this.context?.filter_attribute}
|
this.context?.filter_attribute}
|
||||||
.extraOptions=${this.selector.state?.extra_options}
|
.extraOptions=${this.selector.state?.extra_options}
|
||||||
@@ -80,24 +65,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
></ha-entity-state-picker>
|
></ha-entity-state-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resolveEntityIds(
|
|
||||||
selectorEntityId: string | string[] | undefined,
|
|
||||||
contextFilterEntity: string | string[] | undefined,
|
|
||||||
contextFilterTarget: HassServiceTarget | undefined
|
|
||||||
): Promise<string | string[] | undefined> {
|
|
||||||
if (selectorEntityId !== undefined) {
|
|
||||||
return selectorEntityId;
|
|
||||||
}
|
|
||||||
if (contextFilterEntity !== undefined) {
|
|
||||||
return contextFilterEntity;
|
|
||||||
}
|
|
||||||
if (contextFilterTarget !== undefined) {
|
|
||||||
const result = await extractFromTarget(this.hass, contextFilterTarget);
|
|
||||||
return result.referenced_entities;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
|||||||
import { documentationUrl } from "../util/documentation-url";
|
import { documentationUrl } from "../util/documentation-url";
|
||||||
import "./ha-checkbox";
|
import "./ha-checkbox";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-markdown";
|
|
||||||
import "./ha-selector/ha-selector";
|
import "./ha-selector/ha-selector";
|
||||||
import "./ha-service-picker";
|
import "./ha-service-picker";
|
||||||
import "./ha-service-section-icon";
|
import "./ha-service-section-icon";
|
||||||
@@ -685,14 +684,10 @@ export class HaServiceControl extends LitElement {
|
|||||||
dataField.key}</span
|
dataField.key}</span
|
||||||
>
|
>
|
||||||
<span slot="description"
|
<span slot="description"
|
||||||
><ha-markdown
|
>${this.hass.localize(
|
||||||
breaks
|
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||||
allow-svg
|
) || dataField?.description}</span
|
||||||
.content=${this.hass.localize(
|
>
|
||||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
|
||||||
) || dataField?.description}
|
|
||||||
></ha-markdown>
|
|
||||||
</span>
|
|
||||||
<ha-selector
|
<ha-selector
|
||||||
.context=${this._selectorContext(targetEntities)}
|
.context=${this._selectorContext(targetEntities)}
|
||||||
.disabled=${this.disabled ||
|
.disabled=${this.disabled ||
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
mdiBell,
|
mdiBell,
|
||||||
|
mdiCalendar,
|
||||||
mdiCellphoneCog,
|
mdiCellphoneCog,
|
||||||
|
mdiChartBox,
|
||||||
|
mdiClipboardList,
|
||||||
mdiCog,
|
mdiCog,
|
||||||
|
mdiFormatListBulletedType,
|
||||||
|
mdiHammer,
|
||||||
|
mdiLightningBolt,
|
||||||
mdiMenu,
|
mdiMenu,
|
||||||
mdiMenuOpen,
|
mdiMenuOpen,
|
||||||
|
mdiPlayBoxMultiple,
|
||||||
|
mdiTooltipAccount,
|
||||||
|
mdiViewDashboard,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
eventOptions,
|
eventOptions,
|
||||||
@@ -24,14 +33,6 @@ import { computeRTL } from "../common/util/compute_rtl";
|
|||||||
import { throttle } from "../common/util/throttle";
|
import { throttle } from "../common/util/throttle";
|
||||||
import { subscribeFrontendUserData } from "../data/frontend";
|
import { subscribeFrontendUserData } from "../data/frontend";
|
||||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||||
import {
|
|
||||||
FIXED_PANELS,
|
|
||||||
getDefaultPanelUrlPath,
|
|
||||||
getPanelIcon,
|
|
||||||
getPanelIconPath,
|
|
||||||
getPanelTitle,
|
|
||||||
SHOW_AFTER_SPACER_PANELS,
|
|
||||||
} from "../data/panel";
|
|
||||||
import type { PersistentNotification } from "../data/persistent_notification";
|
import type { PersistentNotification } from "../data/persistent_notification";
|
||||||
import { subscribeNotifications } from "../data/persistent_notification";
|
import { subscribeNotifications } from "../data/persistent_notification";
|
||||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||||
@@ -52,6 +53,8 @@ import "./ha-spinner";
|
|||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./user/ha-user-badge";
|
import "./user/ha-user-badge";
|
||||||
|
|
||||||
|
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||||
|
|
||||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||||
|
|
||||||
const SORT_VALUE_URL_PATHS = {
|
const SORT_VALUE_URL_PATHS = {
|
||||||
@@ -63,6 +66,18 @@ const SORT_VALUE_URL_PATHS = {
|
|||||||
config: 11,
|
config: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PANEL_ICONS = {
|
||||||
|
calendar: mdiCalendar,
|
||||||
|
"developer-tools": mdiHammer,
|
||||||
|
energy: mdiLightningBolt,
|
||||||
|
history: mdiChartBox,
|
||||||
|
logbook: mdiFormatListBulletedType,
|
||||||
|
lovelace: mdiViewDashboard,
|
||||||
|
map: mdiTooltipAccount,
|
||||||
|
"media-browser": mdiPlayBoxMultiple,
|
||||||
|
todo: mdiClipboardList,
|
||||||
|
};
|
||||||
|
|
||||||
const panelSorter = (
|
const panelSorter = (
|
||||||
reverseSort: string[],
|
reverseSort: string[],
|
||||||
defaultPanel: string,
|
defaultPanel: string,
|
||||||
@@ -127,7 +142,7 @@ const defaultPanelSorter = (
|
|||||||
export const computePanels = memoizeOne(
|
export const computePanels = memoizeOne(
|
||||||
(
|
(
|
||||||
panels: HomeAssistant["panels"],
|
panels: HomeAssistant["panels"],
|
||||||
defaultPanel: string,
|
defaultPanel: HomeAssistant["defaultPanel"],
|
||||||
panelsOrder: string[],
|
panelsOrder: string[],
|
||||||
hiddenPanels: string[],
|
hiddenPanels: string[],
|
||||||
locale: HomeAssistant["locale"]
|
locale: HomeAssistant["locale"]
|
||||||
@@ -139,23 +154,14 @@ export const computePanels = memoizeOne(
|
|||||||
const beforeSpacer: PanelInfo[] = [];
|
const beforeSpacer: PanelInfo[] = [];
|
||||||
const afterSpacer: PanelInfo[] = [];
|
const afterSpacer: PanelInfo[] = [];
|
||||||
|
|
||||||
const allPanels = Object.values(panels).filter(
|
Object.values(panels).forEach((panel) => {
|
||||||
(panel) => !FIXED_PANELS.includes(panel.url_path)
|
|
||||||
);
|
|
||||||
|
|
||||||
allPanels.forEach((panel) => {
|
|
||||||
const isDefaultPanel = panel.url_path === defaultPanel;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isDefaultPanel &&
|
hiddenPanels.includes(panel.url_path) ||
|
||||||
(!panel.title ||
|
(!panel.title && panel.url_path !== defaultPanel)
|
||||||
hiddenPanels.includes(panel.url_path) ||
|
|
||||||
(panel.default_visible === false &&
|
|
||||||
!panelsOrder.includes(panel.url_path)))
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
|
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||||
? afterSpacer
|
? afterSpacer
|
||||||
: beforeSpacer
|
: beforeSpacer
|
||||||
).push(panel);
|
).push(panel);
|
||||||
@@ -242,7 +248,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPanel = this.hass.panelUrl;
|
// Show the supervisor as being part of configuration
|
||||||
|
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||||
|
? "config"
|
||||||
|
: this.hass.panelUrl;
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return html`
|
return html`
|
||||||
@@ -287,8 +296,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
hass.localize !== oldHass.localize ||
|
hass.localize !== oldHass.localize ||
|
||||||
hass.locale !== oldHass.locale ||
|
hass.locale !== oldHass.locale ||
|
||||||
hass.states !== oldHass.states ||
|
hass.states !== oldHass.states ||
|
||||||
hass.userData !== oldHass.userData ||
|
hass.defaultPanel !== oldHass.defaultPanel ||
|
||||||
hass.systemData !== oldHass.systemData ||
|
|
||||||
hass.connected !== oldHass.connected
|
hass.connected !== oldHass.connected
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -385,22 +393,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
private _renderAllPanels(selectedPanel: string) {
|
private _renderAllPanels(selectedPanel: string) {
|
||||||
if (!this._panelOrder || !this._hiddenPanels) {
|
if (!this._panelOrder || !this._hiddenPanels) {
|
||||||
return html`
|
return html`
|
||||||
<ha-fade-in .delay=${500}>
|
<ha-fade-in .delay=${500}
|
||||||
<ha-spinner size="small"></ha-spinner>
|
><ha-spinner size="small"></ha-spinner
|
||||||
</ha-fade-in>
|
></ha-fade-in>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
|
||||||
|
|
||||||
const [beforeSpacer, afterSpacer] = computePanels(
|
const [beforeSpacer, afterSpacer] = computePanels(
|
||||||
this.hass.panels,
|
this.hass.panels,
|
||||||
defaultPanel,
|
this.hass.defaultPanel,
|
||||||
this._panelOrder,
|
this._panelOrder,
|
||||||
this._hiddenPanels,
|
this._hiddenPanels,
|
||||||
this.hass.locale
|
this.hass.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list
|
<ha-md-list
|
||||||
class="ha-scrollbar"
|
class="ha-scrollbar"
|
||||||
@@ -412,39 +419,54 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||||
${this._renderSpacer()}
|
${this._renderSpacer()}
|
||||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||||
${this.hass.user?.is_admin
|
${this._renderExternalConfiguration()}
|
||||||
? this._renderConfiguration(selectedPanel)
|
|
||||||
: this._renderExternalConfiguration()}
|
|
||||||
</ha-md-list>
|
</ha-md-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||||
return panels.map((panel) =>
|
return panels.map((panel) =>
|
||||||
this._renderPanel(panel, panel.url_path === selectedPanel)
|
this._renderPanel(
|
||||||
|
panel.url_path,
|
||||||
|
panel.url_path === this.hass.defaultPanel
|
||||||
|
? panel.title || this.hass.localize("panel.states")
|
||||||
|
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||||
|
panel.icon,
|
||||||
|
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||||
|
? PANEL_ICONS.lovelace
|
||||||
|
: panel.url_path in PANEL_ICONS
|
||||||
|
? PANEL_ICONS[panel.url_path]
|
||||||
|
: undefined,
|
||||||
|
selectedPanel
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
|
private _renderPanel(
|
||||||
const title = getPanelTitle(this.hass, panel);
|
urlPath: string,
|
||||||
const urlPath = panel.url_path;
|
title: string | null,
|
||||||
const icon = getPanelIcon(panel);
|
icon: string | null | undefined,
|
||||||
const iconPath = getPanelIconPath(panel);
|
iconPath: string | null | undefined,
|
||||||
|
selectedPanel: string
|
||||||
return html`
|
) {
|
||||||
<ha-md-list-item
|
return urlPath === "config"
|
||||||
.href=${`/${urlPath}`}
|
? this._renderConfiguration(title, selectedPanel)
|
||||||
type="link"
|
: html`
|
||||||
class=${classMap({ selected: isSelected })}
|
<ha-md-list-item
|
||||||
@mouseenter=${this._itemMouseEnter}
|
.href=${`/${urlPath}`}
|
||||||
@mouseleave=${this._itemMouseLeave}
|
type="link"
|
||||||
>
|
class=${classMap({
|
||||||
${iconPath
|
selected: selectedPanel === urlPath,
|
||||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
})}
|
||||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
<span class="item-text" slot="headline">${title}</span>
|
@mouseleave=${this._itemMouseLeave}
|
||||||
</ha-md-list-item>
|
>
|
||||||
`;
|
${iconPath
|
||||||
|
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||||
|
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||||
|
<span class="item-text" slot="headline">${title}</span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderDivider() {
|
private _renderDivider() {
|
||||||
@@ -455,15 +477,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
return html`<div class="spacer" disabled></div>`;
|
return html`<div class="spacer" disabled></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderConfiguration(selectedPanel: string) {
|
private _renderConfiguration(title: string | null, selectedPanel: string) {
|
||||||
if (!this.hass.user?.is_admin) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
const isSelected =
|
|
||||||
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
class="configuration ${classMap({ selected: isSelected })}"
|
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
||||||
type="button"
|
type="button"
|
||||||
href="/config"
|
href="/config"
|
||||||
@mouseenter=${this._itemMouseEnter}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
@@ -477,17 +494,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
${this._updatesCount + this._issuesCount}
|
${this._updatesCount + this._issuesCount}
|
||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
<span class="item-text" slot="headline"
|
<span class="item-text" slot="headline">${title}</span>
|
||||||
>${this.hass.localize("panel.config")}</span
|
|
||||||
>
|
|
||||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||||
? html`
|
? html`
|
||||||
<span class="badge" slot="end"
|
<span class="badge" slot="end"
|
||||||
>${this._updatesCount + this._issuesCount}</span
|
>${this._updatesCount + this._issuesCount}</span
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -510,20 +525,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
? html`
|
? html`
|
||||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
<span class="item-text" slot="headline"
|
<span class="item-text" slot="headline"
|
||||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||||
>
|
>
|
||||||
${this.alwaysExpand && notificationCount > 0
|
${this.alwaysExpand && notificationCount > 0
|
||||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||||
: nothing}
|
: ""}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderUserItem(selectedPanel: string) {
|
private _renderUserItem(selectedPanel: string) {
|
||||||
const isRTL = computeRTL(this.hass);
|
const isRTL = computeRTL(this.hass);
|
||||||
const isSelected = selectedPanel === "profile";
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
@@ -531,7 +545,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
type="link"
|
type="link"
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
user: true,
|
user: true,
|
||||||
selected: isSelected,
|
selected: selectedPanel === "profile",
|
||||||
rtl: isRTL,
|
rtl: isRTL,
|
||||||
})}
|
})}
|
||||||
@mouseenter=${this._itemMouseEnter}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
@@ -542,30 +556,31 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
.user=${this.hass.user}
|
.user=${this.hass.user}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-user-badge>
|
></ha-user-badge>
|
||||||
<span class="item-text" slot="headline">
|
|
||||||
${this.hass.user ? this.hass.user.name : ""}
|
<span class="item-text" slot="headline"
|
||||||
</span>
|
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||||
|
>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderExternalConfiguration() {
|
private _renderExternalConfiguration() {
|
||||||
if (!this.hass.auth.external?.config.hasSettingsScreen) {
|
return html`${!this.hass.user?.is_admin &&
|
||||||
return nothing;
|
this.hass.auth.external?.config.hasSettingsScreen
|
||||||
}
|
? html`
|
||||||
return html`
|
<ha-md-list-item
|
||||||
<ha-md-list-item
|
@click=${this._handleExternalAppConfiguration}
|
||||||
@click=${this._handleExternalAppConfiguration}
|
type="button"
|
||||||
type="button"
|
@mouseenter=${this._itemMouseEnter}
|
||||||
@mouseenter=${this._itemMouseEnter}
|
@mouseleave=${this._itemMouseLeave}
|
||||||
@mouseleave=${this._itemMouseLeave}
|
>
|
||||||
>
|
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
<span class="item-text" slot="headline">
|
||||||
<span class="item-text" slot="headline">
|
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
</span>
|
||||||
</span>
|
</ha-md-list-item>
|
||||||
</ha-md-list-item>
|
`
|
||||||
`;
|
: ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleExternalAppConfiguration(ev: Event) {
|
private _handleExternalAppConfiguration(ev: Event) {
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import { subscribeLabFeatures } from "../data/labs";
|
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
|
||||||
|
|
||||||
interface Snowflake {
|
|
||||||
id: number;
|
|
||||||
left: number;
|
|
||||||
size: number;
|
|
||||||
duration: number;
|
|
||||||
delay: number;
|
|
||||||
blur: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-snowflakes")
|
|
||||||
export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
|
||||||
|
|
||||||
@state() private _enabled = false;
|
|
||||||
|
|
||||||
@state() private _snowflakes: Snowflake[] = [];
|
|
||||||
|
|
||||||
private _maxSnowflakes = 50;
|
|
||||||
|
|
||||||
public hassSubscribe() {
|
|
||||||
return [
|
|
||||||
subscribeLabFeatures(this.hass!.connection, (features) => {
|
|
||||||
this._enabled =
|
|
||||||
features.find(
|
|
||||||
(f) =>
|
|
||||||
f.domain === "frontend" && f.preview_feature === "winter_mode"
|
|
||||||
)?.enabled ?? false;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _generateSnowflakes() {
|
|
||||||
if (!this._enabled) {
|
|
||||||
this._snowflakes = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snowflakes: Snowflake[] = [];
|
|
||||||
for (let i = 0; i < this._maxSnowflakes; i++) {
|
|
||||||
snowflakes.push({
|
|
||||||
id: i,
|
|
||||||
left: Math.random() * 100, // Random position from 0-100%
|
|
||||||
size: Math.random() * 12 + 8, // Random size between 8-20px
|
|
||||||
duration: Math.random() * 8 + 8, // Random duration between 8-16s
|
|
||||||
delay: Math.random() * 8, // Random delay between 0-8s
|
|
||||||
blur: Math.random() * 1, // Random blur between 0-1px
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._snowflakes = snowflakes;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected willUpdate(changedProps: Map<string, unknown>) {
|
|
||||||
super.willUpdate(changedProps);
|
|
||||||
if (changedProps.has("_enabled")) {
|
|
||||||
this._generateSnowflakes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._enabled) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDark = this.hass?.themes.darkMode ?? false;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
|
|
||||||
${this._snowflakes.map(
|
|
||||||
(flake) => html`
|
|
||||||
<div
|
|
||||||
class="snowflake ${this.narrow && flake.id >= 30
|
|
||||||
? "hide-narrow"
|
|
||||||
: ""}"
|
|
||||||
style="
|
|
||||||
left: ${flake.left}%;
|
|
||||||
font-size: ${flake.size}px;
|
|
||||||
animation-duration: ${flake.duration}s;
|
|
||||||
animation-delay: ${flake.delay}s;
|
|
||||||
filter: blur(${flake.blur}px);
|
|
||||||
"
|
|
||||||
>
|
|
||||||
❄
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static readonly styles = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 9999;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflakes {
|
|
||||||
position: absolute;
|
|
||||||
top: -10%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 110%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake {
|
|
||||||
position: absolute;
|
|
||||||
top: -10%;
|
|
||||||
opacity: 0.7;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: fall linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .snowflake {
|
|
||||||
color: #00bcd4;
|
|
||||||
text-shadow:
|
|
||||||
0 0 5px #00bcd4,
|
|
||||||
0 0 10px #00e5ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .snowflake {
|
|
||||||
color: #fff;
|
|
||||||
text-shadow:
|
|
||||||
0 0 5px rgba(255, 255, 255, 0.8),
|
|
||||||
0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake.hide-narrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fall {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-10vh) translateX(0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translateY(30vh) translateX(10px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(60vh) translateX(-10px);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translateY(85vh) translateX(10px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(120vh) translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.snowflake {
|
|
||||||
animation: none;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-snowflakes": HaSnowflakes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ export class HaTooltip extends Tooltip {
|
|||||||
@property({ attribute: "show-delay", type: Number }) showDelay = 150;
|
@property({ attribute: "show-delay", type: Number }) showDelay = 150;
|
||||||
|
|
||||||
/** The amount of time to wait before hiding the tooltip when the user mouses out.. */
|
/** The amount of time to wait before hiding the tooltip when the user mouses out.. */
|
||||||
@property({ attribute: "hide-delay", type: Number }) hideDelay = 150;
|
@property({ attribute: "hide-delay", type: Number }) hideDelay = 400;
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import {
|
|
||||||
mdiAvTimer,
|
|
||||||
mdiCalendar,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCodeBraces,
|
|
||||||
mdiDevices,
|
|
||||||
mdiFormatListBulleted,
|
|
||||||
mdiGestureDoubleTap,
|
|
||||||
mdiHomeAssistant,
|
|
||||||
mdiMapMarker,
|
|
||||||
mdiMapMarkerRadius,
|
|
||||||
mdiMessageAlert,
|
|
||||||
mdiMicrophoneMessage,
|
|
||||||
mdiNfcVariant,
|
|
||||||
mdiNumeric,
|
|
||||||
mdiStateMachine,
|
|
||||||
mdiSwapHorizontal,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
mdiWebhook,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { until } from "lit/directives/until";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-svg-icon";
|
|
||||||
|
|
||||||
export const TRIGGER_ICONS = {
|
|
||||||
calendar: mdiCalendar,
|
|
||||||
device: mdiDevices,
|
|
||||||
event: mdiGestureDoubleTap,
|
|
||||||
state: mdiStateMachine,
|
|
||||||
geo_location: mdiMapMarker,
|
|
||||||
homeassistant: mdiHomeAssistant,
|
|
||||||
mqtt: mdiSwapHorizontal,
|
|
||||||
numeric_state: mdiNumeric,
|
|
||||||
sun: mdiWeatherSunny,
|
|
||||||
conversation: mdiMicrophoneMessage,
|
|
||||||
tag: mdiNfcVariant,
|
|
||||||
template: mdiCodeBraces,
|
|
||||||
time: mdiClockOutline,
|
|
||||||
time_pattern: mdiAvTimer,
|
|
||||||
webhook: mdiWebhook,
|
|
||||||
persistent_notification: mdiMessageAlert,
|
|
||||||
zone: mdiMapMarkerRadius,
|
|
||||||
list: mdiFormatListBulleted,
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-trigger-icon")
|
|
||||||
export class HaTriggerIcon extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public trigger?: string;
|
|
||||||
|
|
||||||
@property() public icon?: string;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (this.icon) {
|
|
||||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.trigger) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.hass) {
|
|
||||||
return this._renderFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = triggerIcon(this.hass, this.trigger).then((icn) => {
|
|
||||||
if (icn) {
|
|
||||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
|
||||||
}
|
|
||||||
return this._renderFallback();
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`${until(icon)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderFallback() {
|
|
||||||
const domain = computeDomain(this.trigger!);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${TRIGGER_ICONS[this.trigger!] || FALLBACK_DOMAIN_ICONS[domain]}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-trigger-icon": HaTriggerIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { css, html, LitElement } from "lit";
|
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
eventOptions,
|
eventOptions,
|
||||||
@@ -6,9 +8,6 @@ import {
|
|||||||
query,
|
query,
|
||||||
state,
|
state,
|
||||||
} from "lit/decorators";
|
} from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { mdiClose } from "@mdi/js";
|
|
||||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -32,8 +31,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
|||||||
*
|
*
|
||||||
* @slot header - Replace the entire header area.
|
* @slot header - Replace the entire header area.
|
||||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
* @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 headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||||
* @slot - Dialog content body.
|
* @slot - Dialog content body.
|
||||||
* @slot footer - Dialog footer content.
|
* @slot footer - Dialog footer content.
|
||||||
@@ -55,8 +52,8 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
|||||||
* @attr {boolean} open - Controls the dialog open state.
|
* @attr {boolean} open - Controls the dialog open state.
|
||||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
* @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 {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-title - Header title text when no custom title slot is provided.
|
||||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
* @attr {string} header-subtitle - Header subtitle text when no custom subtitle slot is provided.
|
||||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
* @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.
|
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
|
||||||
*
|
*
|
||||||
@@ -75,12 +72,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
|||||||
export class HaWaDialog extends LitElement {
|
export class HaWaDialog extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: "aria-labelledby" })
|
|
||||||
public ariaLabelledBy?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "aria-describedby" })
|
|
||||||
public ariaDescribedBy?: string;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
public open = false;
|
public open = false;
|
||||||
|
|
||||||
@@ -90,11 +81,11 @@ export class HaWaDialog extends LitElement {
|
|||||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||||
public preventScrimClose = false;
|
public preventScrimClose = false;
|
||||||
|
|
||||||
@property({ attribute: "header-title" })
|
@property({ type: String, attribute: "header-title" })
|
||||||
public headerTitle?: string;
|
public headerTitle = "";
|
||||||
|
|
||||||
@property({ attribute: "header-subtitle" })
|
@property({ type: String, attribute: "header-subtitle" })
|
||||||
public headerSubtitle?: string;
|
public headerSubtitle = "";
|
||||||
|
|
||||||
@property({ type: String, attribute: "header-subtitle-position" })
|
@property({ type: String, attribute: "header-subtitle-position" })
|
||||||
public headerSubtitlePosition: "above" | "below" = "below";
|
public headerSubtitlePosition: "above" | "below" = "below";
|
||||||
@@ -126,11 +117,6 @@ export class HaWaDialog extends LitElement {
|
|||||||
.open=${this._open}
|
.open=${this._open}
|
||||||
.lightDismiss=${!this.preventScrimClose}
|
.lightDismiss=${!this.preventScrimClose}
|
||||||
without-header
|
without-header
|
||||||
aria-labelledby=${ifDefined(
|
|
||||||
this.ariaLabelledBy ||
|
|
||||||
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
|
|
||||||
)}
|
|
||||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
|
||||||
@wa-show=${this._handleShow}
|
@wa-show=${this._handleShow}
|
||||||
@wa-after-show=${this._handleAfterShow}
|
@wa-after-show=${this._handleAfterShow}
|
||||||
@wa-after-hide=${this._handleAfterHide}
|
@wa-after-hide=${this._handleAfterHide}
|
||||||
@@ -147,14 +133,14 @@ export class HaWaDialog extends LitElement {
|
|||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</slot>
|
</slot>
|
||||||
${this.headerTitle !== undefined
|
${this.headerTitle
|
||||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
? html`<span slot="title" class="title">
|
||||||
${this.headerTitle}
|
${this.headerTitle}
|
||||||
</span>`
|
</span>`
|
||||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
: nothing}
|
||||||
${this.headerSubtitle !== undefined
|
${this.headerSubtitle
|
||||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
: nothing}
|
||||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
</slot>
|
</slot>
|
||||||
@@ -235,7 +221,7 @@ export class HaWaDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host([width="large"]) wa-dialog {
|
:host([width="large"]) wa-dialog {
|
||||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([width="full"]) wa-dialog {
|
:host([width="full"]) wa-dialog {
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
private _candidatesList: RTCIceCandidate[] = [];
|
private _candidatesList: RTCIceCandidate[] = [];
|
||||||
|
|
||||||
private _handleVisibilityChange = () => {
|
private _handleVisibilityChange = () => {
|
||||||
if (document.pictureInPictureElement) {
|
|
||||||
// video is playing in picture-in-picture mode, don't do anything
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
this._cleanUp();
|
this._cleanUp();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
removeLocalMedia,
|
removeLocalMedia,
|
||||||
} from "../../data/media_source";
|
} from "../../data/media_source";
|
||||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-button";
|
import "../ha-button";
|
||||||
import "../ha-check-list-item";
|
import "../ha-check-list-item";
|
||||||
@@ -305,7 +305,6 @@ class DialogMediaManage extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
haStyleDialogFixedTop,
|
|
||||||
css`
|
css`
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--dialog-z-index: 9;
|
--dialog-z-index: 9;
|
||||||
@@ -315,9 +314,9 @@ class DialogMediaManage extends LitElement {
|
|||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--mdc-dialog-max-width: 800px;
|
--mdc-dialog-max-width: 800px;
|
||||||
--mdc-dialog-max-height: calc(
|
--dialog-surface-position: fixed;
|
||||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
--dialog-surface-top: 40px;
|
||||||
);
|
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
MediaPlayerItem,
|
MediaPlayerItem,
|
||||||
MediaPlayerLayoutType,
|
MediaPlayerLayoutType,
|
||||||
} from "../../data/media-player";
|
} from "../../data/media-player";
|
||||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-dialog";
|
import "../ha-dialog";
|
||||||
import "../ha-dialog-header";
|
import "../ha-dialog-header";
|
||||||
@@ -223,7 +223,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
haStyleDialogFixedTop,
|
|
||||||
css`
|
css`
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--dialog-z-index: 9;
|
--dialog-z-index: 9;
|
||||||
@@ -231,27 +230,23 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ha-media-player-browse {
|
ha-media-player-browse {
|
||||||
--media-browser-max-height: calc(
|
--media-browser-max-height: calc(100vh - 65px);
|
||||||
100vh - 65px - var(--safe-area-inset-y)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(.opened) ha-media-player-browse {
|
:host(.opened) ha-media-player-browse {
|
||||||
height: calc(100vh - 65px - var(--safe-area-inset-y));
|
height: calc(100vh - 65px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--mdc-dialog-max-width: 800px;
|
--mdc-dialog-max-width: 800px;
|
||||||
--mdc-dialog-max-height: calc(
|
--dialog-surface-position: fixed;
|
||||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
--dialog-surface-top: 40px;
|
||||||
);
|
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||||
}
|
}
|
||||||
ha-media-player-browse {
|
ha-media-player-browse {
|
||||||
position: initial;
|
position: initial;
|
||||||
--media-browser-max-height: calc(
|
--media-browser-max-height: calc(100vh - 145px);
|
||||||
100vh - 145px - var(--safe-area-inset-y)
|
|
||||||
);
|
|
||||||
width: 700px;
|
width: 700px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class SearchInput extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
autocomplete="off"
|
|
||||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||||
.value=${this.filter || ""}
|
.value=${this.filter || ""}
|
||||||
icon
|
icon
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
mdiLabel,
|
mdiLabel,
|
||||||
mdiTextureBox,
|
mdiTextureBox,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -20,12 +19,9 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
|||||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
|
||||||
import { getConfigEntry } from "../../data/config_entries";
|
import { getConfigEntry } from "../../data/config_entries";
|
||||||
import { labelsContext } from "../../data/context";
|
import { labelsContext } from "../../data/context";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import type { LabelRegistryEntry } from "../../data/label_registry";
|
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
@@ -115,10 +111,10 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const { name, context, iconPath, fallbackIconPath, stateObject, notFound } =
|
const { name, context, iconPath, fallbackIconPath, stateObject } =
|
||||||
this._itemData(this.type, this.itemId);
|
this._itemData(this.type, this.itemId);
|
||||||
|
|
||||||
const showEntities = this.type !== "entity" && !notFound;
|
const showEntities = this.type !== "entity";
|
||||||
|
|
||||||
const entries = this.parentEntries || this._entries;
|
const entries = this.parentEntries || this._entries;
|
||||||
|
|
||||||
@@ -132,7 +128,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item type="text" class=${notFound ? "error" : ""}>
|
<ha-md-list-item type="text">
|
||||||
<div class="icon" slot="start">
|
<div class="icon" slot="start">
|
||||||
${this.subEntry
|
${this.subEntry
|
||||||
? html`
|
? html`
|
||||||
@@ -152,15 +148,11 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
/>`
|
/>`
|
||||||
: fallbackIconPath
|
: fallbackIconPath
|
||||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||||
: this.type === "entity"
|
: stateObject
|
||||||
? html`
|
? html`
|
||||||
<ha-state-icon
|
<ha-state-icon
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObject ||
|
.stateObj=${stateObject}
|
||||||
({
|
|
||||||
entity_id: this.itemId,
|
|
||||||
attributes: {},
|
|
||||||
} as HassEntity)}
|
|
||||||
>
|
>
|
||||||
</ha-state-icon>
|
</ha-state-icon>
|
||||||
`
|
`
|
||||||
@@ -168,14 +160,8 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div slot="headline">${name}</div>
|
<div slot="headline">${name}</div>
|
||||||
${notFound || (context && !this.hideContext)
|
${context && !this.hideContext
|
||||||
? html`<span slot="supporting-text"
|
? html`<span slot="supporting-text">${context}</span>`
|
||||||
>${notFound
|
|
||||||
? this.hass.localize(
|
|
||||||
`ui.components.target-picker.${this.type}_not_found`
|
|
||||||
)
|
|
||||||
: context}</span
|
|
||||||
>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._domainName && this.subEntry
|
${this._domainName && this.subEntry
|
||||||
? html`<span slot="supporting-text" class="domain"
|
? html`<span slot="supporting-text" class="domain"
|
||||||
@@ -488,28 +474,26 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
|
|
||||||
private _itemData = memoizeOne((type: TargetType, item: string) => {
|
private _itemData = memoizeOne((type: TargetType, item: string) => {
|
||||||
if (type === "floor") {
|
if (type === "floor") {
|
||||||
const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item];
|
const floor = this.hass.floors?.[item];
|
||||||
return {
|
return {
|
||||||
name: floor?.name || item,
|
name: floor?.name || item,
|
||||||
iconPath: floor?.icon,
|
iconPath: floor?.icon,
|
||||||
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
|
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
|
||||||
notFound: !floor,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === "area") {
|
if (type === "area") {
|
||||||
const area: AreaRegistryEntry | undefined = this.hass.areas?.[item];
|
const area = this.hass.areas?.[item];
|
||||||
return {
|
return {
|
||||||
name: area?.name || item,
|
name: area?.name || item,
|
||||||
context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name,
|
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
|
||||||
iconPath: area?.icon,
|
iconPath: area?.icon,
|
||||||
fallbackIconPath: mdiTextureBox,
|
fallbackIconPath: mdiTextureBox,
|
||||||
notFound: !area,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === "device") {
|
if (type === "device") {
|
||||||
const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item];
|
const device = this.hass.devices?.[item];
|
||||||
|
|
||||||
if (device?.primary_config_entry) {
|
if (device.primary_config_entry) {
|
||||||
this._getDeviceDomain(device.primary_config_entry);
|
this._getDeviceDomain(device.primary_config_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,25 +501,24 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
|
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
|
||||||
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
|
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
|
||||||
fallbackIconPath: mdiDevices,
|
fallbackIconPath: mdiDevices,
|
||||||
notFound: !device,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === "entity") {
|
if (type === "entity") {
|
||||||
this._setDomainName(computeDomain(item));
|
this._setDomainName(computeDomain(item));
|
||||||
|
|
||||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
const stateObject = this.hass.states[item];
|
||||||
const entityName = stateObject
|
const entityName = computeEntityName(
|
||||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
stateObject,
|
||||||
: item;
|
this.hass.entities,
|
||||||
const { area, device } = stateObject
|
this.hass.devices
|
||||||
? getEntityContext(
|
);
|
||||||
stateObject,
|
const { area, device } = getEntityContext(
|
||||||
this.hass.entities,
|
stateObject,
|
||||||
this.hass.devices,
|
this.hass.entities,
|
||||||
this.hass.areas,
|
this.hass.devices,
|
||||||
this.hass.floors
|
this.hass.areas,
|
||||||
)
|
this.hass.floors
|
||||||
: { area: undefined, device: undefined };
|
);
|
||||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
const context = [areaName, entityName ? deviceName : undefined]
|
const context = [areaName, entityName ? deviceName : undefined]
|
||||||
@@ -545,19 +528,15 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
name: entityName || deviceName || item,
|
name: entityName || deviceName || item,
|
||||||
context,
|
context,
|
||||||
stateObject,
|
stateObject,
|
||||||
notFound: !stateObject && item !== "all" && item !== "none",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// type label
|
// type label
|
||||||
const label: LabelRegistryEntry | undefined = this._labelRegistry.find(
|
const label = this._labelRegistry.find((lab) => lab.label_id === item);
|
||||||
(lab) => lab.label_id === item
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
name: label?.name || item,
|
name: label?.name || item,
|
||||||
iconPath: label?.icon,
|
iconPath: label?.icon,
|
||||||
fallbackIconPath: mdiLabel,
|
fallbackIconPath: mdiLabel,
|
||||||
notFound: !label,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -618,27 +597,17 @@ export class HaTargetPickerItemRow extends LitElement {
|
|||||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
|
||||||
background: var(--ha-color-fill-warning-quiet-resting);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error [slot="supporting-text"] {
|
|
||||||
color: var(--ha-color-on-warning-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
state-badge {
|
state-badge {
|
||||||
color: var(--ha-color-on-neutral-quiet);
|
color: var(--ha-color-on-neutral-quiet);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
--mdc-icon-button-size: 32px;
|
--mdc-icon-button-size: 32px;
|
||||||
|
|||||||
1105
src/components/target-picker/ha-target-picker-selector.ts
Normal file
1105
src/components/target-picker/ha-target-picker-selector.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,9 @@ class HaUserPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this.hass.localize(
|
||||||
|
"ui.components.user-picker.no_match"
|
||||||
|
)}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
@@ -147,11 +149,6 @@ class HaUserPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundLabel = (search: string) =>
|
|
||||||
this.hass.localize("ui.components.user-picker.no_match", {
|
|
||||||
term: html`<b>‘${search}’</b>`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
|||||||
{
|
{
|
||||||
groups: {
|
groups: {
|
||||||
device_id: {},
|
device_id: {},
|
||||||
dynamicGroups: {},
|
serviceGroups: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -117,6 +117,14 @@ export const VIRTUAL_ACTIONS: Partial<
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SERVICE_PREFIX = "__SERVICE__";
|
||||||
|
|
||||||
|
export const isService = (key: string | undefined): boolean | undefined =>
|
||||||
|
key?.startsWith(SERVICE_PREFIX);
|
||||||
|
|
||||||
|
export const getService = (key: string): string =>
|
||||||
|
key.substring(SERVICE_PREFIX.length);
|
||||||
|
|
||||||
export const COLLAPSIBLE_ACTION_ELEMENTS = [
|
export const COLLAPSIBLE_ACTION_ELEMENTS = [
|
||||||
"ha-automation-action-choose",
|
"ha-automation-action-choose",
|
||||||
"ha-automation-action-condition",
|
"ha-automation-action-condition",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export interface AnalyticsPreferences {
|
|||||||
diagnostics?: boolean;
|
diagnostics?: boolean;
|
||||||
usage?: boolean;
|
usage?: boolean;
|
||||||
statistics?: boolean;
|
statistics?: boolean;
|
||||||
snapshots?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Analytics {
|
export interface Analytics {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getAreasFloorHierarchy } from "../common/areas/areas-floor-hierarchy";
|
|
||||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
@@ -13,7 +12,11 @@ import {
|
|||||||
} from "./device_registry";
|
} from "./device_registry";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||||
import type { FloorRegistryEntry } from "./floor_registry";
|
import {
|
||||||
|
floorCompare,
|
||||||
|
getFloorAreaLookup,
|
||||||
|
type FloorRegistryEntry,
|
||||||
|
} from "./floor_registry";
|
||||||
|
|
||||||
export interface FloorComboBoxItem extends PickerComboBoxItem {
|
export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
@@ -21,52 +24,11 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
|||||||
area?: AreaRegistryEntry;
|
area?: AreaRegistryEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
|
||||||
floor?: FloorRegistryEntry;
|
|
||||||
areas: FloorComboBoxItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
|
||||||
areas: FloorComboBoxItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AreaFloorValue {
|
export interface AreaFloorValue {
|
||||||
id: string;
|
id: string;
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAreasNestedInFloors = (
|
|
||||||
states: HomeAssistant["states"],
|
|
||||||
haFloors: HomeAssistant["floors"],
|
|
||||||
haAreas: HomeAssistant["areas"],
|
|
||||||
haDevices: HomeAssistant["devices"],
|
|
||||||
haEntities: HomeAssistant["entities"],
|
|
||||||
formatId: (value: AreaFloorValue) => string,
|
|
||||||
includeDomains?: string[],
|
|
||||||
excludeDomains?: string[],
|
|
||||||
includeDeviceClasses?: string[],
|
|
||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
|
||||||
excludeAreas?: string[],
|
|
||||||
excludeFloors?: string[]
|
|
||||||
) =>
|
|
||||||
getAreasAndFloorsItems(
|
|
||||||
states,
|
|
||||||
haFloors,
|
|
||||||
haAreas,
|
|
||||||
haDevices,
|
|
||||||
haEntities,
|
|
||||||
formatId,
|
|
||||||
includeDomains,
|
|
||||||
excludeDomains,
|
|
||||||
includeDeviceClasses,
|
|
||||||
deviceFilter,
|
|
||||||
entityFilter,
|
|
||||||
excludeAreas,
|
|
||||||
excludeFloors,
|
|
||||||
true
|
|
||||||
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
|
||||||
|
|
||||||
export const getAreasAndFloors = (
|
export const getAreasAndFloors = (
|
||||||
states: HomeAssistant["states"],
|
states: HomeAssistant["states"],
|
||||||
haFloors: HomeAssistant["floors"],
|
haFloors: HomeAssistant["floors"],
|
||||||
@@ -81,43 +43,7 @@ export const getAreasAndFloors = (
|
|||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
excludeAreas?: string[],
|
excludeAreas?: string[],
|
||||||
excludeFloors?: string[]
|
excludeFloors?: string[]
|
||||||
) =>
|
): FloorComboBoxItem[] => {
|
||||||
getAreasAndFloorsItems(
|
|
||||||
states,
|
|
||||||
haFloors,
|
|
||||||
haAreas,
|
|
||||||
haDevices,
|
|
||||||
haEntities,
|
|
||||||
formatId,
|
|
||||||
includeDomains,
|
|
||||||
excludeDomains,
|
|
||||||
includeDeviceClasses,
|
|
||||||
deviceFilter,
|
|
||||||
entityFilter,
|
|
||||||
excludeAreas,
|
|
||||||
excludeFloors
|
|
||||||
) as FloorComboBoxItem[];
|
|
||||||
|
|
||||||
const getAreasAndFloorsItems = (
|
|
||||||
states: HomeAssistant["states"],
|
|
||||||
haFloors: HomeAssistant["floors"],
|
|
||||||
haAreas: HomeAssistant["areas"],
|
|
||||||
haDevices: HomeAssistant["devices"],
|
|
||||||
haEntities: HomeAssistant["entities"],
|
|
||||||
formatId: (value: AreaFloorValue) => string,
|
|
||||||
includeDomains?: string[],
|
|
||||||
excludeDomains?: string[],
|
|
||||||
includeDeviceClasses?: string[],
|
|
||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
|
||||||
excludeAreas?: string[],
|
|
||||||
excludeFloors?: string[],
|
|
||||||
nested = false
|
|
||||||
): (
|
|
||||||
| FloorComboBoxItem
|
|
||||||
| FloorNestedComboBoxItem
|
|
||||||
| UnassignedAreasFloorComboBoxItem
|
|
||||||
)[] => {
|
|
||||||
const floors = Object.values(haFloors);
|
const floors = Object.values(haFloors);
|
||||||
const areas = Object.values(haAreas);
|
const areas = Object.values(haAreas);
|
||||||
const devices = Object.values(haDevices);
|
const devices = Object.values(haDevices);
|
||||||
@@ -256,86 +182,79 @@ const getAreasAndFloorsItems = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
|
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||||
|
const unassignedAreas = Object.values(outputAreas).filter(
|
||||||
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
|
);
|
||||||
|
|
||||||
const items: (
|
const compare = floorCompare(haFloors);
|
||||||
| FloorComboBoxItem
|
|
||||||
| FloorNestedComboBoxItem
|
|
||||||
| UnassignedAreasFloorComboBoxItem
|
|
||||||
)[] = [];
|
|
||||||
|
|
||||||
hierarchy.floors.forEach((f) => {
|
// @ts-ignore
|
||||||
const floor = haFloors[f.id];
|
const floorAreaEntries: [
|
||||||
const floorAreas = f.areas.map((areaId) => haAreas[areaId]);
|
FloorRegistryEntry | undefined,
|
||||||
|
AreaRegistryEntry[],
|
||||||
|
][] = Object.entries(floorAreaLookup)
|
||||||
|
.map(([floorId, floorAreas]) => {
|
||||||
|
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
||||||
|
return [floor, floorAreas] as const;
|
||||||
|
})
|
||||||
|
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||||
|
|
||||||
const floorName = computeFloorName(floor);
|
const items: FloorComboBoxItem[] = [];
|
||||||
|
|
||||||
const areaSearchLabels = floorAreas
|
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||||
.map((area) => {
|
if (floor) {
|
||||||
const areaName = computeAreaName(area);
|
const floorName = computeFloorName(floor);
|
||||||
return [area.area_id, ...(areaName ? [areaName] : []), ...area.aliases];
|
|
||||||
|
const areaSearchLabels = floorAreas
|
||||||
|
.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return [area.area_id, areaName, ...area.aliases];
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||||
|
type: "floor",
|
||||||
|
primary: floorName,
|
||||||
|
floor: floor,
|
||||||
|
icon: floor.icon || undefined,
|
||||||
|
search_labels: [
|
||||||
|
floor.floor_id,
|
||||||
|
floorName,
|
||||||
|
...floor.aliases,
|
||||||
|
...areaSearchLabels,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items.push(
|
||||||
|
...floorAreas.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return {
|
||||||
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
|
type: "area" as const,
|
||||||
|
primary: areaName,
|
||||||
|
area: area,
|
||||||
|
icon: area.icon || undefined,
|
||||||
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.flat();
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
|
items.push(
|
||||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
...unassignedAreas.map((area) => {
|
||||||
type: "floor",
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
primary: floorName,
|
|
||||||
floor: floor,
|
|
||||||
icon: floor.icon || undefined,
|
|
||||||
search_labels: [
|
|
||||||
floor.floor_id,
|
|
||||||
floorName,
|
|
||||||
...floor.aliases,
|
|
||||||
...areaSearchLabels,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
items.push(floorItem);
|
|
||||||
|
|
||||||
const floorAreasItems = floorAreas.map((area) => {
|
|
||||||
const areaName = computeAreaName(area);
|
|
||||||
return {
|
return {
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
type: "area" as const,
|
type: "area" as const,
|
||||||
primary: areaName || area.area_id,
|
primary: areaName,
|
||||||
area: area,
|
area: area,
|
||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
search_labels: [
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
area.area_id,
|
|
||||||
...(areaName ? [areaName] : []),
|
|
||||||
...area.aliases,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
);
|
||||||
if (nested && floor) {
|
|
||||||
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
|
|
||||||
} else {
|
|
||||||
items.push(...floorAreasItems);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
|
|
||||||
const area = haAreas[areaId];
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
|
||||||
return {
|
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
|
||||||
type: "area" as const,
|
|
||||||
primary: areaName,
|
|
||||||
area: area,
|
|
||||||
icon: area.icon || undefined,
|
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nested && unassignedAreaItems.length) {
|
|
||||||
items.push({
|
|
||||||
areas: unassignedAreaItems,
|
|
||||||
} as UnassignedAreasFloorComboBoxItem);
|
|
||||||
} else {
|
|
||||||
items.push(...unassignedAreaItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { DeviceRegistryEntry } from "./device_registry";
|
import type { DeviceRegistryEntry } from "./device_registry";
|
||||||
import type {
|
import type { EntityRegistryEntry } from "./entity_registry";
|
||||||
EntityRegistryDisplayEntry,
|
|
||||||
EntityRegistryEntry,
|
|
||||||
} from "./entity_registry";
|
|
||||||
import type { RegistryEntry } from "./registry";
|
import type { RegistryEntry } from "./registry";
|
||||||
|
|
||||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||||
@@ -21,10 +18,7 @@ export interface AreaRegistryEntry extends RegistryEntry {
|
|||||||
temperature_entity_id: string | null;
|
temperature_entity_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AreaEntityLookup = Record<
|
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||||
string,
|
|
||||||
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
||||||
|
|
||||||
@@ -65,27 +59,12 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
|||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const reorderAreaRegistryEntries = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
areaIds: string[]
|
|
||||||
) =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "config/area_registry/reorder",
|
|
||||||
area_ids: areaIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAreaEntityLookup = (
|
export const getAreaEntityLookup = (
|
||||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
entities: EntityRegistryEntry[]
|
||||||
filterHidden = false
|
|
||||||
): AreaEntityLookup => {
|
): AreaEntityLookup => {
|
||||||
const areaEntityLookup: AreaEntityLookup = {};
|
const areaEntityLookup: AreaEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (
|
if (!entity.area_id) {
|
||||||
!entity.area_id ||
|
|
||||||
(filterHidden &&
|
|
||||||
((entity as EntityRegistryDisplayEntry).hidden ||
|
|
||||||
(entity as EntityRegistryEntry).hidden_by))
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.area_id in areaEntityLookup)) {
|
if (!(entity.area_id in areaEntityLookup)) {
|
||||||
|
|||||||
@@ -214,8 +214,6 @@ export interface PipelineRun {
|
|||||||
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
|
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
|
||||||
run: PipelineRunStartEvent["data"];
|
run: PipelineRunStartEvent["data"];
|
||||||
error?: PipelineErrorEvent["data"];
|
error?: PipelineErrorEvent["data"];
|
||||||
started: Date;
|
|
||||||
finished?: Date;
|
|
||||||
wake_word?: PipelineWakeWordStartEvent["data"] &
|
wake_word?: PipelineWakeWordStartEvent["data"] &
|
||||||
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
|
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
|
||||||
stt?: PipelineSTTStartEvent["data"] &
|
stt?: PipelineSTTStartEvent["data"] &
|
||||||
@@ -237,7 +235,6 @@ export const processEvent = (
|
|||||||
stage: "ready",
|
stage: "ready",
|
||||||
run: event.data,
|
run: event.data,
|
||||||
events: [event],
|
events: [event],
|
||||||
started: new Date(event.timestamp),
|
|
||||||
};
|
};
|
||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
@@ -293,14 +290,9 @@ export const processEvent = (
|
|||||||
tts: { ...run.tts!, ...event.data, done: true },
|
tts: { ...run.tts!, ...event.data, done: true },
|
||||||
};
|
};
|
||||||
} else if (event.type === "run-end") {
|
} else if (event.type === "run-end") {
|
||||||
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
|
run = { ...run, stage: "done" };
|
||||||
} else if (event.type === "error") {
|
} else if (event.type === "error") {
|
||||||
run = {
|
run = { ...run, stage: "error", error: event.data };
|
||||||
...run,
|
|
||||||
finished: new Date(event.timestamp),
|
|
||||||
stage: "error",
|
|
||||||
error: event.data,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
run = { ...run };
|
run = { ...run };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,21 @@
|
|||||||
import type {
|
import type {
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
HassServiceTarget,
|
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
import type { LocalizeKeys } from "../common/translations/localize";
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
import { createSearchParam } from "../common/url/search-params";
|
import { createSearchParam } from "../common/url/search-params";
|
||||||
import type { Context, HomeAssistant } from "../types";
|
import type { Context, HomeAssistant } from "../types";
|
||||||
import type { BlueprintInput } from "./blueprint";
|
import type { BlueprintInput } from "./blueprint";
|
||||||
import type { ConditionDescription } from "./condition";
|
|
||||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import type { Action, Field, MODES } from "./script";
|
import type { Action, Field, MODES } from "./script";
|
||||||
import { migrateAutomationAction } from "./script";
|
import { migrateAutomationAction } from "./script";
|
||||||
import type { TriggerDescription } from "./trigger";
|
|
||||||
|
|
||||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||||
|
|
||||||
export const DYNAMIC_PREFIX = "__DYNAMIC__";
|
|
||||||
|
|
||||||
export const isDynamic = (key: string | undefined): boolean | undefined =>
|
|
||||||
key?.startsWith(DYNAMIC_PREFIX);
|
|
||||||
|
|
||||||
export const getValueFromDynamic = (key: string): string =>
|
|
||||||
key.substring(DYNAMIC_PREFIX.length);
|
|
||||||
|
|
||||||
export interface AutomationEntity extends HassEntityBase {
|
export interface AutomationEntity extends HassEntityBase {
|
||||||
attributes: HassEntityAttributeBase & {
|
attributes: HassEntityAttributeBase & {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -97,12 +85,6 @@ export interface BaseTrigger {
|
|||||||
id?: string;
|
id?: string;
|
||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
options?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlatformTrigger extends BaseTrigger {
|
|
||||||
trigger: Exclude<string, LegacyTrigger["trigger"]>;
|
|
||||||
target?: HassServiceTarget;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateTrigger extends BaseTrigger {
|
export interface StateTrigger extends BaseTrigger {
|
||||||
@@ -212,7 +194,7 @@ export interface CalendarTrigger extends BaseTrigger {
|
|||||||
offset: string;
|
offset: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LegacyTrigger =
|
export type Trigger =
|
||||||
| StateTrigger
|
| StateTrigger
|
||||||
| MqttTrigger
|
| MqttTrigger
|
||||||
| GeoLocationTrigger
|
| GeoLocationTrigger
|
||||||
@@ -229,20 +211,13 @@ export type LegacyTrigger =
|
|||||||
| TemplateTrigger
|
| TemplateTrigger
|
||||||
| EventTrigger
|
| EventTrigger
|
||||||
| DeviceTrigger
|
| DeviceTrigger
|
||||||
| CalendarTrigger;
|
| CalendarTrigger
|
||||||
|
| TriggerList;
|
||||||
export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
|
||||||
|
|
||||||
interface BaseCondition {
|
interface BaseCondition {
|
||||||
condition: string;
|
condition: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
options?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlatformCondition extends BaseCondition {
|
|
||||||
condition: Exclude<string, LegacyCondition["condition"]>;
|
|
||||||
target?: HassServiceTarget;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogicalCondition extends BaseCondition {
|
export interface LogicalCondition extends BaseCondition {
|
||||||
@@ -282,11 +257,13 @@ export interface ZoneCondition extends BaseCondition {
|
|||||||
zone: string;
|
zone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
||||||
|
|
||||||
export interface TimeCondition extends BaseCondition {
|
export interface TimeCondition extends BaseCondition {
|
||||||
condition: "time";
|
condition: "time";
|
||||||
after?: string;
|
after?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
weekday?: WeekdayShort | WeekdayShort[];
|
weekday?: Weekday | Weekday[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateCondition extends BaseCondition {
|
export interface TemplateCondition extends BaseCondition {
|
||||||
@@ -327,7 +304,7 @@ export type AutomationElementGroup = Record<
|
|||||||
{ icon?: string; members?: AutomationElementGroup }
|
{ icon?: string; members?: AutomationElementGroup }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type LegacyCondition =
|
export type Condition =
|
||||||
| StateCondition
|
| StateCondition
|
||||||
| NumericStateCondition
|
| NumericStateCondition
|
||||||
| SunCondition
|
| SunCondition
|
||||||
@@ -338,8 +315,6 @@ export type LegacyCondition =
|
|||||||
| LogicalCondition
|
| LogicalCondition
|
||||||
| TriggerCondition;
|
| TriggerCondition;
|
||||||
|
|
||||||
export type Condition = LegacyCondition | PlatformCondition;
|
|
||||||
|
|
||||||
export type ConditionWithShorthand =
|
export type ConditionWithShorthand =
|
||||||
| Condition
|
| Condition
|
||||||
| ShorthandAndConditionList
|
| ShorthandAndConditionList
|
||||||
@@ -601,7 +576,6 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
|||||||
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
||||||
toggleYamlMode: () => void;
|
toggleYamlMode: () => void;
|
||||||
config: Trigger;
|
config: Trigger;
|
||||||
description?: TriggerDescription;
|
|
||||||
yamlMode: boolean;
|
yamlMode: boolean;
|
||||||
uiSupported: boolean;
|
uiSupported: boolean;
|
||||||
}
|
}
|
||||||
@@ -617,7 +591,6 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
|||||||
insertAfter: (value: Condition | Condition[]) => boolean;
|
insertAfter: (value: Condition | Condition[]) => boolean;
|
||||||
toggleYamlMode: () => void;
|
toggleYamlMode: () => void;
|
||||||
config: Condition;
|
config: Condition;
|
||||||
description?: ConditionDescription;
|
|
||||||
yamlMode: boolean;
|
yamlMode: boolean;
|
||||||
uiSupported: boolean;
|
uiSupported: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,8 @@ import {
|
|||||||
formatListWithAnds,
|
formatListWithAnds,
|
||||||
formatListWithOrs,
|
formatListWithOrs,
|
||||||
} from "../common/string/format-list";
|
} from "../common/string/format-list";
|
||||||
import { hasTemplate } from "../common/string/has-template";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type {
|
import type { Condition, ForDict, Trigger } from "./automation";
|
||||||
Condition,
|
|
||||||
ForDict,
|
|
||||||
LegacyCondition,
|
|
||||||
LegacyTrigger,
|
|
||||||
Trigger,
|
|
||||||
} from "./automation";
|
|
||||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
|
||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import {
|
import {
|
||||||
localizeDeviceAutomationCondition,
|
localizeDeviceAutomationCondition,
|
||||||
@@ -33,7 +25,8 @@ import {
|
|||||||
} from "./device_automation";
|
} from "./device_automation";
|
||||||
import type { EntityRegistryEntry } from "./entity_registry";
|
import type { EntityRegistryEntry } from "./entity_registry";
|
||||||
import type { FrontendLocaleData } from "./translation";
|
import type { FrontendLocaleData } from "./translation";
|
||||||
import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger";
|
import { isTriggerList } from "./trigger";
|
||||||
|
import { hasTemplate } from "../common/string/has-template";
|
||||||
|
|
||||||
const triggerTranslationBaseKey =
|
const triggerTranslationBaseKey =
|
||||||
"ui.panel.config.automation.editor.triggers.type";
|
"ui.panel.config.automation.editor.triggers.type";
|
||||||
@@ -128,37 +121,6 @@ const tryDescribeTrigger = (
|
|||||||
return trigger.alias;
|
return trigger.alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = describeLegacyTrigger(
|
|
||||||
trigger as LegacyTrigger,
|
|
||||||
hass,
|
|
||||||
entityRegistry
|
|
||||||
);
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerType = trigger.trigger;
|
|
||||||
|
|
||||||
const domain = getTriggerDomain(trigger.trigger);
|
|
||||||
const type = getTriggerObjectId(trigger.trigger);
|
|
||||||
|
|
||||||
return (
|
|
||||||
hass.localize(
|
|
||||||
`component.${domain}.triggers.${type}.description_configured`
|
|
||||||
) ||
|
|
||||||
hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
|
|
||||||
) ||
|
|
||||||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const describeLegacyTrigger = (
|
|
||||||
trigger: LegacyTrigger,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entityRegistry: EntityRegistryEntry[]
|
|
||||||
) => {
|
|
||||||
// Event Trigger
|
// Event Trigger
|
||||||
if (trigger.trigger === "event" && trigger.event_type) {
|
if (trigger.trigger === "event" && trigger.event_type) {
|
||||||
const eventTypes: string[] = [];
|
const eventTypes: string[] = [];
|
||||||
@@ -840,7 +802,13 @@ const describeLegacyTrigger = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
|
||||||
|
) ||
|
||||||
|
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const describeCondition = (
|
export const describeCondition = (
|
||||||
@@ -903,39 +871,6 @@ const tryDescribeCondition = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = describeLegacyCondition(
|
|
||||||
condition as LegacyCondition,
|
|
||||||
hass,
|
|
||||||
entityRegistry
|
|
||||||
);
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conditionType = condition.condition;
|
|
||||||
|
|
||||||
const domain = getConditionDomain(condition.condition);
|
|
||||||
const type = getConditionObjectId(condition.condition);
|
|
||||||
|
|
||||||
return (
|
|
||||||
hass.localize(
|
|
||||||
`component.${domain}.conditions.${type}.description_configured`
|
|
||||||
) ||
|
|
||||||
hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
|
|
||||||
) ||
|
|
||||||
hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const describeLegacyCondition = (
|
|
||||||
condition: LegacyCondition,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entityRegistry: EntityRegistryEntry[]
|
|
||||||
) => {
|
|
||||||
if (condition.condition === "or") {
|
if (condition.condition === "or") {
|
||||||
const conditions = ensureArray(condition.conditions);
|
const conditions = ensureArray(condition.conditions);
|
||||||
|
|
||||||
@@ -1327,5 +1262,12 @@ const describeLegacyCondition = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.conditions.type.${condition.condition}.label`
|
||||||
|
) ||
|
||||||
|
hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { formatTime } from "../common/datetime/format_time";
|
import { formatTime } from "../common/datetime/format_time";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import { documentationUrl } from "../util/documentation-url";
|
|
||||||
import { fileDownload } from "../util/file_download";
|
import { fileDownload } from "../util/file_download";
|
||||||
import { handleFetchPromise } from "../util/hass-call-api";
|
import { handleFetchPromise } from "../util/hass-call-api";
|
||||||
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
||||||
@@ -415,7 +414,7 @@ ${hass.auth.data.hassUrl}
|
|||||||
${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
|
${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
|
||||||
${encryptionKey}
|
${encryptionKey}
|
||||||
|
|
||||||
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: documentationUrl(hass, "/more-info/backup-emergency-kit") })}`);
|
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: "https://www.home-assistant.io/more-info/backup-emergency-kit" })}`);
|
||||||
|
|
||||||
export const geneateEmergencyKitFileName = (
|
export const geneateEmergencyKitFileName = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export interface CalendarEventData {
|
|||||||
dtend: string;
|
dtend: string;
|
||||||
rrule?: string;
|
rrule?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
location?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendarEventMutableParams {
|
export interface CalendarEventMutableParams {
|
||||||
@@ -40,7 +39,6 @@ export interface CalendarEventMutableParams {
|
|||||||
dtend: string;
|
dtend: string;
|
||||||
rrule?: string;
|
rrule?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
location?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The scope of a delete/update for a recurring event
|
// The scope of a delete/update for a recurring event
|
||||||
@@ -98,7 +96,6 @@ export const fetchCalendarEvents = async (
|
|||||||
uid: ev.uid,
|
uid: ev.uid,
|
||||||
summary: ev.summary,
|
summary: ev.summary,
|
||||||
description: ev.description,
|
description: ev.description,
|
||||||
location: ev.location,
|
|
||||||
dtstart: eventStart,
|
dtstart: eventStart,
|
||||||
dtend: eventEnd,
|
dtend: eventEnd,
|
||||||
recurrence_id: ev.recurrence_id,
|
recurrence_id: ev.recurrence_id,
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
|
|
||||||
export const enum ChatLogEventType {
|
|
||||||
INITIAL_STATE = "initial_state",
|
|
||||||
CREATED = "created",
|
|
||||||
UPDATED = "updated",
|
|
||||||
DELETED = "deleted",
|
|
||||||
CONTENT_ADDED = "content_added",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatLogAttachment {
|
|
||||||
media_content_id: string;
|
|
||||||
mime_type: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatLogSystemContent {
|
|
||||||
role: "system";
|
|
||||||
content: string;
|
|
||||||
created: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatLogUserContent {
|
|
||||||
role: "user";
|
|
||||||
content: string;
|
|
||||||
created: Date;
|
|
||||||
attachments?: ChatLogAttachment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatLogAssistantContent {
|
|
||||||
role: "assistant";
|
|
||||||
agent_id: string;
|
|
||||||
created: Date;
|
|
||||||
content?: string;
|
|
||||||
thinking_content?: string;
|
|
||||||
tool_calls?: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatLogToolResultContent {
|
|
||||||
role: "tool_result";
|
|
||||||
agent_id: string;
|
|
||||||
tool_call_id: string;
|
|
||||||
tool_name: string;
|
|
||||||
tool_result: any;
|
|
||||||
created: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChatLogContent =
|
|
||||||
| ChatLogSystemContent
|
|
||||||
| ChatLogUserContent
|
|
||||||
| ChatLogAssistantContent
|
|
||||||
| ChatLogToolResultContent;
|
|
||||||
|
|
||||||
export interface ChatLog {
|
|
||||||
conversation_id: string;
|
|
||||||
continue_conversation: boolean;
|
|
||||||
content: ChatLogContent[];
|
|
||||||
created: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal wire format types (not exported)
|
|
||||||
interface ChatLogSystemContentWire {
|
|
||||||
role: "system";
|
|
||||||
content: string;
|
|
||||||
created: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogUserContentWire {
|
|
||||||
role: "user";
|
|
||||||
content: string;
|
|
||||||
created: string;
|
|
||||||
attachments?: ChatLogAttachment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogAssistantContentWire {
|
|
||||||
role: "assistant";
|
|
||||||
agent_id: string;
|
|
||||||
created: string;
|
|
||||||
content?: string;
|
|
||||||
thinking_content?: string;
|
|
||||||
tool_calls?: {
|
|
||||||
tool_name: string;
|
|
||||||
tool_args: Record<string, any>;
|
|
||||||
id: string;
|
|
||||||
external: boolean;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogToolResultContentWire {
|
|
||||||
role: "tool_result";
|
|
||||||
agent_id: string;
|
|
||||||
tool_call_id: string;
|
|
||||||
tool_name: string;
|
|
||||||
tool_result: any;
|
|
||||||
created: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatLogContentWire =
|
|
||||||
| ChatLogSystemContentWire
|
|
||||||
| ChatLogUserContentWire
|
|
||||||
| ChatLogAssistantContentWire
|
|
||||||
| ChatLogToolResultContentWire;
|
|
||||||
|
|
||||||
interface ChatLogWire {
|
|
||||||
conversation_id: string;
|
|
||||||
continue_conversation: boolean;
|
|
||||||
content: ChatLogContentWire[];
|
|
||||||
created: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const processContent = (content: ChatLogContentWire): ChatLogContent => ({
|
|
||||||
...content,
|
|
||||||
created: new Date(content.created),
|
|
||||||
});
|
|
||||||
|
|
||||||
const processChatLog = (chatLog: ChatLogWire): ChatLog => ({
|
|
||||||
...chatLog,
|
|
||||||
created: new Date(chatLog.created),
|
|
||||||
content: chatLog.content.map(processContent),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ChatLogInitialStateEvent {
|
|
||||||
event_type: ChatLogEventType.INITIAL_STATE;
|
|
||||||
data: ChatLogWire;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogIndexInitialStateEvent {
|
|
||||||
event_type: ChatLogEventType.INITIAL_STATE;
|
|
||||||
data: ChatLogWire[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogCreatedEvent {
|
|
||||||
conversation_id: string;
|
|
||||||
event_type: ChatLogEventType.CREATED;
|
|
||||||
data: ChatLogWire;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogUpdatedEvent {
|
|
||||||
conversation_id: string;
|
|
||||||
event_type: ChatLogEventType.UPDATED;
|
|
||||||
data: { chat_log: ChatLogWire };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogDeletedEvent {
|
|
||||||
conversation_id: string;
|
|
||||||
event_type: ChatLogEventType.DELETED;
|
|
||||||
data: ChatLogWire;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatLogContentAddedEvent {
|
|
||||||
conversation_id: string;
|
|
||||||
event_type: ChatLogEventType.CONTENT_ADDED;
|
|
||||||
data: { content: ChatLogContentWire };
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatLogSubscriptionEvent =
|
|
||||||
| ChatLogInitialStateEvent
|
|
||||||
| ChatLogUpdatedEvent
|
|
||||||
| ChatLogDeletedEvent
|
|
||||||
| ChatLogContentAddedEvent;
|
|
||||||
|
|
||||||
type ChatLogIndexSubscriptionEvent =
|
|
||||||
| ChatLogIndexInitialStateEvent
|
|
||||||
| ChatLogCreatedEvent
|
|
||||||
| ChatLogDeletedEvent;
|
|
||||||
|
|
||||||
export const subscribeChatLog = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
conversationId: string,
|
|
||||||
callback: (chatLog: ChatLog | null) => void
|
|
||||||
): Promise<UnsubscribeFunc> => {
|
|
||||||
let chatLog: ChatLog | null = null;
|
|
||||||
|
|
||||||
return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>(
|
|
||||||
(event) => {
|
|
||||||
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
|
|
||||||
chatLog = processChatLog(event.data);
|
|
||||||
callback(chatLog);
|
|
||||||
} else if (event.event_type === ChatLogEventType.CONTENT_ADDED) {
|
|
||||||
if (chatLog) {
|
|
||||||
chatLog = {
|
|
||||||
...chatLog,
|
|
||||||
content: [...chatLog.content, processContent(event.data.content)],
|
|
||||||
};
|
|
||||||
callback(chatLog);
|
|
||||||
}
|
|
||||||
} else if (event.event_type === ChatLogEventType.UPDATED) {
|
|
||||||
chatLog = processChatLog(event.data.chat_log);
|
|
||||||
callback(chatLog);
|
|
||||||
} else if (event.event_type === ChatLogEventType.DELETED) {
|
|
||||||
chatLog = null;
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "conversation/chat_log/subscribe",
|
|
||||||
conversation_id: conversationId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const subscribeChatLogIndex = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
callback: (chatLogs: ChatLog[]) => void
|
|
||||||
): Promise<UnsubscribeFunc> => {
|
|
||||||
let chatLogs: ChatLog[] = [];
|
|
||||||
|
|
||||||
return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>(
|
|
||||||
(event) => {
|
|
||||||
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
|
|
||||||
chatLogs = event.data.map(processChatLog);
|
|
||||||
callback(chatLogs);
|
|
||||||
} else if (event.event_type === ChatLogEventType.CREATED) {
|
|
||||||
chatLogs = [...chatLogs, processChatLog(event.data)];
|
|
||||||
callback(chatLogs);
|
|
||||||
} else if (event.event_type === ChatLogEventType.DELETED) {
|
|
||||||
chatLogs = chatLogs.filter(
|
|
||||||
(chatLog) => chatLog.conversation_id !== event.conversation_id
|
|
||||||
);
|
|
||||||
callback(chatLogs);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "conversation/chat_log/subscribe_index",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,15 +1,38 @@
|
|||||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
import {
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
mdiAmpersand,
|
||||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
mdiClockOutline,
|
||||||
import type { HomeAssistant } from "../types";
|
mdiCodeBraces,
|
||||||
|
mdiDevices,
|
||||||
|
mdiGateOr,
|
||||||
|
mdiIdentifier,
|
||||||
|
mdiMapClock,
|
||||||
|
mdiMapMarkerRadius,
|
||||||
|
mdiNotEqualVariant,
|
||||||
|
mdiNumeric,
|
||||||
|
mdiShape,
|
||||||
|
mdiStateMachine,
|
||||||
|
mdiWeatherSunny,
|
||||||
|
} from "@mdi/js";
|
||||||
import type { AutomationElementGroupCollection } from "./automation";
|
import type { AutomationElementGroupCollection } from "./automation";
|
||||||
import type { Selector, TargetSelector } from "./selector";
|
|
||||||
|
export const CONDITION_ICONS = {
|
||||||
|
device: mdiDevices,
|
||||||
|
and: mdiAmpersand,
|
||||||
|
or: mdiGateOr,
|
||||||
|
not: mdiNotEqualVariant,
|
||||||
|
state: mdiStateMachine,
|
||||||
|
numeric_state: mdiNumeric,
|
||||||
|
sun: mdiWeatherSunny,
|
||||||
|
template: mdiCodeBraces,
|
||||||
|
time: mdiClockOutline,
|
||||||
|
trigger: mdiIdentifier,
|
||||||
|
zone: mdiMapMarkerRadius,
|
||||||
|
};
|
||||||
|
|
||||||
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||||
{
|
{
|
||||||
groups: {
|
groups: {
|
||||||
device: {},
|
device: {},
|
||||||
dynamicGroups: {},
|
|
||||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||||
time_location: {
|
time_location: {
|
||||||
icon: mdiMapClock,
|
icon: mdiMapClock,
|
||||||
@@ -39,33 +62,3 @@ export const COLLAPSIBLE_CONDITION_ELEMENTS = [
|
|||||||
"ha-automation-condition-not",
|
"ha-automation-condition-not",
|
||||||
"ha-automation-condition-or",
|
"ha-automation-condition-or",
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface ConditionDescription {
|
|
||||||
target?: TargetSelector["target"];
|
|
||||||
fields: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
example?: string | boolean | number;
|
|
||||||
default?: unknown;
|
|
||||||
required?: boolean;
|
|
||||||
selector?: Selector;
|
|
||||||
context?: Record<string, string>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConditionDescriptions = Record<string, ConditionDescription>;
|
|
||||||
|
|
||||||
export const subscribeConditions = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
callback: (conditions: ConditionDescriptions) => void
|
|
||||||
) =>
|
|
||||||
hass.connection.subscribeMessage<ConditionDescriptions>(callback, {
|
|
||||||
type: "condition_platforms/subscribe",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getConditionDomain = (condition: string) =>
|
|
||||||
condition.includes(".") ? computeDomain(condition) : condition;
|
|
||||||
|
|
||||||
export const getConditionObjectId = (condition: string) =>
|
|
||||||
condition.includes(".") ? computeObjectId(condition) : "_";
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user