mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 03:07:21 +00:00
Compare commits
66 Commits
dev
...
20251105.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc48d158a | ||
|
|
b11e787f09 | ||
|
|
cdb6562de8 | ||
|
|
d8e8c9aa02 | ||
|
|
be392be1e6 | ||
|
|
10dc432445 | ||
|
|
19187f887d | ||
|
|
dc76a42aaa | ||
|
|
1f2b8047a6 | ||
|
|
e8c9ed0528 | ||
|
|
c7ae78c02f | ||
|
|
dc8f1211e6 | ||
|
|
5c25a63ea5 | ||
|
|
c1787ab994 | ||
|
|
6fea535fdc | ||
|
|
e8cee84380 | ||
|
|
b4613edeb7 | ||
|
|
a8b6e5aa3d | ||
|
|
e842193cd6 | ||
|
|
bb0813333d | ||
|
|
ab4c6f80f4 | ||
|
|
89796e425a | ||
|
|
9c42c8bbc4 | ||
|
|
616237caee | ||
|
|
2d36a0d37f | ||
|
|
1ec432a20f | ||
|
|
afd91b2261 | ||
|
|
cdfb7f914f | ||
|
|
33b0897522 | ||
|
|
5f0cf1b522 | ||
|
|
afb2ad95a4 | ||
|
|
27beab3133 | ||
|
|
435c82489b | ||
|
|
3ba6bf272e | ||
|
|
eec99b2fa3 | ||
|
|
d23e45e410 | ||
|
|
3c82d12609 | ||
|
|
15d67997e7 | ||
|
|
a6dfcb3100 | ||
|
|
26c2369228 | ||
|
|
2eed446492 | ||
|
|
7ebdeab6b2 | ||
|
|
0c35278f51 | ||
|
|
561122f03d | ||
|
|
95311be034 | ||
|
|
1eda44a102 | ||
|
|
d76781eb91 | ||
|
|
82d44e051f | ||
|
|
fdc9f5a3b7 | ||
|
|
ee6c82aba9 | ||
|
|
11d3f5c2ba | ||
|
|
feb68ce373 | ||
|
|
7f9a9de157 | ||
|
|
8e1b6a3d3b | ||
|
|
6e6e5a53e2 | ||
|
|
0408734ec5 | ||
|
|
317519fc08 | ||
|
|
843d79eab4 | ||
|
|
165a757f06 | ||
|
|
ea8b730142 | ||
|
|
e88c97d625 | ||
|
|
7560988b76 | ||
|
|
eecd8077b6 | ||
|
|
cbab5c3f7b | ||
|
|
a5d27c8bb8 | ||
|
|
a6a340b5db |
167
.github/copilot-instructions.md
vendored
167
.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.
|
||||
|
||||
**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
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
@@ -153,10 +151,6 @@ try {
|
||||
### Styling Guidelines
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-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
|
||||
- **Follow Material Design**: Use Material Web Components where appropriate
|
||||
- **Support RTL**: Ensure all layouts work in RTL languages
|
||||
@@ -165,68 +159,21 @@ try {
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
padding: var(--ha-space-4);
|
||||
--spacing: 16px;
|
||||
padding: var(--spacing);
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
padding: var(--ha-space-2);
|
||||
--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
|
||||
|
||||
- **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:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
- `ha-md-dialog` - Preferred for new code (Material Design 3)
|
||||
- `ha-dialog` - Legacy component still widely used
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -265,45 +211,15 @@ fireEvent(this, "show-dialog", {
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
- Use `createCloseHeading()` for standard headers
|
||||
- Import `haStyleDialog` for consistent styling
|
||||
- Return `nothing` when no params (loading state)
|
||||
- Fire `dialog-closed` event in `_dialogClosed()` handler
|
||||
- Use `header-title` attribute for simple titles
|
||||
- Use `header-subtitle` attribute for simple subtitles
|
||||
- Use slots for custom content where the standard attributes are not enough
|
||||
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
|
||||
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
|
||||
- Fire `dialog-closed` event when closing
|
||||
- Add `dialogInitialFocus` for accessibility
|
||||
|
||||
**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)
|
||||
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||
- Built-in validation with error display
|
||||
@@ -319,11 +235,7 @@ See these files for current patterns:
|
||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-form.markdown`
|
||||
````
|
||||
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
@@ -337,35 +249,6 @@ See these files for current patterns:
|
||||
<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
|
||||
|
||||
### Creating a Panel
|
||||
@@ -406,19 +289,11 @@ export class DialogMyFeature
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -429,27 +304,23 @@ export class DialogMyFeature
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-subtitle=${this._params.subtitle}
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this._params.title)}
|
||||
>
|
||||
<p>Dialog content</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<!-- Dialog content -->
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
<ha-button @click=${this._submit} slot="primaryAction">
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</ha-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 }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@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 }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
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 }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
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')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
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
|
||||
steps:
|
||||
- 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 }}
|
||||
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
|
||||
steps:
|
||||
- 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:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
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
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
- name: Tar folder
|
||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
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
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -137,6 +137,6 @@ jobs:
|
||||
- name: Tar folder
|
||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
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
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.11.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
|
||||
@@ -260,6 +260,7 @@ const createRspackConfig = ({
|
||||
),
|
||||
},
|
||||
experiments: {
|
||||
layers: true,
|
||||
outputModule: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,7 +84,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
stat_consumption: "sensor.energy_boiler",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [],
|
||||
})
|
||||
);
|
||||
hass.mockWS(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
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 { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@@ -66,7 +66,7 @@ const triggers = [
|
||||
},
|
||||
];
|
||||
|
||||
const initialTrigger: LegacyTrigger = {
|
||||
const initialTrigger: Trigger = {
|
||||
trigger: "state",
|
||||
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;
|
||||
}
|
||||
}
|
||||
47
package.json
47
package.json
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0",
|
||||
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.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/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.9.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.5",
|
||||
"@vaadin/combo-box": "24.9.2",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.2",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -111,8 +111,8 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.14",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"hls.js": "1.6.13",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"js-yaml": "4.1.1",
|
||||
@@ -122,7 +122,7 @@
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.0",
|
||||
"marked": "16.4.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -152,13 +152,13 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "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",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.3.11",
|
||||
"@rspack/core": "1.6.4",
|
||||
"@octokit/auth-oauth-device": "8.0.2",
|
||||
"@octokit/plugin-retry": "8.0.2",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.3.4",
|
||||
"@rspack/core": "1.5.8",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
@@ -178,12 +178,12 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.8",
|
||||
"@vitest/coverage-v8": "4.0.3",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.1",
|
||||
"eslint": "9.38.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -194,14 +194,14 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "12.0.0",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.1.0",
|
||||
"jsdom": "27.0.1",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.6",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -213,13 +213,13 @@
|
||||
"rspack-manifest-plugin": "5.1.0",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.0",
|
||||
"tar": "7.5.2",
|
||||
"tar": "7.5.1",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.3",
|
||||
"typescript-eslint": "8.46.2",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.0.8",
|
||||
"vitest": "4.0.3",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"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",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"globals": "16.5.0",
|
||||
"globals": "16.4.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
"@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"
|
||||
},
|
||||
"packageManager": "yarn@4.11.0",
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3969_57097)">
|
||||
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 20C0 15.5817 3.58172 12 8 12H68C72.4183 12 76 15.5817 76 20V36C76 40.4183 72.4183 44 68 44H8C3.58172 44 0 40.4183 0 36V20Z" fill="#1C1C1C"/>
|
||||
<path d="M8 12.5H68C72.1421 12.5 75.5 15.8579 75.5 20V36C75.5 40.1421 72.1421 43.5 68 43.5H8C3.85786 43.5 0.5 40.1421 0.5 36V20C0.5 15.8579 3.85786 12.5 8 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M32.9844 27.0156C32.9844 26.0781 32.7031 25.2656 32.1406 24.5781C31.5781 23.8594 30.8594 23.375 29.9844 23.125V22C29.9844 21.4375 30.125 20.9375 30.4062 20.5C30.6875 20.0312 31.0469 19.6719 31.4844 19.4219C31.9531 19.1406 32.4531 19 32.9844 19H43.0156C43.5469 19 44.0312 19.1406 44.4688 19.4219C44.9375 19.6719 45.3125 20.0312 45.5938 20.5C45.875 20.9375 46.0156 21.4375 46.0156 22V23.125C45.1406 23.375 44.4219 23.8594 43.8594 24.5781C43.2969 25.2656 43.0156 26.0781 43.0156 27.0156V28.9844H32.9844V27.0156ZM47 25C47.5625 25 48.0312 25.2031 48.4062 25.6094C48.8125 25.9844 49.0156 26.4531 49.0156 27.0156V31.9844C49.0156 32.5469 48.875 33.0625 48.5938 33.5312C48.3125 33.9688 47.9375 34.3281 47.4688 34.6094C47.0312 34.8594 46.5469 34.9844 46.0156 34.9844V36.0156C46.0156 36.2656 45.9062 36.5 45.6875 36.7188C45.5 36.9062 45.2656 37 44.9844 37C44.7344 37 44.5 36.9062 44.2812 36.7188C44.0938 36.5 44 36.2656 44 36.0156V34.9844H32V36.0156C32 36.2656 31.8906 36.5 31.6719 36.7188C31.4844 36.9062 31.2656 37 31.0156 37C30.7344 37 30.4844 36.9062 30.2656 36.7188C30.0781 36.5 29.9844 36.2656 29.9844 36.0156V34.9844C29.4531 34.9844 28.9531 34.8594 28.4844 34.6094C28.0469 34.3281 27.6875 33.9688 27.4062 33.5312C27.125 33.0625 26.9844 32.5469 26.9844 31.9844V27.0156C26.9844 26.4531 27.1719 25.9844 27.5469 25.6094C27.9531 25.2031 28.4375 25 29 25C29.5625 25 30.0312 25.2031 30.4062 25.6094C30.8125 25.9844 31.0156 26.4531 31.0156 27.0156V31H44.9844V27.0156C44.9844 26.4531 45.1719 25.9844 45.5469 25.6094C45.9531 25.2031 46.4375 25 47 25Z" fill="#03A9F4"/>
|
||||
<path d="M0 56C0 51.5817 3.58172 48 8 48H68C72.4183 48 76 51.5817 76 56V72C76 76.4183 72.4183 80 68 80H8C3.58172 80 0 76.4183 0 72V56Z" fill="#1C1C1C"/>
|
||||
<path d="M8 48.5H68C72.1421 48.5 75.5 51.8579 75.5 56V72C75.5 76.1421 72.1421 79.5 68 79.5H8C3.85786 79.5 0.5 76.1421 0.5 72V56C0.5 51.8579 3.85786 48.5 8 48.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M44 61.9844H47.9844V64H46.0156V72.0156H29.9844V64H28.0156V61.9844H32C31.4375 61.9844 30.9531 61.7969 30.5469 61.4219C30.1719 61.0156 29.9844 60.5469 29.9844 60.0156V55.9844H35.9844V60.0156C35.9844 60.5469 35.7812 61.0156 35.375 61.4219C35 61.7969 34.5469 61.9844 34.0156 61.9844H41.9844V58.9844C41.9844 58.7344 41.8906 58.5156 41.7031 58.3281C41.5156 58.1094 41.2812 58 41 58C40.7188 58 40.4844 58.1094 40.2969 58.3281C40.1094 58.5156 40.0156 58.7344 40.0156 58.9844H38C38 58.4531 38.125 57.9688 38.375 57.5312C38.6562 57.0625 39.0156 56.6875 39.4531 56.4062C39.9219 56.125 40.4375 55.9844 41 55.9844C41.5625 55.9844 42.0625 56.125 42.5 56.4062C42.9688 56.6875 43.3281 57.0625 43.5781 57.5312C43.8594 57.9688 44 58.4531 44 58.9844V61.9844ZM38.9844 70V64H37.0156V70H38.9844Z" fill="#03A9F4"/>
|
||||
<path d="M0 92C0 87.5817 3.58172 84 8 84H68C72.4183 84 76 87.5817 76 92V108C76 112.418 72.4183 116 68 116H8C3.58172 116 0 112.418 0 108V92Z" fill="#1C1C1C"/>
|
||||
<path d="M8 84.5H68C72.1421 84.5 75.5 87.8579 75.5 92V108C75.5 112.142 72.1421 115.5 68 115.5H8C3.85786 115.5 0.5 112.142 0.5 108V92C0.5 87.8579 3.85786 84.5 8 84.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M44.9844 94.9844C46.0781 94.9844 47.0156 95.3906 47.7969 96.2031C48.6094 96.9844 49.0156 97.9219 49.0156 99.0156V108.016H47V105.016H29V108.016H26.9844V93.0156H29V102.016H37.0156V94.9844H44.9844ZM35.0938 100.094C34.5 100.688 33.7969 100.984 32.9844 100.984C32.1719 100.984 31.4688 100.688 30.875 100.094C30.2812 99.5 29.9844 98.7969 29.9844 97.9844C29.9844 97.1719 30.2812 96.4688 30.875 95.875C31.4688 95.2812 32.1719 94.9844 32.9844 94.9844C33.7969 94.9844 34.5 95.2812 35.0938 95.875C35.6875 96.4688 35.9844 97.1719 35.9844 97.9844C35.9844 98.7969 35.6875 99.5 35.0938 100.094Z" fill="#03A9F4"/>
|
||||
<path d="M0 128C0 123.582 3.58172 120 8 120H68C72.4183 120 76 123.582 76 128V144C76 148.418 72.4183 152 68 152H8C3.58172 152 0 148.418 0 144V128Z" fill="#1C1C1C"/>
|
||||
<path d="M8 120.5H68C72.1421 120.5 75.5 123.858 75.5 128V144C75.5 148.142 72.1421 151.5 68 151.5H8C3.85786 151.5 0.5 148.142 0.5 144V128C0.5 123.858 3.85786 120.5 8 120.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M46.0156 136.984H47.9844V142.984C47.9844 143.516 47.7812 143.984 47.375 144.391C47 144.797 46.5469 145 46.0156 145C46.0156 145.281 45.9062 145.516 45.6875 145.703C45.5 145.891 45.2656 145.984 44.9844 145.984H31.0156C30.7344 145.984 30.4844 145.891 30.2656 145.703C30.0781 145.516 29.9844 145.281 29.9844 145C29.4531 145 28.9844 144.797 28.5781 144.391C28.2031 143.984 28.0156 143.516 28.0156 142.984V136.984H31.0156V136.234C31.0156 135.641 31.2344 135.125 31.6719 134.688C32.1406 134.219 32.6719 133.984 33.2656 133.984C33.8906 133.984 34.4531 134.234 34.9531 134.734L36.3125 136.281C36.5 136.5 36.7812 136.734 37.1562 136.984H44V128.828C44 128.609 43.9219 128.422 43.7656 128.266C43.6094 128.078 43.4062 127.984 43.1562 127.984C42.9375 127.984 42.75 128.062 42.5938 128.219L41.3281 129.484C41.3906 129.734 41.4219 129.906 41.4219 130C41.4219 130.344 41.3125 130.703 41.0938 131.078L38.3281 128.312C38.7031 128.094 39.0625 127.984 39.4062 127.984C39.5625 127.984 39.7344 128.016 39.9219 128.078L41.1875 126.812C41.7188 126.281 42.375 126.016 43.1562 126.016C43.9375 126.016 44.6094 126.297 45.1719 126.859C45.7344 127.391 46.0156 128.047 46.0156 128.828V136.984ZM31.5781 132.438C31.2031 132.031 31.0156 131.547 31.0156 130.984C31.0156 130.422 31.2031 129.953 31.5781 129.578C31.9531 129.203 32.4219 129.016 32.9844 129.016C33.5469 129.016 34.0156 129.203 34.3906 129.578C34.7969 129.953 35 130.422 35 130.984C35 131.547 34.7969 132.031 34.3906 132.438C34.0156 132.812 33.5469 133 32.9844 133C32.4219 133 31.9531 132.812 31.5781 132.438Z" fill="#03A9F4"/>
|
||||
<path d="M84 4C84 1.79086 85.7909 0 88 0H100C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8H88C85.7909 8 84 6.20914 84 4V4Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M84 20C84 15.5817 87.5817 12 92 12H152C156.418 12 160 15.5817 160 20V36C160 40.4183 156.418 44 152 44H92C87.5817 44 84 40.4183 84 36V20Z" fill="#1C1C1C"/>
|
||||
<path d="M92 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V36C159.5 40.1421 156.142 43.5 152 43.5H92C87.8579 43.5 84.5 40.1421 84.5 36V20C84.5 15.8579 87.8579 12.5 92 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M131.984 30.0156C131.984 30.7656 131.797 31.4531 131.422 32.0781C131.047 32.6719 130.562 33.1406 129.969 33.4844C129.844 34.2031 129.5 34.8125 128.938 35.3125C128.406 35.7812 127.766 36.0156 127.016 36.0156C126.359 36.0156 125.766 35.8281 125.234 35.4531C124.734 35.0781 124.391 34.5938 124.203 34H119.797C119.609 34.5938 119.25 35.0781 118.719 35.4531C118.219 35.8281 117.641 36.0156 116.984 36.0156C116.234 36.0156 115.578 35.7812 115.016 35.3125C114.484 34.8125 114.156 34.2031 114.031 33.4844C113.438 33.1406 112.953 32.6719 112.578 32.0781C112.203 31.4531 112.016 30.7656 112.016 30.0156C112.016 29.1094 112.266 28.3125 112.766 27.625C113.297 26.9375 113.969 26.4688 114.781 26.2188L113 24.3906L112.719 24.7188C112.5 24.9062 112.25 25 111.969 25C111.719 25 111.5 24.9062 111.312 24.7188C111.094 24.5312 110.984 24.2969 110.984 24.0156C110.984 23.7344 111.094 23.5 111.312 23.3125L113.281 21.2969C113.469 21.1094 113.703 21.0156 113.984 21.0156C114.266 21.0156 114.5 21.1094 114.688 21.2969C114.906 21.4844 115.016 21.7188 115.016 22C115.016 22.2812 114.906 22.5156 114.688 22.7031L114.406 22.9844L115.812 24.3906L116.609 22.0469C116.797 21.4219 117.156 20.9219 117.688 20.5469C118.219 20.1719 118.797 19.9844 119.422 19.9844H124.578C125.203 19.9844 125.781 20.1719 126.312 20.5469C126.844 20.9219 127.203 21.4219 127.391 22.0469L128.75 26.0781C129.375 26.2031 129.922 26.4531 130.391 26.8281C130.891 27.2031 131.281 27.6719 131.562 28.2344C131.844 28.7656 131.984 29.3594 131.984 30.0156ZM116.984 34C117.266 34 117.5 33.9062 117.688 33.7188C117.906 33.5 118.016 33.2656 118.016 33.0156C118.016 32.7344 117.906 32.5 117.688 32.3125C117.5 32.0938 117.266 31.9844 116.984 31.9844C116.734 31.9844 116.5 32.0938 116.281 32.3125C116.094 32.5 116 32.7344 116 33.0156C116 33.2656 116.094 33.5 116.281 33.7188C116.5 33.9062 116.734 34 116.984 34ZM121.016 25.9844V22H119.422C118.953 22 118.641 22.2344 118.484 22.7031L117.406 25.9844H121.016ZM122.984 22V25.9844H126.594L125.516 22.7031C125.359 22.2344 125.047 22 124.578 22H122.984ZM127.016 34C127.266 34 127.484 33.9062 127.672 33.7188C127.891 33.5 128 33.2656 128 33.0156C128 32.7344 127.891 32.5 127.672 32.3125C127.484 32.0938 127.266 31.9844 127.016 31.9844C126.734 31.9844 126.484 32.0938 126.266 32.3125C126.078 32.5 125.984 32.7344 125.984 33.0156C125.984 33.2656 126.078 33.5 126.266 33.7188C126.484 33.9062 126.734 34 127.016 34Z" fill="#03A9F4"/>
|
||||
<path d="M84 56C84 51.5817 87.5817 48 92 48H152C156.418 48 160 51.5817 160 56V72C160 76.4183 156.418 80 152 80H92C87.5817 80 84 76.4183 84 72V56Z" fill="#1C1C1C"/>
|
||||
<path d="M92 48.5H152C156.142 48.5 159.5 51.8579 159.5 56V72C159.5 76.1421 156.142 79.5 152 79.5H92C87.8579 79.5 84.5 76.1421 84.5 72V56C84.5 51.8579 87.8579 48.5 92 48.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M128.984 58.9844C130.078 58.9844 131.016 59.3906 131.797 60.2031C132.609 60.9844 133.016 61.9219 133.016 63.0156V72.0156H131V69.0156H113V72.0156H110.984V57.0156H113V66.0156H121.016V58.9844H128.984ZM119.094 64.0938C118.5 64.6875 117.797 64.9844 116.984 64.9844C116.172 64.9844 115.469 64.6875 114.875 64.0938C114.281 63.5 113.984 62.7969 113.984 61.9844C113.984 61.1719 114.281 60.4688 114.875 59.875C115.469 59.2812 116.172 58.9844 116.984 58.9844C117.797 58.9844 118.5 59.2812 119.094 59.875C119.688 60.4688 119.984 61.1719 119.984 61.9844C119.984 62.7969 119.688 63.5 119.094 64.0938Z" fill="#03A9F4"/>
|
||||
<path d="M84 92C84 87.5817 87.5817 84 92 84H152C156.418 84 160 87.5817 160 92V108C160 112.418 156.418 116 152 116H92C87.5817 116 84 112.418 84 108V92Z" fill="#1C1C1C"/>
|
||||
<path d="M92 84.5H152C156.142 84.5 159.5 87.8579 159.5 92V108C159.5 112.142 156.142 115.5 152 115.5H92C87.8579 115.5 84.5 112.142 84.5 108V92C84.5 87.8579 87.8579 84.5 92 84.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M112.016 94H131.984V106H130.016V103.984H125.984V106H124.016V96.0156H113.984V106H112.016V94ZM130.016 96.0156H125.984V97.9844H130.016V96.0156ZM125.984 102.016H130.016V100H125.984V102.016Z" fill="#03A9F4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3969_57097">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,76 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4744_40067)">
|
||||
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="#1C1C1C"/>
|
||||
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M6 32C6 28.6863 8.68629 26 12 26C15.3137 26 18 28.6863 18 32C18 35.3137 15.3137 38 12 38C8.68629 38 6 35.3137 6 32Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M54.6666 28C54.6666 23.5817 58.2483 20 62.6666 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3333 44H62.6666C58.2484 44 54.6666 40.4183 54.6666 36V28Z" fill="#1C1C1C"/>
|
||||
<path d="M62.6666 20.5H97.3336C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3336 43.5H62.6666C58.5245 43.5 55.1666 40.1421 55.1666 36V28C55.1666 23.8579 58.5245 20.5 62.6666 20.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M60.6666 32C60.6666 28.6863 63.3529 26 66.6666 26C69.9803 26 72.6666 28.6863 72.6666 32C72.6666 35.3137 69.9803 38 66.6666 38C63.3529 38 60.6666 35.3137 60.6666 32Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M78.6666 31C78.6666 29.3431 80.0098 28 81.6666 28H94.3333C95.9901 28 97.3333 29.3431 97.3333 31V33C97.3333 34.6569 95.9901 36 94.3333 36H81.6666C80.0098 36 78.6666 34.6569 78.6666 33V31Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="#1C1C1C"/>
|
||||
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M115.333 32C115.333 28.6863 118.02 26 121.333 26C124.647 26 127.333 28.6863 127.333 32C127.333 35.3137 124.647 38 121.333 38C118.02 38 115.333 35.3137 115.333 32Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M133.333 31C133.333 29.3431 134.677 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.677 36 133.333 34.6569 133.333 33V31Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask0_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
|
||||
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4744_40067)">
|
||||
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask1_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
|
||||
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_4744_40067)">
|
||||
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask2_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
|
||||
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask2_4744_40067)">
|
||||
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask3_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
|
||||
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask3_4744_40067)">
|
||||
<rect x="129.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_40067)"/>
|
||||
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_40067)" stroke-opacity="0.12"/>
|
||||
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_40067)"/>
|
||||
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_40067)" stroke-opacity="0.12"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="#1C1C1C"/>
|
||||
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="#1C1C1C"/>
|
||||
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4744_40067">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,32 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3969_50764)">
|
||||
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 20C0 15.5817 3.58172 12 8 12H68C72.4183 12 76 15.5817 76 20V36C76 40.4183 72.4183 44 68 44H8C3.58172 44 0 40.4183 0 36V20Z" fill="white"/>
|
||||
<path d="M8 12.5H68C72.1421 12.5 75.5 15.8579 75.5 20V36C75.5 40.1421 72.1421 43.5 68 43.5H8C3.85786 43.5 0.5 40.1421 0.5 36V20C0.5 15.8579 3.85786 12.5 8 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M32.9844 27.0156C32.9844 26.0781 32.7031 25.2656 32.1406 24.5781C31.5781 23.8594 30.8594 23.375 29.9844 23.125V22C29.9844 21.4375 30.125 20.9375 30.4062 20.5C30.6875 20.0312 31.0469 19.6719 31.4844 19.4219C31.9531 19.1406 32.4531 19 32.9844 19H43.0156C43.5469 19 44.0312 19.1406 44.4688 19.4219C44.9375 19.6719 45.3125 20.0312 45.5938 20.5C45.875 20.9375 46.0156 21.4375 46.0156 22V23.125C45.1406 23.375 44.4219 23.8594 43.8594 24.5781C43.2969 25.2656 43.0156 26.0781 43.0156 27.0156V28.9844H32.9844V27.0156ZM47 25C47.5625 25 48.0312 25.2031 48.4062 25.6094C48.8125 25.9844 49.0156 26.4531 49.0156 27.0156V31.9844C49.0156 32.5469 48.875 33.0625 48.5938 33.5312C48.3125 33.9688 47.9375 34.3281 47.4688 34.6094C47.0312 34.8594 46.5469 34.9844 46.0156 34.9844V36.0156C46.0156 36.2656 45.9062 36.5 45.6875 36.7188C45.5 36.9062 45.2656 37 44.9844 37C44.7344 37 44.5 36.9062 44.2812 36.7188C44.0938 36.5 44 36.2656 44 36.0156V34.9844H32V36.0156C32 36.2656 31.8906 36.5 31.6719 36.7188C31.4844 36.9062 31.2656 37 31.0156 37C30.7344 37 30.4844 36.9062 30.2656 36.7188C30.0781 36.5 29.9844 36.2656 29.9844 36.0156V34.9844C29.4531 34.9844 28.9531 34.8594 28.4844 34.6094C28.0469 34.3281 27.6875 33.9688 27.4062 33.5312C27.125 33.0625 26.9844 32.5469 26.9844 31.9844V27.0156C26.9844 26.4531 27.1719 25.9844 27.5469 25.6094C27.9531 25.2031 28.4375 25 29 25C29.5625 25 30.0312 25.2031 30.4062 25.6094C30.8125 25.9844 31.0156 26.4531 31.0156 27.0156V31H44.9844V27.0156C44.9844 26.4531 45.1719 25.9844 45.5469 25.6094C45.9531 25.2031 46.4375 25 47 25Z" fill="#03A9F4"/>
|
||||
<path d="M0 56C0 51.5817 3.58172 48 8 48H68C72.4183 48 76 51.5817 76 56V72C76 76.4183 72.4183 80 68 80H8C3.58172 80 0 76.4183 0 72V56Z" fill="white"/>
|
||||
<path d="M8 48.5H68C72.1421 48.5 75.5 51.8579 75.5 56V72C75.5 76.1421 72.1421 79.5 68 79.5H8C3.85786 79.5 0.5 76.1421 0.5 72V56C0.5 51.8579 3.85786 48.5 8 48.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M44 61.9844H47.9844V64H46.0156V72.0156H29.9844V64H28.0156V61.9844H32C31.4375 61.9844 30.9531 61.7969 30.5469 61.4219C30.1719 61.0156 29.9844 60.5469 29.9844 60.0156V55.9844H35.9844V60.0156C35.9844 60.5469 35.7812 61.0156 35.375 61.4219C35 61.7969 34.5469 61.9844 34.0156 61.9844H41.9844V58.9844C41.9844 58.7344 41.8906 58.5156 41.7031 58.3281C41.5156 58.1094 41.2812 58 41 58C40.7188 58 40.4844 58.1094 40.2969 58.3281C40.1094 58.5156 40.0156 58.7344 40.0156 58.9844H38C38 58.4531 38.125 57.9688 38.375 57.5312C38.6562 57.0625 39.0156 56.6875 39.4531 56.4062C39.9219 56.125 40.4375 55.9844 41 55.9844C41.5625 55.9844 42.0625 56.125 42.5 56.4062C42.9688 56.6875 43.3281 57.0625 43.5781 57.5312C43.8594 57.9688 44 58.4531 44 58.9844V61.9844ZM38.9844 70V64H37.0156V70H38.9844Z" fill="#03A9F4"/>
|
||||
<path d="M0 92C0 87.5817 3.58172 84 8 84H68C72.4183 84 76 87.5817 76 92V108C76 112.418 72.4183 116 68 116H8C3.58172 116 0 112.418 0 108V92Z" fill="white"/>
|
||||
<path d="M8 84.5H68C72.1421 84.5 75.5 87.8579 75.5 92V108C75.5 112.142 72.1421 115.5 68 115.5H8C3.85786 115.5 0.5 112.142 0.5 108V92C0.5 87.8579 3.85786 84.5 8 84.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M44.9844 94.9844C46.0781 94.9844 47.0156 95.3906 47.7969 96.2031C48.6094 96.9844 49.0156 97.9219 49.0156 99.0156V108.016H47V105.016H29V108.016H26.9844V93.0156H29V102.016H37.0156V94.9844H44.9844ZM35.0938 100.094C34.5 100.688 33.7969 100.984 32.9844 100.984C32.1719 100.984 31.4688 100.688 30.875 100.094C30.2812 99.5 29.9844 98.7969 29.9844 97.9844C29.9844 97.1719 30.2812 96.4688 30.875 95.875C31.4688 95.2812 32.1719 94.9844 32.9844 94.9844C33.7969 94.9844 34.5 95.2812 35.0938 95.875C35.6875 96.4688 35.9844 97.1719 35.9844 97.9844C35.9844 98.7969 35.6875 99.5 35.0938 100.094Z" fill="#03A9F4"/>
|
||||
<path d="M0 128C0 123.582 3.58172 120 8 120H68C72.4183 120 76 123.582 76 128V144C76 148.418 72.4183 152 68 152H8C3.58172 152 0 148.418 0 144V128Z" fill="white"/>
|
||||
<path d="M8 120.5H68C72.1421 120.5 75.5 123.858 75.5 128V144C75.5 148.142 72.1421 151.5 68 151.5H8C3.85786 151.5 0.5 148.142 0.5 144V128C0.5 123.858 3.85786 120.5 8 120.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M46.0156 136.984H47.9844V142.984C47.9844 143.516 47.7812 143.984 47.375 144.391C47 144.797 46.5469 145 46.0156 145C46.0156 145.281 45.9062 145.516 45.6875 145.703C45.5 145.891 45.2656 145.984 44.9844 145.984H31.0156C30.7344 145.984 30.4844 145.891 30.2656 145.703C30.0781 145.516 29.9844 145.281 29.9844 145C29.4531 145 28.9844 144.797 28.5781 144.391C28.2031 143.984 28.0156 143.516 28.0156 142.984V136.984H31.0156V136.234C31.0156 135.641 31.2344 135.125 31.6719 134.688C32.1406 134.219 32.6719 133.984 33.2656 133.984C33.8906 133.984 34.4531 134.234 34.9531 134.734L36.3125 136.281C36.5 136.5 36.7812 136.734 37.1562 136.984H44V128.828C44 128.609 43.9219 128.422 43.7656 128.266C43.6094 128.078 43.4062 127.984 43.1562 127.984C42.9375 127.984 42.75 128.062 42.5938 128.219L41.3281 129.484C41.3906 129.734 41.4219 129.906 41.4219 130C41.4219 130.344 41.3125 130.703 41.0938 131.078L38.3281 128.312C38.7031 128.094 39.0625 127.984 39.4062 127.984C39.5625 127.984 39.7344 128.016 39.9219 128.078L41.1875 126.812C41.7188 126.281 42.375 126.016 43.1562 126.016C43.9375 126.016 44.6094 126.297 45.1719 126.859C45.7344 127.391 46.0156 128.047 46.0156 128.828V136.984ZM31.5781 132.438C31.2031 132.031 31.0156 131.547 31.0156 130.984C31.0156 130.422 31.2031 129.953 31.5781 129.578C31.9531 129.203 32.4219 129.016 32.9844 129.016C33.5469 129.016 34.0156 129.203 34.3906 129.578C34.7969 129.953 35 130.422 35 130.984C35 131.547 34.7969 132.031 34.3906 132.438C34.0156 132.812 33.5469 133 32.9844 133C32.4219 133 31.9531 132.812 31.5781 132.438Z" fill="#03A9F4"/>
|
||||
<path d="M84 4C84 1.79086 85.7909 0 88 0H100C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8H88C85.7909 8 84 6.20914 84 4V4Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M84 20C84 15.5817 87.5817 12 92 12H152C156.418 12 160 15.5817 160 20V36C160 40.4183 156.418 44 152 44H92C87.5817 44 84 40.4183 84 36V20Z" fill="white"/>
|
||||
<path d="M92 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V36C159.5 40.1421 156.142 43.5 152 43.5H92C87.8579 43.5 84.5 40.1421 84.5 36V20C84.5 15.8579 87.8579 12.5 92 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M131.984 30.0156C131.984 30.7656 131.797 31.4531 131.422 32.0781C131.047 32.6719 130.562 33.1406 129.969 33.4844C129.844 34.2031 129.5 34.8125 128.938 35.3125C128.406 35.7812 127.766 36.0156 127.016 36.0156C126.359 36.0156 125.766 35.8281 125.234 35.4531C124.734 35.0781 124.391 34.5938 124.203 34H119.797C119.609 34.5938 119.25 35.0781 118.719 35.4531C118.219 35.8281 117.641 36.0156 116.984 36.0156C116.234 36.0156 115.578 35.7812 115.016 35.3125C114.484 34.8125 114.156 34.2031 114.031 33.4844C113.438 33.1406 112.953 32.6719 112.578 32.0781C112.203 31.4531 112.016 30.7656 112.016 30.0156C112.016 29.1094 112.266 28.3125 112.766 27.625C113.297 26.9375 113.969 26.4688 114.781 26.2188L113 24.3906L112.719 24.7188C112.5 24.9062 112.25 25 111.969 25C111.719 25 111.5 24.9062 111.312 24.7188C111.094 24.5312 110.984 24.2969 110.984 24.0156C110.984 23.7344 111.094 23.5 111.312 23.3125L113.281 21.2969C113.469 21.1094 113.703 21.0156 113.984 21.0156C114.266 21.0156 114.5 21.1094 114.688 21.2969C114.906 21.4844 115.016 21.7188 115.016 22C115.016 22.2812 114.906 22.5156 114.688 22.7031L114.406 22.9844L115.812 24.3906L116.609 22.0469C116.797 21.4219 117.156 20.9219 117.688 20.5469C118.219 20.1719 118.797 19.9844 119.422 19.9844H124.578C125.203 19.9844 125.781 20.1719 126.312 20.5469C126.844 20.9219 127.203 21.4219 127.391 22.0469L128.75 26.0781C129.375 26.2031 129.922 26.4531 130.391 26.8281C130.891 27.2031 131.281 27.6719 131.562 28.2344C131.844 28.7656 131.984 29.3594 131.984 30.0156ZM116.984 34C117.266 34 117.5 33.9062 117.688 33.7188C117.906 33.5 118.016 33.2656 118.016 33.0156C118.016 32.7344 117.906 32.5 117.688 32.3125C117.5 32.0938 117.266 31.9844 116.984 31.9844C116.734 31.9844 116.5 32.0938 116.281 32.3125C116.094 32.5 116 32.7344 116 33.0156C116 33.2656 116.094 33.5 116.281 33.7188C116.5 33.9062 116.734 34 116.984 34ZM121.016 25.9844V22H119.422C118.953 22 118.641 22.2344 118.484 22.7031L117.406 25.9844H121.016ZM122.984 22V25.9844H126.594L125.516 22.7031C125.359 22.2344 125.047 22 124.578 22H122.984ZM127.016 34C127.266 34 127.484 33.9062 127.672 33.7188C127.891 33.5 128 33.2656 128 33.0156C128 32.7344 127.891 32.5 127.672 32.3125C127.484 32.0938 127.266 31.9844 127.016 31.9844C126.734 31.9844 126.484 32.0938 126.266 32.3125C126.078 32.5 125.984 32.7344 125.984 33.0156C125.984 33.2656 126.078 33.5 126.266 33.7188C126.484 33.9062 126.734 34 127.016 34Z" fill="#03A9F4"/>
|
||||
<path d="M84 56C84 51.5817 87.5817 48 92 48H152C156.418 48 160 51.5817 160 56V72C160 76.4183 156.418 80 152 80H92C87.5817 80 84 76.4183 84 72V56Z" fill="white"/>
|
||||
<path d="M92 48.5H152C156.142 48.5 159.5 51.8579 159.5 56V72C159.5 76.1421 156.142 79.5 152 79.5H92C87.8579 79.5 84.5 76.1421 84.5 72V56C84.5 51.8579 87.8579 48.5 92 48.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M128.984 58.9844C130.078 58.9844 131.016 59.3906 131.797 60.2031C132.609 60.9844 133.016 61.9219 133.016 63.0156V72.0156H131V69.0156H113V72.0156H110.984V57.0156H113V66.0156H121.016V58.9844H128.984ZM119.094 64.0938C118.5 64.6875 117.797 64.9844 116.984 64.9844C116.172 64.9844 115.469 64.6875 114.875 64.0938C114.281 63.5 113.984 62.7969 113.984 61.9844C113.984 61.1719 114.281 60.4688 114.875 59.875C115.469 59.2812 116.172 58.9844 116.984 58.9844C117.797 58.9844 118.5 59.2812 119.094 59.875C119.688 60.4688 119.984 61.1719 119.984 61.9844C119.984 62.7969 119.688 63.5 119.094 64.0938Z" fill="#03A9F4"/>
|
||||
<path d="M84 92C84 87.5817 87.5817 84 92 84H152C156.418 84 160 87.5817 160 92V108C160 112.418 156.418 116 152 116H92C87.5817 116 84 112.418 84 108V92Z" fill="white"/>
|
||||
<path d="M92 84.5H152C156.142 84.5 159.5 87.8579 159.5 92V108C159.5 112.142 156.142 115.5 152 115.5H92C87.8579 115.5 84.5 112.142 84.5 108V92C84.5 87.8579 87.8579 84.5 92 84.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M112.016 94H131.984V106H130.016V103.984H125.984V106H124.016V96.0156H113.984V106H112.016V94ZM130.016 96.0156H125.984V97.9844H130.016V96.0156ZM125.984 102.016H130.016V100H125.984V102.016Z" fill="#03A9F4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3969_50764">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,76 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4744_39984)">
|
||||
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="white"/>
|
||||
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="6" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M54.6667 28C54.6667 23.5817 58.2484 20 62.6667 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3334 44H62.6667C58.2484 44 54.6667 40.4183 54.6667 36V28Z" fill="white"/>
|
||||
<path d="M62.6667 20.5H97.3337C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3337 43.5H62.6667C58.5246 43.5 55.1667 40.1421 55.1667 36V28C55.1667 23.8579 58.5246 20.5 62.6667 20.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="60.6667" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M78.6667 31C78.6667 29.3431 80.0098 28 81.6667 28H94.3334C95.9902 28 97.3334 29.3431 97.3334 31V33C97.3334 34.6569 95.9902 36 94.3334 36H81.6667C80.0098 36 78.6667 34.6569 78.6667 33V31Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="white"/>
|
||||
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="115.333" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M133.333 31C133.333 29.3431 134.676 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.676 36 133.333 34.6569 133.333 33V31Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="white"/>
|
||||
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask0_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
|
||||
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4744_39984)">
|
||||
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="white"/>
|
||||
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask1_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
|
||||
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_4744_39984)">
|
||||
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="white"/>
|
||||
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask2_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
|
||||
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask2_4744_39984)">
|
||||
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="white"/>
|
||||
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask3_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
|
||||
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask3_4744_39984)">
|
||||
<rect x="129.5" y="72" width="24" height="24" fill="#18BCF2"/>
|
||||
</g>
|
||||
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_39984)"/>
|
||||
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_39984)" stroke-opacity="0.12"/>
|
||||
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_39984)"/>
|
||||
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_39984)" stroke-opacity="0.12"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4744_39984">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20251029.0"
|
||||
version = "20251105.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
@@ -118,9 +118,6 @@ export class HaAuthFlow extends LitElement {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.action ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<form>${this._renderForm()}</form>
|
||||
`;
|
||||
|
||||
@@ -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 type { FrontendLocaleData } 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 => {
|
||||
if (locale.first_weekday === FirstWeekday.language) {
|
||||
@@ -12,12 +23,12 @@ export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
||||
}
|
||||
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
||||
}
|
||||
return WEEKDAYS_LONG.includes(locale.first_weekday)
|
||||
? (WEEKDAYS_LONG.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||
return weekdays.includes(locale.first_weekday)
|
||||
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||
: 1;
|
||||
};
|
||||
|
||||
export const firstWeekday = (locale: FrontendLocaleData) => {
|
||||
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 { darkColorVariables } from "../../resources/theme/color";
|
||||
import { darkSemanticVariables } from "../../resources/theme/semantic.globals";
|
||||
import { derivedStyles } from "../../resources/theme/theme";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -53,7 +52,7 @@ export const applyThemesOnElement = (
|
||||
|
||||
if (themeToApply && darkMode) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = { ...darkSemanticVariables, ...darkColorVariables };
|
||||
themeRules = { ...darkColorVariables };
|
||||
}
|
||||
|
||||
if (themeToApply === "default") {
|
||||
|
||||
@@ -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 = {
|
||||
second: 59, // seconds to minute
|
||||
minute: 59, // minutes to hour
|
||||
second: 45, // seconds to minute
|
||||
minute: 45, // minutes to hour
|
||||
hour: 22, // hour to day
|
||||
day: 5, // day to week
|
||||
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();
|
||||
};
|
||||
@@ -427,7 +427,6 @@ export class HaChartBase extends LitElement {
|
||||
...axis.axisPointer?.handle,
|
||||
show: true,
|
||||
},
|
||||
label: { show: false },
|
||||
},
|
||||
}
|
||||
: axis
|
||||
@@ -597,15 +596,10 @@ export class HaChartBase extends LitElement {
|
||||
aria: { show: true },
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
toolbox: {
|
||||
top: Number.MAX_SAFE_INTEGER,
|
||||
left: Number.MAX_SAFE_INTEGER,
|
||||
top: Infinity,
|
||||
left: Infinity,
|
||||
feature: {
|
||||
dataZoom: {
|
||||
show: true,
|
||||
yAxisIndex: false,
|
||||
filterMode: "none",
|
||||
showTitle: false,
|
||||
},
|
||||
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
|
||||
},
|
||||
iconStyle: { opacity: 0 },
|
||||
},
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
@@ -19,7 +16,6 @@ import { deepEqual } from "../../common/util/deep-equal";
|
||||
export interface NetworkNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
context?: string;
|
||||
category?: number;
|
||||
value?: number;
|
||||
symbolSize?: number;
|
||||
@@ -188,30 +184,10 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
layout: physicsEnabled ? "force" : "none",
|
||||
draggable: true,
|
||||
roam: true,
|
||||
roamTrigger: "global",
|
||||
selectedMode: "single",
|
||||
label: {
|
||||
show: showLabels,
|
||||
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: {
|
||||
focus: isMobile ? "none" : "adjacency",
|
||||
@@ -249,7 +225,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
context: node.context,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
|
||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--_label-text-font: var(--ha-font-family-body);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -62,7 +62,6 @@ class HaDataTableLabels extends LitElement {
|
||||
@click=${clickAction ? this._labelClicked : undefined}
|
||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
|
||||
@@ -298,18 +298,6 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
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(
|
||||
(row) => row.selectable !== false
|
||||
).length;
|
||||
|
||||
@@ -197,6 +197,9 @@ export class HaDevicePicker extends LitElement {
|
||||
const placeholder =
|
||||
this.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);
|
||||
|
||||
@@ -206,10 +209,7 @@ export class HaDevicePicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.device-picker.no_devices"
|
||||
)}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -233,11 +233,6 @@ export class HaDevicePicker extends LitElement {
|
||||
this.value = 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 {
|
||||
|
||||
@@ -269,6 +269,9 @@ export class HaEntityPicker extends LitElement {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
||||
const notFoundLabel = this.hass.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -279,7 +282,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.addButton ? undefined : this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -353,11 +356,6 @@ export class HaEntityPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.entity.entity-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -21,6 +21,7 @@ import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-input-helper-text";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
@@ -270,6 +271,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
output.push({
|
||||
@@ -277,6 +279,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
statistic_id: id,
|
||||
primary,
|
||||
secondary,
|
||||
a11y_label: a11yLabel,
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
@@ -455,6 +458,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.statistic-picker.placeholder");
|
||||
const notFoundLabel = this.hass.localize(
|
||||
"ui.components.statistic-picker.no_match"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -462,10 +468,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
)}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -474,7 +477,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.helper=${this.helper}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
@@ -519,11 +521,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.statistic-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -369,10 +369,9 @@ export class HaAreaPicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.area-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
@@ -426,11 +425,6 @@ export class HaAreaPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.area-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { HomeAssistant } from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
import "./ha-markdown";
|
||||
import "./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:last-child")
|
||||
private _lastChatMessage!: LitElement;
|
||||
|
||||
@query(".message:last-child img:last-of-type")
|
||||
private _lastChatMessageImage: HTMLImageElement | undefined;
|
||||
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||
|
||||
@state() private _conversation: AssistMessage[] = [];
|
||||
|
||||
@@ -97,7 +92,10 @@ export class HaAssistChat extends LitElement {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._audioRecorder?.close();
|
||||
this._audioRecorder = undefined;
|
||||
this._unloadAudio();
|
||||
this._conversation = [];
|
||||
this._conversationId = null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -114,7 +112,7 @@ export class HaAssistChat extends LitElement {
|
||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||
|
||||
return html`
|
||||
<div class="messages">
|
||||
<div class="messages" id="scroll-container">
|
||||
${controlHA
|
||||
? nothing
|
||||
: html`
|
||||
@@ -126,17 +124,10 @@ export class HaAssistChat extends LitElement {
|
||||
`}
|
||||
<div class="spacer"></div>
|
||||
${this._conversation!.map(
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
(message) => html`
|
||||
<ha-markdown
|
||||
class="message ${classMap({
|
||||
error: !!message.error,
|
||||
[message.who]: true,
|
||||
})}"
|
||||
breaks
|
||||
cache
|
||||
.content=${message.text}
|
||||
>
|
||||
</ha-markdown>
|
||||
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -198,28 +189,12 @@ export class HaAssistChat extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _scrollMessagesBottom() {
|
||||
const lastChatMessage = this._lastChatMessage;
|
||||
if (!lastChatMessage.hasUpdated) {
|
||||
await lastChatMessage.updateComplete;
|
||||
}
|
||||
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" });
|
||||
private _scrollMessagesBottom() {
|
||||
const scrollContainer = this._scrollContainer;
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
@@ -611,31 +586,42 @@ export class HaAssistChat extends LitElement {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
white-space: pre-line;
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: 24px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
.message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
|
||||
.message p {
|
||||
margin: 0;
|
||||
}
|
||||
.message p:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
@@ -650,21 +636,20 @@ export class HaAssistChat extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.user a {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.message.hass a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
@@ -651,18 +651,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties that should never suggest entities
|
||||
const negativeProperties = ["action"];
|
||||
|
||||
// Create regex pattern for negative properties
|
||||
const negativePropertyPattern = negativeProperties.join("|");
|
||||
const negativeEntityFieldRegex = new RegExp(
|
||||
`^\\s*(-\\s+)?(${negativePropertyPattern}):\\s*`
|
||||
);
|
||||
if (lineText.match(negativeEntityFieldRegex)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Original entity completion logic for non-YAML or when not in entity_id field
|
||||
|
||||
@@ -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) {
|
||||
if (["Space", "Enter"].includes(ev.code)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._openDialog();
|
||||
return;
|
||||
}
|
||||
if (!this.canClear) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,15 +75,11 @@ export class HaDialogHeader extends LitElement {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
color: var(
|
||||
--ha-dialog-header-subtitle-color,
|
||||
var(--secondary-text-color)
|
||||
);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
.header-bar {
|
||||
|
||||
@@ -90,8 +90,7 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
padding: 12px 16px 16px 16px;
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
@@ -101,24 +100,22 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
padding: var(--dialog-container-padding, var(--ha-space-0));
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
|
||||
var(--ha-space-4);
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
.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 {
|
||||
content: unset;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__content {
|
||||
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 {
|
||||
padding-bottom: var(--dialog-content-padding, var(--ha-space-6));
|
||||
padding-bottom: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
@@ -136,7 +133,7 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-surface-background,
|
||||
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 {
|
||||
display: flex;
|
||||
@@ -153,22 +150,22 @@ export class HaDialog extends DialogBase {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding-left: var(--ha-space-1);
|
||||
padding-right: var(--ha-space-1);
|
||||
margin-right: var(--ha-space-3);
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
margin-right: 12px;
|
||||
margin-inline-end: 12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header_button {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: calc(var(--ha-space-3) * -1);
|
||||
inset-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dialog-actions {
|
||||
inset-inline-start: initial !important;
|
||||
inset-inline-end: var(--ha-space-0) !important;
|
||||
inset-inline-end: 0px !important;
|
||||
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"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
@@ -109,10 +109,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
|
||||
@@ -383,9 +383,8 @@ export class HaFloorPicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.floor-picker.no_floors"
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.floor-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
@@ -445,11 +444,6 @@ export class HaFloorPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.floor-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -25,6 +25,9 @@ import "./ha-svg-icon";
|
||||
export class HaGenericPicker extends LitElement {
|
||||
@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 required = false;
|
||||
@@ -46,11 +49,8 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@property({ attribute: false, type: Array })
|
||||
public getItems?: () => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -64,11 +64,8 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
@property({ attribute: "empty-label" })
|
||||
public emptyLabel?: string;
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@property({ attribute: "popover-placement" })
|
||||
public popoverPlacement:
|
||||
@@ -88,25 +85,6 @@ export class HaGenericPicker extends LitElement {
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@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("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -119,11 +97,6 @@ export class HaGenericPicker extends LitElement {
|
||||
|
||||
@state() private _openedNarrow = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
private _narrow = false;
|
||||
|
||||
// helper to set new value after closing picker, to avoid flicker
|
||||
@@ -216,19 +189,16 @@ export class HaGenericPicker extends LitElement {
|
||||
<ha-picker-combo-box
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.searchLabel}
|
||||
.label=${this.searchLabel ??
|
||||
(this.hass?.localize("ui.common.search") || "Search")}
|
||||
.value=${this.value}
|
||||
@value-changed=${this._valueChanged}
|
||||
.rowRenderer=${this.rowRenderer}
|
||||
.notFoundLabel=${this.notFoundLabel}
|
||||
.emptyLabel=${this.emptyLabel}
|
||||
.getItems=${this.getItems}
|
||||
.getAdditionalItems=${this.getAdditionalItems}
|
||||
.searchFn=${this.searchFn}
|
||||
.mode=${dialogMode ? "dialog" : "popover"}
|
||||
.sections=${this.sections}
|
||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||
.selectedSection=${this.selectedSection}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,6 @@ class HaHLSPlayer extends LitElement {
|
||||
private static streamCount = 0;
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
// video is playing in picture-in-picture mode, don't do anything
|
||||
return;
|
||||
}
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
.path=${item.path}
|
||||
></ha-svg-icon>
|
||||
${item.label}
|
||||
</ha-md-menu-item>`
|
||||
</ha-md-menu-item> `
|
||||
)}
|
||||
</ha-md-button-menu>`
|
||||
: html`
|
||||
@@ -103,7 +103,6 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
cursor: initial;
|
||||
}
|
||||
div[role="separator"] {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface DisplayItem {
|
||||
label: string;
|
||||
description?: string;
|
||||
disableSorting?: boolean;
|
||||
disableHiding?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayValue {
|
||||
@@ -102,7 +101,6 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
icon,
|
||||
iconPath,
|
||||
disableSorting,
|
||||
disableHiding,
|
||||
} = item;
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -157,8 +155,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!isVisible || !disableHiding
|
||||
? html`<ha-icon-button
|
||||
<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
@@ -169,9 +166,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
.disabled=${disableHiding || false}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
></ha-icon-button>
|
||||
${isVisible && !disableSorting
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
|
||||
@@ -224,9 +224,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.label-picker.no_labels"
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.label-picker.no_match"
|
||||
)}
|
||||
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
||||
.placeholder=${placeholder}
|
||||
@@ -289,11 +288,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.label-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,32 +1,17 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { uid } from "../common/util/uid";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-label")
|
||||
class HaLabel extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property({ attribute: "description" })
|
||||
public description?: string;
|
||||
|
||||
private _elementId = "label-" + uid();
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-tooltip
|
||||
.for=${this._elementId}
|
||||
.disabled=${!this.description?.trim()}
|
||||
>
|
||||
${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);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.1px;
|
||||
vertical-align: middle;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
color: var(--ha-label-text-color);
|
||||
--mdc-icon-size: 12px;
|
||||
@@ -79,23 +66,14 @@ class HaLabel extends LitElement {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
:host([dense]) {
|
||||
height: 20px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
:host([dense]) .container {
|
||||
padding: 0 12px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
:host([dense]) ::slotted([slot="icon"]) {
|
||||
margin-right: 4px;
|
||||
|
||||
@@ -21,7 +21,6 @@ import "./chips/ha-input-chip";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-label-picker";
|
||||
import type { HaLabelPicker } from "./ha-label-picker";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-labels-picker")
|
||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -143,17 +142,9 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
const color = label?.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
const elementId = "label-" + label.label_id;
|
||||
return html`
|
||||
<ha-tooltip
|
||||
.for=${elementId}
|
||||
.disabled=${!label?.description?.trim()}
|
||||
>
|
||||
${label?.description}
|
||||
</ha-tooltip>
|
||||
<ha-input-chip
|
||||
.item=${label}
|
||||
.id=${elementId}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._openDetail}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -125,10 +125,9 @@ export class HaLanguagePicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
popover-placement="bottom-end"
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
) || "No languages available"}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_match"
|
||||
)}
|
||||
.placeholder=${this.label ??
|
||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||
"Language")}
|
||||
@@ -173,15 +172,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
this.value = ev.detail.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 {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement, render, html } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
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 { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const h = (template: ReturnType<typeof unsafeHTML>) => html`${template}`;
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
@@ -52,26 +48,18 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
private _renderPromise: ReturnType<typeof this._render> = Promise.resolve();
|
||||
|
||||
protected update(changedProps) {
|
||||
super.update(changedProps);
|
||||
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 {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
render(markdownCache.get(key)!, this.renderRoot);
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +75,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
const elements = await renderMarkdown(
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
{
|
||||
breaks: this.breaks,
|
||||
@@ -99,11 +87,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
elements.map((e) => h(unsafeHTML(e))),
|
||||
this.renderRoot
|
||||
);
|
||||
|
||||
this._resize();
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type ReactiveElement,
|
||||
type CSSResultGroup,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-markdown-element";
|
||||
|
||||
@customElement("ha-markdown")
|
||||
@@ -25,14 +18,6 @@ export class HaMarkdown extends LitElement {
|
||||
|
||||
@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() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -68,46 +53,19 @@ export class HaMarkdown extends LitElement {
|
||||
margin: var(--ha-space-1) 0;
|
||||
}
|
||||
a {
|
||||
color: var(--markdown-link-color, var(--primary-color));
|
||||
color: var(--primary-color);
|
||||
}
|
||||
img {
|
||||
background-color: rgba(10, 10, 10, 0.15);
|
||||
border-radius: var(--markdown-image-border-radius);
|
||||
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,
|
||||
pre {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
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 {
|
||||
font-size: var(--ha-font-size-s);
|
||||
@@ -139,24 +97,6 @@ export class HaMarkdown extends LitElement {
|
||||
border-bottom: none;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -175,10 +175,10 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
margin-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
margin-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
slot[name="actions"]::slotted(*) {
|
||||
padding: var(--ha-space-4);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
@@ -195,7 +195,7 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; /* overlay navigation */
|
||||
|
||||
@@ -36,11 +36,6 @@ export class HaMdMenuItem extends MenuItemEl {
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -43,8 +42,13 @@ const createViewNavigationItem = (
|
||||
|
||||
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||
title: getPanelTitle(hass, panel) || "",
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
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")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
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 { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -14,12 +14,11 @@ import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-textfield";
|
||||
@@ -28,18 +27,28 @@ import type { HaTextField } from "./ha-textfield";
|
||||
export interface PickerComboBoxItem {
|
||||
id: string;
|
||||
primary: string;
|
||||
a11y_label?: string;
|
||||
secondary?: string;
|
||||
search_labels?: string[];
|
||||
sorting_label?: string;
|
||||
icon_path?: 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> = (
|
||||
item
|
||||
) => 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
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item.icon_path
|
||||
@@ -78,11 +87,8 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
@state() private _listScrolled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@property({ attribute: false, type: Array })
|
||||
public getItems?: () => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -90,45 +96,21 @@ export class HaPickerComboBox extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
@property({ attribute: "empty-label" })
|
||||
public emptyLabel?: string;
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@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("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||
@state() private _items: PickerComboBoxItemWithLabel[] = [];
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
private _allItems: PickerComboBoxItemWithLabel[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
|
||||
@@ -139,8 +121,6 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
private _removeKeyboardShortcuts?: () => void;
|
||||
|
||||
private _search = "";
|
||||
|
||||
protected firstUpdated() {
|
||||
this._registerKeyboardShortcuts();
|
||||
}
|
||||
@@ -165,142 +145,74 @@ export class HaPickerComboBox extends LitElement {
|
||||
"Search"}
|
||||
@input=${this._filterChanged}
|
||||
></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
|
||||
.keyFunction=${this._keyFunction}
|
||||
@scroll=${this._onScrollList}
|
||||
tabindex="0"
|
||||
scroller
|
||||
.items=${this._items}
|
||||
.renderItem=${this._renderItem}
|
||||
style="min-height: 36px;"
|
||||
class=${this._listScrolled ? "scrolled" : ""}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer> `;
|
||||
}
|
||||
|
||||
private _renderSectionButtons() {
|
||||
if (!this.sections || this.sections.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
private _defaultNotFoundItem = memoizeOne(
|
||||
(
|
||||
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`
|
||||
<ha-chip-set class="sections">
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
private _getAdditionalItems = (searchString?: string) => {
|
||||
const items = this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _visibilityChanged(ev) {
|
||||
if (
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
return items.map<PickerComboBoxItemWithLabel>((item) => ({
|
||||
...item,
|
||||
a11y_label: item.a11y_label || item.primary,
|
||||
}));
|
||||
};
|
||||
|
||||
private _getAdditionalItems = (searchString?: string) =>
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
private _getItems = (): PickerComboBoxItemWithLabel[] => {
|
||||
const items = this.getItems ? this.getItems() : [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [
|
||||
...(this.getItems
|
||||
? this.getItems(this._search, this.selectedSection)
|
||||
: []),
|
||||
];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) =>
|
||||
const sortedItems = items
|
||||
.map<PickerComboBoxItemWithLabel>((item) => ({
|
||||
...item,
|
||||
a11y_label: item.a11y_label || item.primary,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
(entityA as PickerComboBoxItem).sorting_label!,
|
||||
(entityB as PickerComboBoxItem).sorting_label!,
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass?.locale.language ?? navigator.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
items.push(NO_ITEMS_AVAILABLE_ID);
|
||||
if (!sortedItems.length) {
|
||||
sortedItems.push(
|
||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||
);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
items.push("padding"); // padding for safe area inset
|
||||
}
|
||||
|
||||
return items;
|
||||
sortedItems.push(...additionalItems);
|
||||
return sortedItems;
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem | string, 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>`;
|
||||
}
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
||||
return html`<div
|
||||
id=${`list-item-${index}`}
|
||||
@@ -309,7 +221,9 @@ export class HaPickerComboBox extends LitElement {
|
||||
.index=${index}
|
||||
@click=${this._valueSelected}
|
||||
>
|
||||
${renderer(item, index)}
|
||||
${item.id === NO_MATCHING_ITEMS_FOUND_ID
|
||||
? DEFAULT_ROW_RENDERER(item, index)
|
||||
: renderer(item, index)}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
@@ -328,6 +242,10 @@ export class HaPickerComboBox extends LitElement {
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const newValue = value?.trim();
|
||||
|
||||
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
};
|
||||
|
||||
@@ -338,19 +256,15 @@ export class HaPickerComboBox extends LitElement {
|
||||
private _filterChanged = (ev: Event) => {
|
||||
const textfield = ev.target as HaTextField;
|
||||
const searchString = textfield.value.trim();
|
||||
this._search = searchString;
|
||||
|
||||
if (this.sections?.length) {
|
||||
this._items = this._getItems();
|
||||
} else {
|
||||
if (!searchString) {
|
||||
this._items = this._allItems;
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
||||
const index = this._fuseIndex(this._allItems);
|
||||
const fuse = new HaFuse(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this._allItems,
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchString.length, 2),
|
||||
@@ -359,62 +273,34 @@ export class HaPickerComboBox extends LitElement {
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchString);
|
||||
let filteredItems = [...this._allItems];
|
||||
|
||||
let filteredItems = this._allItems as PickerComboBoxItem[];
|
||||
if (results) {
|
||||
const items: (PickerComboBoxItem | string)[] = results.map(
|
||||
(result) => result.item
|
||||
const items = results.map((result) => result.item);
|
||||
if (items.length === 0) {
|
||||
items.push(
|
||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
const additionalItems = this._getAdditionalItems(searchString);
|
||||
items.push(...additionalItems);
|
||||
|
||||
filteredItems = items;
|
||||
}
|
||||
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
filteredItems as PickerComboBoxItem[],
|
||||
this._allItems as PickerComboBoxItem[]
|
||||
filteredItems,
|
||||
this._allItems
|
||||
);
|
||||
}
|
||||
|
||||
this._items = filteredItems as PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
this._items = filteredItems as PickerComboBoxItemWithLabel[];
|
||||
this._selectedItemIndex = -1;
|
||||
if (this._virtualizerElement) {
|
||||
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() {
|
||||
this._removeKeyboardShortcuts = tinykeys(this, {
|
||||
ArrowUp: this._selectPreviousItem,
|
||||
@@ -458,7 +344,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof items[nextIndex] === "string") {
|
||||
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
// Skip titles, padding and empty search
|
||||
if (nextIndex === maxItems) {
|
||||
return;
|
||||
@@ -487,7 +373,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof items[nextIndex] === "string") {
|
||||
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
// Skip titles, padding and empty search
|
||||
if (nextIndex === 0) {
|
||||
return;
|
||||
@@ -509,6 +395,13 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (
|
||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||
NO_MATCHING_ITEMS_FOUND_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
@@ -526,6 +419,13 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
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") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
@@ -553,7 +453,10 @@ export class HaPickerComboBox extends LitElement {
|
||||
ev.stopPropagation();
|
||||
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", {
|
||||
value: firstItem.id,
|
||||
});
|
||||
@@ -569,7 +472,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
const item = this._virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
}
|
||||
};
|
||||
@@ -581,9 +484,6 @@ export class HaPickerComboBox extends LitElement {
|
||||
this._selectedItemIndex = -1;
|
||||
}
|
||||
|
||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||
typeof item === "string" ? item : item.id;
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
@@ -658,80 +558,6 @@ export class HaPickerComboBox extends LitElement {
|
||||
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,8 +1,6 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
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 { extractFromTarget } from "../../data/target";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
@@ -27,29 +25,15 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public context?: {
|
||||
filter_attribute?: 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() {
|
||||
if (this.selector.state?.multiple) {
|
||||
return html`
|
||||
<ha-entity-states-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityIds}
|
||||
.entityId=${this.selector.state?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
@@ -66,7 +50,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
return html`
|
||||
<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityIds}
|
||||
.entityId=${this.selector.state?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
@@ -80,24 +65,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
></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 {
|
||||
|
||||
@@ -33,7 +33,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-markdown";
|
||||
import "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-service-section-icon";
|
||||
@@ -685,14 +684,10 @@ export class HaServiceControl extends LitElement {
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
><ha-markdown
|
||||
breaks
|
||||
allow-svg
|
||||
.content=${this.hass.localize(
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}
|
||||
></ha-markdown>
|
||||
</span>
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.context=${this._selectorContext(targetEntities)}
|
||||
.disabled=${this.disabled ||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -24,14 +33,6 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
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 { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -52,6 +53,8 @@ import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
@@ -63,6 +66,18 @@ const SORT_VALUE_URL_PATHS = {
|
||||
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 = (
|
||||
reverseSort: string[],
|
||||
defaultPanel: string,
|
||||
@@ -127,7 +142,7 @@ const defaultPanelSorter = (
|
||||
export const computePanels = memoizeOne(
|
||||
(
|
||||
panels: HomeAssistant["panels"],
|
||||
defaultPanel: string,
|
||||
defaultPanel: HomeAssistant["defaultPanel"],
|
||||
panelsOrder: string[],
|
||||
hiddenPanels: string[],
|
||||
locale: HomeAssistant["locale"]
|
||||
@@ -139,23 +154,14 @@ export const computePanels = memoizeOne(
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
const allPanels = Object.values(panels).filter(
|
||||
(panel) => !FIXED_PANELS.includes(panel.url_path)
|
||||
);
|
||||
|
||||
allPanels.forEach((panel) => {
|
||||
const isDefaultPanel = panel.url_path === defaultPanel;
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
(!panel.title && panel.url_path !== defaultPanel)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
@@ -242,7 +248,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
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
|
||||
return html`
|
||||
@@ -287,8 +296,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.locale !== oldHass.locale ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.userData !== oldHass.userData ||
|
||||
hass.systemData !== oldHass.systemData ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel ||
|
||||
hass.connected !== oldHass.connected
|
||||
);
|
||||
}
|
||||
@@ -385,22 +393,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
<ha-fade-in .delay=${500}
|
||||
><ha-spinner size="small"></ha-spinner
|
||||
></ha-fade-in>
|
||||
`;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels,
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ha-md-list
|
||||
class="ha-scrollbar"
|
||||
@@ -412,30 +419,45 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this.hass.user?.is_admin
|
||||
? this._renderConfiguration(selectedPanel)
|
||||
: this._renderExternalConfiguration()}
|
||||
${this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
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) {
|
||||
const title = getPanelTitle(this.hass, panel);
|
||||
const urlPath = panel.url_path;
|
||||
const icon = getPanelIcon(panel);
|
||||
const iconPath = getPanelIconPath(panel);
|
||||
|
||||
return html`
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
icon: string | null | undefined,
|
||||
iconPath: string | null | undefined,
|
||||
selectedPanel: string
|
||||
) {
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title, selectedPanel)
|
||||
: html`
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
class=${classMap({ selected: isSelected })}
|
||||
class=${classMap({
|
||||
selected: selectedPanel === urlPath,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
@@ -455,15 +477,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(selectedPanel: string) {
|
||||
if (!this.hass.user?.is_admin) {
|
||||
return nothing;
|
||||
}
|
||||
const isSelected =
|
||||
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
|
||||
private _renderConfiguration(title: string | null, selectedPanel: string) {
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class="configuration ${classMap({ selected: isSelected })}"
|
||||
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
||||
type="button"
|
||||
href="/config"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -477,17 +494,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("panel.config")}</span
|
||||
>
|
||||
: ""}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
@@ -510,20 +525,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
`
|
||||
: nothing}
|
||||
: ""}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -531,7 +545,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
type="link"
|
||||
class=${classMap({
|
||||
user: true,
|
||||
selected: isSelected,
|
||||
selected: selectedPanel === "profile",
|
||||
rtl: isRTL,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -542,18 +556,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
if (!this.hass.auth.external?.config.hasSettingsScreen) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@@ -565,7 +579,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,15 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import { consume } from "@lit/context";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { getDevices, type DevicePickerItem } from "../data/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
|
||||
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
|
||||
import {
|
||||
areaMeetsFilter,
|
||||
deviceMeetsFilter,
|
||||
@@ -34,23 +18,18 @@ import {
|
||||
type TargetTypeFloorless,
|
||||
} from "../data/target";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
import "./target-picker/ha-target-picker-item-group";
|
||||
import "./target-picker/ha-target-picker-selector";
|
||||
import type { HaTargetPickerSelector } from "./target-picker/ha-target-picker-selector";
|
||||
import "./target-picker/ha-target-picker-value-chip";
|
||||
|
||||
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
|
||||
const SEPARATOR = "________";
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -89,54 +68,23 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false;
|
||||
|
||||
@state() private _selectedSection?: TargetTypeFloorless;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
@state() private _addTargetWidth = 0;
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
private _labelRegistry!: LabelRegistryEntry[];
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _pickerFilter?: TargetTypeFloorless;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
|
||||
@query(".add-target-wrapper") private _addTargetWrapper?: HTMLDivElement;
|
||||
|
||||
@query("ha-target-picker-selector")
|
||||
private _targetPickerSelectorElement?: HaTargetPickerSelector;
|
||||
|
||||
private _newTarget?: { type: TargetType; id: string };
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
private get _showEntityId() {
|
||||
return this.hass.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _fuseIndexes = {
|
||||
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
device: memoizeOne((states: DevicePickerItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
|
||||
private _createFuseIndex = (states) =>
|
||||
Fuse.createIndex(["search_labels"], states);
|
||||
|
||||
protected render() {
|
||||
if (this.addOnTop) {
|
||||
return html` ${this._renderPicker()} ${this._renderItems()} `;
|
||||
@@ -341,63 +289,137 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
const sections = [
|
||||
{
|
||||
id: "entity",
|
||||
label: this.hass.localize("ui.components.target-picker.type.entities"),
|
||||
},
|
||||
{
|
||||
id: "device",
|
||||
label: this.hass.localize("ui.components.target-picker.type.devices"),
|
||||
},
|
||||
{
|
||||
id: "area",
|
||||
label: this.hass.localize("ui.components.target-picker.type.areas"),
|
||||
},
|
||||
"separator" as const,
|
||||
{
|
||||
id: "label",
|
||||
label: this.hass.localize("ui.components.target-picker.type.labels"),
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="add-target-wrapper">
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.autofocus=${this.autofocus}
|
||||
.helper=${this.helper}
|
||||
.sections=${sections}
|
||||
.notFoundLabel=${this._noTargetFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.target-picker.no_targets"
|
||||
)}
|
||||
.sectionTitleFunction=${this._sectionTitleFunction}
|
||||
.selectedSection=${this._selectedSection}
|
||||
.rowRenderer=${this._renderRow}
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._targetPicked}
|
||||
.addButtonLabel=${this.hass.localize(
|
||||
<ha-button
|
||||
id="add-target-button"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.target-picker.add_target")}
|
||||
</ha-button>
|
||||
${!this._narrow && (this._pickerWrapperOpen || this._open)
|
||||
? html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._addTargetWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
placement="bottom-start"
|
||||
for="add-target-button"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._showSelector}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_target"
|
||||
)}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
${this._renderTargetSelector()}
|
||||
</wa-popover>
|
||||
`
|
||||
: this._pickerWrapperOpen || this._open
|
||||
? html`<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._showSelector}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_target"
|
||||
)}
|
||||
>
|
||||
${this._renderTargetSelector(true)}
|
||||
</ha-bottom-sheet>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (value.startsWith(CREATE_ID)) {
|
||||
this._createNewDomainElement(value.substring(CREATE_ID.length));
|
||||
return;
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._handleResize();
|
||||
window.addEventListener("resize", this._handleResize);
|
||||
}
|
||||
|
||||
const [type, id] = ev.detail.value.split(SEPARATOR);
|
||||
this._addTarget(id, type as TargetType);
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this._handleResize);
|
||||
}
|
||||
|
||||
private _handleResize = () => {
|
||||
this._narrow =
|
||||
window.matchMedia("(max-width: 870px)").matches ||
|
||||
window.matchMedia("(max-height: 500px)").matches;
|
||||
};
|
||||
|
||||
private _showPicker() {
|
||||
this._addTargetWidth = this._addTargetWrapper?.offsetWidth || 0;
|
||||
this._pickerWrapperOpen = true;
|
||||
}
|
||||
|
||||
// wait for drawer animation to finish
|
||||
private _showSelector = () => {
|
||||
this._open = true;
|
||||
requestAnimationFrame(() => {
|
||||
this._targetPickerSelectorElement?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
private _handleUpdatePickerFilter(
|
||||
ev: CustomEvent<TargetTypeFloorless | undefined>
|
||||
) {
|
||||
this._updatePickerFilter(
|
||||
typeof ev.detail === "string" ? ev.detail : undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _updatePickerFilter = (filter?: TargetTypeFloorless) => {
|
||||
this._pickerFilter = filter;
|
||||
};
|
||||
|
||||
private _hidePicker(ev) {
|
||||
ev.stopPropagation();
|
||||
this._open = false;
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
if (this._newTarget) {
|
||||
this._addTarget(this._newTarget.id, this._newTarget.type);
|
||||
this._newTarget = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderTargetSelector(dialogMode = false) {
|
||||
if (!this._open) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-target-picker-selector
|
||||
.hass=${this.hass}
|
||||
@filter-type-changed=${this._handleUpdatePickerFilter}
|
||||
.filterType=${this._pickerFilter}
|
||||
@target-picked=${this._handleTargetPicked}
|
||||
@create-domain-picked=${this._handleCreateDomain}
|
||||
.targetValue=${this.value}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.createDomains=${this.createDomains}
|
||||
.mode=${dialogMode ? "dialog" : "popover"}
|
||||
></ha-target-picker-selector>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addTarget(id: string, type: TargetType) {
|
||||
@@ -432,7 +454,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
?.removeAttribute("collapsed");
|
||||
}
|
||||
|
||||
private _createNewDomainElement = (domain: string) => {
|
||||
private _handleTargetPicked = async (
|
||||
ev: CustomEvent<{ type: TargetType; id: string }>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
if (!ev.detail.type || !ev.detail.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// save new target temporarily to add it after dialog closes
|
||||
this._newTarget = ev.detail;
|
||||
};
|
||||
|
||||
private _handleCreateDomain = (ev: CustomEvent<string>) => {
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
const domain = ev.detail;
|
||||
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
@@ -634,459 +675,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRowType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === EMPTY_SEARCH) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
private _sectionTitleFunction = ({
|
||||
firstIndex,
|
||||
lastIndex,
|
||||
firstItem,
|
||||
secondItem,
|
||||
itemsCount,
|
||||
}: {
|
||||
firstIndex: number;
|
||||
lastIndex: number;
|
||||
firstItem: PickerComboBoxItem | string;
|
||||
secondItem: PickerComboBoxItem | string;
|
||||
itemsCount: number;
|
||||
}) => {
|
||||
if (
|
||||
firstItem === undefined ||
|
||||
secondItem === undefined ||
|
||||
typeof firstItem === "string" ||
|
||||
(typeof secondItem === "string" && secondItem !== "padding") ||
|
||||
(firstIndex === 0 && lastIndex === itemsCount - 1)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const type = this._getRowType(firstItem as PickerComboBoxItem);
|
||||
const translationType:
|
||||
| "areas"
|
||||
| "entities"
|
||||
| "devices"
|
||||
| "labels"
|
||||
| undefined =
|
||||
type === "area" || type === "floor"
|
||||
? "areas"
|
||||
: type === "entity"
|
||||
? "entities"
|
||||
: type && type !== "empty"
|
||||
? `${type}s`
|
||||
: undefined;
|
||||
|
||||
return translationType
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.type.${translationType}`
|
||||
)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
private _getItems = (searchString: string, section: string) => {
|
||||
this._selectedSection = section as TargetTypeFloorless | undefined;
|
||||
|
||||
return this._getItemsMemoized(
|
||||
this.hass.localize,
|
||||
this.entityFilter,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.value,
|
||||
searchString,
|
||||
this._configEntryLookup,
|
||||
this._selectedSection
|
||||
);
|
||||
};
|
||||
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
localize: HomeAssistant["localize"],
|
||||
entityFilter: this["entityFilter"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
includeDomains: this["includeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
targetValue: this["value"],
|
||||
searchTerm: string,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
filterType?: TargetTypeFloorless
|
||||
) => {
|
||||
const items: (
|
||||
| string
|
||||
| FloorComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| PickerComboBoxItem
|
||||
)[] = [];
|
||||
|
||||
if (!filterType || filterType === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
includeDomains,
|
||||
undefined,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
undefined,
|
||||
undefined,
|
||||
targetValue?.entity_id
|
||||
? ensureArray(targetValue.entity_id)
|
||||
: undefined,
|
||||
undefined,
|
||||
`entity${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
(item: EntityComboBoxItem) =>
|
||||
item.stateObj?.entity_id === searchTerm
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!filterType && entityItems.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.entities"));
|
||||
}
|
||||
|
||||
items.push(...entityItems);
|
||||
}
|
||||
|
||||
if (!filterType || filterType === "device") {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
targetValue?.device_id
|
||||
? ensureArray(targetValue.device_id)
|
||||
: undefined,
|
||||
undefined,
|
||||
`device${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
||||
}
|
||||
|
||||
if (!filterType && deviceItems.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.devices"));
|
||||
}
|
||||
|
||||
items.push(...deviceItems);
|
||||
}
|
||||
|
||||
if (!filterType || filterType === "area") {
|
||||
let areasAndFloors = this._getAreasAndFloorsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(SEPARATOR)
|
||||
),
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
targetValue?.area_id ? ensureArray(targetValue.area_id) : undefined,
|
||||
targetValue?.floor_id ? ensureArray(targetValue.floor_id) : undefined
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!filterType && areasAndFloors.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.areas"));
|
||||
}
|
||||
|
||||
items.push(
|
||||
...areasAndFloors.map((item, index) => {
|
||||
const nextItem = areasAndFloors[index + 1];
|
||||
|
||||
if (
|
||||
!nextItem ||
|
||||
(item.type === "area" && nextItem.type === "floor")
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
last: true,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType || filterType === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this._labelRegistry,
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
|
||||
`label${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup("label", labels, searchTerm);
|
||||
}
|
||||
|
||||
if (!filterType && labels.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.labels"));
|
||||
}
|
||||
|
||||
items.push(...labels);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _filterGroup(
|
||||
type: TargetType,
|
||||
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
||||
searchTerm: string,
|
||||
checkExact?: (
|
||||
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
|
||||
) => boolean
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
const fuse = new HaFuse(
|
||||
items,
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchTerm.length, 2),
|
||||
},
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchTerm);
|
||||
let filteredItems = items;
|
||||
if (results) {
|
||||
filteredItems = results.map((result) => result.item);
|
||||
}
|
||||
|
||||
if (!checkExact) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex((item) => checkExact(item));
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
||||
|
||||
private _getCreateItems = memoizeOne(
|
||||
(createDomains: this["createDomains"]) => {
|
||||
if (!createDomains?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return createDomains.map((domain) => {
|
||||
const primary = this.hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
|
||||
: domainToName(this.hass.localize, domain),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: CREATE_ID + domain,
|
||||
primary: primary,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.entity.entity-picker.new_entity"
|
||||
),
|
||||
icon_path: mdiPlus,
|
||||
} satisfies EntityComboBoxItem;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
}
|
||||
|
||||
private _renderRow = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem,
|
||||
index: number
|
||||
) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const type = this._getRowType(item);
|
||||
let hasFloor = false;
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
|
||||
if (type === "area" || type === "floor") {
|
||||
item.id = item[type]?.[`${type}_id`];
|
||||
|
||||
rtl = computeRTL(this.hass);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
if (type === "entity") {
|
||||
showEntityId = !!this._showEntityId;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
id=${`list-item-${index}`}
|
||||
tabindex="-1"
|
||||
.type=${type === "empty" ? "text" : "button"}
|
||||
class=${type === "empty" ? "empty" : ""}
|
||||
style=${(item as FloorComboBoxItem).type === "area" && hasFloor
|
||||
? "--md-list-item-leading-space: var(--ha-space-12);"
|
||||
: ""}
|
||||
>
|
||||
${(item as FloorComboBoxItem).type === "area" && hasFloor
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
style=${styleMap({
|
||||
width: "var(--ha-space-12)",
|
||||
position: "absolute",
|
||||
top: "var(--ha-space-0)",
|
||||
left: rtl ? undefined : "var(--ha-space-1)",
|
||||
right: rtl ? "var(--ha-space-1)" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
.end=${(
|
||||
item as FloorComboBoxItem & { last?: boolean | undefined }
|
||||
).last}
|
||||
slot="start"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
: nothing}
|
||||
${item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: type === "entity" && (item as EntityComboBoxItem).stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: type === "device" && (item as DevicePickerItem).domain
|
||||
? html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: (item as DevicePickerItem).domain!,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes.darkMode,
|
||||
})}
|
||||
/>
|
||||
`
|
||||
: type === "floor"
|
||||
? html`<ha-floor-icon
|
||||
slot="start"
|
||||
.floor=${(item as FloorComboBoxItem).floor!}
|
||||
></ha-floor-icon>`
|
||||
: type === "area"
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path || mdiTextureBox}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${(item as EntityComboBoxItem).stateObj && showEntityId
|
||||
? html`
|
||||
<span slot="supporting-text" class="code">
|
||||
${(item as EntityComboBoxItem).stateObj?.entity_id}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
${(item as EntityComboBoxItem).domain_name &&
|
||||
(type !== "entity" || !showEntityId)
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${(item as EntityComboBoxItem).domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _noTargetFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.target-picker.no_target_found", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.add-target-wrapper {
|
||||
@@ -1095,8 +683,31 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
margin-top: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: min(max(var(--body-width), 336px), 600px);
|
||||
max-width: min(max(var(--body-width), 336px), 600px);
|
||||
max-height: 500px;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-height: 1000px) {
|
||||
wa-popover::part(body) {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-height: 90vh;
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||
--ha-bottom-sheet-max-width: 600px;
|
||||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
}
|
||||
|
||||
${unsafeCSS(chipStyles)}
|
||||
|
||||
@@ -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 {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -6,9 +8,6 @@ import {
|
||||
query,
|
||||
state,
|
||||
} 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 { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -32,8 +31,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog content body.
|
||||
* @slot footer - Dialog footer content.
|
||||
@@ -55,8 +52,8 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {string} header-title - Header title text when no custom title slot is provided.
|
||||
* @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 {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 {
|
||||
@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 })
|
||||
public open = false;
|
||||
|
||||
@@ -90,11 +81,11 @@ export class HaWaDialog extends LitElement {
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
@property({ type: String, attribute: "header-title" })
|
||||
public headerTitle = "";
|
||||
|
||||
@property({ attribute: "header-subtitle" })
|
||||
public headerSubtitle?: string;
|
||||
@property({ type: String, attribute: "header-subtitle" })
|
||||
public headerSubtitle = "";
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
@@ -126,11 +117,6 @@ export class HaWaDialog extends LitElement {
|
||||
.open=${this._open}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
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-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
@@ -147,14 +133,14 @@ export class HaWaDialog extends LitElement {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle
|
||||
? html`<span slot="title" class="title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
: nothing}
|
||||
${this.headerSubtitle
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
: nothing}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
|
||||
@@ -62,10 +62,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
private _candidatesList: RTCIceCandidate[] = [];
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
// video is playing in picture-in-picture mode, don't do anything
|
||||
return;
|
||||
}
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
removeLocalMedia,
|
||||
} from "../../data/media_source";
|
||||
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 "../ha-button";
|
||||
import "../ha-check-list-item";
|
||||
@@ -305,7 +305,6 @@ class DialogMediaManage extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
@@ -315,9 +314,9 @@ class DialogMediaManage extends LitElement {
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
MediaPlayerItem,
|
||||
MediaPlayerLayoutType,
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
@@ -223,7 +223,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
@@ -231,27 +230,23 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
ha-media-player-browse {
|
||||
--media-browser-max-height: calc(
|
||||
100vh - 65px - var(--safe-area-inset-y)
|
||||
);
|
||||
--media-browser-max-height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
:host(.opened) ha-media-player-browse {
|
||||
height: calc(100vh - 65px - var(--safe-area-inset-y));
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
ha-media-player-browse {
|
||||
position: initial;
|
||||
--media-browser-max-height: calc(
|
||||
100vh - 145px - var(--safe-area-inset-y)
|
||||
);
|
||||
--media-browser-max-height: calc(100vh - 145px);
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ class SearchInput extends LitElement {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
|
||||
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}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.user-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
@@ -147,11 +149,6 @@ class HaUserPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.user-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
{
|
||||
groups: {
|
||||
device_id: {},
|
||||
dynamicGroups: {},
|
||||
serviceGroups: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -117,6 +117,14 @@ export const VIRTUAL_ACTIONS: Partial<
|
||||
},
|
||||
} 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 = [
|
||||
"ha-automation-action-choose",
|
||||
"ha-automation-action-condition",
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface AnalyticsPreferences {
|
||||
diagnostics?: boolean;
|
||||
usage?: boolean;
|
||||
statistics?: boolean;
|
||||
snapshots?: boolean;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getAreasFloorHierarchy } from "../common/areas/areas-floor-hierarchy";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
@@ -13,7 +12,11 @@ import {
|
||||
} from "./device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
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 {
|
||||
type: "floor" | "area";
|
||||
@@ -179,20 +182,34 @@ export const getAreasAndFloors = (
|
||||
);
|
||||
}
|
||||
|
||||
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
|
||||
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||
const unassignedAreas = Object.values(outputAreas).filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
|
||||
const compare = floorCompare(haFloors);
|
||||
|
||||
// @ts-ignore
|
||||
const floorAreaEntries: [
|
||||
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 items: FloorComboBoxItem[] = [];
|
||||
|
||||
hierarchy.floors.forEach((f) => {
|
||||
const floor = haFloors[f.id];
|
||||
const floorAreas = f.areas.map((areaId) => haAreas[areaId]);
|
||||
|
||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||
if (floor) {
|
||||
const floorName = computeFloorName(floor);
|
||||
|
||||
const areaSearchLabels = floorAreas
|
||||
.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
return [area.area_id, ...(areaName ? [areaName] : []), ...area.aliases];
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return [area.area_id, areaName, ...area.aliases];
|
||||
})
|
||||
.flat();
|
||||
|
||||
@@ -209,29 +226,24 @@ export const getAreasAndFloors = (
|
||||
...areaSearchLabels,
|
||||
],
|
||||
});
|
||||
|
||||
}
|
||||
items.push(
|
||||
...floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(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_id,
|
||||
primary: areaName,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [
|
||||
area.area_id,
|
||||
...(areaName ? [areaName] : []),
|
||||
...area.aliases,
|
||||
],
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
items.push(
|
||||
...hierarchy.areas.map((areaId) => {
|
||||
const area = haAreas[areaId];
|
||||
...unassignedAreas.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
|
||||
@@ -59,15 +59,6 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
||||
area_id: areaId,
|
||||
});
|
||||
|
||||
export const reorderAreaRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
areaIds: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/area_registry/reorder",
|
||||
area_ids: areaIds,
|
||||
});
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
): AreaEntityLookup => {
|
||||
|
||||
@@ -214,8 +214,6 @@ export interface PipelineRun {
|
||||
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
|
||||
run: PipelineRunStartEvent["data"];
|
||||
error?: PipelineErrorEvent["data"];
|
||||
started: Date;
|
||||
finished?: Date;
|
||||
wake_word?: PipelineWakeWordStartEvent["data"] &
|
||||
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
|
||||
stt?: PipelineSTTStartEvent["data"] &
|
||||
@@ -237,7 +235,6 @@ export const processEvent = (
|
||||
stage: "ready",
|
||||
run: event.data,
|
||||
events: [event],
|
||||
started: new Date(event.timestamp),
|
||||
};
|
||||
return run;
|
||||
}
|
||||
@@ -293,14 +290,9 @@ export const processEvent = (
|
||||
tts: { ...run.tts!, ...event.data, done: true },
|
||||
};
|
||||
} else if (event.type === "run-end") {
|
||||
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
|
||||
run = { ...run, stage: "done" };
|
||||
} else if (event.type === "error") {
|
||||
run = {
|
||||
...run,
|
||||
finished: new Date(event.timestamp),
|
||||
stage: "error",
|
||||
error: event.data,
|
||||
};
|
||||
run = { ...run, stage: "error", error: event.data };
|
||||
} else {
|
||||
run = { ...run };
|
||||
}
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import type { ConditionDescription } from "./condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, Field, MODES } from "./script";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
import type { TriggerDescription } from "./trigger";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
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 {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
id?: string;
|
||||
@@ -97,12 +85,6 @@ export interface BaseTrigger {
|
||||
id?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PlatformTrigger extends BaseTrigger {
|
||||
trigger: Exclude<string, LegacyTrigger["trigger"]>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
@@ -212,7 +194,7 @@ export interface CalendarTrigger extends BaseTrigger {
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export type LegacyTrigger =
|
||||
export type Trigger =
|
||||
| StateTrigger
|
||||
| MqttTrigger
|
||||
| GeoLocationTrigger
|
||||
@@ -229,20 +211,13 @@ export type LegacyTrigger =
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger;
|
||||
|
||||
export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
||||
| CalendarTrigger
|
||||
| TriggerList;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
alias?: string;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PlatformCondition extends BaseCondition {
|
||||
condition: Exclude<string, LegacyCondition["condition"]>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface LogicalCondition extends BaseCondition {
|
||||
@@ -282,11 +257,13 @@ export interface ZoneCondition extends BaseCondition {
|
||||
zone: string;
|
||||
}
|
||||
|
||||
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
||||
|
||||
export interface TimeCondition extends BaseCondition {
|
||||
condition: "time";
|
||||
after?: string;
|
||||
before?: string;
|
||||
weekday?: WeekdayShort | WeekdayShort[];
|
||||
weekday?: Weekday | Weekday[];
|
||||
}
|
||||
|
||||
export interface TemplateCondition extends BaseCondition {
|
||||
@@ -327,7 +304,7 @@ export type AutomationElementGroup = Record<
|
||||
{ icon?: string; members?: AutomationElementGroup }
|
||||
>;
|
||||
|
||||
export type LegacyCondition =
|
||||
export type Condition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
| SunCondition
|
||||
@@ -338,8 +315,6 @@ export type LegacyCondition =
|
||||
| LogicalCondition
|
||||
| TriggerCondition;
|
||||
|
||||
export type Condition = LegacyCondition | PlatformCondition;
|
||||
|
||||
export type ConditionWithShorthand =
|
||||
| Condition
|
||||
| ShorthandAndConditionList
|
||||
@@ -601,7 +576,6 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
||||
toggleYamlMode: () => void;
|
||||
config: Trigger;
|
||||
description?: TriggerDescription;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
@@ -617,7 +591,6 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
||||
insertAfter: (value: Condition | Condition[]) => boolean;
|
||||
toggleYamlMode: () => void;
|
||||
config: Condition;
|
||||
description?: ConditionDescription;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
@@ -16,16 +16,8 @@ import {
|
||||
formatListWithAnds,
|
||||
formatListWithOrs,
|
||||
} from "../common/string/format-list";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
Condition,
|
||||
ForDict,
|
||||
LegacyCondition,
|
||||
LegacyTrigger,
|
||||
Trigger,
|
||||
} from "./automation";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
import type { Condition, ForDict, Trigger } from "./automation";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import {
|
||||
localizeDeviceAutomationCondition,
|
||||
@@ -33,7 +25,8 @@ import {
|
||||
} from "./device_automation";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger";
|
||||
import { isTriggerList } from "./trigger";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
const triggerTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.triggers.type";
|
||||
@@ -128,37 +121,6 @@ const tryDescribeTrigger = (
|
||||
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
|
||||
if (trigger.trigger === "event" && trigger.event_type) {
|
||||
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 = (
|
||||
@@ -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") {
|
||||
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 type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
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")}
|
||||
${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 = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface CalendarEventData {
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEventMutableParams {
|
||||
@@ -40,7 +39,6 @@ export interface CalendarEventMutableParams {
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
// The scope of a delete/update for a recurring event
|
||||
@@ -98,7 +96,6 @@ export const fetchCalendarEvents = async (
|
||||
uid: ev.uid,
|
||||
summary: ev.summary,
|
||||
description: ev.description,
|
||||
location: ev.location,
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
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 { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
mdiAmpersand,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiGateOr,
|
||||
mdiIdentifier,
|
||||
mdiMapClock,
|
||||
mdiMapMarkerRadius,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiShape,
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
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[] = [
|
||||
{
|
||||
groups: {
|
||||
device: {},
|
||||
dynamicGroups: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
time_location: {
|
||||
icon: mdiMapClock,
|
||||
@@ -39,33 +62,3 @@ export const COLLAPSIBLE_CONDITION_ELEMENTS = [
|
||||
"ha-automation-condition-not",
|
||||
"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) : "_";
|
||||
|
||||
@@ -186,8 +186,7 @@ export const getDevices = (
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
value?: string
|
||||
): DevicePickerItem[] => {
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
@@ -299,7 +298,7 @@ export const getDevices = (
|
||||
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
|
||||
|
||||
return {
|
||||
id: `${idPrefix}${device.id}`,
|
||||
id: device.id,
|
||||
label: "",
|
||||
primary:
|
||||
deviceName ||
|
||||
|
||||
@@ -102,7 +102,6 @@ export type EnergySolarForecasts = Record<string, EnergySolarForecast>;
|
||||
export interface DeviceConsumptionEnergyPreference {
|
||||
// This is an ever increasing value
|
||||
stat_consumption: string;
|
||||
stat_rate?: string;
|
||||
name?: string;
|
||||
included_in_stat?: string;
|
||||
}
|
||||
@@ -131,17 +130,11 @@ export interface FlowToGridSourceEnergyPreference {
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface GridPowerSourceEnergyPreference {
|
||||
// W meter
|
||||
stat_rate: string;
|
||||
}
|
||||
|
||||
export interface GridSourceTypeEnergyPreference {
|
||||
type: "grid";
|
||||
|
||||
flow_from: FlowFromGridSourceEnergyPreference[];
|
||||
flow_to: FlowToGridSourceEnergyPreference[];
|
||||
power?: GridPowerSourceEnergyPreference[];
|
||||
|
||||
cost_adjustment_day: number;
|
||||
}
|
||||
@@ -150,7 +143,6 @@ export interface SolarSourceTypeEnergyPreference {
|
||||
type: "solar";
|
||||
|
||||
stat_energy_from: string;
|
||||
stat_rate?: string;
|
||||
config_entry_solar_forecast: string[] | null;
|
||||
}
|
||||
|
||||
@@ -158,7 +150,6 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
type: "battery";
|
||||
stat_energy_from: string;
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
@@ -200,7 +191,6 @@ export type EnergySource =
|
||||
export interface EnergyPreferences {
|
||||
energy_sources: EnergySource[];
|
||||
device_consumption: DeviceConsumptionEnergyPreference[];
|
||||
device_consumption_water: DeviceConsumptionEnergyPreference[];
|
||||
}
|
||||
|
||||
export interface EnergyInfo {
|
||||
@@ -217,7 +207,6 @@ export interface EnergyValidationIssue {
|
||||
export interface EnergyPreferencesValidation {
|
||||
energy_sources: EnergyValidationIssue[][];
|
||||
device_consumption: EnergyValidationIssue[][];
|
||||
device_consumption_water: EnergyValidationIssue[][];
|
||||
}
|
||||
|
||||
export const getEnergyInfo = (hass: HomeAssistant) =>
|
||||
@@ -358,44 +347,10 @@ export const getReferencedStatisticIds = (
|
||||
if (!(includeTypes && !includeTypes.includes("device"))) {
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
|
||||
}
|
||||
if (!(includeTypes && !includeTypes.includes("water"))) {
|
||||
statIDs.push(
|
||||
...prefs.device_consumption_water.map((d) => d.stat_consumption)
|
||||
);
|
||||
}
|
||||
|
||||
return statIDs;
|
||||
};
|
||||
|
||||
export const getReferencedStatisticIdsPower = (
|
||||
prefs: EnergyPreferences
|
||||
): string[] => {
|
||||
const statIDs: (string | undefined)[] = [];
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "gas" || source.type === "water") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "solar") {
|
||||
statIDs.push(source.stat_rate);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
statIDs.push(source.stat_rate);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.power) {
|
||||
statIDs.push(...source.power.map((p) => p.stat_rate));
|
||||
}
|
||||
}
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
||||
|
||||
return statIDs.filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
export const enum CompareMode {
|
||||
NONE = "",
|
||||
PREVIOUS = "previous",
|
||||
@@ -443,10 +398,9 @@ const getEnergyData = async (
|
||||
"gas",
|
||||
"device",
|
||||
]);
|
||||
const powerStatIds = getReferencedStatisticIdsPower(prefs);
|
||||
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
||||
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds];
|
||||
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
const period =
|
||||
@@ -457,8 +411,6 @@ const getEnergyData = async (
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour";
|
||||
const finePeriod =
|
||||
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const statsMetadataArray = allStatIDs.length
|
||||
@@ -480,9 +432,6 @@ const getEnergyData = async (
|
||||
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
||||
: undefined,
|
||||
};
|
||||
const powerUnits: StatisticsUnitConfiguration = {
|
||||
power: "kW",
|
||||
};
|
||||
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
||||
const waterUnits: StatisticsUnitConfiguration = {
|
||||
volume: waterUnit,
|
||||
@@ -493,12 +442,6 @@ const getEnergyData = async (
|
||||
"change",
|
||||
])
|
||||
: {};
|
||||
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
|
||||
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
|
||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||
"change",
|
||||
@@ -605,7 +548,6 @@ const getEnergyData = async (
|
||||
|
||||
const [
|
||||
energyStats,
|
||||
powerStats,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
@@ -613,14 +555,13 @@ const getEnergyData = async (
|
||||
fossilEnergyConsumptionCompare,
|
||||
] = await Promise.all([
|
||||
_energyStats,
|
||||
_powerStats,
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||
const stats = { ...energyStats, ...waterStats };
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
}
|
||||
@@ -782,7 +723,6 @@ export const getEnergyDataCollection = (
|
||||
hass.locale,
|
||||
hass.config
|
||||
);
|
||||
collection.refresh();
|
||||
scheduleUpdatePeriod();
|
||||
},
|
||||
addHours(
|
||||
|
||||
@@ -344,8 +344,7 @@ export const getEntities = (
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
value?: string
|
||||
): EntityComboBoxItem[] => {
|
||||
let items: EntityComboBoxItem[] = [];
|
||||
|
||||
@@ -396,9 +395,10 @@ export const getEntities = (
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
||||
|
||||
return {
|
||||
id: `${idPrefix}${entityId}`,
|
||||
id: entityId,
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
domain_name: domainName,
|
||||
@@ -411,6 +411,7 @@ export const getEntities = (
|
||||
friendlyName,
|
||||
entityId,
|
||||
].filter(Boolean) as string[],
|
||||
a11y_label: a11yLabel,
|
||||
stateObj: stateObj,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -51,15 +51,6 @@ export const deleteFloorRegistryEntry = (
|
||||
floor_id: floorId,
|
||||
});
|
||||
|
||||
export const reorderFloorRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
floorIds: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/floor_registry/reorder",
|
||||
floor_ids: floorIds,
|
||||
});
|
||||
|
||||
export const getFloorAreaLookup = (
|
||||
areas: AreaRegistryEntry[]
|
||||
): FloorAreaLookup => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user