mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-26 03:17:57 +00:00
Compare commits
4 Commits
dialog-nex
...
swc-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bf5833262 | ||
|
|
8c9da19935 | ||
|
|
4b1eeb0eb1 | ||
|
|
f7ffdabe5d |
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -21,14 +21,6 @@
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If your PR includes visual changes, please add screenshots or a short video
|
||||
showing the before and after. This helps reviewers understand the impact of
|
||||
your changes.
|
||||
Note: Remove this section if this PR has no visual changes.
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
<!--
|
||||
What type of change does your PR introduce to the Home Assistant frontend?
|
||||
@@ -43,6 +35,16 @@
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Example configuration
|
||||
<!--
|
||||
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||
your PR.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
@@ -52,8 +54,6 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
@@ -61,50 +61,18 @@
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||
We're here to help! This is simply a reminder of what we are going to look
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
|
||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
Unfortunately, the number of incoming pull requests is higher than what our
|
||||
reviewers can review and merge so there is a long backlog of pull requests
|
||||
waiting for review. You can help here!
|
||||
|
||||
By reviewing another pull request, you will help raise the code quality of
|
||||
that pull request and the final review will be faster. This way the general
|
||||
pace of pull request reviews will go up and your wait time will go down.
|
||||
|
||||
When picking a pull request to review, try to choose one that hasn't yet
|
||||
been reviewed.
|
||||
|
||||
Thanks for helping out!
|
||||
-->
|
||||
|
||||
To help with the load of incoming pull requests:
|
||||
|
||||
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
|
||||
|
||||
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
|
||||
113
.github/copilot-instructions.md
vendored
113
.github/copilot-instructions.md
vendored
@@ -194,13 +194,13 @@ The View Transitions API creates smooth animations between DOM state changes. Wh
|
||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-duration-fast` (150ms), `--ha-animation-duration-normal` (250ms), `--ha-animation-duration-slow` (350ms) (all respect `prefers-reduced-motion`)
|
||||
- **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-duration-*` CSS variables for consistent timing (`fast`, `normal`, `slow`)
|
||||
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
|
||||
|
||||
@@ -214,6 +214,13 @@ By default, `:root` receives `view-transition-name: root`, creating a full-page
|
||||
- Only one view transition can run at a time
|
||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
||||
|
||||
**Current Usage & Planned Applications:**
|
||||
|
||||
- Launch screen fade out (implemented)
|
||||
- Automation sidebar transitions (planned - #27238)
|
||||
- More info dialog content changes (planned - #27672)
|
||||
- Toolbar navigation, ha-spinner transitions (planned)
|
||||
|
||||
**Specification & Documentation:**
|
||||
|
||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
||||
@@ -239,7 +246,12 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
|
||||
## Component Library
|
||||
|
||||
### Dialog Component
|
||||
### Dialog Components
|
||||
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -253,7 +265,6 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Use `ha-dialog` component
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
@@ -269,6 +280,7 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
||||
- Custom sizing is NOT recommended - use the standard width presets
|
||||
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
@@ -278,9 +290,17 @@ fireEvent(this, "show-dialog", {
|
||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
**Recent Examples:**
|
||||
|
||||
See these files for current patterns:
|
||||
|
||||
- `src/panels/config/repairs/dialog-repairs-issue.ts`
|
||||
- `src/dialogs/restart/dialog-restart.ts`
|
||||
- `src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts`
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-wa-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialogs.markdown`
|
||||
|
||||
### Form Component (ha-form)
|
||||
@@ -288,6 +308,7 @@ fireEvent(this, "show-dialog", {
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||
- Built-in validation with error display
|
||||
- Use `dialogInitialFocus` in dialogs
|
||||
- Use `computeLabel`, `computeError`, `computeHelper` for translations
|
||||
|
||||
```typescript
|
||||
@@ -372,6 +393,81 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Dialog
|
||||
|
||||
```typescript
|
||||
@customElement("dialog-my-feature")
|
||||
export class DialogMyFeature
|
||||
extends LitElement
|
||||
implements HassDialog<MyDialogParams>
|
||||
{
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-subtitle=${this._params.subtitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p>Dialog content</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
```
|
||||
|
||||
### Dialog Design Guidelines
|
||||
|
||||
- Max width: 560px (Alert/confirmation: 320px fixed width)
|
||||
- Close X-icon on top left (all screen sizes)
|
||||
- Submit button grouped with cancel at bottom right
|
||||
- Keep button labels short: "Save", "Delete", "Enable"
|
||||
- Destructive actions use red warning button
|
||||
- Always use a title (best practice)
|
||||
- Strive for minimalism
|
||||
|
||||
#### Creating a Lovelace Card
|
||||
|
||||
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
|
||||
@@ -462,10 +558,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Pull Requests
|
||||
|
||||
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately. Do not omit, reorder, or rewrite the template sections. Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check. If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it.
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
@@ -623,6 +715,9 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
- [ ] Dialogs implement HassDialog interface
|
||||
- [ ] Dialog styling uses haStyleDialog
|
||||
- [ ] Dialog accessibility includes dialogInitialFocus
|
||||
- [ ] ha-alert used correctly for messages
|
||||
- [ ] ha-form uses proper schema structure
|
||||
- [ ] Components handle all states (loading, error, unavailable)
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
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@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
# ℹ️ 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@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
29
.github/workflows/restrict-task-creation.yml
vendored
29
.github/workflows/restrict-task-creation.yml
vendored
@@ -5,38 +5,9 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -31,4 +31,7 @@ module.exports = {
|
||||
isDevContainer() {
|
||||
return isTrue(process.env.DEV_CONTAINER);
|
||||
},
|
||||
jsMinifier() {
|
||||
return (process.env.JS_MINIFIER || "swc").toLowerCase();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -80,7 +80,13 @@ const doneHandler = (done) => (err, stats) => {
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
const durationMs =
|
||||
stats?.startTime && stats?.endTime ? stats.endTime - stats.startTime : 0;
|
||||
const durationLabel = durationMs
|
||||
? ` (${(durationMs / 1000).toFixed(1)}s, minifier: ${env.jsMinifier()})`
|
||||
: ` (minifier: ${env.jsMinifier()})`;
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}${durationLabel}`);
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
|
||||
@@ -13,6 +13,7 @@ const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WebpackBar = require("webpackbar/rspack");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
@@ -100,11 +101,20 @@ const createRspackConfig = ({
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
}),
|
||||
env.jsMinifier() === "terser"
|
||||
? new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
})
|
||||
: new rspack.SwcJsMinimizerRspackPlugin({
|
||||
extractComments: true,
|
||||
minimizerOptions: {
|
||||
ecma: latestBuild ? 2015 : 5,
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
|
||||
@@ -14,28 +14,40 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
flow_from: [
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
flow_to: [
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
power: [
|
||||
{ stat_rate: "sensor.power_grid" },
|
||||
{ stat_rate: "sensor.power_grid_return" },
|
||||
],
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -193,13 +192,5 @@ export default tseslint.config(
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
},
|
||||
rules: {
|
||||
"html/no-invalid-attr-value": "error",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ type DialogType =
|
||||
| "basic"
|
||||
| "basic-subtitle-below"
|
||||
| "basic-subtitle-above"
|
||||
| "allow-mode-change"
|
||||
| "form"
|
||||
| "form-block-mode"
|
||||
| "actions"
|
||||
| "large"
|
||||
| "small";
|
||||
@@ -69,8 +69,8 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<ha-button @click=${this._handleOpenDialog("form")}
|
||||
>Adaptive dialog with form</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("allow-mode-change")}
|
||||
>Adaptive dialog with allow mode change</ha-button
|
||||
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
|
||||
>Adaptive dialog with form (block mode change)</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("actions")}
|
||||
>Adaptive dialog with actions</ha-button
|
||||
@@ -164,15 +164,27 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.allowModeChange=${this._openDialog === "allow-mode-change"}
|
||||
header-title="Adaptive dialog with allow mode change"
|
||||
header-subtitle="Resize the window while this dialog is open"
|
||||
.open=${this._openDialog === "form-block-mode"}
|
||||
header-title="Adaptive dialog with form (block mode change)"
|
||||
header-subtitle="This form will not reset when the viewport size changes"
|
||||
block-mode-change
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>
|
||||
This dialog can switch between dialog mode and bottom sheet mode
|
||||
while open.
|
||||
</div>
|
||||
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="primaryAction"
|
||||
variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
@@ -203,7 +215,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<li>
|
||||
<strong>Dialog mode:</strong> Used on larger screens (width >
|
||||
870px and height > 500px). Renders as a centered dialog using
|
||||
<code>ha-dialog</code>.
|
||||
<code>ha-wa-dialog</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bottom sheet mode:</strong> Used on mobile devices and
|
||||
@@ -213,9 +225,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
By default, the mode is determined at mount time and then stays fixed
|
||||
while the dialog is open. To allow switching modes while the viewport
|
||||
changes, use the <code>allow-mode-change</code> attribute.
|
||||
The mode is determined automatically and updates when the window is
|
||||
resized. To prevent mode changes after the initial mount (useful for
|
||||
preventing form resets), use the <code>block-mode-change</code>
|
||||
attribute.
|
||||
</p>
|
||||
|
||||
<h3>Width</h3>
|
||||
@@ -381,15 +394,15 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<p>
|
||||
If you don't need responsive behavior, use
|
||||
<code>ha-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-wa-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-bottom-sheet</code> for mobile-only sheets.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Use the <code>allow-mode-change</code> attribute when you want the
|
||||
dialog to switch between modes as the viewport changes after opening.
|
||||
For forms, you can keep the default behavior to avoid resetting fields
|
||||
on resize.
|
||||
Use the <code>block-mode-change</code> attribute when you want to
|
||||
prevent the dialog from switching modes after it's opened. This is
|
||||
especially useful for forms, as it prevents form data from being lost
|
||||
when users resize their browser window.
|
||||
</p>
|
||||
|
||||
<h3>Example usage</h3>
|
||||
@@ -397,6 +410,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
width="medium"
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
>
|
||||
@@ -413,10 +427,27 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<p>Example with <code>block-mode-change</code> for forms:</p>
|
||||
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
header-title="Edit configuration"
|
||||
block-mode-change
|
||||
>
|
||||
<ha-form .schema=\${schema} .data=\${data}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Save</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
<p>
|
||||
This component combines <code>ha-dialog</code> and
|
||||
This component combines <code>ha-wa-dialog</code> and
|
||||
<code>ha-bottom-sheet</code> with automatic mode switching based on
|
||||
screen size.
|
||||
</p>
|
||||
@@ -490,10 +521,12 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>allow-mode-change</code></td>
|
||||
<td><code>block-mode-change</code></td>
|
||||
<td>
|
||||
When set, the dialog can switch between modes as the viewport
|
||||
size changes while it is open.
|
||||
When set, the mode is determined at mount time based on the
|
||||
current screen size, but subsequent mode changes are blocked.
|
||||
Useful for preventing forms from resetting when the viewport
|
||||
size changes.
|
||||
</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Dialog (ha-dialog)
|
||||
---
|
||||
@@ -8,7 +8,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
@@ -17,10 +16,7 @@ import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
import {
|
||||
showDialog,
|
||||
type ShowDialogParams,
|
||||
} from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
@@ -615,15 +611,14 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
};
|
||||
};
|
||||
|
||||
private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
|
||||
e.detail;
|
||||
private _dialogManager = (e) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
this.shadowRoot!,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Dialog (ha-wa-dialog)
|
||||
---
|
||||
@@ -6,7 +6,7 @@ import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dialog-footer";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-wa-dialog";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
@@ -22,14 +22,14 @@ type DialogType =
|
||||
| "form"
|
||||
| "actions";
|
||||
|
||||
@customElement("demo-components-ha-dialog")
|
||||
export class DemoHaDialog extends LitElement {
|
||||
@customElement("demo-components-ha-wa-dialog")
|
||||
export class DemoHaWaDialog extends LitElement {
|
||||
@state() private _openDialog: DialogType = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Dialog <code><ha-dialog></code></h1>
|
||||
<h1>Dialog <code><ha-wa-dialog></code></h1>
|
||||
|
||||
<p class="subtitle">Dialog component built with WebAwesome.</p>
|
||||
|
||||
@@ -53,24 +53,24 @@ export class DemoHaDialog extends LitElement {
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic"}
|
||||
header-title="Basic dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-below"}
|
||||
header-title="Basic dialog with subtitle"
|
||||
header-subtitle="This is a basic dialog with a subtitle below"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-above"}
|
||||
header-title="Dialog with subtitle above"
|
||||
header-subtitle="This is a basic dialog with a subtitle above"
|
||||
@@ -78,9 +78,9 @@ export class DemoHaDialog extends LitElement {
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "form"}
|
||||
header-title="Dialog with form"
|
||||
header-subtitle="This is a dialog with a form and a footer"
|
||||
@@ -91,18 +91,17 @@ export class DemoHaDialog extends LitElement {
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
Cancel
|
||||
</ha-button>
|
||||
<ha-button data-dialog="close" slot="primaryAction">
|
||||
Submit
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "actions"}
|
||||
header-title="Dialog with actions"
|
||||
header-subtitle="This is a dialog with header actions"
|
||||
@@ -114,7 +113,7 @@ export class DemoHaDialog extends LitElement {
|
||||
</div>
|
||||
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<h2>Design</h2>
|
||||
|
||||
@@ -229,19 +228,19 @@ export class DemoHaDialog extends LitElement {
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
<th>Appearance to use</th>
|
||||
<th>Variant to use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>secondaryAction</code></td>
|
||||
<td>The secondary action button(s).</td>
|
||||
<td><code>appearance="plain"</code></td>
|
||||
<td><code>plain</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>primaryAction</code></td>
|
||||
<td>The primary action button(s).</td>
|
||||
<td>Default (no appearance attribute)</td>
|
||||
<td><code>accent</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -250,7 +249,7 @@ export class DemoHaDialog extends LitElement {
|
||||
|
||||
<h3>Example Usage</h3>
|
||||
|
||||
<pre><code><ha-dialog
|
||||
<pre><code><ha-wa-dialog
|
||||
open
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
@@ -262,18 +261,12 @@ export class DemoHaDialog extends LitElement {
|
||||
</div>
|
||||
<div>Dialog content</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
<ha-button data-dialog="close" slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
Cancel
|
||||
</ha-button>
|
||||
<ha-button data-dialog="close" slot="primaryAction">
|
||||
Submit
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" variant="accent">Submit</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog></code></pre>
|
||||
</ha-wa-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
@@ -521,6 +514,6 @@ export class DemoHaDialog extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-dialog": DemoHaDialog;
|
||||
"demo-components-ha-wa-dialog": DemoHaWaDialog;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
makeDialogManager(this);
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
|
||||
if (window.innerWidth > 450) {
|
||||
import("../../src/resources/particles");
|
||||
@@ -222,9 +222,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
ha-language-picker {
|
||||
min-width: 200px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -68,6 +68,7 @@
|
||||
"@material/mwc-fab": "0.27.0",
|
||||
"@material/mwc-floating-label": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-icon-button": "0.27.0",
|
||||
"@material/mwc-linear-progress": "0.27.0",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
@@ -149,7 +150,6 @@
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@html-eslint/eslint-plugin": "0.55.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
@@ -210,7 +210,7 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.8",
|
||||
"tar": "7.5.7",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
43
script/benchmark_minifiers
Executable file
43
script/benchmark_minifiers
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT_ROOT="$ROOT_DIR/hass_frontend"
|
||||
OUT_LATEST="$OUT_ROOT/frontend_latest"
|
||||
OUT_ES5="$OUT_ROOT/frontend_es5"
|
||||
|
||||
bytes_dir() {
|
||||
if [ -d "$1" ]; then
|
||||
du -sb "$1" | cut -f1
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
run_build() {
|
||||
minifier="$1"
|
||||
printf "\n==> Building with %s\n" "$minifier"
|
||||
start_time=$(date +%s)
|
||||
JS_MINIFIER="$minifier" "$ROOT_DIR/script/build_frontend"
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
latest_size=$(bytes_dir "$OUT_LATEST")
|
||||
es5_size=$(bytes_dir "$OUT_ES5")
|
||||
total_size=$(bytes_dir "$OUT_ROOT")
|
||||
|
||||
printf "%s|%s|%s|%s\n" "$minifier" "$duration" "$latest_size" "$es5_size" >> "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
printf " duration: %ss\n" "$duration"
|
||||
printf " frontend_latest: %s bytes\n" "$latest_size"
|
||||
printf " frontend_es5: %s bytes\n" "$es5_size"
|
||||
printf " hass_frontend: %s bytes\n" "$total_size"
|
||||
}
|
||||
|
||||
mkdir -p "$ROOT_DIR/temp"
|
||||
rm -f "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
|
||||
run_build swc
|
||||
run_build terser
|
||||
|
||||
printf "\n==> Summary (minifier | seconds | latest bytes | es5 bytes)\n"
|
||||
cat "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
@@ -31,7 +31,7 @@ export class HaAuthFormString extends HaFormString {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -38,18 +38,6 @@ export const getEntityContext = (
|
||||
return getEntityEntryContext(entry, entities, devices, areas, floors);
|
||||
};
|
||||
|
||||
export const getEntityAreaId = (
|
||||
entityId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[entityId];
|
||||
if (!entry) return undefined;
|
||||
const deviceId = entry.device_id;
|
||||
const device = deviceId ? devices[deviceId] : undefined;
|
||||
return entry.area_id || device?.area_id || undefined;
|
||||
};
|
||||
|
||||
export const getEntityEntryContext = (
|
||||
entry:
|
||||
| EntityRegistryDisplayEntry
|
||||
|
||||
@@ -18,11 +18,9 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -30,6 +28,8 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
|
||||
@@ -1115,13 +1115,13 @@ export class HaChartBase extends LitElement {
|
||||
.chart-controls ::slotted(ha-icon-button) {
|
||||
background: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-icon-button-size: 32px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
.chart-controls.small ha-icon-button,
|
||||
.chart-controls.small ::slotted(ha-icon-button) {
|
||||
--ha-icon-button-size: 22px;
|
||||
--mdc-icon-button-size: 22px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.chart-controls ha-icon-button.inactive,
|
||||
|
||||
@@ -306,10 +306,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
visualMap: this._visualMap,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -255,10 +255,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
right: rtl ? labelWidth : 1,
|
||||
},
|
||||
tooltip: {
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -335,10 +335,7 @@ export class StatisticsChart extends LitElement {
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import { stateColorProperties } from "../../common/entity/state_color";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { computeCssValue } from "../../resources/css-variables";
|
||||
|
||||
@@ -33,22 +32,6 @@ function computeTimelineStateColor(
|
||||
return computeCssValue("--history-unknown-color", computedStyles);
|
||||
}
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
// Zone states for person/device_tracker don't have specific CSS color variables,
|
||||
// so they all fall back to the same --state-person-active-color.
|
||||
// Only use a custom CSS variable if explicitly defined (e.g. --state-person-kitchen-color),
|
||||
// otherwise return undefined to get unique colors from the generic color handler.
|
||||
if (
|
||||
(domain === "person" || domain === "device_tracker") &&
|
||||
!((FIXED_DOMAIN_STATES[domain] || []) as readonly string[]).includes(state)
|
||||
) {
|
||||
return computeCssValue(
|
||||
`--state-${domain}-${slugify(state, "_")}-color`,
|
||||
computedStyles
|
||||
);
|
||||
}
|
||||
|
||||
const properties = stateColorProperties(stateObj, state);
|
||||
|
||||
if (!properties) {
|
||||
@@ -58,6 +41,8 @@ function computeTimelineStateColor(
|
||||
const rgb = computeCssValue(properties, computedStyles);
|
||||
|
||||
if (!rgb) return undefined;
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
|
||||
if (!shade) {
|
||||
return rgb;
|
||||
|
||||
@@ -9,13 +9,10 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-icon-button";
|
||||
import { createCloseHeading } from "../ha-dialog";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-sortable";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-dialog";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableColumnData,
|
||||
@@ -32,20 +29,13 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
@state() private _hiddenColumns?: string[];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: DataTableSettingsDialogParams) {
|
||||
this._params = params;
|
||||
this._columnOrder = params.columnOrder;
|
||||
this._hiddenColumns = params.hiddenColumns;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -102,10 +92,12 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${localize("ui.components.data-table.settings.header")}
|
||||
@closed=${this._dialogClosed}
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
localize("ui.components.data-table.settings.header")
|
||||
)}
|
||||
>
|
||||
<ha-sortable
|
||||
@item-moved=${this._columnMoved}
|
||||
@@ -160,17 +152,15 @@ export class DialogDataTableSettings extends LitElement {
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-sortable>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._reset}
|
||||
>${localize("ui.components.data-table.settings.restore")}</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${localize("ui.components.data-table.settings.done")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._reset}
|
||||
>${localize("ui.components.data-table.settings.restore")}</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${localize("ui.components.data-table.settings.done")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -292,9 +282,21 @@ export class DialogDataTableSettings extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
--dialog-z-index: 10;
|
||||
--dialog-content-padding: 0 8px;
|
||||
}
|
||||
@media all and (max-width: 451px) {
|
||||
ha-dialog {
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: 250px;
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-4xl)
|
||||
var(--ha-border-radius-4xl) var(--ha-border-radius-square)
|
||||
var(--ha-border-radius-square);
|
||||
--mdc-dialog-min-height: calc(100% - 250px);
|
||||
--mdc-dialog-max-height: calc(100% - 250px);
|
||||
}
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 12px;
|
||||
overflow: visible;
|
||||
|
||||
@@ -13,7 +13,6 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
@@ -21,6 +20,7 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -1087,12 +1087,9 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row.empty-row {
|
||||
height: max(
|
||||
var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
),
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
height: var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
@@ -215,4 +217,10 @@ export abstract class HaDeviceAutomationPicker<
|
||||
delete value.metadata;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@@ -95,19 +94,12 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const items = this._getItems();
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
@@ -164,7 +164,7 @@ export class HaEntityToggle extends LitElement {
|
||||
min-width: 38px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiChartLine, mdiHelpCircleOutline, mdiShape } from "@mdi/js";
|
||||
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
@@ -163,7 +163,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
icon_path: mdiHelpCircleOutline,
|
||||
icon_path: mdiHelpCircle,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-dialog";
|
||||
import type { DialogWidth } from "./ha-dialog";
|
||||
import "./ha-wa-dialog";
|
||||
import type { DialogWidth } from "./ha-wa-dialog";
|
||||
|
||||
type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
|
||||
@@ -18,11 +18,10 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A responsive dialog component that automatically switches between a full dialog (ha-dialog)
|
||||
* A responsive dialog component that automatically switches between a full dialog (ha-wa-dialog)
|
||||
* and a bottom sheet (ha-bottom-sheet) based on screen size. Uses dialog mode on larger screens
|
||||
* (>870px width and >500px height) and bottom sheet mode on smaller screens or mobile devices.
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
@@ -36,19 +35,15 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog/sheet open state.
|
||||
* @attr {("alert"|"standard")} type - Dialog type (dialog mode only). Defaults to "standard".
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog mode only). Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing by clicking the scrim/overlay.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} flexcontent - Makes the content body a flex container.
|
||||
* @attr {boolean} without-header - Hides the default header.
|
||||
* @attr {boolean} allow-mode-change - When set, the component can switch between dialog and bottom-sheet modes as the viewport changes.
|
||||
* @attr {boolean} block-mode-change - When set, the mode is determined at mount time based on the current screen size, but subsequent mode changes are blocked. Useful for preventing forms from resetting when the viewport size changes.
|
||||
*
|
||||
* @event opened - Fired when the dialog/sheet is shown.
|
||||
* @event opened - Fired when the dialog/sheet is shown (dialog mode only).
|
||||
* @event closed - Fired after the dialog/sheet is hidden.
|
||||
* @event after-show - Fired after show animation completes.
|
||||
* @event after-show - Fired after show animation completes (dialog mode only).
|
||||
*
|
||||
* @remarks
|
||||
* **Responsive Behavior:**
|
||||
@@ -56,9 +51,9 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
* Dialog mode is used for screens wider than 870px and taller than 500px.
|
||||
* Bottom sheet mode is used for mobile devices and smaller screens.
|
||||
*
|
||||
* By default, the mode is determined once at mount time and is then kept stable to avoid state
|
||||
* loss (like form resets) during viewport changes. Set `allow-mode-change` to opt into live
|
||||
* mode switching while the dialog is open.
|
||||
* When `block-mode-change` is set, the mode is determined once at mount time based on the initial
|
||||
* screen size. Subsequent viewport size changes will not trigger mode switches, which is useful
|
||||
* for preventing form resets or other state loss when users resize their browser window.
|
||||
*
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when opened, add the `autofocus` attribute to it.
|
||||
@@ -78,15 +73,9 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: "alert" | "standard" = "standard";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
|
||||
@@ -96,15 +85,12 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-mode-change" })
|
||||
public allowModeChange = false;
|
||||
@property({ type: Boolean, attribute: "block-mode-change" })
|
||||
public blockModeChange = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@state() private _mode: DialogSheetMode = "dialog";
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
@@ -116,7 +102,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
this._unsubMediaQuery = listenMediaQuery(
|
||||
"(max-width: 870px), (max-height: 500px)",
|
||||
(matches) => {
|
||||
if (!this._modeSet || this.allowModeChange) {
|
||||
if (!this._modeSet || !this.blockModeChange) {
|
||||
this._mode = matches ? "bottom-sheet" : "dialog";
|
||||
this._modeSet = true;
|
||||
}
|
||||
@@ -134,50 +120,33 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
render() {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet
|
||||
.ariaLabelledBy=${this.ariaLabelledBy ||
|
||||
(this.headerTitle !== undefined ? "ha-dialog-title" : undefined)}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.flexContent=${this.flexContent}
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
>
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
${!this.withoutHeader
|
||||
? html`
|
||||
<slot name="header" slot="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ??
|
||||
"Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle"
|
||||
>${this.headerSubtitle}</span
|
||||
>`
|
||||
: html`<slot
|
||||
name="headerSubtitle"
|
||||
slot="subtitle"
|
||||
></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
? html`<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
`
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
@@ -186,18 +155,16 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.type=${this.type}
|
||||
.width=${this.width}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
.ariaLabelledBy=${this.ariaLabelledBy}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.headerTitle=${this.headerTitle}
|
||||
.headerSubtitle=${this.headerSubtitle}
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
.flexContent=${this.flexContent}
|
||||
flexcontent
|
||||
.withoutHeader=${this.withoutHeader}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
@@ -212,7 +179,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -220,17 +187,10 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
return [
|
||||
css`
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
--ha-bottom-sheet-surface-background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--ha-bottom-sheet-padding: 0 var(--safe-area-inset-right)
|
||||
var(--safe-area-inset-bottom) var(--safe-area-inset-left);
|
||||
--ha-bottom-sheet-content-padding: var(
|
||||
--dialog-content-padding,
|
||||
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -135,7 +135,7 @@ class HaAlert extends LitElement {
|
||||
}
|
||||
.action ha-icon-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
.issue-type.info > .icon {
|
||||
color: var(--info-color);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-switch";
|
||||
import "./ha-tooltip";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
@@ -33,80 +33,105 @@ export class HaAnalytics extends LitElement {
|
||||
const baseEnabled = !loading && this.analytics!.preferences.base;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="base">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="base">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.description`
|
||||
)}</span
|
||||
>
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!baseEnabled}
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) => html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for=${preference}>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for=${preference}>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.description`
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.id="switch-${preference}"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
></ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip .for="switch-${preference}" placement="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</ha-md-list-item>
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<ha-switch
|
||||
.id="switch-${preference}"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip
|
||||
.for="switch-${preference}"
|
||||
placement="right"
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)}
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="diagnostics">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="diagnostics">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.description`
|
||||
)}</span
|
||||
>
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
this.shadowRoot!.querySelectorAll("*[data-for]").forEach((el) => {
|
||||
const forEl = (el as HTMLElement).dataset.for;
|
||||
delete (el as HTMLElement).dataset.for;
|
||||
|
||||
el.addEventListener("click", () => {
|
||||
const toFocus = this.shadowRoot!.querySelector(
|
||||
`*[name=${forEl}]`
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (toFocus) {
|
||||
toFocus.focus();
|
||||
toFocus.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClick(ev: Event) {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const preference = (target as any).preference;
|
||||
@@ -139,10 +164,13 @@ export class HaAnalytics extends LitElement {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
span[slot="description"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
runAssistPipeline,
|
||||
@@ -115,7 +114,7 @@ export class HaAssistChat extends LitElement {
|
||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||
|
||||
return html`
|
||||
<div class="messages ha-scrollbar">
|
||||
<div class="messages">
|
||||
${controlHA
|
||||
? nothing
|
||||
: html`
|
||||
@@ -586,167 +585,154 @@ export class HaAssistChat extends LitElement {
|
||||
return progress;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-alert {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.messages {
|
||||
flex: 1 1 400px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-4);
|
||||
}
|
||||
.input {
|
||||
padding: var(--ha-space-1) var(--ha-space-4) var(--ha-space-6);
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: var(--ha-space-6);
|
||||
margin: var(--ha-space-2) 0;
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
.message.user {
|
||||
margin-left: var(--ha-space-6);
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(
|
||||
--chat-background-color-user,
|
||||
var(--primary-color)
|
||||
);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.hass {
|
||||
margin-right: var(--ha-space-6);
|
||||
margin-inline-end: var(--ha-space-6);
|
||||
margin-inline-start: initial;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(
|
||||
--chat-background-color-hass,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
static styles = css`
|
||||
:host {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-alert {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.messages {
|
||||
flex: 1;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 12px 16px;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: 24px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(
|
||||
--chat-background-color-hass,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
}
|
||||
.double-bounce1,
|
||||
.double-bounce2 {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||
animation: sk-bounce 2s infinite ease-in-out;
|
||||
}
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
}
|
||||
.double-bounce1,
|
||||
.double-bounce2 {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||
animation: sk-bounce 2s infinite ease-in-out;
|
||||
}
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.listening-icon {
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
.listening-icon {
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
.listening-icon[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.listening-icon[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
--mdc-icon-size: 16px;
|
||||
right: 5px;
|
||||
inset-inline-end: 5px;
|
||||
inset-inline-start: initial;
|
||||
top: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
--mdc-icon-size: 16px;
|
||||
right: 5px;
|
||||
inset-inline-end: 5px;
|
||||
inset-inline-start: initial;
|
||||
top: 0px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-select";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -368,7 +368,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
}
|
||||
ha-icon-button {
|
||||
position: relative;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -1,52 +1,21 @@
|
||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import type WaDrawer from "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
|
||||
const SWIPE_LOCKED_COMPONENTS = new Set([
|
||||
"ha-control-slider",
|
||||
"ha-slider",
|
||||
"ha-control-switch",
|
||||
"ha-control-circular-slider",
|
||||
"ha-hs-color-picker",
|
||||
"ha-map",
|
||||
"ha-more-info-control-select-container",
|
||||
"ha-filter-chip",
|
||||
]);
|
||||
|
||||
const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
|
||||
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean }) public open = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
|
||||
@state() private _sliderInteractionActive = false;
|
||||
|
||||
@query("#drawer") private _drawer!: HTMLElement;
|
||||
|
||||
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||
@@ -59,124 +28,14 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _isDragging = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
private _handleShow = async () => {
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass.auth.external)) {
|
||||
const element = this.renderRoot.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-bottom-sheet-autofocus";
|
||||
}
|
||||
this.hass.auth.external?.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(
|
||||
this.renderRoot.querySelector("[autofocus]") as HTMLElement | null
|
||||
)?.focus();
|
||||
private _handleAfterHide(afterHideEvent: Event) {
|
||||
afterHideEvent.stopPropagation();
|
||||
this.open = false;
|
||||
const ev = new Event("closed", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleSliderInteractionStart = () => {
|
||||
this._sliderInteractionActive = true;
|
||||
};
|
||||
|
||||
private _handleSliderInteractionStop = () => {
|
||||
this._sliderInteractionActive = false;
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (this._sliderInteractionActive) {
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this.open = false;
|
||||
this._drawerOpen = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
private _handleHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
// Ignore bubbled wa-hide events from nested drawers (e.g., picker bottom sheet)
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceIsDrawer = ev.detail.source === (ev.target as WaDrawer).drawer;
|
||||
|
||||
if (this._sliderInteractionActive) {
|
||||
ev.preventDefault();
|
||||
this._drawerOpen = true;
|
||||
this.open = true;
|
||||
this._escapePressed = false;
|
||||
return;
|
||||
}
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDrawer) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
};
|
||||
|
||||
private _handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as WaDrawer).open = false;
|
||||
}
|
||||
};
|
||||
|
||||
private _handleCloseAction = (ev: Event) => {
|
||||
const shouldClose = ev
|
||||
.composedPath()
|
||||
.some(
|
||||
(node) =>
|
||||
node instanceof HTMLElement &&
|
||||
(node.getAttribute("data-dialog") === "close" ||
|
||||
node.getAttribute("data-drawer") === "close")
|
||||
);
|
||||
|
||||
if (shouldClose) {
|
||||
this._drawerOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
"slider-interaction-start",
|
||||
this._handleSliderInteractionStart,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"slider-interaction-stop",
|
||||
this._handleSliderInteractionStop,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
@@ -192,21 +51,10 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
id="drawer"
|
||||
placement="bottom"
|
||||
.open=${this._drawerOpen}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
.ariaLabelledby=${this.ariaLabelledBy}
|
||||
.ariaDescribedby=${this.ariaDescribedBy}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
@click=${this._handleCloseAction}
|
||||
without-header
|
||||
@touchstart=${this._handleTouchStart}
|
||||
>
|
||||
<div class="handle-wrapper" aria-hidden="true">
|
||||
<div class="handle"></div>
|
||||
</div>
|
||||
<slot name="header"></slot>
|
||||
<div class="content-wrapper">
|
||||
<div id="body" class="body ha-scrollbar">
|
||||
@@ -220,37 +68,17 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
if (this.preventScrimClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = ev.composedPath();
|
||||
|
||||
for (const target of path) {
|
||||
if (target === this._drawer) {
|
||||
// Check if any element inside drawer in the composed path has scrollTop > 0
|
||||
for (const path of ev.composedPath()) {
|
||||
const el = path as HTMLElement;
|
||||
if (el === this._drawer) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// Check if any element inside drawer in the composed path has scrollTop > 0 (list)
|
||||
target.scrollTop > 0 ||
|
||||
// Check if the element is a swipe locked component or has a swipe locked class
|
||||
SWIPE_LOCKED_COMPONENTS.has(target.localName) ||
|
||||
Array.from(target.classList).some((cls) =>
|
||||
SWIPE_LOCKED_CLASSES.has(cls)
|
||||
)
|
||||
) {
|
||||
if (el.scrollTop > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop propagation so parent bottom sheets don't also start tracking
|
||||
// this gesture (same pattern as _handleKeyDown for Escape)
|
||||
ev.stopPropagation();
|
||||
this._startResizing(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
@@ -346,20 +174,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(
|
||||
"slider-interaction-start",
|
||||
this._handleSliderInteractionStart,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this.removeEventListener(
|
||||
"slider-interaction-stop",
|
||||
this._handleSliderInteractionStop,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
);
|
||||
this._unregisterResizeHandlers();
|
||||
this._isDragging = false;
|
||||
}
|
||||
@@ -385,7 +199,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
wa-drawer::part(body) {
|
||||
max-width: var(--ha-bottom-sheet-max-width);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
border-top-left-radius: var(
|
||||
--ha-bottom-sheet-border-radius,
|
||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||
@@ -408,35 +221,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host([prevent-scrim-close]) .handle-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 16px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
height: 4px;
|
||||
background: var(--ha-bottom-sheet-handle-color, var(--divider-color));
|
||||
width: 40px;
|
||||
}
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -444,24 +228,16 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.body {
|
||||
padding: var(--ha-bottom-sheet-content-padding, 0);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(
|
||||
--ha-bottom-sheet-content-padding,
|
||||
var(
|
||||
--ha-bottom-sheet-padding,
|
||||
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
|
||||
var(--safe-area-inset-left)
|
||||
)
|
||||
--ha-bottom-sheet-padding,
|
||||
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
|
||||
var(--safe-area-inset-left)
|
||||
);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
slot[name="footer"] {
|
||||
display: block;
|
||||
@@ -486,18 +262,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"slider-interaction-start": undefined;
|
||||
"slider-interaction-stop": undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"slider-interaction-start": HASSDomEvent<
|
||||
HASSDomEvents["slider-interaction-start"]
|
||||
>;
|
||||
"slider-interaction-stop": HASSDomEvent<
|
||||
HASSDomEvents["slider-interaction-stop"]
|
||||
>;
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class HaButton extends Button {
|
||||
Button.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-padding-inline: var(--ha-space-4);
|
||||
--wa-form-control-padding-inline: 16px;
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-form-control-border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
@@ -68,7 +68,7 @@ export class HaButton extends Button {
|
||||
var(--button-height, 32px)
|
||||
);
|
||||
font-size: var(--wa-font-size-s, var(--ha-font-size-m));
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
--wa-form-control-padding-inline: 12px;
|
||||
}
|
||||
|
||||
:host([variant="brand"]) {
|
||||
@@ -212,17 +212,17 @@ export class HaButton extends Button {
|
||||
}
|
||||
|
||||
slot[name="start"]::slotted(*) {
|
||||
margin-inline-end: var(--ha-space-1);
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
slot[name="end"]::slotted(*) {
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.button.has-start {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
padding-inline-start: 8px;
|
||||
}
|
||||
.button.has-end {
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
padding-inline-end: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { STATE_RUNNING } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -59,22 +58,12 @@ export class HaCameraStream extends LitElement {
|
||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
const entityChanged =
|
||||
if (
|
||||
changedProps.has("stateObj") &&
|
||||
this.stateObj &&
|
||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||
this.stateObj.entity_id;
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const backendStarted =
|
||||
changedProps.has("hass") &&
|
||||
this.hass &&
|
||||
this.stateObj &&
|
||||
oldHass &&
|
||||
this.hass.config.state === STATE_RUNNING &&
|
||||
oldHass.config?.state !== STATE_RUNNING;
|
||||
|
||||
if (entityChanged || backendStarted) {
|
||||
this.stateObj.entity_id
|
||||
) {
|
||||
this._getCapabilities();
|
||||
this._getPosterUrl();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
mdiUndo,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, ReactiveElement, render } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -28,7 +28,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { getEntityContext } from "../common/entity/context/get_entity_context";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-code-editor-completion-items";
|
||||
@@ -311,11 +310,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
});
|
||||
this._canCopy = this._value?.length > 0;
|
||||
|
||||
const cmScroller = this.codemirror.dom.querySelector(".cm-scroller");
|
||||
if (cmScroller) {
|
||||
cmScroller.classList.add("ha-scrollbar");
|
||||
}
|
||||
|
||||
// Update the toolbar. Creating it if required
|
||||
this._updateToolbar();
|
||||
}
|
||||
@@ -808,105 +802,100 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return [];
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--code-editor-toolbar-height: 28px;
|
||||
}
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--code-editor-toolbar-height: 28px;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
}
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
}
|
||||
:host(.hasToolbar) .cm-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-content {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
:host(.error-state) .cm-content {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-content {
|
||||
border: none;
|
||||
border-top: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
:host(.hasToolbar) .cm-content {
|
||||
border: none;
|
||||
border-top: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-focused .cm-content {
|
||||
border-top: 2px solid var(--primary-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
:host(.hasToolbar) .cm-focused .cm-content {
|
||||
border-top: 2px solid var(--primary-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: calc(var(--header-height, 56px) + 8px) !important;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 6;
|
||||
border-radius: var(--ha-border-radius-lg) !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
background-color: var(
|
||||
--code-editor-background-color,
|
||||
var(--card-background-color)
|
||||
) !important;
|
||||
margin: 0 !important;
|
||||
padding-top: var(--safe-area-inset-top) !important;
|
||||
padding-left: var(--safe-area-inset-left) !important;
|
||||
padding-right: var(--safe-area-inset-right) !important;
|
||||
padding-bottom: var(--safe-area-inset-bottom) !important;
|
||||
box-sizing: border-box !important;
|
||||
display: block !important;
|
||||
}
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: calc(var(--header-height, 56px) + 8px) !important;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 6;
|
||||
border-radius: var(--ha-border-radius-lg) !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
background-color: var(
|
||||
--code-editor-background-color,
|
||||
var(--card-background-color)
|
||||
) !important;
|
||||
margin: 0 !important;
|
||||
padding-top: var(--safe-area-inset-top) !important;
|
||||
padding-left: var(--safe-area-inset-left) !important;
|
||||
padding-right: var(--safe-area-inset-right) !important;
|
||||
padding-bottom: var(--safe-area-inset-bottom) !important;
|
||||
box-sizing: border-box !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: var(--ha-border-radius-square) !important;
|
||||
}
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: var(--ha-border-radius-square) !important;
|
||||
}
|
||||
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.code-editor-toolbar {
|
||||
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
|
||||
--icon-button-toolbar-color: var(
|
||||
--code-editor-gutter-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.code-editor-toolbar {
|
||||
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
|
||||
--icon-button-toolbar-color: var(
|
||||
--code-editor-gutter-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { localizeContext } from "../data/context";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
@@ -15,6 +13,8 @@ import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
@@ -34,15 +34,12 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize?: HomeAssistant["localize"];
|
||||
|
||||
render() {
|
||||
const effectiveValue = this.value ?? this.defaultColor ?? "";
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.hideClearIcon=${!this.value && !!this.defaultColor}
|
||||
@@ -53,7 +50,7 @@ export class HaColorPicker extends LitElement {
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
.notFoundLabel=${this.localize?.(
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.color-picker.no_colors_found"
|
||||
)}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
@@ -81,9 +78,7 @@ export class HaColorPicker extends LitElement {
|
||||
return [
|
||||
{
|
||||
id: searchString,
|
||||
primary:
|
||||
this.localize?.("ui.components.color-picker.custom_color") ||
|
||||
"Custom color",
|
||||
primary: this.hass.localize("ui.components.color-picker.custom_color"),
|
||||
secondary: searchString,
|
||||
},
|
||||
];
|
||||
@@ -106,15 +101,16 @@ export class HaColorPicker extends LitElement {
|
||||
): PickerComboBoxItem[] => {
|
||||
const items: PickerComboBoxItem[] = [];
|
||||
|
||||
const defaultSuffix =
|
||||
this.localize?.("ui.components.color-picker.default") || "Default";
|
||||
const defaultSuffix = this.hass.localize(
|
||||
"ui.components.color-picker.default"
|
||||
);
|
||||
|
||||
const addDefaultSuffix = (label: string, isDefault: boolean) =>
|
||||
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
|
||||
|
||||
if (includeNone) {
|
||||
const noneLabel =
|
||||
this.localize?.("ui.components.color-picker.none") || "None";
|
||||
this.hass.localize("ui.components.color-picker.none") || "None";
|
||||
items.push({
|
||||
id: "none",
|
||||
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
|
||||
@@ -124,7 +120,7 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
if (includeState) {
|
||||
const stateLabel =
|
||||
this.localize?.("ui.components.color-picker.state") || "State";
|
||||
this.hass.localize("ui.components.color-picker.state") || "State";
|
||||
items.push({
|
||||
id: "state",
|
||||
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
|
||||
@@ -134,7 +130,7 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
Array.from(THEME_COLORS).forEach((color) => {
|
||||
const themeLabel =
|
||||
this.localize?.(
|
||||
this.hass.localize(
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
) || color;
|
||||
items.push({
|
||||
@@ -188,7 +184,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.localize?.("ui.components.color-picker.none") || "None"}
|
||||
${this.hass.localize("ui.components.color-picker.none")}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -196,7 +192,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.localize?.("ui.components.color-picker.state") || "State"}
|
||||
${this.hass.localize("ui.components.color-picker.state")}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -204,7 +200,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<span slot="start">${this._renderColorCircle(value)}</span>
|
||||
<span slot="headline">
|
||||
${this.localize?.(
|
||||
${this.hass.localize(
|
||||
`ui.components.color-picker.colors.${value}` as LocalizeKeys
|
||||
) || value}
|
||||
</span>
|
||||
|
||||
@@ -95,7 +95,7 @@ export class HaCopyTextfield extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { HomeAssistant } from "../types";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-button";
|
||||
import "./ha-dialog-footer";
|
||||
import "./ha-dialog";
|
||||
import "./ha-wa-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
export class HaDialogDatePicker extends LitElement {
|
||||
@@ -49,7 +49,7 @@ export class HaDialogDatePicker extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog
|
||||
return html`<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="small"
|
||||
@@ -97,7 +97,7 @@ export class HaDialogDatePicker extends LitElement {
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>`;
|
||||
</ha-wa-dialog>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -127,7 +127,7 @@ export class HaDialogDatePicker extends LitElement {
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.bottom-actions {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { customElement } from "lit/decorators";
|
||||
*
|
||||
* @summary
|
||||
* A simple footer container for dialog actions,
|
||||
* typically used as the `footer` slot in `ha-dialog`.
|
||||
* typically used as the `footer` slot in `ha-wa-dialog`.
|
||||
*
|
||||
* @slot primaryAction - Primary action button(s), aligned to the end.
|
||||
* @slot secondaryAction - Secondary action button(s), placed before the primary action.
|
||||
|
||||
@@ -1,458 +1,193 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base";
|
||||
import { styles } from "@material/mwc-dialog/mwc-dialog.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { authContext, localizeContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||
|
||||
type DialogHideEvent = CustomEvent<{ source?: Element }>;
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant | undefined,
|
||||
title: string | TemplateResult
|
||||
) => html`
|
||||
<div class="header_title">
|
||||
<ha-icon-button
|
||||
.label=${hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
></ha-icon-button>
|
||||
<span>${title}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Home Assistant dialog component
|
||||
*
|
||||
* @element ha-dialog
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A stylable dialog built using the `wa-dialog` component, providing a standardized header (ha-dialog-header),
|
||||
* body, and footer (preferably using `ha-dialog-footer`) with slots
|
||||
*
|
||||
* You can open and close the dialog declaratively by using the `data-dialog="close"` attribute.
|
||||
* @see https://webawesome.com/docs/components/dialog/#opening-and-closing-dialogs-declaratively
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog content body.
|
||||
* @slot footer - Dialog footer content.
|
||||
*
|
||||
* @csspart dialog - The dialog surface.
|
||||
* @csspart header - The header container.
|
||||
* @csspart body - The scrollable body container.
|
||||
* @csspart footer - The footer container.
|
||||
*
|
||||
* @cssprop --dialog-content-padding - Padding for the dialog content sections.
|
||||
* @cssprop --ha-dialog-show-duration - Show animation duration.
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
|
||||
*
|
||||
* @event opened - Fired when the dialog is shown.
|
||||
* @event closed - Fired after the dialog is hidden.
|
||||
*
|
||||
* @remarks
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when the dialog opens, add the `autofocus` attribute to it.
|
||||
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
|
||||
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/27143
|
||||
*/
|
||||
@customElement("ha-dialog")
|
||||
export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
export class HaDialog extends DialogBase {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: "alert" | "standard" = "standard";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
|
||||
@property({ attribute: "header-subtitle" })
|
||||
public headerSubtitle?: string;
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@query(".body") public bodyContainer!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize?: HomeAssistant["localize"];
|
||||
|
||||
@state()
|
||||
@consume({ context: authContext, subscribe: true })
|
||||
private auth?: ContextType<typeof authContext>;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
public scrollToPos(x: number, y: number) {
|
||||
this.contentElement?.scrollTo(x, y);
|
||||
}
|
||||
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("open")) {
|
||||
this._open = this.open;
|
||||
}
|
||||
protected renderHeading() {
|
||||
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-dialog
|
||||
.open=${this._open}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
without-header
|
||||
aria-labelledby=${ifDefined(
|
||||
this.ariaLabelledBy ||
|
||||
(this.headerTitle !== undefined ? "ha-dialog-title" : undefined)
|
||||
)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
${!this.withoutHeader
|
||||
? html` <slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.localize?.("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>`
|
||||
: nothing}
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleShow = async () => {
|
||||
this._open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.auth?.external && isIosApp(this.auth.external)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-dialog-autofocus";
|
||||
}
|
||||
this.auth.external.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
protected firstUpdated(): void {
|
||||
super.firstUpdated();
|
||||
this.suppressDefaultPressSelector = [
|
||||
this.suppressDefaultPressSelector,
|
||||
SUPPRESS_DEFAULT_PRESS_SELECTOR,
|
||||
].join(", ");
|
||||
this._updateScrolledAttribute();
|
||||
this.contentElement?.addEventListener("scroll", this._onScroll, {
|
||||
passive: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: DialogHideEvent) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._open = false;
|
||||
this.contentElement.removeEventListener("scroll", this._onScroll);
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleBodyScroll(ev: Event) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
private _onScroll = () => {
|
||||
this._updateScrolledAttribute();
|
||||
};
|
||||
|
||||
private _updateScrolledAttribute() {
|
||||
if (!this.contentElement) return;
|
||||
this.toggleAttribute("scrolled", this.contentElement.scrollTop !== 0);
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as WaDialog).open = false;
|
||||
}
|
||||
}
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host([scrolled]) ::slotted(ha-dialog-header) {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(
|
||||
--dialog-scroll-divider-color,
|
||||
var(--divider-color)
|
||||
);
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__scrim {
|
||||
background-color: var(--mdc-dialog-scrim-color, none);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(2) {
|
||||
flex: var(--primary-action-button-flex, unset);
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
padding: var(--dialog-container-padding, 0);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0 var(--ha-space-4);
|
||||
}
|
||||
.mdc-dialog__title:has(span) {
|
||||
padding: var(--ha-space-3) var(--ha-space-3) 0;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
content: unset;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__content {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
margin-top: var(--dialog-surface-margin-top);
|
||||
min-width: var(--mdc-dialog-min-width, auto);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
padding: var(--dialog-surface-padding, 0);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
private _handleHide(ev: DialogHideEvent) {
|
||||
const sourceIsDialog = ev.detail?.source === (ev.target as WaDialog).dialog;
|
||||
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDialog) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
wa-dialog {
|
||||
--full-width: var(
|
||||
--ha-dialog-width-full,
|
||||
min(95vw, var(--safe-width))
|
||||
);
|
||||
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
|
||||
--spacing: var(--dialog-content-padding, var(--ha-space-6));
|
||||
--show-duration: var(--ha-dialog-show-duration, 200ms);
|
||||
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
|
||||
--ha-dialog-surface-background: var(
|
||||
--card-background-color,
|
||||
var(--ha-color-surface-default)
|
||||
);
|
||||
--wa-color-surface-raised: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--wa-panel-border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
max-width: var(--ha-dialog-max-width, var(--safe-width));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-dialog {
|
||||
--show-duration: 0ms;
|
||||
--hide-duration: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
:host([width="small"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
color: var(--primary-text-color);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
--ha-dialog-max-height,
|
||||
calc(var(--safe-height) - var(--ha-space-20))
|
||||
);
|
||||
min-height: var(--ha-dialog-min-height);
|
||||
margin-top: var(--dialog-surface-margin-top, auto);
|
||||
/* Used to offset the dialog from the safe areas when space is limited */
|
||||
transform: translate(
|
||||
calc(
|
||||
var(--safe-area-offset-left, 0px) - var(
|
||||
--safe-area-offset-right,
|
||||
0px
|
||||
)
|
||||
),
|
||||
calc(
|
||||
var(--safe-area-offset-top, 0px) - var(
|
||||
--safe-area-offset-bottom,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
||||
font-size: var(
|
||||
--ha-dialog-header-title-font-size,
|
||||
var(--ha-font-size-2xl)
|
||||
);
|
||||
line-height: var(
|
||||
--ha-dialog-header-title-line-height,
|
||||
var(--ha-line-height-condensed)
|
||||
);
|
||||
font-weight: var(
|
||||
--ha-dialog-header-title-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--ha-space-3);
|
||||
}
|
||||
|
||||
wa-dialog::part(body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(
|
||||
--dialog-content-padding,
|
||||
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
|
||||
);
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
wa-dialog::part(footer) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
.header_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header_title span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding-left: var(--ha-space-1);
|
||||
padding-right: var(--ha-space-1);
|
||||
margin-right: var(--ha-space-3);
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header_button {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: calc(var(--ha-space-3) * -1);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dialog-actions {
|
||||
inset-inline-start: initial !important;
|
||||
inset-inline-end: 0 !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog": HaDialog;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
opened: undefined;
|
||||
"after-show": undefined;
|
||||
closed: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
50
src/components/ha-divider.ts
Normal file
50
src/components/ha-divider.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-divider")
|
||||
export class HaMdDivider extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
role=${ifDefined(this.label ? "separator" : undefined)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
>
|
||||
<span class="line"></span>
|
||||
${this.label
|
||||
? html`
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="line"></span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
width: var(--ha-divider-width, 100%);
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.label {
|
||||
padding: var(--ha-divider-label-padding, 0 16px);
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
background-color: var(--divider-color);
|
||||
height: var(--ha-divider-line-height, 1px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,6 @@ export class HaDropdownItem extends DropdownItem {
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
:host([selected]:hover) {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
@@ -29,25 +29,16 @@ export class HaDropdown extends Dropdown {
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | HaIconButton | undefined {
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | HaIconButton | undefined;
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(
|
||||
element: HTMLButtonElement | HaIconButton | undefined
|
||||
) {
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (this.popup.anchor && this.popup.anchor.localName === "ha-icon-button") {
|
||||
// @ts-ignore
|
||||
(this.popup.anchor as HaIconButton).selected = false;
|
||||
}
|
||||
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
@@ -55,7 +46,7 @@ export class HaDropdown extends Dropdown {
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | HaIconButton | null {
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
@@ -63,28 +54,6 @@ export class HaDropdown extends Dropdown {
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override async showMenu() {
|
||||
// @ts-ignore
|
||||
await super.showMenu();
|
||||
const triggerElement = this.getTrigger();
|
||||
if (triggerElement && triggerElement.localName === "ha-icon-button") {
|
||||
(triggerElement as HaIconButton).selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override async hideMenu() {
|
||||
const triggerElement = this.getTrigger();
|
||||
if (triggerElement && triggerElement.localName === "ha-icon-button") {
|
||||
(triggerElement as HaIconButton).selected = false;
|
||||
}
|
||||
// @ts-ignore
|
||||
await super.hideMenu();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -37,9 +37,6 @@ class HaDurationInput extends LitElement {
|
||||
@property({ attribute: "allow-negative", type: Boolean })
|
||||
public allowNegative = false;
|
||||
|
||||
@property({ attribute: "enable-second", type: Boolean })
|
||||
public enableSecond = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _toggleNegative = false;
|
||||
@@ -68,7 +65,7 @@ class HaDurationInput extends LitElement {
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
.enableSecond=${this.enableSecond}
|
||||
enable-second
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
@@ -165,9 +162,9 @@ class HaDurationInput extends LitElement {
|
||||
if (value) {
|
||||
value.hours ||= 0;
|
||||
value.minutes ||= 0;
|
||||
value.seconds ||= 0;
|
||||
|
||||
if ("days" in value) value.days ||= 0;
|
||||
if ("seconds" in value) value.seconds ||= 0;
|
||||
if ("milliseconds" in value) value.milliseconds ||= 0;
|
||||
|
||||
if (this.allowNegative) {
|
||||
@@ -186,11 +183,8 @@ class HaDurationInput extends LitElement {
|
||||
value.milliseconds %= 1000;
|
||||
}
|
||||
|
||||
if (!this.enableSecond && !value.seconds) {
|
||||
// @ts-ignore
|
||||
delete value.seconds;
|
||||
} else if (this.enableSecond && value.seconds > 59) {
|
||||
value.minutes = (value.minutes ?? 0) + Math.floor(value.seconds / 60);
|
||||
if (value.seconds > 59) {
|
||||
value.minutes += Math.floor(value.seconds / 60);
|
||||
value.seconds %= 60;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class HaFab extends FabBase {
|
||||
}
|
||||
:disabled {
|
||||
--mdc-theme-secondary: var(--disabled-text-color);
|
||||
cursor: not-allowed !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -317,7 +317,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
ha-button {
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
width: 100%;
|
||||
|
||||
@@ -26,12 +26,12 @@ import { showCategoryRegistryDetailDialog } from "../panels/config/category/show
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
|
||||
@customElement("ha-filter-categories")
|
||||
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
@@ -317,7 +317,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
|
||||
@@ -122,10 +122,7 @@ export class HaFilterDevices extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +110,7 @@ export class HaFilterDomains extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +101,7 @@ export class HaFilterEntities extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +99,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +145,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - (49 + 48 + 32 + 4)}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
// 48px - height of ha-list-item
|
||||
`${this.clientHeight - (49 + 48 + 32)}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +76,6 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow user to start typing a negative zero
|
||||
if (rawValue === "-0") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawValue !== "") {
|
||||
value = parseFloat(rawValue);
|
||||
if (isNaN(value)) {
|
||||
|
||||
@@ -3,10 +3,6 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
@@ -15,6 +11,10 @@ import type {
|
||||
HaFormStringData,
|
||||
HaFormStringSchema,
|
||||
} from "./types";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@@ -148,7 +148,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -53,7 +53,7 @@ export class HaFormfield extends FormfieldBase {
|
||||
:host(:not([alignEnd])) ::slotted(ha-switch) {
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-start: inline;
|
||||
}
|
||||
.mdc-form-field {
|
||||
align-items: var(--ha-formfield-align-items, center);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -14,9 +13,10 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { authContext } from "../data/context";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
@@ -33,6 +33,8 @@ import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-generic-picker")
|
||||
export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@@ -111,10 +113,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
@state()
|
||||
@consume({ context: authContext, subscribe: true })
|
||||
private auth?: ContextType<typeof authContext>;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
@@ -144,6 +142,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._setUnknownValue();
|
||||
return;
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
this._throttleUnknownValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +194,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.image=${this.image}
|
||||
.label=${label}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
.required=${this.required}
|
||||
@@ -250,6 +253,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
return html`
|
||||
<ha-picker-combo-box
|
||||
id="combo-box"
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.searchLabel}
|
||||
.value=${this.value}
|
||||
@@ -288,6 +292,13 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _throttleUnknownValue = throttle(
|
||||
this._setUnknownValue,
|
||||
1000,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
const showError = this.invalid && this.errorMessage;
|
||||
const showHelper = !showError && this.helper;
|
||||
@@ -311,8 +322,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
this._comboBox?.setFieldValue(this._initialFieldValue);
|
||||
this._initialFieldValue = undefined;
|
||||
}
|
||||
if (this.auth?.external && isIosApp(this.auth.external)) {
|
||||
this.auth.external.fireMessage({
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: "combo-box",
|
||||
@@ -423,10 +434,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
--wa-space-l: 0;
|
||||
}
|
||||
|
||||
wa-popover::part(dialog)::backdrop {
|
||||
background: none;
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: max(var(--body-width), 250px);
|
||||
max-width: var(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import "./ha-icon-button";
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@@ -245,7 +245,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
}
|
||||
.reset {
|
||||
grid-area: reset;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -25,7 +25,7 @@ export class HaHelpTooltip extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-svg-icon id="svg-icon" .path=${mdiHelpCircleOutline}></ha-svg-icon>
|
||||
<ha-svg-icon id="svg-icon" .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
<ha-tooltip for="svg-icon" .placement=${this.position}>
|
||||
${this.label}
|
||||
</ha-tooltip>
|
||||
|
||||
@@ -14,14 +14,6 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
|
||||
|
||||
@@ -31,10 +23,6 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,14 +14,6 @@ export class HaIconButtonNext extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight;
|
||||
|
||||
@@ -31,10 +23,6 @@ export class HaIconButtonNext extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,14 +14,6 @@ export class HaIconButtonPrev extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
|
||||
|
||||
@@ -31,10 +23,6 @@ export class HaIconButtonPrev extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaIconButton } from "./ha-icon-button";
|
||||
@@ -7,48 +6,41 @@ import { HaIconButton } from "./ha-icon-button";
|
||||
export class HaIconButtonToggle extends HaIconButton {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaIconButton.styles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-button::part(base) {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
ha-button::part(base)::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) ha-button::part(base)::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) ha-button::part(base) {
|
||||
color: var(--primary-background-color);
|
||||
background-color: unset;
|
||||
}
|
||||
:host([selected]:not([disabled])) ha-button::part(base)::before {
|
||||
opacity: 1;
|
||||
}
|
||||
::slotted(*) {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
mwc-icon-button {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) mwc-icon-button::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) mwc-icon-button {
|
||||
color: var(--primary-background-color);
|
||||
}
|
||||
:host([selected]:not([disabled])) mwc-icon-button::before {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -109,7 +109,7 @@ export class HaIconButtonToolbar extends LitElement {
|
||||
|
||||
.icon-toolbar-button {
|
||||
color: var(--secondary-text-color);
|
||||
--ha-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--mdc-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--mdc-icon-size: var(--icon-button-toolbar-icon);
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import "@material/mwc-icon-button";
|
||||
import type { IconButton } from "@material/mwc-icon-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-icon-button")
|
||||
@@ -18,19 +19,15 @@ export class HaIconButton extends LitElement {
|
||||
// These should always be set as properties, not attributes,
|
||||
// so that only the <button> element gets the attribute
|
||||
@property({ type: String, attribute: "aria-haspopup" })
|
||||
ariaHasPopup!: "false" | "true" | "menu" | "listbox" | "tree" | "grid";
|
||||
override ariaHasPopup!: IconButton["ariaHasPopup"];
|
||||
|
||||
@property({ attribute: "hide-title", type: Boolean }) hideTitle = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
@query("mwc-icon-button", true) private _button?: IconButton;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
public override focus() {
|
||||
this._button?.focus();
|
||||
}
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
@@ -39,68 +36,30 @@ export class HaIconButton extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
<mwc-icon-button
|
||||
aria-label=${ifDefined(this.label)}
|
||||
title=${ifDefined(this.hideTitle ? undefined : this.label)}
|
||||
aria-haspopup=${ifDefined(this.ariaHasPopup)}
|
||||
.disabled=${this.disabled}
|
||||
.iconTag=${this.path ? "ha-svg-icon" : "span"}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
>
|
||||
${this.path
|
||||
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
|
||||
: html`<span><slot></slot></span>`}
|
||||
</ha-button>
|
||||
: html`<slot></slot>`}
|
||||
</mwc-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
--ha-button-height: var(--ha-icon-button-size, 48px);
|
||||
}
|
||||
ha-button {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
--wa-form-control-padding-inline: var(
|
||||
--ha-icon-button-padding-inline,
|
||||
--ha-space-2
|
||||
);
|
||||
--wa-color-on-normal: currentColor;
|
||||
--wa-color-fill-quiet: transparent;
|
||||
}
|
||||
ha-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
:host([disabled]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-button::part(base) {
|
||||
width: var(--wa-form-control-height);
|
||||
aspect-ratio: 1;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
ha-button::part(label) {
|
||||
display: flex;
|
||||
}
|
||||
:host([selected]) ha-button::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host(:hover:not([disabled])) ha-button::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-theme-on-primary: currentColor;
|
||||
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-icon";
|
||||
@@ -88,6 +88,8 @@ const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
export class HaIconPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -109,6 +111,7 @@ export class HaIconPicker extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
allow-custom-value
|
||||
.getItems=${this._getIconPickerItems}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -191,6 +191,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
27
src/components/ha-md-select-option.ts
Normal file
27
src/components/ha-md-select-option.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { SelectOptionEl } from "@material/web/select/internal/selectoption/select-option";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select-option")
|
||||
export class HaMdSelectOption extends SelectOptionEl {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select-option": HaMdSelectOption;
|
||||
}
|
||||
}
|
||||
36
src/components/ha-md-select.ts
Normal file
36
src/components/ha-md-select.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { FilledSelect } from "@material/web/select/internal/filled-select";
|
||||
import { styles as sharedStyles } from "@material/web/select/internal/shared-styles";
|
||||
import { styles } from "@material/web/select/internal/filled-select-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select")
|
||||
export class HaMdSelect extends FilledSelect {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
|
||||
--md-sys-color-surface-container-highest: var(--input-fill-color);
|
||||
--md-sys-color-on-surface: var(--input-ink-color);
|
||||
|
||||
--md-sys-color-surface-container: var(--input-fill-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-sys-color-secondary-container: var(--input-fill-color);
|
||||
--md-menu-container-color: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select": HaMdSelect;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiDevices, mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import { mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -237,22 +237,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
addGroup("views", views);
|
||||
addGroup("other_routes", otherRoutes);
|
||||
|
||||
if (
|
||||
searchString &&
|
||||
!this._navigationItems.some((navItem) => navItem.id === searchString)
|
||||
) {
|
||||
items.push({
|
||||
id: searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.add_custom_path"
|
||||
),
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
sorting_label: searchString,
|
||||
group: "other_routes",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ export class HaPasswordField extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -15,7 +14,6 @@ import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { localeContext, localizeContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
@@ -92,6 +90,8 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
||||
|
||||
@customElement("ha-picker-combo-box")
|
||||
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@@ -162,14 +162,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize?: HomeAssistant["localize"];
|
||||
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private locale?: HomeAssistant["locale"];
|
||||
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
|
||||
@state() private _selectedSection?: string;
|
||||
@@ -223,9 +215,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
const searchLabel =
|
||||
this.label ??
|
||||
(this.allowCustomValue
|
||||
? (this.localize?.("ui.components.combo-box.search_or_custom") ??
|
||||
? (this.hass?.localize("ui.components.combo-box.search_or_custom") ??
|
||||
"Search | Add custom value")
|
||||
: (this.localize?.("ui.common.search") ?? "Search"));
|
||||
: (this.hass?.localize("ui.common.search") ?? "Search"));
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
@@ -236,7 +228,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.localize?.("ui.common.clear") || "Clear"}
|
||||
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
@@ -358,7 +350,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return caseInsensitiveStringCompare(
|
||||
sortLabelA,
|
||||
sortLabelB,
|
||||
this.locale?.language ?? navigator.language
|
||||
this.hass?.locale.language ?? navigator.language
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -370,18 +362,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.allowCustomValue && this._search) {
|
||||
items.push({
|
||||
id: this._search,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${this._search}"`,
|
||||
icon_path: mdiPlus,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
items.push({ id: PADDING_ID, primary: "" }); // padding for safe area inset
|
||||
}
|
||||
@@ -409,10 +389,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
? typeof this.notFoundLabel === "function"
|
||||
? this.notFoundLabel(this._search)
|
||||
: this.notFoundLabel ||
|
||||
this.localize?.("ui.components.combo-box.no_match") ||
|
||||
this.hass?.localize("ui.components.combo-box.no_match") ||
|
||||
"No matching items found"
|
||||
: this.emptyLabel ||
|
||||
this.localize?.("ui.components.combo-box.no_items") ||
|
||||
this.hass?.localize("ui.components.combo-box.no_items") ||
|
||||
"No items available"}</span
|
||||
>
|
||||
</ha-combo-box-item>
|
||||
@@ -515,7 +495,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
id: searchString,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
this.hass?.localize("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
@@ -804,7 +784,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-4);
|
||||
padding-top: var(--ha-space-3);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,6 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
ha-combo-box-item {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: 0;
|
||||
@@ -185,8 +184,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--ha-icon-button-size: 32px;
|
||||
--ha-icon-button-padding-inline: var(--ha-space-1);
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.arrow {
|
||||
--mdc-icon-size: 20px;
|
||||
|
||||
@@ -61,7 +61,6 @@ class HaSegmentedBar extends LitElement {
|
||||
: html`
|
||||
<ha-tooltip for="segment-${index}" placement="top">
|
||||
${segment.label}
|
||||
(${((segment.value / totalValue) * 100).toFixed(1)}%)
|
||||
</ha-tooltip>
|
||||
`}
|
||||
<div
|
||||
|
||||
@@ -5,7 +5,6 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
@@ -76,7 +75,7 @@ export class HaSelect extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
if (this.disabled) {
|
||||
return html`${this._renderField()}${this._renderHelper()}`;
|
||||
return this._renderField();
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -117,7 +116,6 @@ export class HaSelect extends LitElement {
|
||||
)
|
||||
: html`<slot></slot>`}
|
||||
</ha-dropdown>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -133,6 +131,7 @@ export class HaSelect extends LitElement {
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@clear=${this._clearValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.value=${valueLabel}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@@ -145,14 +144,6 @@ export class HaSelect extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
@@ -203,11 +194,6 @@ export class HaSelect extends LitElement {
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--select-menu-width);
|
||||
}
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
|
||||
@@ -66,7 +66,6 @@ export class HaTimeDuration extends LitElement {
|
||||
.enableDay=${this.selector.duration?.enable_day}
|
||||
.enableMillisecond=${this.selector.duration?.enable_millisecond}
|
||||
.allowNegative=${this.selector.duration?.allow_negative}
|
||||
.enableSecond=${this.selector.duration?.enable_second ?? true}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export class HaSelectSelector extends LitElement {
|
||||
<ha-select-box
|
||||
.options=${options}
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._selectChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
.maxColumns=${this.selector.select?.box_max_columns}
|
||||
.hass=${this.hass}
|
||||
></ha-select-box>
|
||||
@@ -120,7 +120,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.checked=${item.value === this.value}
|
||||
.value=${item.value}
|
||||
.disabled=${item.disabled || this.disabled}
|
||||
@change=${this._radioChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
`
|
||||
@@ -236,7 +236,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
@selected=${this._selectChanged}
|
||||
@selected=${this._valueChanged}
|
||||
.options=${options}
|
||||
>
|
||||
</ha-select>
|
||||
@@ -282,24 +282,16 @@ export class HaSelectSelector extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _radioChanged(ev) {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
this._valueChanged(ev);
|
||||
}
|
||||
|
||||
private _selectChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
// Additional handling for reset of select elements
|
||||
if (ev.detail?.value === undefined && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._valueChanged(ev);
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
const value = ev.detail?.value || ev.target.value;
|
||||
if (this.disabled || value === undefined || value === (this.value ?? "")) {
|
||||
return;
|
||||
|
||||
@@ -64,15 +64,6 @@ const SELECTOR_SCHEMAS = {
|
||||
name: "enable_millisecond",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "enable_second",
|
||||
default: true,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "allow_negative",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
entity: [
|
||||
{
|
||||
|
||||
@@ -141,7 +141,7 @@ export class HaTextSelector extends LitElement {
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type {
|
||||
HassService,
|
||||
HassServices,
|
||||
@@ -507,7 +507,7 @@ export class HaServiceControl extends LitElement {
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircleOutline}
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { Segment } from "../data/vacuum";
|
||||
import { getVacuumSegments } from "../data/vacuum";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
|
||||
type AreaSegmentMapping = Record<string, string[]>; // area ID -> segment IDs
|
||||
|
||||
@customElement("ha-vacuum-segment-area-mapper")
|
||||
export class HaVacuumSegmentAreaMapper extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "entity-id" }) public entityId!: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreaSegmentMapping;
|
||||
|
||||
@state() private _segments?: Segment[];
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public get lastSeenSegments() {
|
||||
return this._segments;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
this._loadSegments();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSegments() {
|
||||
this._loading = true;
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
const result = await getVacuumSegments(this.hass, this.entityId);
|
||||
this._segments = result.segments;
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Failed to load segments";
|
||||
this._segments = undefined;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._loading) {
|
||||
return html`
|
||||
<div class="loading">${this.hass.localize("ui.common.loading")}...</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html` <ha-alert alert-type="error">${this._error}</ha-alert> `;
|
||||
}
|
||||
|
||||
if (!this._segments || this._segments.length === 0) {
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize("ui.dialogs.vacuum_segment_mapping.no_segments")}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
// Group segments by group (if available)
|
||||
const groupedSegments = this._groupSegments(this._segments);
|
||||
|
||||
return html`
|
||||
${Object.entries(groupedSegments).map(
|
||||
([groupName, segments]) => html`
|
||||
${groupName ? html`<h2>${groupName}</h2>` : nothing}
|
||||
<ha-md-list>
|
||||
${segments.map((segment) => this._renderSegment(segment))}
|
||||
</ha-md-list>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _groupSegments(segments: Segment[]): Record<string, Segment[]> {
|
||||
const grouped: Record<string, Segment[]> = {};
|
||||
|
||||
for (const segment of segments) {
|
||||
const group = segment.group || "";
|
||||
if (!grouped[group]) {
|
||||
grouped[group] = [];
|
||||
}
|
||||
grouped[group].push(segment);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private _renderSegment(segment: Segment) {
|
||||
const mappedAreas = this._getSegmentAreas(segment.id);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">${segment.name}</span>
|
||||
<ha-area-picker
|
||||
slot="end"
|
||||
.hass=${this.hass}
|
||||
.value=${mappedAreas}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.area_label"
|
||||
)}
|
||||
@value-changed=${this._handleAreaChanged}
|
||||
data-segment-id=${segment.id}
|
||||
></ha-area-picker>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAreaChanged = (ev: CustomEvent) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const segmentId = target.dataset.segmentId;
|
||||
if (segmentId) {
|
||||
this._areaChanged(segmentId, ev);
|
||||
}
|
||||
};
|
||||
|
||||
private _getSegmentAreas(segmentId: string): string | undefined {
|
||||
if (!this.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find which area(s) contain this segment
|
||||
for (const [areaId, segmentIds] of Object.entries(this.value)) {
|
||||
if (segmentIds.includes(segmentId)) {
|
||||
return areaId;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _areaChanged(segmentId: string, ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newAreaId = ev.detail.value as string | undefined;
|
||||
|
||||
// Create a copy of the current mapping
|
||||
const newMapping: AreaSegmentMapping = { ...this.value };
|
||||
|
||||
// Remove segment from all areas
|
||||
for (const areaId of Object.keys(newMapping)) {
|
||||
newMapping[areaId] = newMapping[areaId].filter((id) => id !== segmentId);
|
||||
// Remove empty area entries
|
||||
if (newMapping[areaId].length === 0) {
|
||||
delete newMapping[areaId];
|
||||
}
|
||||
}
|
||||
|
||||
// Add segment to new area if specified
|
||||
if (newAreaId) {
|
||||
if (!newMapping[newAreaId]) {
|
||||
newMapping[newAreaId] = [];
|
||||
}
|
||||
newMapping[newAreaId].push(segmentId);
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newMapping });
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-area-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: var(--ha-space-4);
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-vacuum-segment-area-mapper": HaVacuumSegmentAreaMapper;
|
||||
}
|
||||
}
|
||||
451
src/components/ha-wa-dialog.ts
Normal file
451
src/components/ha-wa-dialog.ts
Normal file
@@ -0,0 +1,451 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
|
||||
/**
|
||||
* Home Assistant dialog component
|
||||
*
|
||||
* @element ha-wa-dialog
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A stylable dialog built using the `wa-dialog` component, providing a standardized header (ha-dialog-header),
|
||||
* body, and footer (preferably using `ha-dialog-footer`) with slots
|
||||
*
|
||||
* You can open and close the dialog declaratively by using the `data-dialog="close"` attribute.
|
||||
* @see https://webawesome.com/docs/components/dialog/#opening-and-closing-dialogs-declaratively
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog content body.
|
||||
* @slot footer - Dialog footer content.
|
||||
*
|
||||
* @csspart dialog - The dialog surface.
|
||||
* @csspart header - The header container.
|
||||
* @csspart body - The scrollable body container.
|
||||
* @csspart footer - The footer container.
|
||||
*
|
||||
* @cssprop --dialog-content-padding - Padding for the dialog content sections.
|
||||
* @cssprop --ha-dialog-show-duration - Show animation duration.
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
|
||||
*
|
||||
* @event opened - Fired when the dialog is shown.
|
||||
* @event closed - Fired after the dialog is hidden.
|
||||
*
|
||||
* @remarks
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when the dialog opens, add the `autofocus` attribute to it.
|
||||
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
|
||||
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/27143
|
||||
*/
|
||||
@customElement("ha-wa-dialog")
|
||||
export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: "alert" | "standard" = "standard";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
|
||||
@property({ attribute: "header-subtitle" })
|
||||
public headerSubtitle?: string;
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@query(".body") public bodyContainer!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("open")) {
|
||||
this._open = this.open;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-dialog
|
||||
.open=${this._open}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
without-header
|
||||
aria-labelledby=${ifDefined(
|
||||
this.ariaLabelledBy ||
|
||||
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
|
||||
)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
${!this.withoutHeader
|
||||
? html` <slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>`
|
||||
: nothing}
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleShow = async () => {
|
||||
this._open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleBodyScroll(ev: Event) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleHide(ev: CustomEvent<{ source: Element }>) {
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDialog).dialog
|
||||
) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
wa-dialog {
|
||||
--full-width: var(
|
||||
--ha-dialog-width-full,
|
||||
min(95vw, var(--safe-width))
|
||||
);
|
||||
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
|
||||
--spacing: var(--dialog-content-padding, var(--ha-space-6));
|
||||
--show-duration: var(--ha-dialog-show-duration, 200ms);
|
||||
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
|
||||
--ha-dialog-surface-background: var(
|
||||
--card-background-color,
|
||||
var(--ha-color-surface-default)
|
||||
);
|
||||
--wa-color-surface-raised: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--wa-panel-border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
max-width: var(--ha-dialog-max-width, var(--safe-width));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-dialog {
|
||||
--show-duration: 0ms;
|
||||
--hide-duration: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
:host([width="small"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
color: var(--primary-text-color);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
--ha-dialog-max-height,
|
||||
calc(var(--safe-height) - var(--ha-space-20))
|
||||
);
|
||||
min-height: var(--ha-dialog-min-height);
|
||||
margin-top: var(--dialog-surface-margin-top, auto);
|
||||
/* Used to offset the dialog from the safe areas when space is limited */
|
||||
transform: translate(
|
||||
calc(
|
||||
var(--safe-area-offset-left, 0px) - var(
|
||||
--safe-area-offset-right,
|
||||
0px
|
||||
)
|
||||
),
|
||||
calc(
|
||||
var(--safe-area-offset-top, 0px) - var(
|
||||
--safe-area-offset-bottom,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
||||
font-size: var(
|
||||
--ha-dialog-header-title-font-size,
|
||||
var(--ha-font-size-2xl)
|
||||
);
|
||||
line-height: var(
|
||||
--ha-dialog-header-title-line-height,
|
||||
var(--ha-line-height-condensed)
|
||||
);
|
||||
font-weight: var(
|
||||
--ha-dialog-header-title-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--ha-space-3);
|
||||
}
|
||||
|
||||
wa-dialog::part(body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(
|
||||
--dialog-content-padding,
|
||||
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
|
||||
);
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
wa-dialog::part(footer) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-wa-dialog": HaWaDialog;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
opened: undefined;
|
||||
"after-show": undefined;
|
||||
closed: undefined;
|
||||
}
|
||||
}
|
||||
@@ -423,77 +423,31 @@ export class HaMap extends ReactiveElement {
|
||||
? baseOpacity! + pointIndex * opacityStep!
|
||||
: undefined;
|
||||
|
||||
const thisPoint = path.points[pointIndex];
|
||||
const nextPoint = path.points[pointIndex + 1];
|
||||
|
||||
// DRAW point
|
||||
this._mapPaths.push(
|
||||
Leaflet.circleMarker(thisPoint.point, {
|
||||
Leaflet.circleMarker(path.points[pointIndex].point, {
|
||||
radius: isTouch ? 8 : 3,
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
fillOpacity: opacity,
|
||||
interactive: true,
|
||||
}).bindTooltip(this._computePathTooltip(path, thisPoint), {
|
||||
direction: "top",
|
||||
})
|
||||
}).bindTooltip(
|
||||
this._computePathTooltip(path, path.points[pointIndex]),
|
||||
{ direction: "top" }
|
||||
)
|
||||
);
|
||||
|
||||
// DRAW line between this and next point
|
||||
if (Math.abs(thisPoint.point[1] - nextPoint.point[1]) <= 180) {
|
||||
// if the path does not cross the antimeridian, draw a simple line
|
||||
// between the two points
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline([thisPoint.point, nextPoint.point], {
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline(
|
||||
[path.points[pointIndex].point, path.points[pointIndex + 1].point],
|
||||
{
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// if the path crosses the antimeridian, split the line into two, to
|
||||
// avoid it being drawn across the entire map
|
||||
const longitudeDifference =
|
||||
((nextPoint.point[1] - thisPoint.point[1] + 540) % 360) - 180;
|
||||
let intersectionLatitude: number;
|
||||
if (longitudeDifference === 0) {
|
||||
// very, very unlikely edge case
|
||||
intersectionLatitude =
|
||||
(thisPoint.point[0] + nextPoint.point[0]) / 2;
|
||||
} else {
|
||||
intersectionLatitude =
|
||||
thisPoint.point[0] +
|
||||
((nextPoint.point[0] - thisPoint.point[0]) *
|
||||
(thisPoint.point[1] > 0
|
||||
? 180 - thisPoint.point[1]
|
||||
: -180 - thisPoint.point[1])) /
|
||||
longitudeDifference;
|
||||
}
|
||||
|
||||
const intersectionPoint1: LatLngTuple = [
|
||||
intersectionLatitude,
|
||||
thisPoint.point[1] > 0 ? 180 : -180,
|
||||
];
|
||||
const intersectionPoint2: LatLngTuple = [
|
||||
intersectionLatitude,
|
||||
nextPoint.point[1] > 0 ? 180 : -180,
|
||||
];
|
||||
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline([thisPoint.point, intersectionPoint1], {
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
this._mapPaths.push(
|
||||
Leaflet.polyline([intersectionPoint2, nextPoint.point], {
|
||||
color: path.color || darkPrimaryColor,
|
||||
opacity,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
const pointIndex = path.points.length - 1;
|
||||
if (pointIndex >= 0) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-dialog";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-player-toggle";
|
||||
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
|
||||
|
||||
@@ -76,7 +76,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
|
||||
const entityId = this._entityId;
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@@ -136,7 +136,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-dialog";
|
||||
import "../ha-wa-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-icon-button";
|
||||
@@ -99,7 +99,7 @@ class DialogMediaManage extends LitElement {
|
||||
let fileIndex = 0;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
?prevent-scrim-close=${this._uploading || this._deleting}
|
||||
@@ -244,7 +244,7 @@ class DialogMediaManage extends LitElement {
|
||||
)}
|
||||
</ha-tip>`
|
||||
: nothing}
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ class DialogMediaManage extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-dialog-header ha-media-upload-button,
|
||||
|
||||
@@ -24,7 +24,7 @@ import "../ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-icon-button-arrow-prev";
|
||||
import "../ha-dialog";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-manage-button";
|
||||
import "./ha-media-player-browse";
|
||||
import type {
|
||||
@@ -76,7 +76,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@@ -169,7 +169,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-max-width: 800px;
|
||||
--ha-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
|
||||
@@ -242,10 +242,6 @@ class BrowseMediaTTS extends LitElement {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
ha-language-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
@@ -264,7 +260,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
}
|
||||
.footer {
|
||||
--mdc-icon-size: 14px;
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -584,7 +584,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
})}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ha-scrollbar ${classMap({
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait",
|
||||
@@ -612,7 +612,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
style=${styleMap({
|
||||
height: `${children.length * 72 + 26}px`,
|
||||
})}
|
||||
class="ha-scrollbar"
|
||||
.renderItem=${this._renderListItem}
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
@@ -980,7 +979,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -1234,7 +1232,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
.child .play:not(.can_expand) {
|
||||
--ha-icon-button-size: 70px;
|
||||
--mdc-icon-button-size: 70px;
|
||||
--mdc-icon-size: 48px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
@@ -1295,7 +1293,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
}
|
||||
|
||||
ha-list-item:hover .graphic .play {
|
||||
|
||||
@@ -93,8 +93,8 @@ class SearchInputOutlined extends LitElement {
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 24px;
|
||||
height: var(--ha-icon-button-size);
|
||||
--mdc-icon-button-size: 24px;
|
||||
height: var(--mdc-icon-button-size);
|
||||
display: flex;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import "../../ha-icon-next";
|
||||
import "../../ha-md-list";
|
||||
import "../../ha-md-list-item";
|
||||
import "../../ha-svg-icon";
|
||||
import "../../ha-dialog";
|
||||
import "../../ha-wa-dialog";
|
||||
import "../ha-target-picker-item-row";
|
||||
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
|
||||
|
||||
@@ -42,7 +42,7 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._opened}
|
||||
header-title=${this.hass.localize(
|
||||
@@ -64,7 +64,7 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
.includeDeviceClasses=${this._params.includeDeviceClasses}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,7 +641,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
z-index: 1;
|
||||
}
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 32px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
|
||||
@@ -247,7 +247,7 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
cursor: default;
|
||||
}
|
||||
.mdc-chip ha-icon-button {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
|
||||
@@ -629,7 +629,6 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Action) => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
continueOnError: () => void;
|
||||
duplicate: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
|
||||
@@ -34,5 +34,3 @@ export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||
|
||||
export const configEntriesContext =
|
||||
createContext<ConfigEntry[]>("configEntries");
|
||||
|
||||
export const authContext = createContext<HomeAssistant["auth"]>("auth");
|
||||
|
||||
@@ -22,13 +22,11 @@ import {
|
||||
} from "../common/datetime/calc_date";
|
||||
import { formatTime24h } from "../common/datetime/format_time";
|
||||
import { groupBy } from "../common/util/group-by";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
StatisticsUnitConfiguration,
|
||||
StatisticValue,
|
||||
} from "./recorder";
|
||||
import {
|
||||
fetchStatistics,
|
||||
@@ -42,17 +40,27 @@ import { formatNumber } from "../common/number/format_number";
|
||||
|
||||
const energyCollectionKeys: (string | undefined)[] = [];
|
||||
|
||||
export const emptyGridSourceEnergyPreference =
|
||||
(): GridSourceTypeEnergyPreference => ({
|
||||
type: "grid",
|
||||
stat_energy_from: null,
|
||||
stat_energy_to: null,
|
||||
export const emptyFlowFromGridSourceEnergyPreference =
|
||||
(): FlowFromGridSourceEnergyPreference => ({
|
||||
stat_energy_from: "",
|
||||
stat_cost: null,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
});
|
||||
|
||||
export const emptyFlowToGridSourceEnergyPreference =
|
||||
(): FlowToGridSourceEnergyPreference => ({
|
||||
stat_energy_to: "",
|
||||
stat_compensation: null,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
});
|
||||
|
||||
export const emptyGridSourceEnergyPreference =
|
||||
(): GridSourceTypeEnergyPreference => ({
|
||||
type: "grid",
|
||||
flow_from: [],
|
||||
flow_to: [],
|
||||
cost_adjustment_day: 0,
|
||||
});
|
||||
|
||||
@@ -100,6 +108,30 @@ export interface DeviceConsumptionEnergyPreference {
|
||||
included_in_stat?: string;
|
||||
}
|
||||
|
||||
export interface FlowFromGridSourceEnergyPreference {
|
||||
// kWh meter
|
||||
stat_energy_from: string;
|
||||
|
||||
// $ meter
|
||||
stat_cost: string | null;
|
||||
|
||||
// Can be used to generate costs if stat_cost omitted
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface FlowToGridSourceEnergyPreference {
|
||||
// kWh meter
|
||||
stat_energy_to: string;
|
||||
|
||||
// $ meter
|
||||
stat_compensation: string | null;
|
||||
|
||||
// Can be used to generate costs if stat_compensation omitted
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface PowerConfig {
|
||||
stat_rate?: string; // Standard single sensor
|
||||
stat_rate_inverted?: string; // Inverted single sensor
|
||||
@@ -107,33 +139,29 @@ export interface PowerConfig {
|
||||
stat_rate_to?: string; // Battery: charge / Grid: return
|
||||
}
|
||||
|
||||
export interface GridPowerSourceEnergyPreference {
|
||||
stat_rate: string;
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid source format.
|
||||
* Each grid connection is a single object with import/export/power together.
|
||||
* Multiple grid sources are allowed.
|
||||
* Input type for saving grid power sources.
|
||||
* Core requires EITHER stat_rate (legacy) OR power_config (new format).
|
||||
* When reading from backend, stat_rate is always populated.
|
||||
*/
|
||||
export type GridPowerSourceInput = Omit<
|
||||
GridPowerSourceEnergyPreference,
|
||||
"stat_rate"
|
||||
> & {
|
||||
stat_rate?: string;
|
||||
};
|
||||
|
||||
export interface GridSourceTypeEnergyPreference {
|
||||
type: "grid";
|
||||
|
||||
// Import meter
|
||||
stat_energy_from: string | null;
|
||||
|
||||
// Export meter
|
||||
stat_energy_to: string | null;
|
||||
|
||||
// Import cost tracking
|
||||
stat_cost: string | null;
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
|
||||
// Export compensation tracking
|
||||
stat_compensation: string | null;
|
||||
entity_energy_price_export: string | null;
|
||||
number_energy_price_export: number | null;
|
||||
|
||||
// Power measurement
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
flow_from: FlowFromGridSourceEnergyPreference[];
|
||||
flow_to: FlowToGridSourceEnergyPreference[];
|
||||
power?: GridPowerSourceEnergyPreference[];
|
||||
|
||||
cost_adjustment_day: number;
|
||||
}
|
||||
@@ -150,7 +178,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
type: "battery";
|
||||
stat_energy_from: string;
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
stat_rate?: string;
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
@@ -159,9 +187,6 @@ export interface GasSourceTypeEnergyPreference {
|
||||
// kWh/volume meter
|
||||
stat_energy_from: string;
|
||||
|
||||
// Flow rate (m³/h, L/min, etc.)
|
||||
stat_rate?: string;
|
||||
|
||||
// $ meter
|
||||
stat_cost: string | null;
|
||||
|
||||
@@ -177,9 +202,6 @@ export interface WaterSourceTypeEnergyPreference {
|
||||
// volume meter
|
||||
stat_energy_from: string;
|
||||
|
||||
// Flow rate (L/min, gal/min, m³/h, etc.)
|
||||
stat_rate?: string;
|
||||
|
||||
// $ meter
|
||||
stat_cost: string | null;
|
||||
|
||||
@@ -333,25 +355,24 @@ export const getReferencedStatisticIds = (
|
||||
}
|
||||
|
||||
// grid source
|
||||
if (source.stat_energy_from) {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
if (source.stat_cost) {
|
||||
statIDs.push(source.stat_cost);
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statIDs.push(flowFrom.stat_energy_from);
|
||||
if (flowFrom.stat_cost) {
|
||||
statIDs.push(flowFrom.stat_cost);
|
||||
}
|
||||
const importCostStatId = info.cost_sensors[source.stat_energy_from];
|
||||
if (importCostStatId) {
|
||||
statIDs.push(importCostStatId);
|
||||
const costStatId = info.cost_sensors[flowFrom.stat_energy_from];
|
||||
if (costStatId) {
|
||||
statIDs.push(costStatId);
|
||||
}
|
||||
}
|
||||
|
||||
if (source.stat_energy_to) {
|
||||
statIDs.push(source.stat_energy_to);
|
||||
if (source.stat_compensation) {
|
||||
statIDs.push(source.stat_compensation);
|
||||
for (const flowTo of source.flow_to) {
|
||||
statIDs.push(flowTo.stat_energy_to);
|
||||
if (flowTo.stat_compensation) {
|
||||
statIDs.push(flowTo.stat_compensation);
|
||||
}
|
||||
const exportCostStatId = info.cost_sensors[source.stat_energy_to];
|
||||
if (exportCostStatId) {
|
||||
statIDs.push(exportCostStatId);
|
||||
const costStatId = info.cost_sensors[flowTo.stat_energy_to];
|
||||
if (costStatId) {
|
||||
statIDs.push(costStatId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,9 +395,6 @@ export const getReferencedStatisticIdsPower = (
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "gas" || source.type === "water") {
|
||||
if (source.stat_rate) {
|
||||
statIDs.push(source.stat_rate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -386,19 +404,15 @@ export const getReferencedStatisticIdsPower = (
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
if (source.stat_rate) {
|
||||
statIDs.push(source.stat_rate);
|
||||
}
|
||||
statIDs.push(source.stat_rate);
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
if (source.stat_rate) {
|
||||
statIDs.push(source.stat_rate);
|
||||
if (source.power) {
|
||||
statIDs.push(...source.power.map((p) => p.stat_rate));
|
||||
}
|
||||
}
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
||||
statIDs.push(...prefs.device_consumption_water.map((d) => d.stat_rate));
|
||||
|
||||
return statIDs.filter(Boolean) as string[];
|
||||
};
|
||||
@@ -436,8 +450,11 @@ const getEnergyData = async (
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "grid" && source.stat_energy_from) {
|
||||
consumptionStatIDs.push(source.stat_energy_from);
|
||||
// grid source
|
||||
if (source.type === "grid") {
|
||||
for (const flowFrom of source.flow_from) {
|
||||
consumptionStatIDs.push(flowFrom.stat_energy_from);
|
||||
}
|
||||
}
|
||||
}
|
||||
const energyStatIds = getReferencedStatisticIds(prefs, info, [
|
||||
@@ -1038,18 +1055,18 @@ const getSummedDataPartial = (
|
||||
}
|
||||
|
||||
// grid source
|
||||
if (source.stat_energy_from) {
|
||||
for (const flowFrom of source.flow_from) {
|
||||
if (statIds.from_grid) {
|
||||
statIds.from_grid.push(source.stat_energy_from);
|
||||
statIds.from_grid.push(flowFrom.stat_energy_from);
|
||||
} else {
|
||||
statIds.from_grid = [source.stat_energy_from];
|
||||
statIds.from_grid = [flowFrom.stat_energy_from];
|
||||
}
|
||||
}
|
||||
if (source.stat_energy_to) {
|
||||
for (const flowTo of source.flow_to) {
|
||||
if (statIds.to_grid) {
|
||||
statIds.to_grid.push(source.stat_energy_to);
|
||||
statIds.to_grid.push(flowTo.stat_energy_to);
|
||||
} else {
|
||||
statIds.to_grid = [source.stat_energy_to];
|
||||
statIds.to_grid = [flowTo.stat_energy_to];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1469,311 +1486,3 @@ export function getSuggestedPeriod(
|
||||
? "day"
|
||||
: "hour";
|
||||
}
|
||||
|
||||
export const downloadEnergyData = (
|
||||
hass: HomeAssistant,
|
||||
collectionKey?: string
|
||||
) => {
|
||||
const energyData = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
|
||||
if (!energyData.prefs || !energyData.state.stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gasUnit = energyData.state.gasUnit;
|
||||
const electricUnit = "kWh";
|
||||
|
||||
const energy_sources = energyData.prefs.energy_sources;
|
||||
const device_consumption = energyData.prefs.device_consumption;
|
||||
const device_consumption_water = energyData.prefs.device_consumption_water;
|
||||
const stats = energyData.state.stats;
|
||||
|
||||
const timeSet = new Set<number>();
|
||||
Object.values(stats).forEach((stat) => {
|
||||
stat.forEach((datapoint) => {
|
||||
timeSet.add(datapoint.start);
|
||||
});
|
||||
});
|
||||
const times = Array.from(timeSet).sort();
|
||||
|
||||
const headers =
|
||||
"entity_id,type,unit," +
|
||||
times.map((t) => new Date(t).toISOString()).join(",") +
|
||||
"\n";
|
||||
const csv: string[] = [];
|
||||
csv[0] = headers;
|
||||
|
||||
const processCsvRow = function (
|
||||
id: string,
|
||||
type: string,
|
||||
unit: string,
|
||||
data: StatisticValue[]
|
||||
) {
|
||||
let n = 0;
|
||||
const row: string[] = [];
|
||||
row.push(id);
|
||||
row.push(type);
|
||||
row.push(unit.normalize("NFKD"));
|
||||
times.forEach((t) => {
|
||||
if (n < data.length && data[n].start === t) {
|
||||
row.push((data[n].change ?? "").toString());
|
||||
n++;
|
||||
} else {
|
||||
row.push("");
|
||||
}
|
||||
});
|
||||
csv.push(row.join(",") + "\n");
|
||||
};
|
||||
|
||||
const processStat = function (stat: string, type: string, unit: string) {
|
||||
if (!stats[stat]) {
|
||||
return;
|
||||
}
|
||||
|
||||
processCsvRow(stat, type, unit, stats[stat]);
|
||||
};
|
||||
|
||||
const currency = hass.config.currency;
|
||||
|
||||
const printCategory = function (
|
||||
type: string,
|
||||
statIds: string[],
|
||||
unit: string,
|
||||
costType?: string,
|
||||
costStatIds?: string[]
|
||||
) {
|
||||
if (statIds.length) {
|
||||
statIds.forEach((stat) => processStat(stat, type, unit));
|
||||
if (costType && costStatIds) {
|
||||
costStatIds.forEach((stat) => processStat(stat, costType, currency));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const grid_consumptions: string[] = [];
|
||||
const grid_productions: string[] = [];
|
||||
const grid_consumptions_cost: string[] = [];
|
||||
const grid_productions_cost: string[] = [];
|
||||
energy_sources
|
||||
.filter((s) => s.type === "grid")
|
||||
.forEach((source) => {
|
||||
const gridSource = source as GridSourceTypeEnergyPreference;
|
||||
if (gridSource.stat_energy_from) {
|
||||
grid_consumptions.push(gridSource.stat_energy_from);
|
||||
const importCostId =
|
||||
gridSource.stat_cost ||
|
||||
energyData.state.info.cost_sensors[gridSource.stat_energy_from];
|
||||
if (importCostId) {
|
||||
grid_consumptions_cost.push(importCostId);
|
||||
}
|
||||
}
|
||||
if (gridSource.stat_energy_to) {
|
||||
grid_productions.push(gridSource.stat_energy_to);
|
||||
const exportCostId =
|
||||
gridSource.stat_compensation ||
|
||||
energyData.state.info.cost_sensors[gridSource.stat_energy_to];
|
||||
if (exportCostId) {
|
||||
grid_productions_cost.push(exportCostId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
printCategory(
|
||||
"grid_consumption",
|
||||
grid_consumptions,
|
||||
electricUnit,
|
||||
"grid_consumption_cost",
|
||||
grid_consumptions_cost
|
||||
);
|
||||
printCategory(
|
||||
"grid_return",
|
||||
grid_productions,
|
||||
electricUnit,
|
||||
"grid_return_compensation",
|
||||
grid_productions_cost
|
||||
);
|
||||
|
||||
const battery_ins: string[] = [];
|
||||
const battery_outs: string[] = [];
|
||||
energy_sources
|
||||
.filter((s) => s.type === "battery")
|
||||
.forEach((source) => {
|
||||
source = source as BatterySourceTypeEnergyPreference;
|
||||
battery_ins.push(source.stat_energy_to);
|
||||
battery_outs.push(source.stat_energy_from);
|
||||
});
|
||||
|
||||
printCategory("battery_in", battery_ins, electricUnit);
|
||||
printCategory("battery_out", battery_outs, electricUnit);
|
||||
|
||||
const solar_productions: string[] = [];
|
||||
energy_sources
|
||||
.filter((s) => s.type === "solar")
|
||||
.forEach((source) => {
|
||||
source = source as SolarSourceTypeEnergyPreference;
|
||||
solar_productions.push(source.stat_energy_from);
|
||||
});
|
||||
|
||||
printCategory("solar_production", solar_productions, electricUnit);
|
||||
|
||||
const gas_consumptions: string[] = [];
|
||||
const gas_consumptions_cost: string[] = [];
|
||||
energy_sources
|
||||
.filter((s) => s.type === "gas")
|
||||
.forEach((source) => {
|
||||
source = source as GasSourceTypeEnergyPreference;
|
||||
const statId = source.stat_energy_from;
|
||||
gas_consumptions.push(statId);
|
||||
const costId =
|
||||
source.stat_cost || energyData.state.info.cost_sensors[statId];
|
||||
if (costId) {
|
||||
gas_consumptions_cost.push(costId);
|
||||
}
|
||||
});
|
||||
|
||||
printCategory(
|
||||
"gas_consumption",
|
||||
gas_consumptions,
|
||||
gasUnit,
|
||||
"gas_consumption_cost",
|
||||
gas_consumptions_cost
|
||||
);
|
||||
|
||||
const water_consumptions: string[] = [];
|
||||
const water_consumptions_cost: string[] = [];
|
||||
energy_sources
|
||||
.filter((s) => s.type === "water")
|
||||
.forEach((source) => {
|
||||
source = source as WaterSourceTypeEnergyPreference;
|
||||
const statId = source.stat_energy_from;
|
||||
water_consumptions.push(statId);
|
||||
const costId =
|
||||
source.stat_cost || energyData.state.info.cost_sensors[statId];
|
||||
if (costId) {
|
||||
water_consumptions_cost.push(costId);
|
||||
}
|
||||
});
|
||||
|
||||
printCategory(
|
||||
"water_consumption",
|
||||
water_consumptions,
|
||||
energyData.state.waterUnit,
|
||||
"water_consumption_cost",
|
||||
water_consumptions_cost
|
||||
);
|
||||
|
||||
const devices: string[] = [];
|
||||
device_consumption.forEach((source) => {
|
||||
source = source as DeviceConsumptionEnergyPreference;
|
||||
devices.push(source.stat_consumption);
|
||||
});
|
||||
|
||||
printCategory("device_consumption", devices, electricUnit);
|
||||
|
||||
if (device_consumption_water) {
|
||||
const waterDevices: string[] = [];
|
||||
device_consumption_water.forEach((source) => {
|
||||
source = source as DeviceConsumptionEnergyPreference;
|
||||
waterDevices.push(source.stat_consumption);
|
||||
});
|
||||
|
||||
printCategory(
|
||||
"device_consumption_water",
|
||||
waterDevices,
|
||||
energyData.state.waterUnit
|
||||
);
|
||||
}
|
||||
|
||||
const { summedData } = getSummedData(energyData.state);
|
||||
const { consumption } = computeConsumptionData(summedData, undefined);
|
||||
|
||||
const processConsumptionData = function (
|
||||
type: string,
|
||||
unit: string,
|
||||
data: Record<number, number>
|
||||
) {
|
||||
const data2: StatisticValue[] = [];
|
||||
|
||||
Object.entries(data).forEach(([t, value]) => {
|
||||
data2.push({
|
||||
start: Number(t),
|
||||
end: NaN,
|
||||
change: value,
|
||||
});
|
||||
});
|
||||
|
||||
processCsvRow("", type, unit, data2);
|
||||
};
|
||||
|
||||
const hasSolar = !!solar_productions.length;
|
||||
const hasBattery = !!battery_ins.length;
|
||||
const hasGridReturn = !!grid_productions.length;
|
||||
const hasGridSource = !!grid_consumptions.length;
|
||||
|
||||
if (hasGridSource) {
|
||||
processConsumptionData(
|
||||
"calculated_consumed_grid",
|
||||
electricUnit,
|
||||
consumption.used_grid
|
||||
);
|
||||
if (hasBattery) {
|
||||
processConsumptionData(
|
||||
"calculated_grid_to_battery",
|
||||
electricUnit,
|
||||
consumption.grid_to_battery
|
||||
);
|
||||
}
|
||||
}
|
||||
if (hasGridReturn && hasBattery) {
|
||||
processConsumptionData(
|
||||
"calculated_battery_to_grid",
|
||||
electricUnit,
|
||||
consumption.battery_to_grid
|
||||
);
|
||||
}
|
||||
if (hasBattery) {
|
||||
processConsumptionData(
|
||||
"calculated_consumed_battery",
|
||||
electricUnit,
|
||||
consumption.used_battery
|
||||
);
|
||||
}
|
||||
|
||||
if (hasSolar) {
|
||||
processConsumptionData(
|
||||
"calculated_consumed_solar",
|
||||
electricUnit,
|
||||
consumption.used_solar
|
||||
);
|
||||
if (hasBattery) {
|
||||
processConsumptionData(
|
||||
"calculated_solar_to_battery",
|
||||
electricUnit,
|
||||
consumption.solar_to_battery
|
||||
);
|
||||
}
|
||||
if (hasGridReturn) {
|
||||
processConsumptionData(
|
||||
"calculated_solar_to_grid",
|
||||
electricUnit,
|
||||
consumption.solar_to_grid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ((hasGridSource ? 1 : 0) + (hasSolar ? 1 : 0) + (hasBattery ? 1 : 0) > 1) {
|
||||
processConsumptionData(
|
||||
"calculated_total_consumption",
|
||||
electricUnit,
|
||||
consumption.used_total
|
||||
);
|
||||
}
|
||||
|
||||
const blob = new Blob(csv, {
|
||||
type: "text/csv",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
fileDownload(url, "energy.csv");
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { formatDurationDigital } from "../../common/datetime/format_duration";
|
||||
import type { FrontendLocaleData } from "../translation";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
|
||||
// These attributes are hidden from the more-info window for all entities.
|
||||
export const STATE_ATTRIBUTES = [
|
||||
@@ -189,15 +187,3 @@ export const NON_NUMERIC_ATTRIBUTES = [
|
||||
"unit_of_measurement",
|
||||
"xy_color",
|
||||
];
|
||||
|
||||
export const computeShownAttributes = (stateObj: HassEntity) => {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const filtersArray = STATE_ATTRIBUTES.concat(
|
||||
STATE_ATTRIBUTES_DOMAIN_CLASS[domain]?.[
|
||||
stateObj.attributes?.device_class
|
||||
] || []
|
||||
);
|
||||
return Object.keys(stateObj.attributes).filter(
|
||||
(key) => filtersArray.indexOf(key) === -1
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user