Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Bottein
7144b7802e Invert order 2025-07-08 15:06:59 +02:00
Paul Bottein
ca315b88ce Add sections dialog 2025-07-08 12:35:51 +02:00
1405 changed files with 25197 additions and 77262 deletions

View File

@@ -11,7 +11,7 @@ body:
**Please do not report issues for custom cards.**
[fr]: https://github.com/orgs/home-assistant/discussions
[fr]: https://github.com/home-assistant/frontend/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases
- type: checkboxes
attributes:

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Request a feature for the UI / Dashboards
url: https://github.com/orgs/home-assistant/discussions
url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request a new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI / Dashboards
url: https://github.com/home-assistant/core/issues

View File

@@ -1,53 +0,0 @@
name: Task
description: For staff only - Create a task
type: Task
body:
- type: markdown
attributes:
value: |
## ⚠️ RESTRICTED ACCESS
**This form is restricted to Open Home Foundation staff and authorized contributors only.**
If you are a community member wanting to contribute, please:
- For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
---
### For authorized contributors
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
- type: textarea
id: description
attributes:
label: Description
description: |
Provide a clear and detailed description of the task that needs to be accomplished.
Be specific about what needs to be done, why it's important, and any constraints or requirements.
placeholder: |
Describe the task, including:
- What needs to be done
- Why this task is needed
- Expected outcome
- Any constraints or requirements
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: Additional context
description: |
Any additional information, links, research, or context that would be helpful.
Include links to related issues, research, prototypes, roadmap opportunities etc.
placeholder: |
- Roadmap opportunity: [link]
- Epic: [link]
- Feature request: [link]
- Technical design documents: [link]
- Prototype/mockup: [link]
- Dependencies: [links]
validations:
required: false

View File

@@ -2,8 +2,6 @@
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
## Table of Contents
- [Quick Reference](#quick-reference)
@@ -153,10 +151,6 @@ try {
### Styling Guidelines
- **Use CSS custom properties**: Leverage the theme system
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
- Defined in `src/resources/theme/core.globals.ts`
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
- **Mobile-first responsive**: Design for mobile, enhance for desktop
- **Follow Material Design**: Use Material Web Components where appropriate
- **Support RTL**: Ensure all layouts work in RTL languages
@@ -165,68 +159,21 @@ try {
static get styles() {
return css`
:host {
padding: var(--ha-space-4);
--spacing: 16px;
padding: var(--spacing);
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.content {
gap: var(--ha-space-2);
}
@media (max-width: 600px) {
:host {
padding: var(--ha-space-2);
--spacing: 8px;
}
}
`;
}
```
### View Transitions
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
**Core Resources:**
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
**Implementation Guidelines:**
1. Always use `withViewTransition()` wrapper for automatic fallback
2. Keep transitions simple (subtle crossfades and fades work best)
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
**Default Root Transition:**
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
**Important Constraints:**
- Each `view-transition-name` must be unique at any given time
- Only one view transition can run at a time
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
**Current Usage & Planned Applications:**
- Launch screen fade out (implemented)
- Automation sidebar transitions (planned - #27238)
- More info dialog content changes (planned - #27672)
- Toolbar navigation, ha-spinner transitions (planned)
**Specification & Documentation:**
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
### Performance Best Practices
- **Code split**: Split code at the panel/dialog level
@@ -248,9 +195,8 @@ For browser support, API details, and current specifications, refer to these aut
**Available Dialog Types:**
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
- `ha-md-dialog` - Material Design 3 dialog component
- `ha-dialog` - Legacy component (still widely used)
- `ha-md-dialog` - Preferred for new code (Material Design 3)
- `ha-dialog` - Legacy component still widely used
**Opening Dialogs (Fire Event Pattern - Recommended):**
@@ -265,45 +211,15 @@ fireEvent(this, "show-dialog", {
**Dialog Implementation Requirements:**
- Implement `HassDialog<T>` interface
- Use `@state() private _open = false` to control dialog visibility
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
- Use `createCloseHeading()` for standard headers
- Import `haStyleDialog` for consistent styling
- Return `nothing` when no params (loading state)
- Fire `dialog-closed` event in `_dialogClosed()` handler
- Use `header-title` attribute for simple titles
- Use `header-subtitle` attribute for simple subtitles
- Use slots for custom content where the standard attributes are not enough
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
- Fire `dialog-closed` event when closing
- Add `dialogInitialFocus` for accessibility
**Dialog Sizing:**
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
- Custom sizing is NOT recommended - use the standard width presets
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
**Button Appearance Guidelines:**
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
**Recent Examples:**
See these files for current patterns:
- `src/panels/config/repairs/dialog-repairs-issue.ts`
- `src/dialogs/restart/dialog-restart.ts`
- `src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts`
**Gallery Documentation:**
- `gallery/src/pages/components/ha-wa-dialog.markdown`
- `gallery/src/pages/components/ha-dialogs.markdown`
````
### Form Component (ha-form)
- Schema-driven using `HaFormSchema[]`
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
- Built-in validation with error display
@@ -319,11 +235,7 @@ See these files for current patterns:
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
@value-changed=${this._valueChanged}
></ha-form>
```
**Gallery Documentation:**
- `gallery/src/pages/components/ha-form.markdown`
````
### Alert Component (ha-alert)
@@ -337,35 +249,6 @@ See these files for current patterns:
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
```
**Gallery Documentation:**
- `gallery/src/pages/components/ha-alert.markdown`
### Keyboard Shortcuts (ShortcutManager)
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
**Key Features:**
- Automatically blocks shortcuts when input fields are focused
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
**Implementation:**
- **Class definition**: `src/common/keyboard/shortcuts.ts`
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
### Tooltip Component (ha-tooltip)
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
**Implementation:**
- **Component definition**: `src/components/ha-tooltip.ts`
- **Usage example**: `src/components/ha-label.ts`
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
## Common Patterns
### Creating a Panel
@@ -406,19 +289,11 @@ export class DialogMyFeature
@state()
private _params?: MyDialogParams;
@state()
private _open = false;
public async showDialog(params: MyDialogParams): Promise<void> {
this._params = params;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -429,27 +304,19 @@ export class DialogMyFeature
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._params.title}
header-subtitle=${this._params.subtitle}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._params.title)}
>
<p>Dialog content</p>
<ha-dialog-footer slot="footer">
<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>
<!-- Dialog content -->
<ha-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._submit} slot="primaryAction">
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -42,7 +42,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev
npx -y netlify-cli deploy --dir=cast/dist --alias dev
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
@@ -56,12 +56,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
with:
ref: master
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -77,7 +77,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod
npx -y netlify-cli deploy --dir=cast/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@v4.2.3
with:
path: |
node_modules/.cache/prettier
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@v4.6.2
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@v4.6.2
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/init@v3
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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/autobuild@v3
# 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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/analyze@v3

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -43,7 +43,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
with:
ref: master
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -35,7 +35,7 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod
npx -y netlify-cli deploy --dir=gallery/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -40,7 +40,7 @@ jobs:
- name: Deploy preview to Netlify
id: deploy
run: |
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
--json > deploy_output.json
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply labels
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
uses: actions/labeler@v5.0.0
with:
sync-labels: true

View File

@@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
- uses: dessant/lock-threads@v5.0.1
with:
github-token: ${{ github.token }}
process-only: "issues, prs"

View File

@@ -20,15 +20,15 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@v4.6.2
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@v4.6.2
with:
name: translations
path: translations.tar.gz

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
uses: relative-ci/agent-action@v3.0.0
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
- uses: release-drafter/release-drafter@v6.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -23,10 +23,10 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@v2.3.2
with:
files: |
dist/*.whl
@@ -73,9 +73,8 @@ jobs:
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
echo "home-assistant-frontend==$version" > ./requirements.txt
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels
uses: home-assistant/wheels@2025.12.0
uses: home-assistant/wheels@2025.03.0
with:
abi: cp313
tag: musllinux_1_2
@@ -91,9 +90,9 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -108,7 +107,7 @@ jobs:
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@v2.3.2
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -120,9 +119,9 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -137,6 +136,6 @@ jobs:
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@v2.3.2
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -1,58 +0,0 @@
name: Restrict task creation
# yamllint disable-line rule:truthy
on:
issues:
types: [opened]
jobs:
check-authorization:
runs-on: ubuntu-latest
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.type.name == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const issueAuthor = context.payload.issue.user.login;
// Check if user is an organization member
try {
await github.rest.orgs.checkMembershipForUser({
org: 'home-assistant',
username: issueAuthor
});
console.log(`✅ ${issueAuthor} is an organization member`);
return; // Authorized
} catch (error) {
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
}
// Close the issue with a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
`If you would like to:\n` +
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
`If you believe you should have access to create Task issues, please contact the maintainers.`
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
});
// Add a label to indicate this was auto-closed
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['auto-closed']
});

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

@@ -1,7 +1,6 @@
name: Translations
on:
workflow_dispatch:
push:
branches:
- dev
@@ -14,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v4.2.2
- name: Upload Translations
run: |

2
.nvmrc
View File

@@ -1 +1 @@
22.21.1
lts/iron

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.12.0.cjs
yarnPath: .yarn/releases/yarn-4.9.2.cjs

View File

@@ -1 +0,0 @@
.github/copilot-instructions.md

View File

@@ -1,8 +0,0 @@
# People marked here will be automatically requested for a review
# when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Part of the frontend that mobile developper should review
src/external_app/ @bgoncal @TimoPtr
test/external_app/ @bgoncal @TimoPtr

View File

@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
module.exports.emptyPackages = ({ isHassioBuild }) =>
[
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
(isHassioBuild || isLandingPageBuild) &&
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
(isHassioBuild || isLandingPageBuild) &&
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -183,6 +183,7 @@ module.exports.babelOptions = ({
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@shoelace-style",
"@?lit(?:-labs|-element|-html)?",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},
@@ -337,7 +338,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
isLandingPageBuild: true,
};
},
};

View File

@@ -156,9 +156,7 @@ const createTestTranslation = () =>
*/
const createMasterTranslation = () =>
gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
allowEmpty: true,
})
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));

View File

@@ -41,7 +41,6 @@ const createRspackConfig = ({
isStatsBuild,
isTestBuild,
isHassioBuild,
isLandingPageBuild,
dontHash,
}) => {
if (!dontHash) {
@@ -169,9 +168,7 @@ const createRspackConfig = ({
},
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
),
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),
@@ -260,6 +257,7 @@ const createRspackConfig = ({
),
},
experiments: {
layers: true,
outputModule: true,
},
};

View File

@@ -14,5 +14,5 @@
"name": "Home Assistant Cast",
"short_name": "HA Cast",
"start_url": "/?homescreen=1",
"theme_color": "#009ac7"
"theme_color": "#03A9F4"
}

View File

@@ -1,3 +1,5 @@
import "@material/mwc-button/mwc-button";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import type { Auth, Connection } from "home-assistant-js-websocket";
@@ -16,7 +18,6 @@ import {
} from "../../../../src/common/auth/token_storage";
import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-list";
import "../../../../src/components/ha-list-item";
@@ -28,6 +29,7 @@ import {
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout";
@customElement("hc-cast")
@@ -61,20 +63,12 @@ class HcCast extends LitElement {
<p class="question action-item">
Stay logged in?
<span>
<ha-button
appearance="plain"
size="small"
@click=${this._handleSaveTokens}
>
<mwc-button @click=${this._handleSaveTokens}>
YES
</ha-button>
<ha-button
appearance="plain"
size="small"
@click=${this._handleSkipSaveTokens}
>
</mwc-button>
<mwc-button @click=${this._handleSkipSaveTokens}>
NO
</ha-button>
</mwc-button>
</span>
</p>
`
@@ -84,10 +78,10 @@ class HcCast extends LitElement {
: !this.castManager.status
? html`
<p class="center-item">
<ha-button @click=${this._handleLaunch}>
<ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon>
<mwc-button raised @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting
</ha-button>
</mwc-button>
</p>
`
: html`
@@ -95,9 +89,7 @@ class HcCast extends LitElement {
<ha-list @action=${this._handlePickView} activatable>
${(
this.lovelaceViews ?? [
{
title: "Home",
},
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) => html`
@@ -129,22 +121,14 @@ class HcCast extends LitElement {
<div class="card-actions">
${this.castManager.status
? html`
<ha-button appearance="plain" @click=${this._handleLaunch}>
<ha-svg-icon
slot="start"
.path=${mdiCastConnected}
></ha-svg-icon>
<mwc-button @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon>
Manage
</ha-button>
</mwc-button>
`
: ""}
<div class="spacer"></div>
<ha-button
variant="danger"
appearance="plain"
@click=${this._handleLogout}
>Log out</ha-button
>
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
</div>
</hc-layout>
`;
@@ -243,7 +227,7 @@ class HcCast extends LitElement {
}
.question:before {
border-radius: var(--ha-border-radius-sm);
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
@@ -261,6 +245,13 @@ class HcCast extends LitElement {
color: var(--secondary-text-color);
}
mwc-button ha-svg-icon {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
height: 18px;
}
ha-list-item ha-icon,
ha-list-item ha-svg-icon {
padding: 12px;

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js";
import type {
Auth,
@@ -27,7 +28,6 @@ import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-button";
const seeFAQ = (qid) => html`
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
@@ -83,14 +83,11 @@ export class HcConnect extends LitElement {
Unable to connect to ${tokens!.hassUrl}.
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/">Retry</ha-button>
<a href="/">
<mwc-button> Retry </mwc-button>
</a>
<div class="spacer"></div>
<ha-button
appearance="plain"
variant="danger"
@click=${this._handleLogout}
>Log out</ha-button
>
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
</div>
</hc-layout>
`;
@@ -131,19 +128,16 @@ export class HcConnect extends LitElement {
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
</div>
<div class="card-actions">
<ha-button appearance="plain" @click=${this._handleDemo}>
<mwc-button @click=${this._handleDemo}>
Show Demo
<ha-svg-icon
slot="end"
.path=${this.castManager.castState === "CONNECTED"
? mdiCastConnected
: mdiCast}
></ha-svg-icon>
</ha-button>
</mwc-button>
<div class="spacer"></div>
<ha-button appearance="plain" @click=${this._handleConnect}
>Authorize</ha-button
>
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
</div>
</hc-layout>
`;
@@ -315,6 +309,10 @@ export class HcConnect extends LitElement {
color: darkred;
}
mwc-button ha-svg-icon {
margin-left: 8px;
}
.spacer {
flex: 1;
}

View File

@@ -95,8 +95,7 @@ class HcLayout extends LitElement {
}
.hero {
border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm)
var(--ha-border-radius-square) var(--ha-border-radius-square);
border-radius: 4px 4px 0 0;
}
.subtitle {
font-size: var(--ha-font-size-m);

View File

@@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () =>
longitude: 4.8903147,
radius: 100,
friendly_name: "Home",
icon: "mdi:home",
icon: "hass:home",
},
},
"input_number.harmonyvolume": {
@@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () =>
step: 1,
mode: "slider",
friendly_name: "Volume",
icon: "mdi:volume-high",
icon: "hass:volume-high",
},
},
"climate.upstairs": {

View File

@@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
type: "weblink",
url: "/lovelace/climate",
name: "Climate controls",
icon: "mdi:arrow-right",
icon: "hass:arrow-right",
},
],
},
@@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
type: "weblink",
url: "/lovelace/overview",
name: "Back",
icon: "mdi:arrow-left",
icon: "hass:arrow-left",
},
],
},

View File

@@ -305,8 +305,9 @@ export class HcMain extends HassElement {
await llColl.refresh();
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
if (isStrategyDashboard(rawConfig)) {
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig,
this.hass!
@@ -346,8 +347,9 @@ export class HcMain extends HassElement {
}
private async _generateDefaultLovelaceConfig() {
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);

View File

@@ -75,5 +75,5 @@
"name": "Home Assistant Demo",
"short_name": "HA Demo",
"start_url": "/?homescreen=1",
"theme_color": "#009ac7"
"theme_color": "#03A9F4"
}

View File

@@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
state: "on",
attributes: {
friendly_name: "Home Automation",
icon: "mdi:home-automation",
icon: "hass:home-automation",
},
},
"input_boolean.tvtime": {

View File

@@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
title: "Home Assistant",
views: [
{
icon: "mdi:home-assistant",
icon: "hass:home-assistant",
id: "home",
title: "Home",
cards: [

View File

@@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
},
],
path: "security",
icon: "mdi:shield-home",
icon: "hass:shield-home",
name: "Security",
background:
'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',

View File

@@ -89,14 +89,11 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)}
</div>
<div class="actions small-hidden">
<ha-button
appearance="plain"
size="small"
href="https://www.home-assistant.io"
target="_blank"
>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
<a href="https://www.home-assistant.io" target="_blank">
<ha-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
</a>
</div>
</ha-card>
`;

View File

@@ -68,7 +68,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -76,7 +76,7 @@
padding-top: 48px;
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -1,4 +1,4 @@
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockDeviceRegistry = (

View File

@@ -44,24 +44,18 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
number_energy_price: null,
},
],
power: [
{ stat_rate: "sensor.power_grid" },
{ stat_rate: "sensor.power_grid_return" },
],
cost_adjustment_day: 0,
},
{
type: "solar",
stat_energy_from: "sensor.solar_production",
stat_rate: "sensor.power_solar",
config_entry_solar_forecast: ["solar_forecast"],
},
{
/* {
type: "battery",
stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input",
stat_rate: "sensor.power_battery",
},
}, */
{
type: "gas",
stat_energy_from: "sensor.energy_gas",
@@ -69,46 +63,25 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
entity_energy_price: null,
number_energy_price: null,
},
{
type: "water",
stat_energy_from: "sensor.energy_water",
stat_cost: "sensor.energy_water_cost",
entity_energy_price: null,
number_energy_price: null,
},
],
device_consumption: [
{
stat_consumption: "sensor.energy_car",
stat_rate: "sensor.power_car",
},
{
stat_consumption: "sensor.energy_ac",
stat_rate: "sensor.power_ac",
},
{
stat_consumption: "sensor.energy_washing_machine",
stat_rate: "sensor.power_washing_machine",
},
{
stat_consumption: "sensor.energy_dryer",
stat_rate: "sensor.power_dryer",
},
{
stat_consumption: "sensor.energy_heat_pump",
stat_rate: "sensor.power_heat_pump",
},
{
stat_consumption: "sensor.energy_boiler",
stat_rate: "sensor.power_boiler",
},
],
device_consumption_water: [
{
stat_consumption: "sensor.water_kitchen",
},
{
stat_consumption: "sensor.water_garden",
},
],
})

View File

@@ -154,38 +154,6 @@ export const energyEntities = () =>
unit_of_measurement: "EUR",
},
},
"sensor.power_grid": {
entity_id: "sensor.power_grid",
state: "500",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_grid_return": {
entity_id: "sensor.power_grid_return",
state: "-100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_solar": {
entity_id: "sensor.power_solar",
state: "200",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_battery": {
entity_id: "sensor.power_battery",
state: "100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.energy_gas_cost": {
entity_id: "sensor.energy_gas_cost",
state: "2",
@@ -203,15 +171,6 @@ export const energyEntities = () =>
unit_of_measurement: "m³",
},
},
"sensor.energy_water": {
entity_id: "sensor.energy_water",
state: "4000",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Water",
unit_of_measurement: "L",
},
},
"sensor.energy_car": {
entity_id: "sensor.energy_car",
state: "4",
@@ -266,58 +225,4 @@ export const energyEntities = () =>
unit_of_measurement: "kWh",
},
},
"sensor.power_car": {
entity_id: "sensor.power_car",
state: "40",
attributes: {
state_class: "measurement",
friendly_name: "Electric car",
unit_of_measurement: "W",
},
},
"sensor.power_ac": {
entity_id: "sensor.power_ac",
state: "30",
attributes: {
state_class: "measurement",
friendly_name: "Air conditioning",
unit_of_measurement: "W",
},
},
"sensor.power_washing_machine": {
entity_id: "sensor.power_washing_machine",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Washing machine",
unit_of_measurement: "W",
},
},
"sensor.power_dryer": {
entity_id: "sensor.power_dryer",
state: "55",
attributes: {
state_class: "measurement",
friendly_name: "Dryer",
unit_of_measurement: "W",
},
},
"sensor.power_heat_pump": {
entity_id: "sensor.power_heat_pump",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Heat pump",
unit_of_measurement: "W",
},
},
"sensor.power_boiler": {
entity_id: "sensor.power_boiler",
state: "70",
attributes: {
state_class: "measurement",
friendly_name: "Boiler",
unit_of_measurement: "W",
},
},
});

View File

@@ -1,4 +1,4 @@
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (

View File

@@ -1,4 +1,4 @@
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockLabelRegistry = (

View File

@@ -17,15 +17,17 @@ const generateMeanStatistics = (
end: Date,
// eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = delta;
const mean = lastVal + delta;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
@@ -36,6 +38,7 @@ const generateMeanStatistics = (
state: mean,
sum: null,
});
lastVal = mean;
currentDate =
period === "day"
? addDays(currentDate, 1)
@@ -333,6 +336,7 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
start,
end,
period,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}

View File

@@ -1,11 +1,11 @@
import "@material/mwc-button/mwc-button";
import type { Button } from "@material/mwc-button";
import type { TemplateResult } from "lit";
import { html, LitElement, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import type { HaButton } from "../../../src/components/ha-button";
@customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement {
@@ -25,9 +25,12 @@ class DemoBlackWhiteRow extends LitElement {
<slot name="light"></slot>
</div>
<div class="card-actions">
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}>
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</ha-button>
</mwc-button>
</div>
</ha-card>
</div>
@@ -37,9 +40,12 @@ class DemoBlackWhiteRow extends LitElement {
<slot name="dark"></slot>
</div>
<div class="card-actions">
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}>
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</ha-button>
</mwc-button>
</div>
</ha-card>
${this.value
@@ -68,7 +74,7 @@ class DemoBlackWhiteRow extends LitElement {
}
handleSubmit(ev) {
const content = (ev.target as HaButton).closest(".content")!;
const content = (ev.target as Button).closest(".content")!;
fireEvent(this, "submitted" as any, {
slot: content.classList.contains("light") ? "light" : "dark",
});

View File

@@ -1,11 +1,10 @@
import { LitElement, css, html, nothing } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import type { HomeAssistant } from "../../../src/types";
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
@customElement("demo-more-info")
class DemoMoreInfo extends LitElement {
@@ -22,13 +21,11 @@ class DemoMoreInfo extends LitElement {
<div class="root">
<div id="card">
<ha-card>
${!computeShowNewMoreInfo(state)
? html`<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>`
: nothing}
<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>
<more-info-content
.hass=${this.hass}

View File

@@ -1106,7 +1106,7 @@ export default {
friendly_name: "Philips Hue",
entity_picture: null,
description:
"Press the button on the bridge to register Philips Hue with Home Assistant.",
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Description image](/static/images/config_philips_hue.jpg)",
submit_caption: "I have pressed the button",
},
last_changed: "2018-07-19T10:44:46.515160+00:00",

View File

@@ -17,10 +17,6 @@ export const createMediaPlayerEntities = () => [
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
source: "AirPlay",
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
sound_mode: "Music",
}),
getEntity("media_player", "music_playing", "playing", {
friendly_name: "Playing The Music",
@@ -28,8 +24,8 @@ export const createMediaPlayerEntities = () => [
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping
supported_features: 784959,
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 195135,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
media_position: 0,
@@ -38,9 +34,6 @@ export const createMediaPlayerEntities = () => [
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
sound_mode: "Music",
group_members: ["media_player.playing", "media_player.stream_playing"],
}),
getEntity("media_player", "stream_playing", "playing", {
friendly_name: "Playing the Stream",
@@ -156,18 +149,15 @@ export const createMediaPlayerEntities = () => [
}),
getEntity("media_player", "receiver_on", "on", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
volume_level: 0.63,
is_volume_muted: false,
source: "TV",
sound_mode: "Movie",
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,
}),
getEntity("media_player", "receiver_off", "off", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,

View File

@@ -208,7 +208,7 @@ class HaGallery extends LitElement {
}
.sidebar a[active]::before {
border-radius: var(--ha-border-radius-lg);
border-radius: 12px;
position: absolute;
top: 0;
right: 2px;
@@ -241,7 +241,7 @@ class HaGallery extends LitElement {
text-align: center;
margin: 16px;
padding: 16px;
border-radius: var(--ha-border-radius-lg);
border-radius: 12px;
background-color: var(--primary-background-color);
}

View File

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], this._action)
? describeAction(this.hass, [], [], {}, this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { LegacyTrigger } from "../../../../src/data/automation";
import type { Trigger } from "../../../../src/data/automation";
import { describeTrigger } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -66,7 +66,7 @@ const triggers = [
},
];
const initialTrigger: LegacyTrigger = {
const initialTrigger: Trigger = {
trigger: "state",
entity_id: "light.kitchen",
};

View File

@@ -18,6 +18,7 @@ import { HaDeviceAction } from "../../../../src/panels/config/automation/action/
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media";
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
@@ -31,6 +32,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },

View File

@@ -18,6 +18,7 @@ import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
@@ -37,6 +38,11 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [{ ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],

View File

@@ -1,3 +0,0 @@
---
title: Adaptive dialog (ha-adaptive-dialog)
---

View File

@@ -1,732 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-adaptive-dialog";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type DialogType =
| false
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "form"
| "form-block-mode"
| "actions"
| "large"
| "small";
@customElement("demo-components-ha-adaptive-dialog")
export class DemoHaAdaptiveDialog extends LitElement {
@state() private _openDialog: DialogType = false;
@state() private _hass?: HomeAssistant;
protected firstUpdated() {
const hass = provideHass(this);
this._hass = hass;
}
protected render() {
return html`
<div class="content">
<h1>Adaptive dialog <code>&lt;ha-adaptive-dialog&gt;</code></h1>
<p class="subtitle">
Responsive dialog component that automatically switches between a full
dialog and bottom sheet based on screen size.
</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenDialog("basic")}
>Basic adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
>Adaptive dialog with subtitle below</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
>Adaptive dialog with subtitle above</ha-button
>
<ha-button @click=${this._handleOpenDialog("small")}
>Small width adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("large")}
>Large width adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("form")}
>Adaptive dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
>Adaptive dialog with form (block mode change)</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Adaptive dialog with actions</ha-button
>
</div>
<ha-card>
<div class="card-content">
<p>
<strong>Tip:</strong> Resize your browser window to see the
responsive behavior. The dialog automatically switches to a bottom
sheet on narrow screens (&lt;870px width) or short screens
(&lt;500px height).
</p>
</div>
</ha-card>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic"}
header-title="Basic adaptive dialog"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-below"}
header-title="Adaptive dialog with subtitle"
header-subtitle="This is an adaptive dialog with a subtitle below"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-above"}
header-title="Adaptive dialog with subtitle above"
header-subtitle="This is an adaptive dialog with a subtitle above"
header-subtitle-position="above"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "small"}
width="small"
header-title="Small adaptive dialog"
@closed=${this._handleClosed}
>
<div>This dialog uses the small width preset (320px).</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "large"}
width="large"
header-title="Large adaptive dialog"
@closed=${this._handleClosed}
>
<div>This dialog uses the large width preset (1024px).</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form"}
header-title="Adaptive dialog with form"
header-subtitle="This is an adaptive dialog with a form"
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form-block-mode"}
header-title="Adaptive dialog with form (block mode change)"
header-subtitle="This form will not reset when the viewport size changes"
block-mode-change
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "actions"}
header-title="Adaptive dialog with actions"
header-subtitle="This is an adaptive dialog with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<h2>Design</h2>
<h3>Responsive behavior</h3>
<p>
The <code>ha-adaptive-dialog</code> component automatically switches
between two modes based on screen size:
</p>
<ul>
<li>
<strong>Dialog mode:</strong> Used on larger screens (width &gt;
870px and height &gt; 500px). Renders as a centered dialog using
<code>ha-wa-dialog</code>.
</li>
<li>
<strong>Bottom sheet mode:</strong> Used on mobile devices and
smaller screens (width ≤ 870px or height ≤ 500px). Renders as a
drawer from the bottom using <code>ha-bottom-sheet</code>.
</li>
</ul>
<p>
The mode is determined automatically and updates when the window is
resized. To prevent mode changes after the initial mount (useful for
preventing form resets), use the <code>block-mode-change</code>
attribute.
</p>
<h3>Width</h3>
<p>
In dialog mode, there are multiple width presets available. These are
ignored in bottom sheet mode.
</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>small</code></td>
<td><code>min(320px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>medium</code></td>
<td><code>min(580px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(1024px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
<td><code>var(--full-width)</code></td>
</tr>
</tbody>
</table>
<p>Adaptive dialogs have a default width of <code>medium</code>.</p>
<h3>Header</h3>
<p>
The header contains a navigation icon, title, subtitle, and action
items.
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>headerNavigationIcon</code></td>
<td>
Leading header action (e.g., close/back button). In bottom sheet
mode, defaults to a close button if not provided.
</td>
</tr>
<tr>
<td><code>headerTitle</code></td>
<td>The header title content.</td>
</tr>
<tr>
<td><code>headerSubtitle</code></td>
<td>The header subtitle content.</td>
</tr>
<tr>
<td><code>headerActionItems</code></td>
<td>Trailing header actions (e.g., icon buttons, menus).</td>
</tr>
</tbody>
</table>
<h4>Header title</h4>
<p>
The header title can be set using the <code>header-title</code>
attribute or by providing custom content in the
<code>headerTitle</code> slot.
</p>
<h4>Header subtitle</h4>
<p>
The header subtitle can be set using the
<code>header-subtitle</code> attribute or by providing custom content
in the <code>headerSubtitle</code> slot. The subtitle position
relative to the title can be controlled with the
<code>header-subtitle-position</code> attribute.
</p>
<h4>Header navigation icon</h4>
<p>
In bottom sheet mode, a close button is automatically provided if no
custom navigation icon is specified. In dialog mode, the dialog can be
closed via the standard dialog close button.
</p>
<h4>Header action items</h4>
<p>
The header action items usually contain icon buttons and/or menu
buttons.
</p>
<h3>Body</h3>
<p>The body is the content of the adaptive dialog.</p>
<h3>Footer</h3>
<p>The footer is the footer of the adaptive dialog.</p>
<p>
It is recommended to use the <code>ha-dialog-footer</code> component
for the footer and to style the buttons inside the footer as follows:
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
<th>Variant to use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>secondaryAction</code></td>
<td>The secondary action button(s).</td>
<td><code>plain</code></td>
</tr>
<tr>
<td><code>primaryAction</code></td>
<td>The primary action button(s).</td>
<td><code>accent</code></td>
</tr>
</tbody>
</table>
<h2>Implementation</h2>
<h3>When to use</h3>
<p>
Use <code>ha-adaptive-dialog</code> when you need a dialog that should
adapt to different screen sizes automatically. This is particularly
useful for:
</p>
<ul>
<li>Forms and data entry that need to work well on mobile devices</li>
<li>
Content that benefits from full-screen presentation on small devices
</li>
<li>
Interfaces that need consistent behavior across desktop and mobile
</li>
</ul>
<p>
If you don't need responsive behavior, use
<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>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>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
width="medium"
header-title="Dialog title"
header-subtitle="Dialog subtitle"
&gt;
&lt;div slot="headerActionItems"&gt;
&lt;ha-icon-button label="Settings" path="mdiCog"&gt;&lt;/ha-icon-button&gt;
&lt;ha-icon-button label="Help" path="mdiHelp"&gt;&lt;/ha-icon-button&gt;
&lt;/div&gt;
&lt;div&gt;Dialog content&lt;/div&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Submit&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<p>Example with <code>block-mode-change</code> for forms:</p>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
header-title="Edit configuration"
block-mode-change
&gt;
&lt;ha-form .schema=\${schema} .data=\${data}&gt;&lt;/ha-form&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Save&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<h3>API</h3>
<p>
This component combines <code>ha-wa-dialog</code> and
<code>ha-bottom-sheet</code> with automatic mode switching based on
screen size.
</p>
<h4>Attributes</h4>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>open</code></td>
<td>Controls the adaptive dialog open state.</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>
Preferred dialog width preset (dialog mode only, ignored in
bottom sheet mode).
</td>
<td><code>medium</code></td>
<td>
<code>small</code>, <code>medium</code>, <code>large</code>,
<code>full</code>
</td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle</code></td>
<td>
Header subtitle text when no custom subtitle slot is provided.
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle-position</code></td>
<td>Position of the subtitle relative to the title.</td>
<td><code>below</code></td>
<td><code>above</code>, <code>below</code></td>
</tr>
<tr>
<td><code>aria-labelledby</code></td>
<td>
The ID of the element that labels the dialog (for
accessibility).
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>aria-describedby</code></td>
<td>
The ID of the element that describes the dialog (for
accessibility).
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>block-mode-change</code></td>
<td>
When set, the mode is determined at mount time based on the
current screen size, but subsequent mode changes are blocked.
Useful for preventing forms from resetting when the viewport
size changes.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
</tbody>
</table>
<h4>CSS custom properties</h4>
<table>
<thead>
<tr>
<th>CSS Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog/sheet background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-show-duration</code></td>
<td>Show animation duration (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration (dialog mode only).</td>
</tr>
</tbody>
</table>
<h4>Events</h4>
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opened</code></td>
<td>
Fired when the adaptive dialog is shown (dialog mode only).
</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>
Fired after the adaptive dialog is hidden (dialog mode only).
</td>
</tr>
<tr>
<td><code>after-show</code></td>
<td>Fired after show animation completes (dialog mode only).</td>
</tr>
</tbody>
</table>
<h3>Focus management</h3>
<p>
To automatically focus an element when the adaptive dialog opens, add
the
<code>autofocus</code> attribute to it. Components with
<code>delegatesFocus: true</code> (like <code>ha-form</code>) will
forward focus to their first focusable child.
</p>
<p>Example:</p>
<pre><code>&lt;ha-adaptive-dialog .hass=\${this.hass} open&gt;
&lt;ha-form autofocus .schema=\${schema}&gt;&lt;/ha-form&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
</div>
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
this._openDialog = dialog;
};
private _handleClosed = () => {
this._openDialog = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
h3,
h4 {
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-2);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
ul {
margin: var(--ha-space-2) 0;
padding-left: var(--ha-space-5);
}
li {
margin: var(--ha-space-1) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--ha-space-3) 0;
}
th,
td {
text-align: left;
padding: var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
th {
font-weight: 500;
}
code {
background-color: var(--secondary-background-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
pre {
background-color: var(--secondary-background-color);
padding: var(--ha-space-3);
border-radius: 8px;
overflow-x: auto;
margin: var(--ha-space-3) 0;
}
pre code {
background-color: transparent;
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
.card-content {
padding: var(--ha-space-3);
}
a {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-adaptive-dialog": DemoHaAdaptiveDialog;
}
}

View File

@@ -147,13 +147,13 @@ The `title ` option should not be used without a description.
<ha-alert alert-type="success">
This is a success alert — check it out!
<ha-button slot="action">Undo</ha-button>
<mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert>
```html
<ha-alert alert-type="success">
This is a success alert — check it out!
<ha-button slot="action">Undo</ha-button>
<mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert>
```

View File

@@ -1,10 +1,10 @@
import "@material/mwc-button/mwc-button";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-logo-svg";
const alerts: {
@@ -78,13 +78,13 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action",
type: "error",
actionSlot: html`<ha-button size="small" slot="action">restart</ha-button>`,
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
},
{
title: "Unsaved data",
description: "You have unsaved data",
type: "warning",
actionSlot: html`<ha-button size="small" slot="action">save</ha-button>`,
actionSlot: html`<mwc-button slot="action" label="save"></mwc-button>`,
},
{
title: "Slotted icon",
@@ -108,7 +108,7 @@ const alerts: {
title: "Slotted action",
description: "Alert with slotted action",
type: "info",
actionSlot: html`<ha-button slot="action">action</ha-button>`,
actionSlot: html`<mwc-button slot="action" label="action"></mwc-button>`,
},
{
description: "Dismissable information (RTL)",
@@ -120,7 +120,7 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action (RTL)",
type: "error",
actionSlot: html`<ha-button slot="action">restart</ha-button>`,
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
rtl: true,
},
{
@@ -211,7 +211,7 @@ export class DemoHaAlert extends LitElement {
max-height: 24px;
width: 24px;
}
ha-button {
mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
`;

View File

@@ -117,7 +117,7 @@ export class DemoHaBadge extends LitElement {
}
.card-content {
display: flex;
gap: var(--ha-space-6);
gap: 24px;
}
`;
}

View File

@@ -1,67 +0,0 @@
---
title: Button
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Button `<ha-button>`
## Implementation
### Example Usage
<div class="wrapper">
<ha-button>
simple button
</ha-button>
<ha-button appearance="plain">
plain button
</ha-button>
<ha-button appearance="filled">
filled button
</ha-button>
<ha-button size="small">
small
</ha-button>
</div>
```html
<ha-button> simple button </ha-button>
<ha-button size="small"> small </ha-button>
```
### API
This component is based on the webawesome button component.
Check the [webawesome documentation](https://webawesome.com/docs/components/button/) for more details.
**Slots**
- default slot: Label of the button
` - no default
- `start`: The prefix container (usually for icons).
` - no default
- `end`: The suffix container (usually for icons).
` - no default
**Properties/Attributes**
| Name | Type | Default | Description |
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| size | "small"/"medium" | "medium" | Sets the button size. |
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
| disabled | Boolean | false | Disables the button and prevents user interaction. |
**CSS Custom Properties**
- `--ha-button-height` - Height of the button.
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--ha-border-radius-pill)`.

View File

@@ -1,171 +0,0 @@
import { mdiHome } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import { titleCase } from "../../../../src/common/string/title-case";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
const appearances = ["accent", "filled", "plain"];
const variants = ["brand", "danger", "neutral", "warning", "success"];
@customElement("demo-components-ha-button")
export class DemoHaButton extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-button in ${mode}">
<div class="card-content">
${variants.map(
(variant) => html`
<div>
${appearances.map(
(appearance) => html`
<ha-button
.appearance=${appearance}
.variant=${variant}
>
<ha-svg-icon
.path=${mdiHomeAssistant}
slot="start"
></ha-svg-icon>
${titleCase(`${variant} ${appearance}`)}
<ha-svg-icon
.path=${mdiHome}
slot="end"
></ha-svg-icon>
</ha-button>
`
)}
</div>
<div>
${appearances.map(
(appearance) => html`
<ha-button
.appearance=${appearance}
.variant=${variant}
size="small"
>
${titleCase(`${variant} ${appearance}`)}
</ha-button>
`
)}
</div>
<div>
${appearances.map(
(appearance) => html`
<ha-button
.appearance=${appearance}
.variant=${variant}
loading
>
<ha-svg-icon
.path=${mdiHomeAssistant}
slot="start"
></ha-svg-icon>
${titleCase(`${variant} ${appearance}`)}
<ha-svg-icon
.path=${mdiHome}
slot="end"
></ha-svg-icon>
</ha-button>
`
)}
</div>
`
)}
${variants.map(
(variant) => html`
<div>
${appearances.map(
(appearance) => html`
<ha-button
.variant=${variant}
.appearance=${appearance}
disabled
>
${titleCase(`${appearance}`)}
</ha-button>
`
)}
</div>
<div>
${appearances.map(
(appearance) => html`
<ha-button
.variant=${variant}
.appearance=${appearance}
size="small"
disabled
>
${titleCase(`${appearance}`)}
</ha-button>
`
)}
</div>
`
)}
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
.card-content div {
display: flex;
gap: var(--ha-space-2);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-button": DemoHaButton;
}
}

View File

@@ -9,10 +9,10 @@ import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-button";
import "../../../../src/components/ha-control-button-group";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-control-button-group";
interface Button {
label: string;
@@ -156,17 +156,17 @@ export class DemoHaBarButton extends LitElement {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: var(--ha-border-radius-xl);
--control-button-border-radius: 18px;
height: 100px;
width: 100px;
}
.custom-group {
--control-button-group-thickness: 100px;
--control-button-group-border-radius: var(--ha-border-radius-6xl);
--control-button-group-border-radius: 36px;
--control-button-group-spacing: 20px;
}
.custom-group ha-control-button {
--control-button-border-radius: var(--ha-border-radius-xl);
--control-button-border-radius: 18px;
--mdc-icon-size: 32px;
}
.vertical-buttons {

View File

@@ -1,10 +1,10 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-number-buttons";
import { repeat } from "lit/directives/repeat";
import { ifDefined } from "lit/directives/if-defined";
const buttons: {
id: string;
@@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement {
--control-number-buttons-background-color: #2196f3;
--control-number-buttons-background-opacity: 0.1;
--control-number-buttons-thickness: 100px;
--control-number-buttons-border-radius: var(--ha-border-radius-6xl);
--control-number-buttons-border-radius: 36px;
}
`;
}

View File

@@ -131,7 +131,7 @@ export class DemoHaControlSelectMenu extends LitElement {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: var(--ha-border-radius-xl);
--control-button-border-radius: 18px;
height: 100px;
width: 100px;
}

View File

@@ -135,7 +135,7 @@ export class DemoHaControlSelect extends LitElement {
.options=${options}
class=${ifDefined(config.class)}
@value-changed=${this.handleValueChanged}
.label=${label}
aria-labelledby=${id}
?disabled=${config.disabled}
>
</ha-control-select>
@@ -156,7 +156,7 @@ export class DemoHaControlSelect extends LitElement {
vertical
class=${ifDefined(config.class)}
@value-changed=${this.handleValueChanged}
.label=${label}
aria-labelledby=${id}
?disabled=${config.disabled}
>
</ha-control-select>
@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: var(--ha-border-radius-6xl);
--control-select-border-radius: 36px;
}
.vertical-selects {
height: 300px;

View File

@@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-slider";
import "../../../../src/components/ha-card";
const sliders: {
id: string;
@@ -97,7 +97,7 @@ export class DemoHaBarSlider extends LitElement {
class=${ifDefined(config.class)}
@value-changed=${this.handleValueChanged}
@slider-moved=${this.handleSliderMoved}
.label=${label}
aria-labelledby=${id}
.unit=${config.unit}
>
</ha-control-slider>
@@ -119,7 +119,7 @@ export class DemoHaBarSlider extends LitElement {
class=${ifDefined(config.class)}
@value-changed=${this.handleValueChanged}
@slider-moved=${this.handleSliderMoved}
.label=${label}
aria-label=${label}
.unit=${config.unit}
>
</ha-control-slider>
@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
--control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 130px;
--control-slider-border-radius: var(--ha-border-radius-6xl);
--control-slider-border-radius: 36px;
}
.vertical-sliders {
height: 300px;

View File

@@ -9,8 +9,8 @@ import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-switch";
import "../../../../src/components/ha-card";
const switches: {
id: string;
@@ -63,7 +63,7 @@ export class DemoHaControlSwitch extends LitElement {
@change=${this.handleValueChanged}
.pathOn=${mdiLightbulb}
.pathOff=${mdiLightbulbOff}
.label=${label}
aria-labelledby=${id}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
@@ -84,7 +84,7 @@ export class DemoHaControlSwitch extends LitElement {
vertical
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.label=${label}
aria-label=${label}
.pathOn=${mdiGarageOpen}
.pathOff=${mdiGarage}
?disabled=${config.disabled}
@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
--control-switch-on-color: var(--green-color);
--control-switch-off-color: var(--red-color);
--control-switch-thickness: 130px;
--control-switch-border-radius: var(--ha-border-radius-6xl);
--control-switch-border-radius: 36px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}

View File

@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
# Material Design 3
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
# Guidelines
## Design
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- A best practice is to always use a title, even if it is optional by Material guidelines.
- People mainly read the title and a button. Put the most important information in those two.
- Try to avoid user generated content in the title, this could make the title unreadably long.
- Try to avoid user generated content in the title, this could make the title unreadable long.
- If users become unsure, they read the description. Make sure this explains what will happen.
- Strive for minimalism.

View File

@@ -1,55 +0,0 @@
---
title: Dropdown
---
# Dropdown `<ha-dropdown>`
## Implementation
A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdown` supports composed menu items (`<ha-dropdown-item>`) for icons, submenus, checkboxes, disabled entries, and destructive variants. Use composition with `slot="trigger"` to control the trigger button and use `<ha-dropdown-item>` for rich item content.
### Example usage (composition)
```html
<ha-dropdown>
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
<ha-dropdown-item>
<ha-svg-icon .path="mdiContentCut" slot="icon"></ha-svg-icon>
Cut
</ha-dropdown-item>
<ha-dropdown-item>
<ha-svg-icon .path="mdiContentCopy" slot="icon"></ha-svg-icon>
Copy
</ha-dropdown-item>
<ha-dropdown-item disabled>
<ha-svg-icon .path="mdiContentPaste" slot="icon"></ha-svg-icon>
Paste
</ha-dropdown-item>
<ha-dropdown-item>
Show images
<ha-dropdown-item slot="submenu" value="show-all-images"
>Show all images</ha-dropdown-item
>
<ha-dropdown-item slot="submenu" value="show-thumbnails"
>Show thumbnails</ha-dropdown-item
>
</ha-dropdown-item>
<ha-dropdown-item type="checkbox" checked>Emoji shortcuts</ha-dropdown-item>
<ha-dropdown-item type="checkbox" checked>Word wrap</ha-dropdown-item>
<ha-dropdown-item variant="danger">
<ha-svg-icon .path="mdiDelete" slot="icon"></ha-svg-icon>
Delete
</ha-dropdown-item>
</ha-dropdown>
```
### API
This component is based on the webawesome dropdown component.
Check the [webawesome documentation](https://webawesome.com/docs/components/dropdown/) for more details.

View File

@@ -1,133 +0,0 @@
import "@home-assistant/webawesome/dist/components/button/button";
import "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import "@home-assistant/webawesome/dist/components/icon/icon";
import "@home-assistant/webawesome/dist/components/popup/popup";
import {
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
mdiDelete,
} from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dropdown";
import "../../../../src/components/ha-dropdown-item";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-svg-icon";
@customElement("demo-components-ha-dropdown")
export class DemoHaDropdown extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-button in ${mode}">
<div class="card-content">
<ha-dropdown>
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
<ha-dropdown-item>
<ha-svg-icon
.path=${mdiContentCut}
slot="icon"
></ha-svg-icon>
Cut
</ha-dropdown-item>
<ha-dropdown-item>
<ha-svg-icon
.path=${mdiContentCopy}
slot="icon"
></ha-svg-icon>
Copy
</ha-dropdown-item>
<ha-dropdown-item disabled>
<ha-svg-icon
.path=${mdiContentPaste}
slot="icon"
></ha-svg-icon>
Paste
</ha-dropdown-item>
<ha-dropdown-item>
Show images
<ha-dropdown-item slot="submenu" value="show-all-images"
>Show All Images</ha-dropdown-item
>
<ha-dropdown-item slot="submenu" value="show-thumbnails"
>Show Thumbnails</ha-dropdown-item
>
</ha-dropdown-item>
<ha-dropdown-item type="checkbox" checked
>Emoji Shortcuts</ha-dropdown-item
>
<ha-dropdown-item type="checkbox" checked
>Word Wrap</ha-dropdown-item
>
<ha-dropdown-item variant="danger">
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
Delete
</ha-dropdown-item>
</ha-dropdown>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.card-content div {
display: flex;
gap: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-dropdown": DemoHaDropdown;
}
}

View File

@@ -1,4 +1,5 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
@@ -11,11 +12,11 @@ import { computeInitialHaFormData } from "../../../../src/components/ha-form/com
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {

View File

@@ -1,37 +0,0 @@
---
title: Marquee Text
---
# Marquee Text `<ha-marquee-text>`
Marquee text component scrolls text horizontally if it overflows its container. It supports pausing on hover and customizable speed and pause duration.
## Implementation
### Example Usage
<ha-marquee-text style="width: 200px;">
This is a long text that will scroll horizontally if it overflows the container.
</ha-marquee-text>
```html
<ha-marquee-text style="width: 200px;">
This is a long text that will scroll horizontally if it overflows the
container.
</ha-marquee-text>
```
### API
**Slots**
- default slot: The text content to be displayed and scrolled.
- no default
**Properties/Attributes**
| Name | Type | Default | Description |
| -------------- | ------- | ------- | ---------------------------------------------------------------------------- |
| speed | number | `15` | The speed of the scrolling animation. Higher values result in faster scroll. |
| pause-on-hover | boolean | `true` | Whether to pause the scrolling animation when |
| pause-duration | number | `1000` | The delay in milliseconds before the scrolling animation starts/restarts. |

View File

@@ -1,25 +0,0 @@
import { css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-marquee-text";
@customElement("demo-components-ha-marquee-text")
export class DemoHaMarqueeText extends LitElement {
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-marquee-text": DemoHaMarqueeText;
}
}

View File

@@ -1,32 +0,0 @@
---
title: Progress Button
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Progress Button `<ha-progress-button>`
### API
This component is a wrapper around `<ha-button>` that adds support for showing progress
**Slots**
- default slot: Label of the button
` - no default
**Properties/Attributes**
| Name | Type | Default | Description |
| ---------- | ---------------------------------------------- | --------- | -------------------------------------------------- |
| label | string | "accent" | Sets the button label. |
| disabled | Boolean | false | Disables the button if true. |
| progress | Boolean | false | Shows a progress indicator on the button. |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| iconPath | string | undefined | Sets the icon path for the button. |

View File

@@ -1,139 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
@customElement("demo-components-ha-progress-button")
export class DemoHaProgressButton extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-progress-button in ${mode}">
<div class="card-content">
<ha-progress-button @click=${this._clickedSuccess}>
Success
</ha-progress-button>
<ha-progress-button @click=${this._clickedFail}>
Fail
</ha-progress-button>
<ha-progress-button size="small" @click=${this._clickedSuccess}>
small
</ha-progress-button>
<ha-progress-button
appearance="filled"
@click=${this._clickedSuccess}
>
filled
</ha-progress-button>
<ha-progress-button
appearance="plain"
@click=${this._clickedSuccess}
>
plain
</ha-progress-button>
<ha-progress-button
variant="warning"
@click=${this._clickedSuccess}
>
warning
</ha-progress-button>
<ha-progress-button
variant="neutral"
@click=${this._clickedSuccess}
label="with icon"
.iconPath=${mdiHomeAssistant}
>
With Icon
</ha-progress-button>
<ha-progress-button progress @click=${this._clickedSuccess}>
progress
</ha-progress-button>
<ha-progress-button disabled @click=${this._clickedSuccess}>
disabled
</ha-progress-button>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
private async _clickedSuccess(ev: CustomEvent): Promise<void> {
console.log("Clicked success");
const button = ev.currentTarget as any;
button.progress = true;
setTimeout(() => {
button.actionSuccess();
button.progress = false;
}, 1000);
}
private async _clickedFail(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
setTimeout(() => {
button.actionError();
button.progress = false;
}, 1000);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
.card-content div {
display: flex;
gap: var(--ha-space-2);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-progress-button": DemoHaProgressButton;
}
}

View File

@@ -131,7 +131,7 @@ export class DemoHaSelectBox extends LitElement {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: var(--ha-border-radius-6xl);
--control-select-border-radius: 36px;
}
p.title {

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
@@ -13,9 +14,9 @@ import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label_registry";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";

View File

@@ -1,38 +0,0 @@
---
title: Slider
subtitle: A slider component for selecting a value from a range.
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Slider `<ha-slider>`
## Implementation
### Example Usage
<div class="wrapper">
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="medium"></ha-slider>
</div>
```html
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="medium"></ha-slider>
```
### API
This component is based on the webawesome slider component.
Check the [webawesome documentation](https://webawesome.com/docs/components/slider/) for more details.
**CSS Custom Properties**
- `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`.
- `--ha-slider-thumb-color` - Color of the slider thumb. Defaults to `var(--primary-color)`.
- `--ha-slider-indicator-color` - Color of the filled portion of the slider track. Defaults to `var(--primary-color)`.

View File

@@ -1,100 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-bar";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-slider";
import type { HomeAssistant } from "../../../../src/types";
@customElement("demo-components-ha-slider")
export class DemoHaSlider extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-slider ${mode} demo">
<div class="card-content">
<span>Default (disabled)</span>
<ha-slider
disabled
min="0"
max="8"
value="4"
with-markers
></ha-slider>
<span>Small</span>
<ha-slider
size="small"
min="0"
max="8"
value="4"
with-markers
></ha-slider>
<span>Medium</span>
<ha-slider
size="medium"
min="0"
max="8"
value="4"
with-markers
></ha-slider>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
margin: 16px;
border-radius: var(--ha-border-radius-md);
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--ha-space-6);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-slider": DemoHaSlider;
}
}

View File

@@ -61,7 +61,7 @@ export class DemoHaSpinner extends LitElement {
background-color: var(--primary-background-color);
padding: 0 50px;
margin: 16px;
border-radius: var(--ha-border-radius-md);
border-radius: 8px;
}
ha-card {
margin: 24px auto;
@@ -70,7 +70,7 @@ export class DemoHaSpinner extends LitElement {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--ha-space-6);
gap: 24px;
}
`;
}

View File

@@ -6,23 +6,21 @@ A tooltip's target is its _first child element_, so you should only wrap one ele
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
<ha-button id="hover">Hover Me</ha-button>
<ha-tooltip for="hover">
This is a tooltip
<ha-tooltip content="This is a tooltip">
<ha-button>Hover Me</ha-button>
</ha-tooltip>
```
<ha-button id="hover">Hover Me</ha-button>
<ha-tooltip for="hover">
This is a tooltip
<ha-tooltip content="This is a tooltip">
<ha-button>Hover Me</ha-button>
</ha-tooltip>
```
## Documentation
This element is based on webawesome `wa-tooltip` it only sets some css tokens and has a custom show/hide animation.
This element is based on shoelace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
<a href="https://webawesome.com/docs/components/tooltip/" target="_blank" rel="noopener noreferrer">Webawesome documentation</a>
<a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a>
### HA style tokens
@@ -30,7 +28,7 @@ In your theme settings use this without the prefixed `--`.
- `--ha-tooltip-border-radius` (Default: 4px)
- `--ha-tooltip-arrow-size` (Default: 8px)
- `--wa-tooltip-font-family` (Default: `var(--ha-font-family-body)`)
- `--sl-tooltip-font-family` (Default: `var(--ha-font-family-body)`)
- `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`)
- `--wa-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`)
- `--wa-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`)
- `--sl-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`)
- `--sl-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`)

View File

@@ -1,3 +0,0 @@
---
title: Dialog (ha-wa-dialog)
---

View File

@@ -1,519 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-wa-dialog";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type DialogType =
| false
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "form"
| "actions";
@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>&lt;ha-wa-dialog&gt;</code></h1>
<p class="subtitle">Dialog component built with WebAwesome.</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenDialog("basic")}
>Basic dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
>Basic dialog with subtitle below</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
>Basic dialog with subtitle above</ha-button
>
<ha-button @click=${this._handleOpenDialog("form")}
>Dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Dialog with actions</ha-button
>
</div>
<ha-wa-dialog
.open=${this._openDialog === "basic"}
header-title="Basic dialog"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-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-wa-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"
header-subtitle-position="above"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-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"
prevent-scrim-close
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
data-dialog="close"
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "actions"}
header-title="Dialog with actions"
header-subtitle="This is a dialog with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Dialog content</div>
</ha-wa-dialog>
<h2>Design</h2>
<h3>Width</h3>
<p>There are multiple widths available for the dialog.</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>small</code></td>
<td><code>min(320px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>medium</code></td>
<td><code>min(580px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(1024px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
<td><code>var(--full-width)</code></td>
</tr>
</tbody>
</table>
<p>
<code>--full-width</code> is calculated based on the available width
of the screen. 95vw is the maximum width of the dialog on a large
screen, while on a small screen it is 100vw minus the safe area
insets.
</p>
<p>Dialogs have a default width of <code>medium</code>.</p>
<h3>Prevent scrim close</h3>
<p>
You can prevent the dialog from being closed by clicking the
scrim/overlay. This is allowed by default.
</p>
<h3>Header</h3>
<p>The header contains a title, a subtitle and action items.</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>header</code></td>
<td>The entire header area.</td>
</tr>
<tr>
<td><code>headerTitle</code></td>
<td>The header title text.</td>
</tr>
<tr>
<td><code>headerSubtitle</code></td>
<td>The header subtitle text.</td>
</tr>
<tr>
<td><code>headerActionItems</code></td>
<td>The header action items.</td>
</tr>
</tbody>
</table>
<h4>Header title</h4>
<p>The header title is a text string.</p>
<h4>Header subtitle</h4>
<p>The header subtitle is a text string.</p>
<h4>Header action items</h4>
<p>
The header action items usually containing icon buttons and/or menu
buttons.
</p>
<h3>Body</h3>
<p>The body is the content of the dialog.</p>
<h3>Footer</h3>
<p>The footer is the footer of the dialog.</p>
<p>
It is recommended to use the <code>ha-dialog-footer</code> component
for the footer and to style the buttons inside the footer as so:
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
<th>Variant to use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>secondaryAction</code></td>
<td>The secondary action button(s).</td>
<td><code>plain</code></td>
</tr>
<tr>
<td><code>primaryAction</code></td>
<td>The primary action button(s).</td>
<td><code>accent</code></td>
</tr>
</tbody>
</table>
<h2>Implementation</h2>
<h3>Example Usage</h3>
<pre><code>&lt;ha-wa-dialog
open
header-title="Dialog title"
header-subtitle="Dialog subtitle"
prevent-scrim-close
&gt;
&lt;div slot="headerActionItems"&gt;
&lt;ha-icon-button label="Settings" path="mdiCog"&gt;&lt;/ha-icon-button&gt;
&lt;ha-icon-button label="Help" path="mdiHelp"&gt;&lt;/ha-icon-button&gt;
&lt;/div&gt;
&lt;div&gt;Dialog content&lt;/div&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button data-dialog="close" slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Submit&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-wa-dialog&gt;</code></pre>
<h3>API</h3>
<p>
This component is based on the webawesome dialog component. Check the
<a
href="https://webawesome.com/docs/components/dialog/"
target="_blank"
rel="noopener noreferrer"
>webawesome documentation</a
>
for more details.
</p>
<h4>Attributes</h4>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>open</code></td>
<td>Controls the dialog open state.</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>Preferred dialog width preset.</td>
<td><code>medium</code></td>
<td>
<code>small</code>, <code>medium</code>, <code>large</code>,
<code>full</code>
</td>
</tr>
<tr>
<td><code>prevent-scrim-close</code></td>
<td>
Prevents closing the dialog by clicking the scrim/overlay.
</td>
<td><code>false</code></td>
<td><code>true</code></td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle</code></td>
<td>
Header subtitle text when no custom subtitle slot is provided.
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle-position</code></td>
<td>Position of the subtitle relative to the title.</td>
<td><code>below</code></td>
<td><code>above</code>, <code>below</code></td>
</tr>
<tr>
<td><code>flexcontent</code></td>
<td>
Makes the dialog body a flex container for flexible layouts.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
</tbody>
</table>
<h4>CSS Custom Properties</h4>
<table>
<thead>
<tr>
<th>CSS Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--dialog-content-padding</code></td>
<td>Padding for dialog content sections.</td>
</tr>
<tr>
<td><code>--ha-dialog-show-duration</code></td>
<td>Show animation duration.</td>
</tr>
<tr>
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>
<td>Top margin for the dialog surface.</td>
</tr>
</tbody>
</table>
<h4>Events</h4>
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opened</code></td>
<td>Fired when the dialog is shown.</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>Fired after the dialog is hidden.</td>
</tr>
</tbody>
</table>
</div>
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
this._openDialog = dialog;
};
private _handleClosed = () => {
this._openDialog = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
h3,
h4 {
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-2);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--ha-space-3) 0;
}
th,
td {
text-align: left;
padding: var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
th {
font-weight: 500;
}
code {
background-color: var(--secondary-background-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
pre {
background-color: var(--secondary-background-color);
padding: var(--ha-space-3);
border-radius: 8px;
overflow-x: auto;
margin: var(--ha-space-3) 0;
}
pre code {
background-color: transparent;
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
a {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-wa-dialog": DemoHaWaDialog;
}
}

View File

@@ -11,7 +11,6 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate";
import { FanEntityFeature } from "../../../../src/data/fan";
const ENTITIES = [
getEntity("switch", "tv_outlet", "on", {
@@ -101,15 +100,6 @@ const ENTITIES = [
ClimateEntityFeature.FAN_MODE +
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
}),
getEntity("fan", "fan_demo", "on", {
friendly_name: "Ceiling fan",
device_class: "fan",
direction: "reverse",
supported_features:
FanEntityFeature.DIRECTION +
FanEntityFeature.SET_SPEED +
FanEntityFeature.OSCILLATE,
}),
];
const CONFIGS = [
@@ -271,33 +261,6 @@ const CONFIGS = [
- type: target-temperature
`,
},
{
heading: "Fan direction feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-direction
`,
},
{
heading: "Fan speed feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-speed
`,
},
{
heading: "Fan oscillate feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-oscillate
`,
},
];
@customElement("demo-lovelace-tile-card")

View File

@@ -5,13 +5,13 @@ import type {
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { computeDomain } from "../../../../src/common/entity/compute_domain";
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
import "../../../../src/components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
import "../../../../src/components/entity/state-badge";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import type { HomeAssistant } from "../../../../src/types";
const SENSOR_DEVICE_CLASSES = [
@@ -39,7 +39,6 @@ const SENSOR_DEVICE_CLASSES = [
"pm1",
"pm10",
"pm25",
"pm4",
"power_factor",
"power",
"precipitation",
@@ -435,7 +434,7 @@ export class DemoEntityState extends LitElement {
display: block;
height: 20px;
width: 20px;
border-radius: var(--ha-border-radius-md);
border-radius: 10px;
background-color: rgb(--color);
}
`;

View File

@@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { IntegrationManifest } from "../../../../src/data/integration";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
import type { EntityRegistryEntry } from "../../../../src/data/entity_registry";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../../../src/panels/config/integrations/ha-config-flow-card";
import type {

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import type { ActionHandlerEvent } from "../../../../src/data/lovelace/action_handler";
import { actionHandler } from "../../../../src/panels/lovelace/common/directives/action-handler-directive";
@@ -13,16 +13,12 @@ export class DemoUtilLongPress extends LitElement {
${[1, 2, 3].map(
() => html`
<ha-card>
<ha-button
appearance="plain"
<mwc-button
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: true,
hasDoubleClick: true,
})}
.actionHandler=${actionHandler({})}
>
(long) press me!
</ha-button>
</mwc-button>
<textarea></textarea>

View File

@@ -1,3 +0,0 @@
---
title: Fan
---

View File

@@ -1,50 +0,0 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { FanEntityFeature } from "../../../../src/data/fan";
const ENTITIES = [
getEntity("fan", "fan", "on", {
friendly_name: "Fan",
device_class: "fan",
supported_features:
FanEntityFeature.OSCILLATE +
FanEntityFeature.DIRECTION +
FanEntityFeature.SET_SPEED,
}),
];
@customElement("demo-more-info-fan")
class DemoMoreInfoFan extends LitElement {
@property({ attribute: false }) public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-fan": DemoMoreInfoFan;
}
}

View File

@@ -11,10 +11,7 @@ import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type {
HaFormSchema,
HaFormDataContainer,
} from "../../../../src/components/ha-form/types";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list-item";
@@ -36,7 +33,6 @@ import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
const SUPPORTED_UI_TYPES = [
"string",
@@ -82,125 +78,78 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
private _getTranslationEntry(
language: string,
entry: HaFormSchema,
options?: { path?: string[] }
) {
let parent = this.addon.translations[language]?.configuration;
if (!parent) return undefined;
if (options?.path) {
for (const key of options.path) {
parent = parent[key]?.fields;
if (!parent) return undefined;
}
}
return parent[entry.name];
}
public computeLabel = (
entry: HaFormSchema,
_data: HaFormDataContainer,
options?: { path?: string[] }
): string =>
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
this._getTranslationEntry("en", entry, options)?.name ||
public computeLabel = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
entry.name;
public computeHelper = (
entry: HaFormSchema,
options?: { path?: string[] }
): string =>
this._getTranslationEntry(this.hass.language, entry, options)
public computeHelper = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.description ||
this._getTranslationEntry("en", entry, options)?.description ||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
"";
private _convertSchema = memoizeOne(
// Convert supervisor schema to selectors
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
this._convertSchemaElements(schema)
(schema: Record<string, any>): HaFormSchema[] =>
schema.map((entry) =>
entry.type === "select"
? {
name: entry.name,
required: entry.required,
selector: { select: { options: entry.options } },
}
: entry.type === "string"
? entry.multiple
? {
name: entry.name,
required: entry.required,
selector: {
select: { options: [], multiple: true, custom_value: true },
},
}
: {
name: entry.name,
required: entry.required,
selector: {
text: {
type: entry.format
? entry.format
: MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
},
}
: entry.type === "boolean"
? {
name: entry.name,
required: entry.required,
selector: { boolean: {} },
}
: entry.type === "schema"
? {
name: entry.name,
required: entry.required,
selector: { object: {} },
}
: entry.type === "float" || entry.type === "integer"
? {
name: entry.name,
required: entry.required,
selector: {
number: {
mode: "box",
step: entry.type === "float" ? "any" : undefined,
},
},
}
: entry
)
);
private _convertSchemaElements(
schema: readonly HaFormSchema[]
): HaFormSchema[] {
return schema.map((entry) => this._convertSchemaElement(entry));
}
private _convertSchemaElement(entry: any): HaFormSchema {
if (entry.type === "schema" && !entry.multiple) {
return {
name: entry.name,
type: "expandable",
required: entry.required,
schema: this._convertSchemaElements(entry.schema),
};
}
const selector = this._convertSchemaElementToSelector(entry, false);
if (selector) {
return {
name: entry.name,
required: entry.required,
selector,
};
}
return entry;
}
private _convertSchemaElementToSelector(
entry: any,
force: boolean
): Selector | null {
if (entry.type === "select") {
return { select: { options: entry.options } };
}
if (entry.type === "string") {
return entry.multiple
? { select: { options: [], multiple: true, custom_value: true } }
: {
text: {
type: entry.format
? entry.format
: MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
};
}
if (entry.type === "boolean") {
return { boolean: {} };
}
if (entry.type === "schema") {
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
for (const child_entry of entry.schema) {
fields[child_entry.name] = {
required: child_entry.required,
selector: this._convertSchemaElementToSelector(child_entry, true)!,
};
}
return {
object: {
multiple: entry.multiple,
fields,
},
};
}
if (entry.type === "float" || entry.type === "integer") {
return {
number: {
mode: "box",
step: entry.type === "float" ? "any" : undefined,
},
};
}
if (force) {
return { object: {} };
}
return null;
}
private _filteredSchema = memoizeOne(
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required)
);
@@ -212,7 +161,7 @@ class HassioAddonConfig extends LitElement {
showForm &&
JSON.stringify(this.addon.schema) !==
JSON.stringify(
this._filteredSchema(this.addon.options, this.addon.schema!)
this._filteredShchema(this.addon.options, this.addon.schema!)
);
return html`
<h1>${this.addon.name}</h1>
@@ -250,7 +199,6 @@ class HassioAddonConfig extends LitElement {
<div class="card-content">
${showForm
? html`<ha-form
.hass=${this.hass}
.disabled=${this.disabled}
.data=${this._options!}
@value-changed=${this._configChanged}
@@ -259,7 +207,7 @@ class HassioAddonConfig extends LitElement {
.schema=${this._convertSchema(
this._showOptional
? this.addon.schema!
: this._filteredSchema(
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)

View File

@@ -99,8 +99,7 @@ class HassioAddonNetwork extends LitElement {
: nothing}
<div class="card-actions">
<ha-progress-button
variant="danger"
appearance="plain"
class="warning"
.disabled=${this.disabled}
@click=${this._resetTapped}
>

View File

@@ -25,7 +25,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
@@ -188,13 +187,12 @@ class HassioAddonInfo extends LitElement {
"addon.dashboard.protection_mode.content"
)}
<ha-button
variant="danger"
slot="action"
@click=${this._protectionToggled}
>
${this.supervisor.localize(
.label=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@click=${this._protectionToggled}
>
</ha-button>
</ha-alert>
`
@@ -694,16 +692,14 @@ class HassioAddonInfo extends LitElement {
? this._computeIsRunning
? html`
<ha-progress-button
variant="danger"
appearance="plain"
class="warning"
@click=${this._stopClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.stop")}
</ha-progress-button>
<ha-progress-button
variant="danger"
appearance="plain"
class="warning"
@click=${this._restartClicked}
>
${this.supervisor.localize("addon.dashboard.restart")}
@@ -713,60 +709,10 @@ class HassioAddonInfo extends LitElement {
<ha-progress-button
@click=${this._startClicked}
.progress=${this.addon.state === "startup"}
appearance="plain"
>
${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button>
`
: nothing}
</div>
<div>
${this.addon.version
? html`
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._rebuildClicked}
>
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-progress-button>
`
: nothing}
${this._computeShowWebUI || this._computeShowIngressUI
? html`
<ha-button
href=${ifDefined(
!this._computeShowIngressUI
? this._pathWebui!
: nothing
)}
target=${ifDefined(
!this._computeShowIngressUI ? "_blank" : nothing
)}
rel=${ifDefined(
!this._computeShowIngressUI ? "noopener" : nothing
)}
@click=${!this._computeShowWebUI
? this._openIngress
: undefined}
>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
`
: nothing}
`
: html`
<ha-progress-button
.disabled=${!this.addon.available}
@@ -776,12 +722,58 @@ class HassioAddonInfo extends LitElement {
</ha-progress-button>
`}
</div>
<div>
${this.addon.version
? html` ${this._computeShowWebUI
? html`
<a
href=${this._pathWebui!}
tabindex="-1"
target="_blank"
rel="noopener"
>
<ha-button>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
</a>
`
: nothing}
${this._computeShowIngressUI
? html`
<ha-button @click=${this._openIngress}>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
`
: nothing}
<ha-progress-button
class="warning"
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
<ha-progress-button
class="warning"
@click=${this._rebuildClicked}
>
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-progress-button>
`
: nothing}`
: nothing}
</div>
</div>
</ha-card>
${this.addon.long_description
? html`
<ha-card class="long-description" outlined>
<ha-card outlined>
<div class="card-content">
<ha-markdown
.content=${this.addon.long_description}
@@ -1154,17 +1146,15 @@ class HassioAddonInfo extends LitElement {
),
dismissText: this.supervisor.localize("common.cancel"),
});
button.actionError();
button.progress = false;
return;
}
} catch (err: any) {
button.actionError();
button.progress = false;
showAlertDialog(this, {
title: "Failed to validate addon configuration",
text: extractApiErrorMessage(err),
});
button.progress = false;
return;
}
@@ -1178,15 +1168,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
button.actionError();
button.progress = false;
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.start"),
text: extractApiErrorMessage(err),
});
return;
}
button.actionSuccess();
button.progress = false;
}
@@ -1242,7 +1228,6 @@ class HassioAddonInfo extends LitElement {
path: "uninstall",
};
fireEvent(this, "hass-api-called", eventdata);
button.actionSuccess();
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
@@ -1250,7 +1235,6 @@ class HassioAddonInfo extends LitElement {
),
text: extractApiErrorMessage(err),
});
button.actionError();
}
button.progress = false;
}
@@ -1333,9 +1317,6 @@ class HassioAddonInfo extends LitElement {
.description a {
color: var(--primary-color);
}
.long-description {
direction: ltr;
}
ha-assist-chip {
--md-sys-color-primary: var(--text-primary-color);
--md-sys-color-on-surface: var(--text-primary-color);

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
@@ -16,7 +17,6 @@ import type {
} from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab";
import "../../../src/components/ha-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/ha-svg-icon";
@@ -241,13 +241,12 @@ export class HassioBackups extends LitElement {
<div class="header-btns">
${!this.narrow
? html`
<ha-button
appearance="plain"
variant="danger"
<mwc-button
@click=${this._deleteSelected}
class="warning"
>
${this.supervisor.localize("backup.delete_selected")}
</ha-button>
</mwc-button>
`
: html`
<ha-icon-button
@@ -409,7 +408,7 @@ export class HassioBackups extends LitElement {
margin-inline-end: -12px;
margin-inline-start: initial;
}
.header-btns > ha-button,
.header-btns > mwc-button,
.header-btns > ha-icon-button {
margin: 8px;
}

View File

@@ -121,7 +121,7 @@ class HassioCardContent extends LitElement {
height: 12px;
top: 8px;
right: 8px;
border-radius: var(--ha-border-radius-circle);
border-radius: 50%;
}
.topbar {
position: absolute;

View File

@@ -1,9 +1,10 @@
import "@material/mwc-button";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import type { HassioHassOSInfo } from "../../../src/data/hassio/host";
@@ -108,9 +109,10 @@ export class HassioUpdate extends LitElement {
</ha-settings-row>
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/hassio/update-available/${key}">
${this.supervisor.localize("common.show")}
</ha-button>
<a href="/hassio/update-available/${key}">
<mwc-button .label=${this.supervisor.localize("common.show")}>
</mwc-button>
</a>
</div>
</ha-card>
`;

View File

@@ -1,10 +1,10 @@
import "@material/mwc-button/mwc-button";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -77,21 +77,20 @@ class HassioBackupLocationDialog extends LitElement {
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-button
appearance="plain"
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this._dialogParams.supervisor.localize("common.cancel")}
</ha-button>
<ha-button
</mwc-button>
<mwc-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this._dialogParams.supervisor.localize("common.save")}
</ha-button>
</mwc-button>
</ha-dialog>
`;
}

View File

@@ -8,6 +8,7 @@ import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-button-menu";

View File

@@ -1,9 +1,10 @@
import "@material/mwc-button";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-spinner";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import {
@@ -68,20 +69,16 @@ class HassioCreateBackupDialog extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this._dialogParams.supervisor.localize("common.close")}
</ha-button>
<ha-button
</mwc-button>
<mwc-button
.disabled=${this._creatingBackup}
slot="primaryAction"
@click=${this._createBackup}
>
${this._dialogParams.supervisor.localize("backup.create")}
</ha-button>
</mwc-button>
</ha-dialog>
`;
}

Some files were not shown because too many files have changed in this diff Show More