mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-06 16:32:03 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 153290593b | |||
| 189477a1d4 |
@@ -8,7 +8,6 @@ You are an assistant helping with development of the Home Assistant frontend. Th
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
- [Core Architecture](#core-architecture)
|
||||
- [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
|
||||
- [Development Standards](#development-standards)
|
||||
- [Component Library](#component-library)
|
||||
- [Common Patterns](#common-patterns)
|
||||
@@ -41,7 +40,7 @@ script/develop # Development server
|
||||
```typescript
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-alert-dialog";
|
||||
```
|
||||
|
||||
## Core Architecture
|
||||
@@ -53,64 +52,13 @@ The Home Assistant frontend is a modern web application that:
|
||||
- Communicates with the backend via WebSocket API
|
||||
- Provides comprehensive theming and internationalization
|
||||
|
||||
## State Access: Contexts Instead of `hass`
|
||||
|
||||
Every component used to take the whole `hass: HomeAssistant` object — a god-object that re-renders on any unrelated `hass` change, forces tests to mock everything, and hides what a component actually reads. We're moving leaf components to **fine-grained [Lit context](https://lit.dev/docs/data/context/)**: consume only the slice you need and re-render only when it changes.
|
||||
|
||||
For new code, consume the matching context instead of adding a `hass` property. `hass` stays for container components that own it and feed the providers; the canonical migration is [`hui-button-card.ts`](src/panels/lovelace/cards/hui-button-card.ts). Infrastructure: contexts in [`src/data/context/index.ts`](src/data/context/index.ts), the `consume…` helpers in [`src/common/decorators/consume-context-entry.ts`](src/common/decorators/consume-context-entry.ts), and `@transform` in [`src/common/decorators/transform.ts`](src/common/decorators/transform.ts). Providers are wired automatically by `contextMixin` on `HassBaseEl` — you only consume.
|
||||
|
||||
### Contexts
|
||||
|
||||
Consume the narrowest context that covers your reads:
|
||||
|
||||
| Context | Replaces |
|
||||
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `statesContext` | `hass.states` |
|
||||
| `entitiesContext` / `devicesContext` / `areasContext` / `floorsContext` | `hass.entities` / `.devices` / `.areas` / `.floors` (or `registriesContext` for all four) |
|
||||
| `servicesContext` | `hass.services` |
|
||||
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
|
||||
| `formattersContext` | `hass.formatEntityName`, `hass.formatEntityState`, `hass.formatEntityAttributeName`, … |
|
||||
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
|
||||
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.hassUrl` |
|
||||
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
|
||||
| `uiContext` | `hass.themes`, `hass.selectedTheme`, `hass.panels`, `hass.dockedSidebar`, … |
|
||||
| `narrowViewportContext` | narrow-layout boolean |
|
||||
|
||||
Lazy contexts (subscribe on first consumer, tear down after the last): `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`. The single-field contexts (`localizeContext`, `themesContext`, `userContext`, …) are **deprecated** — use the grouped ones above.
|
||||
|
||||
### Consuming
|
||||
|
||||
Use the `consume…` helpers for entity-scoped and `localize` reads. `entityIdPath` is resolved against `this`, so these watch `this._config.entity`:
|
||||
|
||||
```ts
|
||||
@state() @consumeEntityState({ entityIdPath: ["_config", "entity"] })
|
||||
private _stateObj?: HassEntity; // consumeEntityStates(...) for a record of several
|
||||
|
||||
@state() @consumeEntityRegistryEntry({ entityIdPath: ["_config", "entity"] })
|
||||
private _entity?: EntityRegistryDisplayEntry;
|
||||
|
||||
@state() @consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
```
|
||||
|
||||
For any other single field, pair `@consume` with `@transform`:
|
||||
|
||||
```ts
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, Themes>({ transformer: ({ themes }) => themes })
|
||||
private _themes!: Themes;
|
||||
```
|
||||
|
||||
`@transform`'s `watch` option re-runs the transformer when a host prop changes — needed when an entity id is computed, since `consumeEntityState` only watches the first path segment. To consume a whole group untransformed, drop `@transform` and type it `ContextType<typeof statesContext>`.
|
||||
|
||||
## Development Standards
|
||||
|
||||
### Code Quality Requirements
|
||||
|
||||
**Linting and Formatting (Enforced by Tools)**
|
||||
|
||||
- ESLint config (flat config) extends TypeScript strict, Lit, Web Components, Accessibility (lit-a11y), and import-x
|
||||
- ESLint config extends Airbnb, TypeScript strict, Lit, Web Components, Accessibility
|
||||
- Prettier with ES5 trailing commas enforced
|
||||
- No console statements (`no-console: "error"`) - use proper logging
|
||||
- Import organization: No unused imports, consistent type imports
|
||||
@@ -188,7 +136,6 @@ export class HaMyComponent extends LitElement {
|
||||
### Data Management
|
||||
|
||||
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
|
||||
- **Prefer contexts over `hass`**: For state reads, consume the relevant Lit context instead of taking the whole `hass` object — see [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
|
||||
- **Cache appropriately**: Use collections and caching for frequently accessed data
|
||||
- **Handle errors gracefully**: All API calls should have error handling
|
||||
- **Update real-time**: Subscribe to state changes for live updates
|
||||
@@ -213,7 +160,7 @@ try {
|
||||
- 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
|
||||
- **Prefer `ha-*` components**: Build on the Home Assistant component library (many now wrap Web Awesome components); avoid new use of legacy Material Web Components (`mwc-*`), which are being phased out
|
||||
- **Follow Material Design**: Use Material Web Components where appropriate
|
||||
- **Support RTL**: Ensure all layouts work in RTL languages
|
||||
|
||||
```typescript
|
||||
@@ -320,22 +267,15 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
**Dialog Sizing:**
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (580px - default), `"large"` (1024px), or `"full"`
|
||||
- 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
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
`ha-button` (wraps the Web Awesome button — see `src/components/ha-button.ts`) has two independent axes plus size:
|
||||
|
||||
- **`variant`** (color): `"brand"` (default), `"neutral"`, `"danger"`, `"warning"`, `"success"`
|
||||
- **`appearance`** (fill style): `"accent"`, `"filled"`, `"outlined"`, `"plain"`
|
||||
- **`size`**: `"xs"` (extra small, 40px), `"s"` (small, 32px), `"m"` (medium, 40px - default), `"l"` (large, 48px), `"xl"` (extra large, 40px)
|
||||
|
||||
Common patterns:
|
||||
|
||||
- **Primary action**: `appearance="filled"` for emphasis (or the default appearance for a lighter look)
|
||||
- **Secondary action**: `appearance="plain"` for cancel/dismiss actions
|
||||
- **Destructive actions**: `variant="danger"` for delete/remove operations (the generic confirmation dialog uses `variant="danger"` for its confirm button — see `src/dialogs/generic/dialog-box.ts`)
|
||||
- **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`
|
||||
|
||||
**Gallery Documentation:**
|
||||
@@ -368,8 +308,7 @@ Common patterns:
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
- Types: `error`, `warning`, `info`, `success`
|
||||
- Properties: `title`, `alert-type`, `dismissable`, `narrow`
|
||||
- Slots: `icon` (override the leading icon), `action` (custom action content)
|
||||
- Properties: `title`, `alert-type`, `dismissable`, `icon`, `action`, `rtl`
|
||||
- Content announced by screen readers when dynamically displayed
|
||||
|
||||
```html
|
||||
@@ -510,7 +449,7 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
|
||||
### Common Pitfalls to Avoid
|
||||
|
||||
- Don't manually query the DOM with `querySelector` - use the `@query`/`@queryAll` decorators or component properties
|
||||
- Don't use `querySelector` - Use refs or component properties
|
||||
- Don't manipulate DOM directly - Let Lit handle rendering
|
||||
- Don't use global styles - Scope styles to components
|
||||
- Don't block the main thread - Use web workers for heavy computation
|
||||
@@ -601,24 +540,35 @@ When creating a pull request, you **must** use the PR template located at `.gith
|
||||
|
||||
#### Translation Considerations
|
||||
|
||||
All user-facing text must be translatable — see the **Internationalization** section (under Common Patterns) for the `localize` API and placeholder usage. From a copy perspective:
|
||||
|
||||
- **Add translation keys**: All user-facing text must be translatable
|
||||
- **Use placeholders**: Support dynamic content in translations
|
||||
- **Keep context**: Provide enough context for translators
|
||||
- **Avoid concatenation**: Prefer full localized strings with placeholders over stitching translated fragments together
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
name: automation.alias,
|
||||
});
|
||||
|
||||
// Bad - hardcoded text
|
||||
("Are you sure you want to delete this automation?");
|
||||
```
|
||||
|
||||
### Common Review Issues (From PR Analysis)
|
||||
|
||||
Recurring, easy-to-miss problems surfaced in real PR reviews. These complement the standards above rather than repeating them — items already covered earlier (loading states, error handling, mobile layout, theming, import hygiene) are intentionally not duplicated here.
|
||||
|
||||
#### User Experience and Accessibility
|
||||
|
||||
- **Form validation**: Always provide proper field labels and validation feedback
|
||||
- **Form accessibility**: Prevent password managers from incorrectly identifying fields
|
||||
- **Loading states**: Show clear progress indicators during async operations
|
||||
- **Error handling**: Display meaningful error messages when operations fail
|
||||
- **Mobile responsiveness**: Ensure components work well on small screens
|
||||
- **Hit targets**: Make clickable areas large enough for touch interaction
|
||||
- **Visual feedback**: Provide clear indication of interactive states (hover, active, focus)
|
||||
- **Visual feedback**: Provide clear indication of interactive states
|
||||
|
||||
#### Dialog and Modal Patterns
|
||||
|
||||
- **Dialog width constraints**: Respect minimum and maximum width requirements
|
||||
- **Interview progress**: Show clear progress for multi-step operations
|
||||
- **State persistence**: Handle dialog state properly during background operations
|
||||
- **Cancel behavior**: Ensure cancel/close buttons work consistently
|
||||
@@ -630,12 +580,15 @@ Recurring, easy-to-miss problems surfaced in real PR reviews. These complement t
|
||||
- **Visual hierarchy**: Ensure proper font sizes and spacing ratios
|
||||
- **Grid alignment**: Components should align to the design grid system
|
||||
- **Badge placement**: Position badges and indicators consistently
|
||||
- **Color theming**: Respect theme variables and design system colors
|
||||
|
||||
#### Code Quality Issues
|
||||
|
||||
- **Null checking**: Always check if entities exist before accessing properties
|
||||
- **TypeScript safety**: Handle potentially undefined array/object access
|
||||
- **Event handling and cleanup**: Subscribe/unsubscribe correctly and remove listeners to avoid memory leaks
|
||||
- **Import organization**: Remove unused imports and use proper type imports
|
||||
- **Event handling**: Properly subscribe and unsubscribe from events
|
||||
- **Memory leaks**: Clean up subscriptions and event listeners
|
||||
|
||||
#### Configuration and Props
|
||||
|
||||
@@ -646,12 +599,39 @@ Recurring, easy-to-miss problems surfaced in real PR reviews. These complement t
|
||||
|
||||
## Review Guidelines
|
||||
|
||||
Final pre-submission checklist. Linting and formatting are enforced by tooling, so this focuses on what tools can't catch rather than restating every rule above.
|
||||
### Core Requirements Checklist
|
||||
|
||||
- [ ] `yarn lint` passes (TypeScript, ESLint, Prettier, Lit analyzer) and `yarn test` is green
|
||||
- [ ] Tests added for new data processing/utilities (where applicable)
|
||||
- [ ] All user-facing text is localized and follows the Text and Copy guidelines (sentence case, "Home Assistant" in full, Delete/Remove + Create/Add)
|
||||
- [ ] TypeScript strict mode passes (`yarn lint:types`)
|
||||
- [ ] No ESLint errors or warnings (`yarn lint:eslint`)
|
||||
- [ ] Prettier formatting applied (`yarn lint:prettier`)
|
||||
- [ ] Lit analyzer passes (`yarn lint:lit`)
|
||||
- [ ] Component follows Lit best practices
|
||||
- [ ] Proper error handling implemented
|
||||
- [ ] Loading states handled
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Theme variables used
|
||||
- [ ] Translations added
|
||||
- [ ] Accessible to screen readers
|
||||
- [ ] Tests added (where applicable)
|
||||
- [ ] No console statements (use proper logging)
|
||||
- [ ] Unused imports removed
|
||||
- [ ] Proper naming conventions
|
||||
|
||||
### Text and Copy Checklist
|
||||
|
||||
- [ ] Follows terminology guidelines (Delete vs Remove, Create vs Add)
|
||||
- [ ] Localization keys added for all user-facing text
|
||||
- [ ] Uses "Home Assistant" (never "HA" or "HASS")
|
||||
- [ ] Sentence case for ALL text (titles, headings, buttons, labels)
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
- [ ] ha-alert used correctly for messages
|
||||
- [ ] ha-form uses proper schema structure
|
||||
- [ ] Components handle all states (loading, error, unavailable)
|
||||
- [ ] Entity existence checked before property access
|
||||
- [ ] Event/subscription listeners cleaned up (no memory leaks)
|
||||
- [ ] Accessible to screen readers and keyboard
|
||||
- [ ] Event subscriptions properly cleaned up
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli deploy --dir=cast/dist --alias dev
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli deploy --dir=cast/dist --prod
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli deploy --dir=gallery/dist --prod
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Deploy preview to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
||||
--json > deploy_output.json
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -77,22 +77,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
# Sleep to give pypi time to populate the new version across mirrors
|
||||
sleep 240
|
||||
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
|
||||
# Wait for the package to become available on PyPI
|
||||
echo "Waiting for home-assistant-frontend==$version to appear on PyPI..."
|
||||
for i in $(seq 1 30); do
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/home-assistant-frontend/$version/json")
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "Package is available on PyPI!"
|
||||
break
|
||||
fi
|
||||
if [ "$i" = "30" ]; then
|
||||
echo "Timed out waiting for package to appear on PyPI"
|
||||
exit 1
|
||||
fi
|
||||
echo "Not available yet (HTTP $status), retrying in 30 seconds... ($i/30)"
|
||||
sleep 30
|
||||
done
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
diff --git a/dist/tinykeys.cjs b/dist/tinykeys.cjs
|
||||
index 08c98b6eff3b8fb4b727fe8e6b096951d6ef6347..9c44f14862f582766ea1733b6dc0e97f962800d8 100644
|
||||
--- a/dist/tinykeys.cjs
|
||||
+++ b/dist/tinykeys.cjs
|
||||
@@ -61,6 +61,18 @@ function defaultKeybindingsHandlerIgnore(event) {
|
||||
function getModifierState(event, mod) {
|
||||
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
|
||||
}
|
||||
+function splitKeybindingPress(press) {
|
||||
+ let parts = [];
|
||||
+ let start = 0;
|
||||
+ for (let index = 0; index < press.length; index++) {
|
||||
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
|
||||
+ parts.push(press.slice(start, index));
|
||||
+ start = index + 1;
|
||||
+ }
|
||||
+ }
|
||||
+ parts.push(press.slice(start));
|
||||
+ return parts;
|
||||
+}
|
||||
/**
|
||||
* Parses a keybinding string into its parts.
|
||||
*
|
||||
@@ -76,10 +88,10 @@ function getModifierState(event, mod) {
|
||||
*/
|
||||
function parseKeybinding(str) {
|
||||
return str.trim().split(" ").map((press) => {
|
||||
- let parts = press.split(/(?<=\w|\])\+/);
|
||||
+ let parts = splitKeybindingPress(press);
|
||||
let last = parts.pop();
|
||||
let regex = last.match(/^\((.+)\)$/);
|
||||
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
|
||||
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
|
||||
let requiredModifiers = [];
|
||||
let optionalModifiers = [];
|
||||
for (const part of parts) {
|
||||
@@ -201,5 +213,3 @@ exports.defaultKeybindingsHandlerIgnore = defaultKeybindingsHandlerIgnore;
|
||||
exports.matchKeybindingPress = matchKeybindingPress;
|
||||
exports.parseKeybinding = parseKeybinding;
|
||||
exports.tinykeys = tinykeys;
|
||||
-
|
||||
-//# sourceMappingURL=tinykeys.cjs.map
|
||||
\ No newline at end of file
|
||||
diff --git a/dist/tinykeys.mjs b/dist/tinykeys.mjs
|
||||
index c289972d2728e03d9b272268c38fd3392e8845bf..e22897b00aae6cdb0dbbb971445227c07be52918 100644
|
||||
--- a/dist/tinykeys.mjs
|
||||
+++ b/dist/tinykeys.mjs
|
||||
@@ -60,6 +60,18 @@ function defaultKeybindingsHandlerIgnore(event) {
|
||||
function getModifierState(event, mod) {
|
||||
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
|
||||
}
|
||||
+function splitKeybindingPress(press) {
|
||||
+ let parts = [];
|
||||
+ let start = 0;
|
||||
+ for (let index = 0; index < press.length; index++) {
|
||||
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
|
||||
+ parts.push(press.slice(start, index));
|
||||
+ start = index + 1;
|
||||
+ }
|
||||
+ }
|
||||
+ parts.push(press.slice(start));
|
||||
+ return parts;
|
||||
+}
|
||||
/**
|
||||
* Parses a keybinding string into its parts.
|
||||
*
|
||||
@@ -75,10 +87,10 @@ function getModifierState(event, mod) {
|
||||
*/
|
||||
function parseKeybinding(str) {
|
||||
return str.trim().split(" ").map((press) => {
|
||||
- let parts = press.split(/(?<=\w|\])\+/);
|
||||
+ let parts = splitKeybindingPress(press);
|
||||
let last = parts.pop();
|
||||
let regex = last.match(/^\((.+)\)$/);
|
||||
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
|
||||
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
|
||||
let requiredModifiers = [];
|
||||
let optionalModifiers = [];
|
||||
for (const part of parts) {
|
||||
@@ -196,5 +208,3 @@ function tinykeys(target, keybindingMap, options = {}) {
|
||||
}
|
||||
//#endregion
|
||||
export { createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
|
||||
-
|
||||
-//# sourceMappingURL=tinykeys.mjs.map
|
||||
\ No newline at end of file
|
||||
+940
File diff suppressed because one or more lines are too long
Vendored
-944
File diff suppressed because one or more lines are too long
+1
-1
@@ -13,4 +13,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.16.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.15.0.cjs
|
||||
|
||||
@@ -29,7 +29,7 @@ const LICENSE_OVERRIDES = [
|
||||
// type-fest ships two license files (MIT for code, CC0 for types).
|
||||
// We use the MIT license since that covers the bundled code.
|
||||
packageName: "type-fest",
|
||||
version: "5.7.0",
|
||||
version: "5.6.0",
|
||||
licensePath: path.resolve(
|
||||
paths.root_dir,
|
||||
"node_modules/type-fest/license-mit"
|
||||
|
||||
+48
-48
@@ -94,55 +94,55 @@ class HaGallery extends LitElement {
|
||||
<div slot="title">
|
||||
${PAGES[this._page].metadata.title || this._page.split("/")[1]}
|
||||
</div>
|
||||
<div class="content">
|
||||
${PAGES[this._page].description
|
||||
? html`
|
||||
<page-description .page=${this._page}></page-description>
|
||||
`
|
||||
: ""}
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
</div>
|
||||
<div class="page-footer">
|
||||
<div class="edit-docs">
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for
|
||||
this page.
|
||||
</div>
|
||||
<div>
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rtl-toggle">
|
||||
<ha-icon-button
|
||||
@click=${this._toggleRtl}
|
||||
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</ha-top-app-bar-fixed>
|
||||
<div class="content">
|
||||
${PAGES[this._page].description
|
||||
? html`
|
||||
<page-description .page=${this._page}></page-description>
|
||||
`
|
||||
: ""}
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
</div>
|
||||
<div class="page-footer">
|
||||
<div class="edit-docs">
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for this
|
||||
page.
|
||||
</div>
|
||||
<div>
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rtl-toggle">
|
||||
<ha-icon-button
|
||||
@click=${this._toggleRtl}
|
||||
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-drawer>
|
||||
<notification-manager
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
title: "Brand Personality"
|
||||
---
|
||||
|
||||
# Brand Personality
|
||||
|
||||
These five traits describe who Home Assistant is, not how it speaks. Tone of voice – the playfulness, the informality, the warmth, etc – should flow naturally from these, and will help guide writers on how to bring the brand personality to life.
|
||||
|
||||
The first four traits are relational: they describe how Home Assistant behaves toward its users.
|
||||
_Welcoming_ is about how we receive them.\
|
||||
_Candid_ is about how we communicate with them.\
|
||||
_Supportive_ is about how we help them.\
|
||||
_Generous_ is about how/what we give to them.\
|
||||
If any of these feel similar, it’s because they’re all expressions of the same underlying character, just in different moments of the user relationship.
|
||||
|
||||
_Independent_ is different. It’s foundational: it describes who Home Assistant is at its core.\
|
||||
And it’s because of that independence that the other four traits feel genuine rather than performed. A corporate brand can try to be welcoming, candid, supportive, and generous,
|
||||
but without independence, those traits will always be managed and moderated.
|
||||
|
||||
## Welcoming
|
||||
|
||||
**Warm and open, kind, friendly, approachable, accommodating**\
|
||||
_But not: people pleasing, appeasing, sycophantic, ingratiating_\
|
||||
Home Assistant feels like a knowledgeable friend, not a product. We meet you at your own level, never talk down to you, and make you feel valued regardless of your technical ability. This isn’t performative, it’s expressed naturally in the small things: how errors are explained, documentation is written, and how the community talks to newcomers.
|
||||
|
||||
## Candid
|
||||
|
||||
**Direct, honest, transparent, unpretentious**\
|
||||
_But not: unfriendly, rude, blunt, unempathetic_\
|
||||
Home Assistant says what it means. We don’t hide complexity behind false simplicity, fall back on marketing fluff, or pretend limitations don’t exist. We respect users enough to be straight with them: about what Home Assistant can do, what it can't, and why it works the way it does. This is what builds real trust with users.
|
||||
|
||||
## Supportive
|
||||
|
||||
**Helpful, guiding, encouraging, empathetic, genuine**\
|
||||
_But not: directive, hollow, condescending, over-bearing_\
|
||||
Home Assistant always has your back. Whether you’re just starting out or deep into a complex setup, we steer you forward without taking over. Our support is genuine: practical, patient, and there when it’s needed most. Because Home Assistant wants every user to succeed in building a smart home with privacy, choice, and sustainability at its heart.
|
||||
|
||||
## Generous
|
||||
|
||||
**Empowering, trusting, giving, sharing**\
|
||||
_But not: overwhelming, indiscriminate, patronizing, controlling_\
|
||||
Home Assistant gives you everything you need today, and as your setup evolves. There are no strings attached: no artificial limits, features locked behind tiers, or deceptive patterns designed to tie you to a closed platform. We trust users with control, access, and transparency. This generosity is reciprocal: the time, knowledge, and care our community gives freely is what keeps Home Assistant thriving and truly open.
|
||||
|
||||
## Independent
|
||||
|
||||
**Principled, liberated, confident, imperfect**\
|
||||
_But not: conceited, obstinate, erratic, insular_\
|
||||
Home Assistant doesn’t feel the need to behave like an established tech brand or follow corporate rules. With no shareholders or VCs to answer to, we can say what we think, do things our own way, and not take ourselves too seriously. This freedom of spirit comes from the confidence of knowing our own values, and that our community shares them.
|
||||
@@ -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="s" slot="action">restart</ha-button>`,
|
||||
actionSlot: html`<ha-button size="small" slot="action">restart</ha-button>`,
|
||||
},
|
||||
{
|
||||
title: "Unsaved data",
|
||||
description: "You have unsaved data",
|
||||
type: "warning",
|
||||
actionSlot: html`<ha-button size="s" slot="action">save</ha-button>`,
|
||||
actionSlot: html`<ha-button size="small" slot="action">save</ha-button>`,
|
||||
},
|
||||
{
|
||||
title: "Slotted icon",
|
||||
|
||||
@@ -26,7 +26,7 @@ title: Button
|
||||
filled button
|
||||
</ha-button>
|
||||
|
||||
<ha-button size="s">
|
||||
<ha-button size="small">
|
||||
small
|
||||
</ha-button>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@ title: Button
|
||||
```html
|
||||
<ha-button> simple button </ha-button>
|
||||
|
||||
<ha-button size="s"> small </ha-button>
|
||||
<ha-button size="small"> small </ha-button>
|
||||
```
|
||||
|
||||
### API
|
||||
@@ -57,7 +57,7 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
|
||||
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
|
||||
| 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 | "xs"/"s"/"m"/"l"/"xl" | "m" | Sets the button size. |
|
||||
| size | "small"/"medium"/"large" | "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. |
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export class DemoHaButton extends LitElement {
|
||||
<ha-button
|
||||
.appearance=${appearance}
|
||||
.variant=${variant}
|
||||
size="s"
|
||||
size="small"
|
||||
>
|
||||
${titleCase(`${variant} ${appearance}`)}
|
||||
</ha-button>
|
||||
@@ -100,7 +100,7 @@ export class DemoHaButton extends LitElement {
|
||||
<ha-button
|
||||
.variant=${variant}
|
||||
.appearance=${appearance}
|
||||
size="s"
|
||||
size="small"
|
||||
disabled
|
||||
>
|
||||
${titleCase(`${appearance}`)}
|
||||
|
||||
@@ -62,11 +62,10 @@ host reflects `aria-multiselectable`.
|
||||
|
||||
**Events**
|
||||
|
||||
- `ha-list-item-selected` — an option was selected. Detail is the option's
|
||||
index (`number`). In single mode this is the only selection event; in multi
|
||||
mode it fires for each option added to the selection.
|
||||
- `ha-list-item-deselected` — an option was deselected (multi mode only). Detail
|
||||
is the option's index (`number`).
|
||||
- `ha-list-selected` — selection changed. Detail
|
||||
`{ index: number | Set<number>, diff: { added: Set<number>, removed: Set<number> } }`.
|
||||
`index` is a `number` in single mode (`-1` when nothing selected) and a
|
||||
`Set<number>` in multi mode.
|
||||
|
||||
**Methods / getters**
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import "../../../../src/components/item/ha-list-item-option";
|
||||
import "../../../../src/components/list/ha-list-base";
|
||||
import "../../../../src/components/list/ha-list-nav";
|
||||
import "../../../../src/components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../src/components/list/types";
|
||||
|
||||
type Appearance = "line" | "checkbox";
|
||||
type Position = "start" | "end";
|
||||
@@ -184,7 +185,7 @@ export class DemoHaList extends LitElement {
|
||||
<ha-card header="Single select, appearance=line">
|
||||
<ha-list-selectable
|
||||
aria-label="Single select"
|
||||
@ha-list-item-selected=${this._onSingle}
|
||||
@ha-list-selected=${this._onSingle}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -204,8 +205,7 @@ export class DemoHaList extends LitElement {
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi select line"
|
||||
@ha-list-item-selected=${this._onMultiLineSelected}
|
||||
@ha-list-item-deselected=${this._onMultiLineDeselected}
|
||||
@ha-list-selected=${this._onMultiLine}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -227,8 +227,7 @@ export class DemoHaList extends LitElement {
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox start"
|
||||
@ha-list-item-selected=${this._onMultiCheckStartSelected}
|
||||
@ha-list-item-deselected=${this._onMultiCheckStartDeselected}
|
||||
@ha-list-selected=${this._onMultiCheckStart}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -254,8 +253,7 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox end"
|
||||
@ha-list-item-selected=${this._onMultiCheckEndSelected}
|
||||
@ha-list-item-deselected=${this._onMultiCheckEndDeselected}
|
||||
@ha-list-selected=${this._onMultiCheckEnd}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -349,58 +347,20 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
|
||||
this._buttonClicks++;
|
||||
};
|
||||
|
||||
private _withIndex(
|
||||
value: number | Set<number>,
|
||||
index: number,
|
||||
selected: boolean
|
||||
): Set<number> {
|
||||
const next = new Set(value instanceof Set ? value : []);
|
||||
if (selected) {
|
||||
next.add(index);
|
||||
} else {
|
||||
next.delete(index);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private _onSingle = (ev: CustomEvent<number>) => {
|
||||
this._single = ev.detail;
|
||||
private _onSingle = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._single = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiLineSelected = (ev: CustomEvent<number>) => {
|
||||
this._multiLine = this._withIndex(this._multiLine, ev.detail, true);
|
||||
private _onMultiLine = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiLine = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiLineDeselected = (ev: CustomEvent<number>) => {
|
||||
this._multiLine = this._withIndex(this._multiLine, ev.detail, false);
|
||||
private _onMultiCheckStart = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckStart = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiCheckStartSelected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckStart = this._withIndex(
|
||||
this._multiCheckStart,
|
||||
ev.detail,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
private _onMultiCheckStartDeselected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckStart = this._withIndex(
|
||||
this._multiCheckStart,
|
||||
ev.detail,
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
private _onMultiCheckEndSelected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckEnd = this._withIndex(this._multiCheckEnd, ev.detail, true);
|
||||
};
|
||||
|
||||
private _onMultiCheckEndDeselected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckEnd = this._withIndex(
|
||||
this._multiCheckEnd,
|
||||
ev.detail,
|
||||
false
|
||||
);
|
||||
private _onMultiCheckEnd = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckEnd = ev.detail.index;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -17,13 +17,13 @@ subtitle: A slider component for selecting a value from a range.
|
||||
### Example Usage
|
||||
|
||||
<div class="wrapper">
|
||||
<ha-slider size="s" with-markers min="0" max="8" value="4"></ha-slider>
|
||||
<ha-slider size="m"></ha-slider>
|
||||
<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="s" with-markers min="0" max="8" value="4"></ha-slider>
|
||||
<ha-slider size="m"></ha-slider>
|
||||
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
|
||||
<ha-slider size="medium"></ha-slider>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
@@ -29,7 +29,7 @@ export class DemoHaSlider extends LitElement {
|
||||
></ha-slider>
|
||||
<span>Small</span>
|
||||
<ha-slider
|
||||
size="s"
|
||||
size="small"
|
||||
min="0"
|
||||
max="8"
|
||||
value="4"
|
||||
@@ -37,7 +37,7 @@ export class DemoHaSlider extends LitElement {
|
||||
></ha-slider>
|
||||
<span>Medium</span>
|
||||
<ha-slider
|
||||
size="m"
|
||||
size="medium"
|
||||
min="0"
|
||||
max="8"
|
||||
value="4"
|
||||
|
||||
@@ -13,28 +13,6 @@ export interface NetworkInfo {
|
||||
supervisor_internet: boolean;
|
||||
}
|
||||
|
||||
interface SupervisorJob {
|
||||
name: string;
|
||||
reference: string | null;
|
||||
uuid: string;
|
||||
progress: number; // float, 0–100
|
||||
stage: string | null;
|
||||
done: boolean;
|
||||
errors: {
|
||||
type: string;
|
||||
message: string;
|
||||
stage: string | null;
|
||||
}[];
|
||||
created: string; // ISO datetime string
|
||||
extra: Record<string, unknown> | null;
|
||||
child_jobs: SupervisorJob[];
|
||||
}
|
||||
|
||||
export interface SupervisorJobInfo {
|
||||
ignore_conditions: string[];
|
||||
jobs: SupervisorJob[];
|
||||
}
|
||||
|
||||
export const ALTERNATIVE_DNS_SERVERS: {
|
||||
ipv4: string[];
|
||||
ipv6: string[];
|
||||
@@ -79,15 +57,6 @@ export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
|
||||
return responseData?.data;
|
||||
}
|
||||
|
||||
export async function getSupervisorJobsInfo(): Promise<
|
||||
HassioResponse<SupervisorJobInfo>
|
||||
> {
|
||||
const responseData = await handleFetchPromise<
|
||||
HassioResponse<SupervisorJobInfo>
|
||||
>(fetch("/supervisor-api/jobs/info"));
|
||||
return responseData;
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
dnsServerIndex: number,
|
||||
primaryInterface: string
|
||||
|
||||
@@ -2,9 +2,9 @@ import { mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import "../../src/components/animation/ha-fade-in";
|
||||
import "../../src/components/ha-alert";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/animation/ha-fade-in";
|
||||
import "../../src/components/ha-spinner";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/progress/ha-progress-bar";
|
||||
@@ -15,7 +15,6 @@ import { haStyle } from "../../src/resources/styles";
|
||||
import "./components/landing-page-logs";
|
||||
import "./components/landing-page-network";
|
||||
import {
|
||||
getSupervisorJobsInfo,
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
type NetworkInfo,
|
||||
@@ -25,7 +24,6 @@ import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
export const ASSUME_CORE_START_SECONDS = 60;
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
const SCHEDULE_FETCH_JOBS_INFO_SECONDS = 2;
|
||||
|
||||
@customElement("ha-landing-page")
|
||||
class HaLandingPage extends LandingPageBaseElement {
|
||||
@@ -41,8 +39,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
|
||||
@state() private _coreCheckActive = false;
|
||||
|
||||
@state() private _progress = -1;
|
||||
|
||||
private _mobileApp =
|
||||
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
|
||||
|
||||
@@ -64,14 +60,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
${!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<ha-progress-bar
|
||||
.indeterminate=${this._progress <= 0}
|
||||
.value=${this._progress > 0 ? this._progress : undefined}
|
||||
.loading=${this._progress >= 0}
|
||||
>${this._progress > 0
|
||||
? `${Math.round(this._progress)}%`
|
||||
: nothing}</ha-progress-bar
|
||||
>
|
||||
<ha-progress-bar indeterminate></ha-progress-bar>
|
||||
`
|
||||
: nothing}
|
||||
${networkIssue || this._networkInfoError
|
||||
@@ -137,7 +126,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
import("../../src/components/ha-language-picker");
|
||||
|
||||
this._fetchSupervisorInfo(true);
|
||||
this._fetchSupervisorJobsInfo();
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
@@ -150,13 +138,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorJobsInfo() {
|
||||
setTimeout(
|
||||
() => this._fetchSupervisorJobsInfo(),
|
||||
SCHEDULE_FETCH_JOBS_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private _scheduleTurnOffCoreCheck() {
|
||||
setTimeout(() => {
|
||||
this._coreCheckActive = false;
|
||||
@@ -184,7 +165,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
// assume supervisor update if ping fails -> don't show an error
|
||||
if (!this._coreCheckActive && err.message !== "ping-failed") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to fetch supervisor info", err);
|
||||
console.error(err);
|
||||
this._networkInfoError = true;
|
||||
}
|
||||
}
|
||||
@@ -194,33 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSupervisorJobsInfo() {
|
||||
try {
|
||||
const jobsInfo = await getSupervisorJobsInfo();
|
||||
const coreInstallJob =
|
||||
jobsInfo.result === "ok"
|
||||
? jobsInfo.data.jobs.find(
|
||||
(job) => job.name === "home_assistant_core_install"
|
||||
)
|
||||
: undefined;
|
||||
if (coreInstallJob) {
|
||||
this._progress = coreInstallJob.progress;
|
||||
} else {
|
||||
this._progress = -1;
|
||||
}
|
||||
} catch (err: any) {
|
||||
await this._checkCoreAvailability();
|
||||
|
||||
if (!this._coreCheckActive) {
|
||||
this._progress = -1;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to fetch supervisor jobs info", err);
|
||||
}
|
||||
}
|
||||
|
||||
this._scheduleFetchSupervisorJobsInfo();
|
||||
}
|
||||
|
||||
private async _checkCoreAvailability() {
|
||||
try {
|
||||
const response = await fetch("/manifest.json");
|
||||
@@ -268,27 +222,21 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
ha-language-picker {
|
||||
min-width: 200px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
.footer ha-svg-icon {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
}
|
||||
ha-language-picker {
|
||||
margin-inline-start: calc(-1 * var(--ha-space-4));
|
||||
}
|
||||
ha-button {
|
||||
margin-inline-end: calc(-1 * var(--ha-space-2));
|
||||
}
|
||||
ha-fade-in {
|
||||
min-height: calc(100vh - 64px - 88px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
ha-progress-bar {
|
||||
--ha-progress-bar-track-height: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+14
-14
@@ -70,12 +70,12 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.1.2",
|
||||
"@tsparticles/preset-links": "4.1.2",
|
||||
"@tsparticles/engine": "4.1.0",
|
||||
"@tsparticles/preset-links": "4.1.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.2.0",
|
||||
"barcode-detector": "3.1.3",
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
@@ -88,14 +88,14 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.4.1",
|
||||
"fuse.js": "7.3.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.5",
|
||||
"idb-keyval": "6.2.4",
|
||||
"intl-messageformat": "11.2.7",
|
||||
"js-yaml": "4.2.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -114,7 +114,7 @@
|
||||
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch",
|
||||
"tinykeys": "4.0.0",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
"workbox-core": "7.4.1",
|
||||
@@ -137,7 +137,7 @@
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.12",
|
||||
"@rspack/core": "2.0.6",
|
||||
"@rspack/core": "2.0.5",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -153,7 +153,7 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@vitest/coverage-v8": "4.1.8",
|
||||
"@vitest/coverage-v8": "4.1.7",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
@@ -180,7 +180,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.7",
|
||||
"lint-staged": "17.0.6",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -190,13 +190,13 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.16",
|
||||
"tar": "7.5.15",
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.60.1",
|
||||
"typescript-eslint": "8.60.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.8",
|
||||
"vitest": "4.1.7",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
@@ -212,7 +212,7 @@
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.16.0",
|
||||
"packageManager": "yarn@4.15.0",
|
||||
"volta": {
|
||||
"node": "24.16.0"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isComponentLoaded } from "./is_component_loaded";
|
||||
|
||||
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
(isCore(page) || isLoadedIntegration(hass, page)) &&
|
||||
(!page.filter || page.filter(hass));
|
||||
isNotLoadedIntegration(hass, page);
|
||||
|
||||
export const isLoadedIntegration = (
|
||||
hass: HomeAssistant,
|
||||
@@ -16,4 +16,13 @@ export const isLoadedIntegration = (
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isNotLoadedIntegration = (
|
||||
hass: HomeAssistant,
|
||||
page: PageNavigation
|
||||
) =>
|
||||
!page.not_component ||
|
||||
!ensureArray(page.not_component).some((integration) =>
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isCore = (page: PageNavigation) => page.core;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
export interface EntityLocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
gpsAccuracy?: number;
|
||||
}
|
||||
|
||||
const findFirstActiveZone = (
|
||||
inZones: readonly string[],
|
||||
states: HassEntities
|
||||
): HassEntity | undefined => {
|
||||
for (const zoneId of inZones) {
|
||||
const zone = states[zoneId];
|
||||
if (
|
||||
zone &&
|
||||
!zone.attributes.passive &&
|
||||
typeof zone.attributes.latitude === "number" &&
|
||||
typeof zone.attributes.longitude === "number"
|
||||
) {
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getEntityLocation = (
|
||||
stateObj: HassEntity,
|
||||
states: HassEntities
|
||||
): EntityLocation | undefined => {
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
gps_accuracy: gpsAccuracy,
|
||||
} = stateObj.attributes;
|
||||
if (typeof latitude === "number" && typeof longitude === "number") {
|
||||
return { latitude, longitude, gpsAccuracy };
|
||||
}
|
||||
|
||||
if (computeStateDomain(stateObj) !== "person") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inZones = stateObj.attributes.in_zones;
|
||||
if (!Array.isArray(inZones) || inZones.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const zone = findFirstActiveZone(inZones, states);
|
||||
if (!zone) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
latitude: zone.attributes.latitude,
|
||||
longitude: zone.attributes.longitude,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -12,6 +13,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -27,38 +31,39 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: var(--ha-border-width-md) solid;
|
||||
border: 3px solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-orange-60);
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-red-60);
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -165,8 +165,7 @@ export class HaAutomationRow extends LitElement {
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) ::slotted([slot="leading-icon"].action-icon),
|
||||
:host([building-block]) ::slotted(#condition-icon) {
|
||||
:host([building-block]) ::slotted([slot="leading-icon"]) {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
color: var(--white-color);
|
||||
transform: rotate(-45deg);
|
||||
|
||||
@@ -101,22 +101,18 @@ export class HaSankeyChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
// Keep numbers and units left-to-right, even in RTL locales.
|
||||
const formattedValue = html`<div style="direction:ltr; display: inline;">
|
||||
${value}
|
||||
</div>`;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${formattedValue}`;
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${formattedValue}`;
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -167,7 +167,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
)}`}
|
||||
${this.syncCharts && this._hasZoomedCharts
|
||||
? html`<ha-button
|
||||
size="l"
|
||||
size="large"
|
||||
class="reset-button"
|
||||
@click=${this._handleGlobalZoomReset}
|
||||
>
|
||||
|
||||
@@ -2,17 +2,12 @@ import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
type HASSDomEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-sortable";
|
||||
import "./ha-entity-picker";
|
||||
import type { HaEntityPicker } from "./ha-entity-picker";
|
||||
|
||||
@customElement("ha-entities-picker")
|
||||
class HaEntitiesPicker extends LitElement {
|
||||
@@ -156,7 +151,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityMoved(e: HASSDomEvent<HASSDomEvents["item-moved"]>) {
|
||||
private _entityMoved(e: CustomEvent) {
|
||||
e.stopPropagation();
|
||||
const { oldIndex, newIndex } = e.detail;
|
||||
const currentEntities = this._currentEntities;
|
||||
@@ -183,7 +178,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateEntities(entities: string[]) {
|
||||
private async _updateEntities(entities) {
|
||||
this.value = entities;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -191,12 +186,9 @@ class HaEntitiesPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _entityChanged(
|
||||
event: ValueChangedEvent<string> &
|
||||
HASSDomCurrentTargetEvent<HaEntityPicker & { curValue: string }>
|
||||
) {
|
||||
private _entityChanged(event: ValueChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const curValue = event.currentTarget.curValue;
|
||||
const curValue = (event.currentTarget as any).curValue;
|
||||
const newValue = event.detail.value;
|
||||
if (
|
||||
newValue === curValue ||
|
||||
@@ -214,15 +206,13 @@ class HaEntitiesPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _addEntity(
|
||||
event: ValueChangedEvent<string> & HASSDomCurrentTargetEvent<HaEntityPicker>
|
||||
) {
|
||||
private async _addEntity(event: ValueChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.value = "";
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
@@ -249,7 +239,6 @@ class HaEntitiesPicker extends LitElement {
|
||||
}
|
||||
.entity ha-entity-picker {
|
||||
flex: 1;
|
||||
min-width: var(--ha-entities-picker-entity-min-width, auto);
|
||||
}
|
||||
.entity-handle {
|
||||
padding: 8px;
|
||||
|
||||
@@ -117,7 +117,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-button-toggle-group
|
||||
size="s"
|
||||
size="small"
|
||||
.buttons=${modeButtons}
|
||||
.active=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./input/ha-input-multi";
|
||||
|
||||
@customElement("ha-aliases-editor")
|
||||
class AliasesEditor extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public aliases!: string[];
|
||||
|
||||
@@ -28,9 +25,9 @@ class AliasesEditor extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.sortable=${this.sortable}
|
||||
update-on-blur
|
||||
.label=${this._localize("ui.dialogs.aliases.label")}
|
||||
.removeLabel=${this._localize("ui.dialogs.aliases.remove")}
|
||||
.addLabel=${this._localize("ui.dialogs.aliases.add")}
|
||||
.label=${this.hass!.localize("ui.dialogs.aliases.label")}
|
||||
.removeLabel=${this.hass!.localize("ui.dialogs.aliases.remove")}
|
||||
.addLabel=${this.hass!.localize("ui.dialogs.aliases.add")}
|
||||
item-index
|
||||
@value-changed=${this._aliasesChanged}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
@@ -11,19 +10,10 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
@@ -37,6 +27,8 @@ const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -92,37 +84,16 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _pendingAreaId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingAreaId &&
|
||||
changedProperties.has("_areas") &&
|
||||
this._areas[this._pendingAreaId]
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.areas !== changedProperties.get("hass")?.areas &&
|
||||
this.hass.areas[this._pendingAreaId]
|
||||
) {
|
||||
this._setValue(this._pendingAreaId);
|
||||
this._pendingAreaId = undefined;
|
||||
@@ -136,11 +107,11 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
private _getAreasMemoized = memoizeOne(
|
||||
(
|
||||
haAreas: ContextType<typeof areasContext>,
|
||||
haFloors: ContextType<typeof floorsContext>,
|
||||
haDevices: ContextType<typeof devicesContext>,
|
||||
haEntities: ContextType<typeof entitiesContext>,
|
||||
haStates: ContextType<typeof statesContext>,
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
@@ -160,9 +131,9 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(haAreas: ContextType<typeof areasContext>): PickerValueRenderer =>
|
||||
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
|
||||
(value) => {
|
||||
const area = haAreas[value];
|
||||
const area = this.hass.areas[value];
|
||||
|
||||
if (!area) {
|
||||
return html`
|
||||
@@ -171,7 +142,7 @@ export class HaAreaPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const { floor } = getAreaContext(area, this._floors);
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
@@ -195,11 +166,11 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreasMemoized(
|
||||
this._areas,
|
||||
this._floors,
|
||||
this._devices,
|
||||
this._entities,
|
||||
this._states,
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.states,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@@ -209,7 +180,7 @@ export class HaAreaPicker extends LitElement {
|
||||
);
|
||||
|
||||
private _allAreaNames = memoizeOne(
|
||||
(areas: ContextType<typeof areasContext>) =>
|
||||
(areas: HomeAssistant["areas"]) =>
|
||||
Object.values(areas)
|
||||
.map((area) => computeAreaName(area)?.toLowerCase())
|
||||
.filter(Boolean) as string[]
|
||||
@@ -222,13 +193,13 @@ export class HaAreaPicker extends LitElement {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allAreas = this._allAreaNames(this._areas);
|
||||
const allAreas = this._allAreaNames(this.hass.areas);
|
||||
|
||||
if (searchString && !allAreas.includes(searchString.toLowerCase())) {
|
||||
return [
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this._i18n.localize(
|
||||
primary: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
@@ -242,7 +213,7 @@ export class HaAreaPicker extends LitElement {
|
||||
return [
|
||||
{
|
||||
id: ADD_NEW_ID,
|
||||
primary: this._i18n.localize("ui.components.area-picker.add_new"),
|
||||
primary: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
icon_path: mdiPlus,
|
||||
},
|
||||
];
|
||||
@@ -250,17 +221,15 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const baseLabel =
|
||||
this.label ?? this._i18n.localize("ui.components.area-picker.area");
|
||||
const areas = this._areas;
|
||||
const floors = this._floors;
|
||||
const valueRenderer = this._computeValueRenderer(areas);
|
||||
this.label ?? this.hass.localize("ui.components.area-picker.area");
|
||||
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
||||
|
||||
// Only show label if there's no floor
|
||||
let label: string | undefined = baseLabel;
|
||||
if (this.value && baseLabel) {
|
||||
const area = areas[this.value];
|
||||
const area = this.hass.areas[this.value];
|
||||
if (area) {
|
||||
const { floor } = getAreaContext(area, floors);
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
if (floor) {
|
||||
label = undefined;
|
||||
}
|
||||
@@ -269,11 +238,12 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this._i18n.localize("ui.components.area-picker.no_areas")}
|
||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${this.value}
|
||||
@@ -282,7 +252,7 @@ export class HaAreaPicker extends LitElement {
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${areaComboBoxKeys}
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -301,7 +271,7 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
|
||||
if (value.startsWith(ADD_NEW_ID)) {
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
|
||||
const suggestedName = value.substring(ADD_NEW_ID.length);
|
||||
|
||||
@@ -309,15 +279,15 @@ export class HaAreaPicker extends LitElement {
|
||||
suggestedName: suggestedName,
|
||||
createEntry: async (values) => {
|
||||
try {
|
||||
const area = await createAreaRegistryEntry(this._api, values);
|
||||
if (this._areas[area.area_id]) {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
if (this.hass.areas[area.area_id]) {
|
||||
this._setValue(area.area_id);
|
||||
} else {
|
||||
this._pendingAreaId = area.area_id;
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this._i18n.localize(
|
||||
title: this.hass.localize(
|
||||
"ui.components.area-picker.failed_create_area"
|
||||
),
|
||||
text: err.message,
|
||||
@@ -338,7 +308,7 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this._i18n.localize("ui.components.area-picker.no_match", {
|
||||
this.hass.localize("ui.components.area-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-area-picker
|
||||
.curValue=${area}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${area}
|
||||
.label=${this.pickedAreaLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@@ -111,6 +112,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
|
||||
<div>
|
||||
<ha-area-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickAreaLabel}
|
||||
.helper=${this.helper}
|
||||
.includeDomains=${this.includeDomains}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { formattersContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-attribute-value")
|
||||
class HaAttributeValue extends LitElement {
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@@ -57,7 +53,7 @@ class HaAttributeValue extends LitElement {
|
||||
}
|
||||
|
||||
if (this.hideUnit) {
|
||||
const parts = this._formatters!.formatEntityAttributeValueToParts(
|
||||
const parts = this.hass.formatEntityAttributeValueToParts(
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
@@ -67,10 +63,7 @@ class HaAttributeValue extends LitElement {
|
||||
.join("");
|
||||
}
|
||||
|
||||
return this._formatters!.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -80,6 +80,7 @@ class HaAttributes extends LitElement {
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-attribute-value>
|
||||
|
||||
@@ -15,7 +15,7 @@ import "./ha-svg-icon";
|
||||
*
|
||||
* @attr {ToggleButton[]} buttons - the button config
|
||||
* @attr {string} active - The value of the currently active button.
|
||||
* @attr {("s"|"m")} size - The size of the buttons in the group.
|
||||
* @attr {("small"|"medium")} size - The size of the buttons in the group.
|
||||
* @attr {("brand"|"neutral"|"success"|"warning"|"danger")} variant - The variant of the buttons in the group.
|
||||
*
|
||||
* @fires value-changed - Dispatched when the active button changes.
|
||||
@@ -26,7 +26,7 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@property({ reflect: true }) size: "s" | "m" = "m";
|
||||
@property({ reflect: true }) size: "small" | "medium" = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "no-wrap" })
|
||||
public nowrap = false;
|
||||
|
||||
@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
* @cssprop --ha-button-height - The height of the button.
|
||||
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
|
||||
*
|
||||
* @attr {("xs"|"s"|"m"|"l"|"xl")} size - Sets the button size.
|
||||
* @attr {("small"|"medium"|"large")} size - Sets the button size.
|
||||
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
|
||||
* @attr {("accent"|"filled"|"plain")} appearance - Sets the button appearance.
|
||||
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
|
||||
@@ -65,7 +65,7 @@ export class HaButton extends Button {
|
||||
box-shadow: var(--ha-button-box-shadow);
|
||||
}
|
||||
|
||||
:host([size="s"]) .button {
|
||||
:host([size="small"]) .button {
|
||||
--wa-form-control-height: var(
|
||||
--ha-button-height,
|
||||
var(--button-height, 32px)
|
||||
@@ -74,7 +74,7 @@ export class HaButton extends Button {
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([size="l"]) .button {
|
||||
:host([size="large"]) .button {
|
||||
--wa-form-control-height: var(
|
||||
--ha-button-height,
|
||||
var(--button-height, 48px)
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { ClimateEntity } from "../data/climate";
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import { formattersContext } from "../data/context";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
class HaClimateState extends LitElement {
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: ClimateEntity;
|
||||
|
||||
@@ -32,7 +24,7 @@ class HaClimateState extends LitElement {
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${this._formatters!.formatEntityAttributeValue(
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"preset_mode"
|
||||
)}`
|
||||
@@ -45,7 +37,7 @@ class HaClimateState extends LitElement {
|
||||
${currentStatus && !noValue
|
||||
? html`
|
||||
<div class="current">
|
||||
${this._localize("ui.card.climate.currently")}:
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>
|
||||
`
|
||||
@@ -53,32 +45,32 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
private _computeCurrentStatus(): string | undefined {
|
||||
if (!this._formatters || !this.stateObj) {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.current_temperature != null &&
|
||||
this.stateObj.attributes.current_humidity != null
|
||||
) {
|
||||
return `${this._formatters.formatEntityAttributeValue(
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
)}/
|
||||
${this._formatters.formatEntityAttributeValue(
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return this._formatters.formatEntityAttributeValue(
|
||||
return this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return this._formatters.formatEntityAttributeValue(
|
||||
return this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
);
|
||||
@@ -88,7 +80,7 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
private _computeTarget(): string {
|
||||
if (!this._formatters || !this.stateObj) {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -96,39 +88,33 @@ class HaClimateState extends LitElement {
|
||||
this.stateObj.attributes.target_temp_low != null &&
|
||||
this.stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${this._formatters.formatEntityAttributeValue(
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_temp_low"
|
||||
)}-${this._formatters.formatEntityAttributeValue(
|
||||
)}-${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_temp_high"
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return this._formatters.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"temperature"
|
||||
);
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj, "temperature");
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.target_humidity_low != null &&
|
||||
this.stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${this._formatters.formatEntityAttributeValue(
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_humidity_low"
|
||||
)}-${this._formatters.formatEntityAttributeValue(
|
||||
)}-${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"target_humidity_high"
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return this._formatters.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"humidity"
|
||||
);
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj, "humidity");
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -139,13 +125,13 @@ class HaClimateState extends LitElement {
|
||||
this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
) {
|
||||
return this._localize(`state.default.${this.stateObj.state}`);
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = this._formatters!.formatEntityState(this.stateObj);
|
||||
const stateString = this.hass.formatEntityState(this.stateObj);
|
||||
|
||||
if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
|
||||
const actionString = this._formatters!.formatEntityAttributeValue(
|
||||
const actionString = this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"hvac_action"
|
||||
);
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiStop } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import type { CoverEntity } from "../data/cover";
|
||||
import { canClose, canOpen, canStop, CoverEntityFeature } from "../data/cover";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-controls")
|
||||
class HaCoverControls extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
@@ -32,7 +26,7 @@ class HaCoverControls extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: !supportsFeature(this.stateObj, CoverEntityFeature.OPEN),
|
||||
})}
|
||||
.label=${this._localize("ui.card.cover.open_cover")}
|
||||
.label=${this.hass.localize("ui.card.cover.open_cover")}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${!canOpen(this.stateObj)}
|
||||
.path=${computeOpenIcon(this.stateObj)}
|
||||
@@ -42,7 +36,7 @@ class HaCoverControls extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: !supportsFeature(this.stateObj, CoverEntityFeature.STOP),
|
||||
})}
|
||||
.label=${this._localize("ui.card.cover.stop_cover")}
|
||||
.label=${this.hass.localize("ui.card.cover.stop_cover")}
|
||||
.path=${mdiStop}
|
||||
@click=${this._onStopTap}
|
||||
.disabled=${!canStop(this.stateObj)}
|
||||
@@ -51,7 +45,7 @@ class HaCoverControls extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: !supportsFeature(this.stateObj, CoverEntityFeature.CLOSE),
|
||||
})}
|
||||
.label=${this._localize("ui.card.cover.close_cover")}
|
||||
.label=${this.hass.localize("ui.card.cover.close_cover")}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${!canClose(this.stateObj)}
|
||||
.path=${computeCloseIcon(this.stateObj)}
|
||||
@@ -63,21 +57,21 @@ class HaCoverControls extends LitElement {
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("cover", "open_cover", {
|
||||
this.hass.callService("cover", "open_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("cover", "close_cover", {
|
||||
this.hass.callService("cover", "close_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("cover", "stop_cover", {
|
||||
this.hass.callService("cover", "stop_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import type { CoverEntity } from "../data/cover";
|
||||
import {
|
||||
canCloseTilt,
|
||||
@@ -14,14 +10,12 @@ import {
|
||||
canStopTilt,
|
||||
CoverEntityFeature,
|
||||
} from "../data/cover";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-tilt-controls")
|
||||
class HaCoverTiltControls extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) stateObj!: CoverEntity;
|
||||
|
||||
@@ -37,7 +31,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
),
|
||||
})}
|
||||
.label=${this._localize("ui.card.cover.open_tilt_cover")}
|
||||
.label=${this.hass.localize("ui.card.cover.open_tilt_cover")}
|
||||
.path=${mdiArrowTopRight}
|
||||
@click=${this._onOpenTiltTap}
|
||||
.disabled=${!canOpenTilt(this.stateObj)}
|
||||
@@ -49,7 +43,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
CoverEntityFeature.STOP_TILT
|
||||
),
|
||||
})}
|
||||
.label=${this._localize("ui.card.cover.stop_cover")}
|
||||
.label=${this.hass.localize("ui.card.cover.stop_cover")}
|
||||
.path=${mdiStop}
|
||||
@click=${this._onStopTiltTap}
|
||||
.disabled=${!canStopTilt(this.stateObj)}
|
||||
@@ -61,7 +55,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
CoverEntityFeature.CLOSE_TILT
|
||||
),
|
||||
})}
|
||||
.label=${this._localize("ui.card.cover.close_tilt_cover")}
|
||||
.label=${this.hass.localize("ui.card.cover.close_tilt_cover")}
|
||||
.path=${mdiArrowBottomLeft}
|
||||
@click=${this._onCloseTiltTap}
|
||||
.disabled=${!canCloseTilt(this.stateObj)}
|
||||
@@ -70,21 +64,21 @@ class HaCoverTiltControls extends LitElement {
|
||||
|
||||
private _onOpenTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("cover", "open_cover_tilt", {
|
||||
this.hass.callService("cover", "open_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("cover", "close_cover_tilt", {
|
||||
this.hass.callService("cover", "close_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("cover", "stop_cover_tilt", {
|
||||
this.hass.callService("cover", "stop_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class HaDurationInput extends LitElement {
|
||||
${this.allowNegative
|
||||
? html`
|
||||
<ha-button-toggle-group
|
||||
size="s"
|
||||
size="small"
|
||||
.buttons=${[
|
||||
{ label: "+", iconPath: mdiPlusThick, value: "+" },
|
||||
{ label: "-", iconPath: mdiMinusThick, value: "-" },
|
||||
|
||||
@@ -107,7 +107,6 @@ export class HaExpansionPanel extends LitElement {
|
||||
}
|
||||
const newExpanded = !this.expanded;
|
||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
||||
|
||||
this._container.style.overflow = "hidden";
|
||||
|
||||
if (newExpanded) {
|
||||
|
||||
@@ -120,7 +120,7 @@ export class HaFileUpload extends LitElement {
|
||||
@dragend=${this._handleDragEnd}
|
||||
>${!this.value
|
||||
? html`<ha-button
|
||||
size="s"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this._openFilePicker}
|
||||
>
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
subscribeCategoryRegistry,
|
||||
updateCategoryRegistryEntry,
|
||||
} from "../data/category_registry";
|
||||
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { confirmDeleteCategory } from "../panels/config/category/confirm-delete-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -199,17 +199,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _deleteCategory(id: string) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.category.editor.confirm_delete"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.category.editor.confirm_delete_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) {
|
||||
if (!(await confirmDeleteCategory(this, this.hass))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
+134
-138
@@ -1,5 +1,5 @@
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -9,18 +9,14 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
import "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListSelectableVirtualized } from "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListVirtualizedItem } from "./list/ha-list-virtualized";
|
||||
|
||||
interface HaFilterDevicesItem extends HaListVirtualizedItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@@ -38,12 +34,15 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
@query("ha-list-selectable-virtualized")
|
||||
private _listElement?: HaListSelectableVirtualized;
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
@@ -52,20 +51,6 @@ export class HaFilterDevices extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded || !this._listElement) {
|
||||
return;
|
||||
}
|
||||
this._listElement.style.height = `${this.clientHeight - 49 - 4 - 38}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 38px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -81,7 +66,6 @@ export class HaFilterDevices extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
@keydown=${this._handleClearFilterKeydown}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -90,45 +74,75 @@ export class HaFilterDevices extends LitElement {
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleSearchKeydown}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(this.hass.devices, this._filter || "")}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
></ha-list-selectable-virtualized>`
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
this.hass.devices,
|
||||
this._filter || "",
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderItem = (item?: HaFilterDevicesItem) =>
|
||||
!item
|
||||
? nothing
|
||||
: html`<ha-list-item-option
|
||||
style="width: 100%;"
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${item.id}
|
||||
.selected=${this.value?.includes(item.id) ?? false}
|
||||
>
|
||||
<span slot="headline">${item.name}</span>
|
||||
</ha-list-item-option>`;
|
||||
private _keyFunction = (device) => device?.id;
|
||||
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
this.value = [
|
||||
...(this.value ?? []),
|
||||
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
|
||||
];
|
||||
private _renderItem = (device) =>
|
||||
!device
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
tabindex="0"
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
private _handleItemKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
|
||||
.id;
|
||||
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
listItem.selected = this.value?.includes(value);
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
@@ -141,38 +155,30 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = target.value ?? "";
|
||||
}
|
||||
|
||||
private _handleSearchKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "ArrowDown" && this._listElement) {
|
||||
ev.preventDefault();
|
||||
this._listElement.focus();
|
||||
}
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
devices: HomeAssistant["devices"],
|
||||
filter: string
|
||||
): HaFilterDevicesItem[] => {
|
||||
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
||||
const values = Object.values(devices);
|
||||
return values
|
||||
.map((device) => ({
|
||||
id: device.id,
|
||||
interactive: true,
|
||||
name: computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
),
|
||||
}))
|
||||
.filter(
|
||||
({ name }) =>
|
||||
!filter || name.toLowerCase().includes(filter.toLowerCase())
|
||||
(device) =>
|
||||
!filter ||
|
||||
computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
stringCompare(
|
||||
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
|
||||
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -211,13 +217,6 @@ export class HaFilterDevices extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClearFilterKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._clearFilter(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
@@ -225,61 +224,58 @@ export class HaFilterDevices extends LitElement {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
this._listElement?.clearSelection();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
:host([expanded]) ha-expansion-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable-virtualized {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -23,6 +23,7 @@ import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "./list/types";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@@ -41,7 +42,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@query("ha-list-selectable") private _list?: HaListSelectable;
|
||||
@query("ha-list-selectable") private _list?: HTMLElement;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
@@ -74,7 +75,6 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
@keydown=${this._handleClearFilterKeydown}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -83,8 +83,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
<ha-list-selectable
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
@ha-list-selected=${this._handleListChanged}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
@@ -164,47 +163,46 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
|
||||
const addedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
ev.detail
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
if (!addedItem) {
|
||||
private _handleListChanged(ev: CustomEvent<HaListSelectedDetail>) {
|
||||
if (!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
[addedItem.type]: [
|
||||
...(this.value[addedItem.type] || []),
|
||||
addedItem.value,
|
||||
],
|
||||
};
|
||||
}
|
||||
if (ev.detail.diff?.added.size) {
|
||||
const addedIndex = ev.detail.diff.added.values().next().value;
|
||||
if (addedIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const addedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
addedIndex
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
if (!this.value) {
|
||||
return;
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
this.value = {
|
||||
...this.value,
|
||||
[addedItem.type]: [
|
||||
...(this.value[addedItem.type] || []),
|
||||
addedItem.value,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const removedIndex = ev.detail.diff?.removed.values().next().value;
|
||||
if (removedIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const removedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
removedIndex
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
[removedItem.type]: this.value![removedItem.type].filter(
|
||||
(val) => val !== removedItem.value
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const removedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
ev.detail
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
if (!removedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
[removedItem.type]: this.value![removedItem.type].filter(
|
||||
(val) => val !== removedItem.value
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
@@ -288,13 +286,6 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClearFilterKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._clearFilter(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
@@ -302,7 +293,6 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
this._list?.clearSelection();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, manifestsContext } from "../data/context";
|
||||
import type { IntegrationManifest } from "../data/integration";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { domainToName, fetchIntegrationManifests } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
@@ -23,26 +21,15 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-integrations")
|
||||
export class HaFilterIntegrations extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
@state()
|
||||
private _manifests?: ContextType<typeof manifestsContext>;
|
||||
|
||||
private _manifestList = memoizeOne(
|
||||
(manifests: ContextType<typeof manifestsContext>) =>
|
||||
Object.values(manifests)
|
||||
);
|
||||
@state() private _manifests?: IntegrationManifest[];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@@ -51,10 +38,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected render() {
|
||||
const manifests = this._manifests
|
||||
? this._manifestList(this._manifests)
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@@ -63,7 +46,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.integrations.caption")}
|
||||
${this.hass.localize("ui.panel.config.integrations.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -72,7 +55,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${manifests && this._shouldRender
|
||||
${this._manifests && this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@@ -86,11 +69,10 @@ export class HaFilterIntegrations extends LitElement {
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(
|
||||
this._localize,
|
||||
manifests,
|
||||
this.hass.localize,
|
||||
this._manifests,
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
this.value
|
||||
),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
@@ -135,8 +117,9 @@ export class HaFilterIntegrations extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._i18n.loadBackendTranslation("title");
|
||||
protected async firstUpdated() {
|
||||
this._manifests = await fetchIntegrationManifests(this.hass);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _integrations = memoizeOne(
|
||||
@@ -144,8 +127,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
localize: LocalizeFunc,
|
||||
manifest: IntegrationManifest[],
|
||||
filter: string | undefined,
|
||||
_value: string[] | undefined,
|
||||
language: string
|
||||
_value
|
||||
) =>
|
||||
manifest
|
||||
.map((mnfst) => ({
|
||||
@@ -162,20 +144,17 @@ export class HaFilterIntegrations extends LitElement {
|
||||
mnfst.name.toLowerCase().includes(filter) ||
|
||||
mnfst.domain.toLowerCase().includes(filter))
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
)
|
||||
);
|
||||
|
||||
private _itemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!this._manifests) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = this._integrations(
|
||||
this._localize,
|
||||
this._manifestList(this._manifests),
|
||||
this.hass.localize,
|
||||
this._manifests!,
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
this.value
|
||||
);
|
||||
|
||||
const visibleDomains = new Set(integrations.map((i) => i.domain));
|
||||
|
||||
@@ -129,7 +129,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
@wa-select=${this._handleAddAction}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-button slot="trigger" appearance="filled" size="s">
|
||||
<ha-button slot="trigger" appearance="filled" size="small">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.localize?.("ui.components.form-optional-actions.add") ||
|
||||
"Add interaction"}
|
||||
|
||||
@@ -174,7 +174,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
<slot name="field">
|
||||
${this.addButtonLabel && !this.value
|
||||
? html`<ha-button
|
||||
size="s"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this.open}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -8,12 +8,13 @@ import { conditionalClamp } from "../common/number/clamp";
|
||||
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: CardGridSize;
|
||||
|
||||
@property({ attribute: false }) public rows = 8;
|
||||
@@ -34,10 +35,6 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._localValue = this.value;
|
||||
@@ -65,7 +62,9 @@ export class HaGridSizeEditor extends LitElement {
|
||||
return html`
|
||||
<div class="grid">
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this._localize("ui.components.grid-size-picker.columns")}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.columns"
|
||||
)}
|
||||
id="columns"
|
||||
.min=${columnMin}
|
||||
.max=${columnMax}
|
||||
@@ -79,7 +78,9 @@ export class HaGridSizeEditor extends LitElement {
|
||||
></ha-grid-layout-slider>
|
||||
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this._localize("ui.components.grid-size-picker.rows")}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.rows"
|
||||
)}
|
||||
id="rows"
|
||||
.min=${rowMin}
|
||||
.max=${rowMax}
|
||||
@@ -97,10 +98,10 @@ export class HaGridSizeEditor extends LitElement {
|
||||
@click=${this._reset}
|
||||
class="reset"
|
||||
.path=${mdiRestore}
|
||||
label=${this._localize(
|
||||
label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.reset_default"
|
||||
)}
|
||||
title=${this._localize(
|
||||
title=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.reset_default"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { formattersContext } from "../data/context";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
|
||||
import type { HumidifierEntity } from "../data/humidifier";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-humidifier-state")
|
||||
class HaHumidifierState extends LitElement {
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HumidifierEntity;
|
||||
|
||||
@@ -30,7 +22,7 @@ class HaHumidifierState extends LitElement {
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.mode
|
||||
? html`-
|
||||
${this._formatters!.formatEntityAttributeValue(
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"mode"
|
||||
)}`
|
||||
@@ -42,19 +34,19 @@ class HaHumidifierState extends LitElement {
|
||||
|
||||
${currentStatus && !noValue
|
||||
? html`<div class="current">
|
||||
${this._localize("ui.card.humidifier.currently")}:
|
||||
${this.hass.localize("ui.card.humidifier.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _computeCurrentStatus(): string | undefined {
|
||||
if (!this._formatters || !this.stateObj) {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${this._formatters.formatEntityAttributeValue(
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}`;
|
||||
@@ -64,12 +56,12 @@ class HaHumidifierState extends LitElement {
|
||||
}
|
||||
|
||||
private _computeTarget(): string {
|
||||
if (!this._formatters || !this.stateObj) {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${this._formatters.formatEntityAttributeValue(
|
||||
return `${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"humidity"
|
||||
)}`;
|
||||
@@ -83,13 +75,13 @@ class HaHumidifierState extends LitElement {
|
||||
this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
) {
|
||||
return this._localize(`state.default.${this.stateObj.state}`);
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = this._formatters!.formatEntityState(this.stateObj);
|
||||
const stateString = this.hass.formatEntityState(this.stateObj);
|
||||
|
||||
if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
|
||||
const actionString = this._formatters!.formatEntityAttributeValue(
|
||||
const actionString = this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"action"
|
||||
);
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-icon-button-arrow-next")
|
||||
export class HaIconButtonArrowNext extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -16,15 +17,11 @@ export class HaIconButtonArrowNext extends LitElement {
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowLeft : mdiArrowRight;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this._localize("ui.common.next") || "Next"}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
.path=${this._icon}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-icon-button-arrow-prev")
|
||||
export class HaIconButtonArrowPrev extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -24,15 +25,11 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this._localize("ui.common.back") || "Back"}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-icon-button-next")
|
||||
export class HaIconButtonNext extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -24,15 +25,11 @@ export class HaIconButtonNext extends LitElement {
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this._localize("ui.common.next") || "Next"}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-icon-button-prev")
|
||||
export class HaIconButtonPrev extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -24,15 +25,11 @@ export class HaIconButtonPrev extends LitElement {
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this._localize("ui.common.back") || "Back"}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
|
||||
@@ -171,7 +171,7 @@ export class HaLabelsPicker extends LitElement {
|
||||
: nothing}
|
||||
<ha-button
|
||||
id="picker"
|
||||
size="s"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this._openPicker}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -53,7 +53,7 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this.callService}
|
||||
.service=${action.service}
|
||||
size="s"
|
||||
size="small"
|
||||
>
|
||||
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
</ha-button>
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
connectionContext,
|
||||
narrowViewportContext,
|
||||
uiContext,
|
||||
} from "../data/context";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-menu-button")
|
||||
class HaMenuButton extends LitElement {
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: narrowViewportContext, subscribe: true })
|
||||
private _narrow = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui?: ContextType<typeof uiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _hasNotifications = false;
|
||||
|
||||
@@ -44,7 +26,7 @@ class HaMenuButton extends LitElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._attachNotifOnConnect && this._connection) {
|
||||
if (this._attachNotifOnConnect) {
|
||||
this._attachNotifOnConnect = false;
|
||||
this._subscribeNotifications();
|
||||
}
|
||||
@@ -60,15 +42,15 @@ class HaMenuButton extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._show || !this._ui) {
|
||||
if (!this._show) {
|
||||
return nothing;
|
||||
}
|
||||
const hasNotifications =
|
||||
this._hasNotifications &&
|
||||
(this._narrow || this._ui.dockedSidebar === "always_hidden");
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden");
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.label=${this._localize("ui.sidebar.sidebar_toggle")}
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
.path=${mdiMenu}
|
||||
@click=${this._toggleMenu}
|
||||
></ha-icon-button>
|
||||
@@ -76,25 +58,30 @@ class HaMenuButton extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
!changedProps.has("_narrow") &&
|
||||
!changedProps.has("_ui") &&
|
||||
!changedProps.has("_connection")
|
||||
) {
|
||||
if (!changedProps.has("narrow") && !changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProps.has("_connection") && this._unsubNotifications) {
|
||||
this._unsubNotifications();
|
||||
this._unsubNotifications = undefined;
|
||||
}
|
||||
const oldHass = changedProps.has("hass")
|
||||
? (changedProps.get("hass") as HomeAssistant | undefined)
|
||||
: this.hass;
|
||||
const oldNarrow = changedProps.has("narrow")
|
||||
? (changedProps.get("narrow") as boolean | undefined)
|
||||
: this.narrow;
|
||||
|
||||
const oldShowButton =
|
||||
oldHass?.kioskMode === false &&
|
||||
(oldNarrow || oldHass?.dockedSidebar === "always_hidden");
|
||||
const showButton =
|
||||
this._ui?.kioskMode === false &&
|
||||
(this._narrow || this._ui.dockedSidebar === "always_hidden");
|
||||
this.hass.kioskMode === false &&
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden");
|
||||
|
||||
if (this.hasUpdated && oldShowButton === showButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._show = showButton || this._alwaysVisible;
|
||||
|
||||
@@ -106,10 +93,7 @@ class HaMenuButton extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._unsubNotifications && this._connection) {
|
||||
this._attachNotifOnConnect = false;
|
||||
this._subscribeNotifications();
|
||||
}
|
||||
this._subscribeNotifications();
|
||||
}
|
||||
|
||||
private _subscribeNotifications() {
|
||||
@@ -117,7 +101,7 @@ class HaMenuButton extends LitElement {
|
||||
throw new Error("Already subscribed");
|
||||
}
|
||||
this._unsubNotifications = subscribeNotifications(
|
||||
this._connection!.connection,
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
this._hasNotifications = notifications.length > 0;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@ import type {
|
||||
NetworkConfig,
|
||||
} from "../data/network";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-checkbox";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HaCheckbox } from "./ha-checkbox";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-svg-icon";
|
||||
@@ -42,14 +41,12 @@ declare global {
|
||||
}
|
||||
@customElement("ha-network")
|
||||
export class HaNetwork extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public networkConfig?: NetworkConfig;
|
||||
|
||||
@state() private _expanded?: boolean;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
if (this.networkConfig === undefined) {
|
||||
return nothing;
|
||||
@@ -60,14 +57,14 @@ export class HaNetwork extends LitElement {
|
||||
@change=${this._handleAutoConfigureCheckboxClick}
|
||||
.checked=${!configured_adapters.length}
|
||||
.hint=${!configured_adapters.length
|
||||
? this._localize(
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.network.adapter.auto_configure_manual_hint"
|
||||
)
|
||||
: ""}
|
||||
>
|
||||
${this._localize("ui.panel.config.network.adapter.auto_configure")}
|
||||
${this.hass.localize("ui.panel.config.network.adapter.auto_configure")}
|
||||
<div class="description">
|
||||
${this._localize("ui.panel.config.network.adapter.detected")}:
|
||||
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
|
||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
||||
</div>
|
||||
</ha-checkbox>
|
||||
@@ -80,11 +77,13 @@ export class HaNetwork extends LitElement {
|
||||
.checked=${configured_adapters.includes(adapter.name)}
|
||||
.adapter=${adapter.name}
|
||||
>
|
||||
${this._localize("ui.panel.config.network.adapter.adapter")}:
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.adapter"
|
||||
)}:
|
||||
${adapter.name}
|
||||
${adapter.default
|
||||
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
|
||||
(${this._localize("ui.common.default")})`
|
||||
(${this.hass.localize("ui.common.default")})`
|
||||
: nothing}
|
||||
<div class="description">
|
||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||
|
||||
@@ -102,7 +102,7 @@ export class HaPictureUpload extends LitElement {
|
||||
<div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
variant="danger"
|
||||
@click=${this._handleChangeClick}
|
||||
>
|
||||
|
||||
@@ -91,6 +91,7 @@ export class HaAreaSelector extends LitElement {
|
||||
if (!this.selector.area?.multiple) {
|
||||
return html`
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -63,7 +63,7 @@ export class HaChooseSelector extends LitElement {
|
||||
return html`<div class="multi-header">
|
||||
<span>${this.label}${this.required ? "*" : ""}</span>
|
||||
<ha-button-toggle-group
|
||||
size="s"
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key,
|
||||
|
||||
@@ -190,7 +190,7 @@ export class HaMediaSelector extends LitElement {
|
||||
? html`<div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
variant="danger"
|
||||
@click=${this._clearValue}
|
||||
>
|
||||
|
||||
@@ -307,7 +307,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
>`
|
||||
: nothing}
|
||||
<ha-button-toggle-group
|
||||
size="s"
|
||||
size="small"
|
||||
.buttons=${choiceToggleButtons}
|
||||
.active=${activeChoice}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -9,11 +9,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
type HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -36,11 +32,9 @@ import {
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import type { HaCheckbox } from "./ha-checkbox";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-markdown";
|
||||
import "./ha-selector/ha-selector";
|
||||
import type { HaSelector } from "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-service-section-icon";
|
||||
import "./ha-settings-row";
|
||||
@@ -62,22 +56,6 @@ const showOptionalToggle = (field) =>
|
||||
!field.required &&
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
const FULL_WIDTH_SELECTOR_TYPES = new Set([
|
||||
"action",
|
||||
"area",
|
||||
"device",
|
||||
"entity",
|
||||
"floor",
|
||||
"label",
|
||||
"target",
|
||||
]);
|
||||
|
||||
const isFullWidthSelector = (selector?: Selector): boolean =>
|
||||
!!selector &&
|
||||
Object.keys(selector).some((selectorType) =>
|
||||
FULL_WIDTH_SELECTOR_TYPES.has(selectorType)
|
||||
);
|
||||
|
||||
interface Field extends Omit<HassService["fields"][string], "selector"> {
|
||||
key: string;
|
||||
selector?: Selector;
|
||||
@@ -497,14 +475,6 @@ export class HaServiceControl extends LitElement {
|
||||
)) ||
|
||||
serviceData?.description;
|
||||
|
||||
const targetSelector =
|
||||
serviceData && "target" in serviceData
|
||||
? this._targetSelector(
|
||||
serviceData.target as TargetSelector,
|
||||
this._value?.target
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return html`${this.hidePicker
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
@@ -542,15 +512,16 @@ export class HaServiceControl extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row
|
||||
.narrow=${this.narrow || isFullWidthSelector(targetSelector)}
|
||||
>
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.components.service-control.target")}</span
|
||||
>
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${targetSelector}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector,
|
||||
this._value?.target
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this._value?.target}
|
||||
@@ -693,9 +664,7 @@ export class HaServiceControl extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return dataField.selector
|
||||
? html`<ha-settings-row
|
||||
.narrow=${this.narrow || isFullWidthSelector(selector)}
|
||||
>
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
@@ -768,16 +737,14 @@ export class HaServiceControl extends LitElement {
|
||||
);
|
||||
};
|
||||
|
||||
private _toggleCheckbox(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
|
||||
private _toggleCheckbox(ev: Event) {
|
||||
const checkbox = (
|
||||
ev.currentTarget as HTMLElement
|
||||
)?.parentElement?.querySelector("ha-checkbox");
|
||||
checkbox?.click();
|
||||
}
|
||||
|
||||
private _checkboxChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaCheckbox & { key: string }>
|
||||
) {
|
||||
private _checkboxChanged(ev) {
|
||||
const checked = ev.currentTarget.checked;
|
||||
const key = ev.currentTarget.key;
|
||||
let data;
|
||||
@@ -900,7 +867,7 @@ export class HaServiceControl extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _entityPicked(ev: HASSDomEvent<{ value: string | undefined }>) {
|
||||
private _entityPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (this._value?.data?.entity_id === newValue) {
|
||||
@@ -921,12 +888,7 @@ export class HaServiceControl extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _targetChanged(
|
||||
ev: HASSDomEvent<{
|
||||
value: HassServiceTarget | undefined;
|
||||
isValid?: boolean;
|
||||
}>
|
||||
) {
|
||||
private _targetChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
@@ -948,16 +910,13 @@ export class HaServiceControl extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _serviceDataChanged(
|
||||
ev: HASSDomEvent<{ value: unknown; isValid?: boolean }> &
|
||||
HASSDomCurrentTargetEvent<HaSelector & { key: string }>
|
||||
) {
|
||||
private _serviceDataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const key = ev.currentTarget.key;
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this._value?.data?.[key] === value ||
|
||||
@@ -972,9 +931,7 @@ export class HaServiceControl extends LitElement {
|
||||
if (
|
||||
value === "" ||
|
||||
value === undefined ||
|
||||
(value !== null &&
|
||||
typeof value === "object" &&
|
||||
!Object.keys(value).length)
|
||||
(typeof value === "object" && !Object.keys(value).length)
|
||||
) {
|
||||
delete data[key];
|
||||
delete this._stickySelector[key];
|
||||
@@ -988,12 +945,7 @@ export class HaServiceControl extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _dataChanged(
|
||||
ev: HASSDomEvent<{
|
||||
value: NonNullable<HaServiceControl["value"]>["data"];
|
||||
isValid?: boolean;
|
||||
}>
|
||||
) {
|
||||
private _dataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
@@ -1017,15 +969,14 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
padding: var(--service-control-padding, 0 var(--ha-space-4));
|
||||
padding: var(--service-control-padding, 0 16px);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: var(--ha-space-2);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
--ha-entities-picker-entity-min-width: 0;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
@@ -1035,20 +986,20 @@ export class HaServiceControl extends LitElement {
|
||||
ha-entity-picker,
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: var(--service-control-padding, 0 var(--ha-space-4));
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
padding: 16px 0;
|
||||
}
|
||||
p {
|
||||
margin: var(--service-control-padding, 0 var(--ha-space-4));
|
||||
padding: var(--ha-space-4) 0;
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host([hide-picker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: var(--ha-space-8);
|
||||
width: 32px;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
@@ -1069,7 +1020,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-summary-padding: 0 var(--ha-space-4);
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
@customElement("ha-slider")
|
||||
export class HaSlider extends Slider {
|
||||
@property({ reflect: true }) size: "s" | "m" = "s";
|
||||
@property({ reflect: true }) size: "small" | "medium" = "small";
|
||||
|
||||
@property({ type: Boolean, attribute: "with-tooltip" }) withTooltip = true;
|
||||
|
||||
@@ -110,12 +110,12 @@ export class HaSlider extends Slider {
|
||||
);
|
||||
}
|
||||
|
||||
:host([size="m"]) {
|
||||
:host([size="medium"]) {
|
||||
--thumb-width: 20px;
|
||||
--thumb-height: 20px;
|
||||
}
|
||||
|
||||
:host([size="s"]) {
|
||||
:host([size="small"]) {
|
||||
--thumb-width: 16px;
|
||||
--thumb-height: 16px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
@@ -8,10 +8,6 @@ const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
export const haTopAppBarFixedStyles = css`
|
||||
:host {
|
||||
display: block;
|
||||
--total-top-app-bar-height: calc(
|
||||
var(--header-height, 0px) + var(--sub-row-height, 0px)
|
||||
);
|
||||
--sub-row-height: 0px;
|
||||
}
|
||||
|
||||
.top-app-bar {
|
||||
@@ -41,37 +37,14 @@ export const haTopAppBarFixedStyles = css`
|
||||
}
|
||||
|
||||
.row {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
|
||||
.top-app-bar.has-sub-row .row {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.sub-row {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
|
||||
.sub-row slot,
|
||||
.sub-row ::slotted(*) {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-row[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -113,14 +86,8 @@ export const haTopAppBarFixedStyles = css`
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust {
|
||||
height: calc(
|
||||
100vh - var(--total-top-app-bar-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
padding-top: calc(
|
||||
var(--total-top-app-bar-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
@@ -139,14 +106,8 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
|
||||
@query(".top-app-bar") protected _barElement!: HTMLElement;
|
||||
|
||||
@query(".sub-row") protected _subRowElement?: HTMLElement;
|
||||
|
||||
@state() private _hasSubRow = false;
|
||||
|
||||
private _scrollTarget?: HTMLElement | Window;
|
||||
|
||||
private _subRowResizeObserver?: ResizeObserver;
|
||||
|
||||
@property({ attribute: false })
|
||||
public get scrollTarget(): HTMLElement | Window {
|
||||
return this._scrollTarget || window;
|
||||
@@ -176,8 +137,6 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
super.connectedCallback();
|
||||
|
||||
if (this.hasUpdated) {
|
||||
this._observeSubRowHeight();
|
||||
this._updateSubRowHeight();
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
@@ -194,7 +153,6 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
<header
|
||||
class="top-app-bar ${classMap({
|
||||
"pane-header": paneHeader,
|
||||
"has-sub-row": this._hasSubRow,
|
||||
})}"
|
||||
>
|
||||
<div class="row">
|
||||
@@ -218,9 +176,6 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
<div class="sub-row" ?hidden=${!this._hasSubRow}>
|
||||
<slot name="subRow" @slotchange=${this._subRowSlotChanged}></slot>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
@@ -233,23 +188,13 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._observeSubRowHeight();
|
||||
this._updateSubRowHeight();
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("_hasSubRow")) {
|
||||
this._updateSubRowHeight();
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unobserveSubRowHeight();
|
||||
this._unregisterListeners();
|
||||
}
|
||||
|
||||
@@ -280,40 +225,6 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
this.scrollTarget.removeEventListener("scroll", this._syncScrollState);
|
||||
}
|
||||
|
||||
private _observeSubRowHeight() {
|
||||
if (
|
||||
this._subRowResizeObserver ||
|
||||
!this._subRowElement ||
|
||||
typeof ResizeObserver === "undefined"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._subRowResizeObserver = new ResizeObserver(this._updateSubRowHeight);
|
||||
this._subRowResizeObserver.observe(this._subRowElement);
|
||||
}
|
||||
|
||||
private _unobserveSubRowHeight() {
|
||||
this._subRowResizeObserver?.disconnect();
|
||||
this._subRowResizeObserver = undefined;
|
||||
}
|
||||
|
||||
private _subRowSlotChanged = (ev: Event) => {
|
||||
const slot = ev.currentTarget as HTMLSlotElement;
|
||||
this._hasSubRow = slot
|
||||
.assignedNodes({ flatten: true })
|
||||
.some(
|
||||
(node) =>
|
||||
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim())
|
||||
);
|
||||
};
|
||||
|
||||
private _updateSubRowHeight = () => {
|
||||
const subRowHeight = this._hasSubRow
|
||||
? this._subRowElement?.offsetHeight || 0
|
||||
: 0;
|
||||
this.style.setProperty("--sub-row-height", `${subRowHeight}px`);
|
||||
};
|
||||
|
||||
static override styles: CSSResultGroup = haTopAppBarFixedStyles;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class HaTracePicker extends LitElement {
|
||||
slot="field"
|
||||
appearance="filled"
|
||||
variant="neutral"
|
||||
size="s"
|
||||
size="small"
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
${this._renderTracePickerValue(this.value!)}
|
||||
|
||||
@@ -108,9 +108,9 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
css`
|
||||
.shadow-container {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--total-top-app-bar-height));
|
||||
top: calc(-1 * var(--header-height));
|
||||
width: 100%;
|
||||
height: var(--total-top-app-bar-height);
|
||||
height: var(--header-height);
|
||||
z-index: 1;
|
||||
transition: box-shadow 200ms linear;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
.top-app-bar-fixed-adjust--pane {
|
||||
display: flex;
|
||||
height: calc(
|
||||
100vh - var(--total-top-app-bar-height, 0px) - var(
|
||||
100vh - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
|
||||
@@ -108,6 +108,7 @@ export class HaVacuumSegmentAreaMapper extends LitElement {
|
||||
<span class="segment-name">${segment.name}</span>
|
||||
<ha-svg-icon class="arrow" .path=${mdiArrowRightThin}></ha-svg-icon>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${mappedAreas}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.area_label"
|
||||
|
||||
@@ -58,7 +58,7 @@ export class HaVacuumState extends LitElement {
|
||||
return html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
@click=${this._callService}
|
||||
.disabled=${!interceptable}
|
||||
>
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import type { ValveEntity } from "../data/valve";
|
||||
import { ValveEntityFeature, canClose, canOpen, canStop } from "../data/valve";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-valve-controls")
|
||||
class HaValveControls extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: ValveEntity;
|
||||
|
||||
@@ -31,7 +25,7 @@ class HaValveControls extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.OPEN),
|
||||
})}
|
||||
.label=${this._localize("ui.card.valve.open_valve")}
|
||||
.label=${this.hass.localize("ui.card.valve.open_valve")}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${!canOpen(this.stateObj)}
|
||||
.path=${mdiValveOpen}
|
||||
@@ -41,7 +35,7 @@ class HaValveControls extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.STOP),
|
||||
})}
|
||||
.label=${this._localize("ui.card.valve.stop_valve")}
|
||||
.label=${this.hass.localize("ui.card.valve.stop_valve")}
|
||||
@click=${this._onStopTap}
|
||||
.disabled=${!canStop(this.stateObj)}
|
||||
.path=${mdiStop}
|
||||
@@ -50,7 +44,7 @@ class HaValveControls extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.CLOSE),
|
||||
})}
|
||||
.label=${this._localize("ui.card.valve.close_valve")}
|
||||
.label=${this.hass.localize("ui.card.valve.close_valve")}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${!canClose(this.stateObj)}
|
||||
.path=${mdiValveClosed}
|
||||
@@ -62,21 +56,21 @@ class HaValveControls extends LitElement {
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("valve", "open_valve", {
|
||||
this.hass.callService("valve", "open_valve", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("valve", "close_valve", {
|
||||
this.hass.callService("valve", "close_valve", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._api.callService("valve", "stop_valve", {
|
||||
this.hass.callService("valve", "stop_valve", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export class HaInputCopy extends LitElement {
|
||||
: nothing}
|
||||
</ha-input>
|
||||
</div>
|
||||
<ha-button @click=${this._copy} appearance="plain" size="s">
|
||||
<ha-button @click=${this._copy} appearance="plain" size="small">
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.label || this._i18n.localize("ui.common.copy")}
|
||||
</ha-button>
|
||||
|
||||
@@ -130,7 +130,7 @@ class HaInputMulti extends LitElement {
|
||||
</ha-sortable>
|
||||
<div class="layout horizontal">
|
||||
<ha-button
|
||||
size="s"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled ||
|
||||
|
||||
@@ -38,9 +38,6 @@ export class HaListItemBase extends HaRowItem {
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("ha-list-item")) {
|
||||
this.setAttribute("ha-list-item", "");
|
||||
}
|
||||
if (!this.hasAttribute("role")) {
|
||||
this.setAttribute("role", this.defaultRole);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { compareNodeOrder } from "../../common/dom/compare-node-order";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import "./types";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
@@ -45,13 +45,13 @@ export class HaListBase extends LitElement {
|
||||
/** Host `role` attribute. Empty string means no role is set. */
|
||||
protected readonly hostRole: string = "list";
|
||||
|
||||
protected activeItemIndex = -1;
|
||||
private _activeItemIndex = -1;
|
||||
|
||||
protected firstFocusableIndex = -1;
|
||||
private _firstFocusableIndex = -1;
|
||||
|
||||
protected lastFocusableIndex = -1;
|
||||
private _lastFocusableIndex = -1;
|
||||
|
||||
protected hasFocusableItem = false;
|
||||
private _hasFocusableItem = false;
|
||||
|
||||
private _unbindKeys?: () => void;
|
||||
|
||||
@@ -63,28 +63,22 @@ export class HaListBase extends LitElement {
|
||||
if (!this.hasAttribute("role") && this.hostRole) {
|
||||
this.setAttribute("role", this.hostRole);
|
||||
}
|
||||
this._unbindKeys = tinykeys(
|
||||
this,
|
||||
{
|
||||
ArrowDown: this._onForward,
|
||||
ArrowUp: this._onBack,
|
||||
Home: this._onHome,
|
||||
End: this._onEnd,
|
||||
PageDown: this._onPageDown,
|
||||
PageUp: this._onPageUp,
|
||||
Enter: this.onActivate,
|
||||
Space: this.onActivate,
|
||||
},
|
||||
{ ignore: this._ignoreKeyEvent }
|
||||
);
|
||||
this.addEventListener("focusin", this.onFocusIn);
|
||||
this._unbindKeys = tinykeys(this, {
|
||||
ArrowDown: this._onForward,
|
||||
ArrowUp: this._onBack,
|
||||
Home: this._onHome,
|
||||
End: this._onEnd,
|
||||
Enter: this._onActivate,
|
||||
Space: this._onActivate,
|
||||
});
|
||||
this.addEventListener("focusin", this._onFocusIn);
|
||||
this.addEventListener(
|
||||
"ha-list-item-register",
|
||||
this.onItemRegister as EventListener
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.addEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this.onItemUnregister as EventListener
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,23 +86,25 @@ export class HaListBase extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
this._unbindKeys?.();
|
||||
this._unbindKeys = undefined;
|
||||
this.removeEventListener("focusin", this.onFocusIn);
|
||||
this.removeEventListener("focusin", this._onFocusIn);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-register",
|
||||
this.onItemRegister as EventListener
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this.onItemUnregister as EventListener
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public focus(options?: FocusOptions) {
|
||||
if (!this.itemCount) {
|
||||
if (!this.items.length) {
|
||||
super.focus(options);
|
||||
return;
|
||||
}
|
||||
this.focusItemAtIndex(this.activeItemIndex >= 0 ? this.activeItemIndex : 0);
|
||||
this.focusItemAtIndex(
|
||||
this._activeItemIndex >= 0 ? this._activeItemIndex : 0
|
||||
);
|
||||
}
|
||||
|
||||
public focusItemAtIndex(index: number) {
|
||||
@@ -119,19 +115,19 @@ export class HaListBase extends LitElement {
|
||||
}
|
||||
|
||||
public getActiveItemIndex(): number {
|
||||
return this.activeItemIndex;
|
||||
return this._activeItemIndex;
|
||||
}
|
||||
|
||||
public setActiveItemIndex(index: number, focusItem = false) {
|
||||
if (!this.hasFocusableItem) {
|
||||
this.activeItemIndex = -1;
|
||||
if (!this._hasFocusableItem) {
|
||||
this._activeItemIndex = -1;
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = Math.max(0, Math.min(this.itemCount - 1, index));
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
this._activeItemIndex = Math.max(0, Math.min(this.items.length - 1, index));
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
}
|
||||
this.applyActive(focusItem);
|
||||
this._applyActive(focusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,18 +135,18 @@ export class HaListBase extends LitElement {
|
||||
* to layer in extra bookkeeping (e.g. selection state sync).
|
||||
*/
|
||||
public updateListItems() {
|
||||
this.recomputeFocusableIndexes();
|
||||
this._recomputeFocusableIndexes();
|
||||
if (
|
||||
this.activeItemIndex >= this.itemCount ||
|
||||
!this.hasFocusableItem ||
|
||||
this.activeItemIndex < 0
|
||||
this._activeItemIndex >= this.items.length ||
|
||||
!this._hasFocusableItem ||
|
||||
this._activeItemIndex < 0
|
||||
) {
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
}
|
||||
this.applyActive(false);
|
||||
this._applyActive(false);
|
||||
}
|
||||
|
||||
protected onItemRegister = (
|
||||
private _onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
@@ -164,7 +160,7 @@ export class HaListBase extends LitElement {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
protected onItemUnregister = (
|
||||
private _onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
@@ -176,190 +172,136 @@ export class HaListBase extends LitElement {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
protected recomputeFocusableIndexes() {
|
||||
private _recomputeFocusableIndexes() {
|
||||
let first = -1;
|
||||
let last = -1;
|
||||
for (let i = 0; i < this.itemCount; i++) {
|
||||
if (this.isFocusable(i)) {
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (this._isFocusable(i)) {
|
||||
if (first === -1) {
|
||||
first = i;
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
this.firstFocusableIndex = first;
|
||||
this.lastFocusableIndex = last;
|
||||
this.hasFocusableItem = first !== -1;
|
||||
this._firstFocusableIndex = first;
|
||||
this._lastFocusableIndex = last;
|
||||
this._hasFocusableItem = first !== -1;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
return html`<div part="base" class="base ha-scrollbar">
|
||||
protected render(): TemplateResult {
|
||||
return html`<div part="base" class="base">
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected isFocusable(index: number): boolean {
|
||||
private _isFocusable(index: number): boolean {
|
||||
const item = this.items[index];
|
||||
return !!item && item.interactive && !item.disabled;
|
||||
}
|
||||
|
||||
protected applyActive(focusItem: boolean) {
|
||||
private _applyActive(focusItem: boolean) {
|
||||
this.items.forEach((item, i) => {
|
||||
if (!item.interactive || item.disabled) {
|
||||
item.removeAttribute("tabindex");
|
||||
return;
|
||||
}
|
||||
item.tabIndex = i === this.activeItemIndex ? 0 : -1;
|
||||
item.tabIndex = i === this._activeItemIndex ? 0 : -1;
|
||||
});
|
||||
if (focusItem && this.activeItemIndex >= 0) {
|
||||
this.items[this.activeItemIndex]?.focus();
|
||||
if (focusItem && this._activeItemIndex >= 0) {
|
||||
this.items[this._activeItemIndex]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected onFocusIn = (ev: FocusEvent) => {
|
||||
private _onFocusIn = (ev: FocusEvent) => {
|
||||
const path = ev.composedPath();
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (path.includes(this.items[i])) {
|
||||
if (i !== this.activeItemIndex) {
|
||||
this.activeItemIndex = i;
|
||||
this.applyActive(false);
|
||||
if (i !== this._activeItemIndex) {
|
||||
this._activeItemIndex = i;
|
||||
this._applyActive(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _ignoreKeyEvent = (ev: KeyboardEvent): boolean => {
|
||||
if (ev.repeat && (ev.key === "Enter" || ev.key === " ")) {
|
||||
return true;
|
||||
}
|
||||
if (ev.isComposing) {
|
||||
return true;
|
||||
}
|
||||
const target = ev.target as HTMLElement | null;
|
||||
// Allow held arrow/Home/End to repeat for continuous navigation
|
||||
return (
|
||||
!!target &&
|
||||
target !== ev.currentTarget &&
|
||||
target.matches("[contenteditable],input,select,textarea")
|
||||
);
|
||||
};
|
||||
|
||||
private _onForward = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(ev, this._stepIndex(this.activeItemIndex, 1));
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, 1));
|
||||
};
|
||||
|
||||
private _onBack = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(ev, this._stepIndex(this.activeItemIndex, -1));
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, -1));
|
||||
};
|
||||
|
||||
private _onHome = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(ev, this.firstFocusableIndex);
|
||||
this._moveFocus(ev, this._firstFocusableIndex);
|
||||
};
|
||||
|
||||
private _onEnd = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(ev, this.lastFocusableIndex);
|
||||
this._moveFocus(ev, this._lastFocusableIndex);
|
||||
};
|
||||
|
||||
private _onPageDown = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(
|
||||
ev,
|
||||
this._stepIndex(this.activeItemIndex, 1, this.getPageSize())
|
||||
);
|
||||
};
|
||||
|
||||
private _onPageUp = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(
|
||||
ev,
|
||||
this._stepIndex(this.activeItemIndex, -1, this.getPageSize())
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Number of items to jump for PageUp/PageDown. Defaults to 10 (per WAI-ARIA
|
||||
* Authoring Practices: "moves focus a manageable number of nodes,
|
||||
* typically 10"). Subclasses with a known viewport (e.g. virtualized lists)
|
||||
* can override to use the visible page size.
|
||||
*/
|
||||
protected getPageSize(): number {
|
||||
return 10;
|
||||
}
|
||||
|
||||
protected onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
private _onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const active = this.items[this.activeItemIndex];
|
||||
const active = this.items[this._activeItemIndex];
|
||||
active.activate();
|
||||
fireEvent(this, "ha-list-activated", {
|
||||
index: this.activeItemIndex,
|
||||
index: this._activeItemIndex,
|
||||
item: active,
|
||||
});
|
||||
};
|
||||
|
||||
protected moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this.hasFocusableItem) {
|
||||
private _moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this._hasFocusableItem || next < 0 || next === this._activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
if (next < 0 || next === this.activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = next;
|
||||
this.applyActive(true);
|
||||
}
|
||||
|
||||
protected get itemCount(): number {
|
||||
return this.items.length;
|
||||
this._activeItemIndex = next;
|
||||
this._applyActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step from `from` by `delta`, skipping non-interactive and disabled items.
|
||||
* Pass `count` > 1 to advance by multiple focusable items (PageUp/Down).
|
||||
* Returns the last focusable index reached, or `from` when none is.
|
||||
* Returns `from` when no other focusable item can be reached (honouring
|
||||
* `wrapFocus`).
|
||||
*/
|
||||
private _stepIndex(from: number, delta: 1 | -1, count = 1): number {
|
||||
const n = this.itemCount;
|
||||
if (!n || !this.hasFocusableItem) {
|
||||
private _stepIndex(from: number, delta: 1 | -1): number {
|
||||
const n = this.items.length;
|
||||
if (!n || !this._hasFocusableItem) {
|
||||
return from;
|
||||
}
|
||||
let last = from;
|
||||
let i = from;
|
||||
let landed = 0;
|
||||
for (let step = 0; step < n && landed < count; step++) {
|
||||
for (let step = 0; step < n; step++) {
|
||||
i += delta;
|
||||
if (i < 0 || i >= n) {
|
||||
if (!this.wrapFocus) {
|
||||
return last;
|
||||
return from;
|
||||
}
|
||||
i = (i + n) % n;
|
||||
}
|
||||
if (this.isFocusable(i)) {
|
||||
last = i;
|
||||
landed++;
|
||||
if (this._isFocusable(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
return from;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap, 0);
|
||||
padding: var(--ha-list-padding, 0);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap, 0);
|
||||
padding: var(--ha-list-padding, 0);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class HaListNav extends HaListBase {
|
||||
part="nav"
|
||||
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
|
||||
>
|
||||
<div part="base" class="base ha-scrollbar" role="list">
|
||||
<div part="base" class="base" role="list">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</nav>`;
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { Constructor } from "../../types";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import type { HaListBase } from "./ha-list-base";
|
||||
|
||||
export const SelectableMixin = <T extends Constructor<HaListBase>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class SelectableClass extends superClass {
|
||||
@property({ type: Boolean, reflect: true }) public multi = false;
|
||||
|
||||
protected override readonly hostRole = "listbox";
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._onOptionClick);
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("click", this._onOptionClick);
|
||||
}
|
||||
|
||||
public updated(changed: Map<string, unknown>) {
|
||||
super.updated(changed);
|
||||
if (changed.has("multi")) {
|
||||
this.setAttribute(
|
||||
"aria-multiselectable",
|
||||
this.multi ? "true" : "false"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Hook: index of a clicked option element, or `-1` if it's not ours. */
|
||||
protected optionIndexOf(opt: HaListItemOption): number {
|
||||
return this.items.indexOf(opt);
|
||||
}
|
||||
|
||||
public clearSelection() {
|
||||
(this.items as HaListItemOption[]).forEach((opt) => {
|
||||
if (opt.selected) {
|
||||
opt.toggleAttribute("selected", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _onOptionClick = (ev: Event) => {
|
||||
const path = ev.composedPath();
|
||||
for (const el of path) {
|
||||
if (el === this) {
|
||||
return;
|
||||
}
|
||||
if (el instanceof HaListItemOption) {
|
||||
if (el.disabled) {
|
||||
return;
|
||||
}
|
||||
const index = this.optionIndexOf(el);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.multi) {
|
||||
fireEvent(
|
||||
this,
|
||||
`ha-list-item-${el.selected ? "deselected" : "selected"}`,
|
||||
index
|
||||
);
|
||||
el.toggleAttribute("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!el.selected) {
|
||||
fireEvent(this, "ha-list-item-selected", index);
|
||||
// deselect the other optional selected item
|
||||
this.clearSelection();
|
||||
el.toggleAttribute("selected", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return SelectableClass;
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import { SelectableMixin } from "./ha-list-selectable-mixin";
|
||||
import { HaListVirtualized } from "./ha-list-virtualized";
|
||||
|
||||
/**
|
||||
* @element ha-list-selectable-virtualized
|
||||
* @extends {HaListVirtualized}
|
||||
*
|
||||
* @summary
|
||||
* Virtualized selection list (role `listbox`). Rows must render
|
||||
* `<ha-list-item-option>` as their top-level element. Selection is index-based:
|
||||
* clicking a row fires `ha-list-item-selected` / `ha-list-item-deselected` with
|
||||
* the row's index, and the row's `selected` attribute is toggled. Consumers own
|
||||
* the source of truth — set each row's `selected` from their own state (for
|
||||
* example, keyed by the option's `value`) and update it in the event handlers.
|
||||
*
|
||||
* Because selection is tracked per-row by the consumer, filtering the visible
|
||||
* `rows` doesn't affect selections for items outside the current view.
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once. In
|
||||
* single-select mode, selecting a row clears any previous selection.
|
||||
*
|
||||
* @fires ha-list-item-selected - Fires when the user selects a row.
|
||||
* `detail` is the row's index (number).
|
||||
* @fires ha-list-item-deselected - Fires when the user deselects a row (multi-select only).
|
||||
* `detail` is the row's index (number).
|
||||
*/
|
||||
@customElement("ha-list-selectable-virtualized")
|
||||
export class HaListSelectableVirtualized extends SelectableMixin(
|
||||
HaListVirtualized
|
||||
) {
|
||||
/**
|
||||
* Hook: maps a clicked option to its absolute index by offsetting its
|
||||
* position among the rendered (virtualized) children by `rangeStart`.
|
||||
* Returns `-1` if it's not one of our rows or nothing is rendered yet.
|
||||
*/
|
||||
protected optionIndexOf(opt: HaListItemOption): number {
|
||||
if (!this.virtualizerElement || this.rangeStart === -1) {
|
||||
return -1;
|
||||
}
|
||||
const index = Array.from(this.virtualizerElement.children).indexOf(opt);
|
||||
if (index === -1) {
|
||||
return -1;
|
||||
}
|
||||
return this.rangeStart + index;
|
||||
}
|
||||
|
||||
/** Deselects every currently rendered (visible) option. */
|
||||
public clearSelection() {
|
||||
if (!this.virtualizerElement || this.rangeStart === -1) {
|
||||
return;
|
||||
}
|
||||
Array.from(this.virtualizerElement.children).forEach((opt) => {
|
||||
if (opt instanceof HaListItemOption && opt.selected) {
|
||||
opt.toggleAttribute("selected", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-selectable-virtualized": HaListSelectableVirtualized;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
import { SelectableMixin } from "./ha-list-selectable-mixin";
|
||||
import type { HaListSelectedDetail } from "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-selectable
|
||||
@@ -12,11 +14,196 @@ import { SelectableMixin } from "./ha-list-selectable-mixin";
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once.
|
||||
*
|
||||
* @fires ha-list-item-selected - An option was selected. `detail: number` (option index).
|
||||
* @fires ha-list-item-deselected - An option was deselected (multi mode only). `detail: number` (option index).
|
||||
* @fires ha-list-selected - Fired when the selection changes. `detail: HaListSelectedDetail`.
|
||||
*/
|
||||
@customElement("ha-list-selectable")
|
||||
export class HaListSelectable extends SelectableMixin(HaListBase) {}
|
||||
export class HaListSelectable extends HaListBase {
|
||||
@property({ type: Boolean, reflect: true }) public multi = false;
|
||||
|
||||
protected override readonly hostRole = "listbox";
|
||||
|
||||
private _selectedIndices?: Set<number>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._onOptionClick);
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("click", this._onOptionClick);
|
||||
}
|
||||
|
||||
public updated(changed: Map<string, unknown>) {
|
||||
super.updated(changed);
|
||||
if (changed.has("multi")) {
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
if (!this.multi && (this._selectedIndices?.size ?? 0) > 1) {
|
||||
const first = Math.min(...this._selectedIndices!);
|
||||
this._setSelection(new Set([first]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current selection. `number` (or `-1` if nothing) when single,
|
||||
* `Set<number>` when multi.
|
||||
*/
|
||||
public get selected(): number | Set<number> {
|
||||
if (this.multi) {
|
||||
return new Set(this._selectedIndices);
|
||||
}
|
||||
return (this._selectedIndices?.size ?? 0) === 0
|
||||
? -1
|
||||
: this._selectedIndices!.values().next().value!;
|
||||
}
|
||||
|
||||
public get selectedItems(): HaListItemOption[] {
|
||||
return this._sortedSelectedIndices()
|
||||
.map((i) => this.items[i] as HaListItemOption | undefined)
|
||||
.filter((it): it is HaListItemOption => !!it);
|
||||
}
|
||||
|
||||
/** Replace the entire selection. */
|
||||
public setSelected(indices: number | number[] | Set<number>): void {
|
||||
const next =
|
||||
typeof indices === "number"
|
||||
? indices < 0
|
||||
? new Set<number>()
|
||||
: new Set([indices])
|
||||
: new Set(indices);
|
||||
if (!this.multi && next.size > 1) {
|
||||
const first = Math.min(...next);
|
||||
this._setSelection(new Set([first]));
|
||||
return;
|
||||
}
|
||||
this._setSelection(next);
|
||||
}
|
||||
|
||||
public select(index: number): void {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
const next = new Set(this._selectedIndices);
|
||||
next.add(index);
|
||||
this._setSelection(next);
|
||||
} else {
|
||||
this._setSelection(new Set([index]));
|
||||
}
|
||||
}
|
||||
|
||||
public toggle(index: number, force?: boolean): void {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
const next = new Set(this._selectedIndices);
|
||||
const isSelected = next.has(index);
|
||||
const shouldSelect = force !== undefined ? force : !isSelected;
|
||||
if (shouldSelect) {
|
||||
next.add(index);
|
||||
} else {
|
||||
next.delete(index);
|
||||
}
|
||||
this._setSelection(next);
|
||||
} else {
|
||||
const isSelected = this._selectedIndices!.has(index);
|
||||
const shouldSelect = force !== undefined ? force : !isSelected;
|
||||
this._setSelection(shouldSelect ? new Set([index]) : new Set());
|
||||
}
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this._setSelection(new Set());
|
||||
}
|
||||
|
||||
public updateListItems() {
|
||||
super.updateListItems();
|
||||
this._syncItemSelectedState(true);
|
||||
}
|
||||
|
||||
private _sortedSelectedIndices(): number[] {
|
||||
return [...this._selectedIndices!].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private _syncItemSelectedState(reset = false): void {
|
||||
if (!this._selectedIndices || reset) {
|
||||
this._selectedIndices = new Set<number>();
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
if (opt.selected) {
|
||||
this._selectedIndices!.add(i);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
const shouldBe = this._selectedIndices!.has(i);
|
||||
if (opt.selected !== shouldBe) {
|
||||
opt.selected = shouldBe;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setSelection(next: Set<number>): void {
|
||||
const prev = this._selectedIndices!;
|
||||
const added = new Set<number>();
|
||||
const removed = new Set<number>();
|
||||
next.forEach((i) => {
|
||||
if (!prev.has(i)) {
|
||||
added.add(i);
|
||||
}
|
||||
});
|
||||
prev.forEach((i) => {
|
||||
if (!next.has(i)) {
|
||||
removed.add(i);
|
||||
}
|
||||
});
|
||||
if (!added.size && !removed.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectedIndices = next;
|
||||
this._syncItemSelectedState();
|
||||
|
||||
const detail: HaListSelectedDetail = this.multi
|
||||
? { index: new Set(next), diff: { added, removed } }
|
||||
: {
|
||||
index: next.size === 0 ? -1 : next.values().next().value!,
|
||||
diff: { added, removed },
|
||||
};
|
||||
fireEvent(this, "ha-list-selected", detail);
|
||||
}
|
||||
|
||||
private _onOptionClick = (ev: Event) => {
|
||||
const path = ev.composedPath();
|
||||
for (const el of path) {
|
||||
if (el === this) {
|
||||
return;
|
||||
}
|
||||
if (el instanceof HaListItemOption) {
|
||||
const index = this.items.indexOf(el);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
const item = this.items[index];
|
||||
if (item.disabled) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
this.toggle(index);
|
||||
} else {
|
||||
this.select(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize.js";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
|
||||
/**
|
||||
* A single row in a {@link HaListVirtualized}. Identified by a stable `id`
|
||||
* used as the virtualizer key. Extra fields are passed through to the
|
||||
* `rowRenderer`.
|
||||
*/
|
||||
export interface HaListVirtualizedItem {
|
||||
/** Stable key used by the virtualizer to track the row across re-renders. */
|
||||
id: string;
|
||||
/** Whether the row can be focused and activated. Defaults to `false`. */
|
||||
interactive?: boolean;
|
||||
disabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @element ha-list-virtualized
|
||||
* @extends {HaListBase}
|
||||
*
|
||||
* @summary
|
||||
* Virtualized list. Renders only the rows currently in view to keep large
|
||||
* lists performant, while preserving the roving-tabindex keyboard navigation
|
||||
* of {@link HaListBase}.
|
||||
*
|
||||
* @csspart base - The scrollable outer container (`<div>`).
|
||||
*
|
||||
* @attr {number} pin-index - Row index to scroll to when the list first
|
||||
* renders. Cleared once the user scrolls.
|
||||
* @attr {string} pin-block - Block alignment for `pin-index`: `start`,
|
||||
* `center` (default), `end`, or `nearest`.
|
||||
*
|
||||
* @fires ha-list-activated - Fired when a row is activated via Enter/Space. `detail: { index, item }`.
|
||||
*/
|
||||
@customElement("ha-list-virtualized")
|
||||
export class HaListVirtualized extends HaListBase {
|
||||
@state() private _virtualizerReady = false;
|
||||
|
||||
/**
|
||||
* The list data. Each item is rendered by `rowRenderer`; its `interactive`
|
||||
* and `disabled` flags determine whether the row is focusable.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public rows!: HaListVirtualizedItem[];
|
||||
|
||||
/** Renders a single row from its data and index. */
|
||||
@property({ attribute: false })
|
||||
public rowRenderer?: RenderItemFunction<HaListVirtualizedItem>;
|
||||
|
||||
/** Row index to scroll to on first render (the "pinned" row). */
|
||||
@property({ attribute: "pin-index", type: Number }) public pinIndex?: number;
|
||||
|
||||
/** Block alignment used when scrolling to `pinIndex`. */
|
||||
@property({ attribute: "pin-block" }) public pinBlock:
|
||||
| "start"
|
||||
| "center"
|
||||
| "end"
|
||||
| "nearest" = "center";
|
||||
|
||||
@state() private _unpinned = false;
|
||||
|
||||
@query("lit-virtualizer")
|
||||
protected virtualizerElement?: LitVirtualizer<HaListVirtualizedItem>;
|
||||
|
||||
protected rangeStart = -1;
|
||||
protected rangeEnd = -1;
|
||||
private _activeItemFocus = false;
|
||||
private _scrollToActiveItem = false;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this._loadVirtualizer();
|
||||
}
|
||||
|
||||
if (changedProps.has("rows")) {
|
||||
this.recomputeFocusableIndexes();
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadVirtualizer() {
|
||||
await loadVirtualizer();
|
||||
this._virtualizerReady = true;
|
||||
}
|
||||
|
||||
protected override render(): TemplateResult | typeof nothing {
|
||||
if (!this._virtualizerReady) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div part="base" class="base ha-scrollbar">
|
||||
<lit-virtualizer
|
||||
.keyFunction=${this._keyFunction}
|
||||
tabindex="-1"
|
||||
scroller
|
||||
.items=${this.rows}
|
||||
.renderItem=${this.rowRenderer}
|
||||
style="min-height: 36px; height: 100%;"
|
||||
.layout=${!this._unpinned && this.pinIndex !== undefined
|
||||
? {
|
||||
pin: {
|
||||
index: this.pinIndex,
|
||||
block: this.pinBlock,
|
||||
},
|
||||
}
|
||||
: undefined}
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@rangeChanged=${this._handleRangeChanged}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active (roving-tabindex) row. If the row is outside the rendered
|
||||
* range it is scrolled into view first, then activated/focused once the
|
||||
* virtualizer has laid it out.
|
||||
* @param index - Row index to make active; clamped to the valid range.
|
||||
* @param focusItem - Whether to move DOM focus to the row.
|
||||
*/
|
||||
public setActiveItemIndex(index: number, focusItem = false) {
|
||||
if (!this.hasFocusableItem) {
|
||||
this.activeItemIndex = -1;
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = Math.max(0, Math.min(this.rows.length - 1, index));
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
}
|
||||
if (
|
||||
this.activeItemIndex >= this.rangeStart &&
|
||||
this.activeItemIndex <= this.rangeEnd
|
||||
) {
|
||||
this.applyActive(focusItem);
|
||||
} else {
|
||||
this._activeItemFocus = focusItem;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement
|
||||
?.element(index)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the row at `index`, scrolling it into view if needed. No-op until
|
||||
* the virtualizer is ready or when `index` is negative.
|
||||
*/
|
||||
public override focusItemAtIndex(index: number) {
|
||||
if (!this._virtualizerReady || index < 0) {
|
||||
return;
|
||||
}
|
||||
this.setActiveItemIndex(index, true);
|
||||
}
|
||||
|
||||
protected override applyActive(focusItem: boolean) {
|
||||
if (this.virtualizerElement && this.rangeStart > -1) {
|
||||
Array.from(this.virtualizerElement.children).forEach((child, index) => {
|
||||
const el = child as HTMLElement;
|
||||
if (index + this.rangeStart === this.activeItemIndex) {
|
||||
el.tabIndex = 0;
|
||||
if (focusItem) {
|
||||
el.focus();
|
||||
}
|
||||
} else {
|
||||
el.removeAttribute("tabindex");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private async _handleRangeChanged(ev: { first: number; last: number }) {
|
||||
this.rangeStart = ev.first;
|
||||
this.rangeEnd = ev.last;
|
||||
|
||||
await this.virtualizerElement?.layoutComplete;
|
||||
this._applySetSize();
|
||||
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
const inRange =
|
||||
this.activeItemIndex >= this.rangeStart &&
|
||||
this.activeItemIndex <= this.rangeEnd;
|
||||
const focus = this._scrollToActiveItem && inRange && this._activeItemFocus;
|
||||
this.applyActive(focus);
|
||||
if (this._scrollToActiveItem && inRange) {
|
||||
this._activeItemFocus = false;
|
||||
this._scrollToActiveItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose total count + position to assistive tech, since only a slice of
|
||||
// items is in the DOM at any time.
|
||||
private _applySetSize() {
|
||||
if (!this.virtualizerElement || this.rangeStart < 0) {
|
||||
return;
|
||||
}
|
||||
const total = this.rows?.length ?? 0;
|
||||
Array.from(this.virtualizerElement.children).forEach((child, index) => {
|
||||
const el = child as HTMLElement;
|
||||
el.setAttribute("aria-setsize", String(total));
|
||||
el.setAttribute("aria-posinset", String(this.rangeStart + index + 1));
|
||||
});
|
||||
}
|
||||
|
||||
protected onFocusIn = (ev: FocusEvent) => {
|
||||
if (
|
||||
!this.virtualizerElement ||
|
||||
this.rangeStart === -1 ||
|
||||
this.rangeEnd === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const path = ev.composedPath();
|
||||
const children = Array.from(this.virtualizerElement.children);
|
||||
for (let i = this.rangeStart; i <= this.rangeEnd; i++) {
|
||||
if (path.includes(children[i - this.rangeStart])) {
|
||||
if (i !== this.activeItemIndex) {
|
||||
this.activeItemIndex = i;
|
||||
if (i < this.rangeStart || i > this.rangeEnd) {
|
||||
this._activeItemFocus = true;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement
|
||||
?.element(this.activeItemIndex)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
} else {
|
||||
this.applyActive(false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected override onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.virtualizerElement &&
|
||||
this.activeItemIndex >= this.rangeStart &&
|
||||
this.activeItemIndex <= this.rangeEnd
|
||||
) {
|
||||
const active = this.virtualizerElement?.children[
|
||||
this.activeItemIndex - this.rangeStart
|
||||
] as HaListItemBase | undefined;
|
||||
if (active && active instanceof HaListItemBase) {
|
||||
ev.preventDefault();
|
||||
active.activate();
|
||||
fireEvent(this, "ha-list-activated", {
|
||||
index: this.activeItemIndex,
|
||||
item: active,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected isFocusable(index: number): boolean {
|
||||
const item = this.rows[index];
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
const { disabled = false, interactive = false } = this.rows[index];
|
||||
return interactive && !disabled;
|
||||
}
|
||||
|
||||
protected override get itemCount(): number {
|
||||
return this.rows?.length ?? 0;
|
||||
}
|
||||
|
||||
protected override moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this.hasFocusableItem) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
if (next < 0 || next === this.activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = next;
|
||||
if (next < this.rangeStart || next > this.rangeEnd) {
|
||||
this._activeItemFocus = true;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement?.element(this.activeItemIndex)?.scrollIntoView({
|
||||
block: "nearest",
|
||||
});
|
||||
} else {
|
||||
this.applyActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override getPageSize(): number {
|
||||
if (this.rangeStart < 0 || this.rangeEnd < 0) {
|
||||
return super.getPageSize();
|
||||
}
|
||||
return Math.max(1, this.rangeEnd - this.rangeStart + 1);
|
||||
}
|
||||
|
||||
private _keyFunction = (item: HaListVirtualizedItem) => item.id;
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleUnpinned() {
|
||||
this._unpinned = true;
|
||||
}
|
||||
|
||||
protected override onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
protected override onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
// ignore
|
||||
};
|
||||
|
||||
static styles = [
|
||||
...HaListBase.styles,
|
||||
css`
|
||||
.base {
|
||||
height: 100%;
|
||||
}
|
||||
[ha-list-item] {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-virtualized": HaListVirtualized;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
|
||||
export interface HaListSelectedDetail {
|
||||
index: number | Set<number>;
|
||||
diff?: { added: Set<number>; removed: Set<number> };
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
export interface HaListActivatedDetail {
|
||||
index: number;
|
||||
item: HaListItemBase;
|
||||
@@ -11,8 +17,7 @@ export interface HaListItemRegistrationDetail {
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ha-list-item-selected": number;
|
||||
"ha-list-item-deselected": number;
|
||||
"ha-list-selected": HaListSelectedDetail;
|
||||
"ha-list-activated": HaListActivatedDetail;
|
||||
"ha-list-item-register": HaListItemRegistrationDetail;
|
||||
"ha-list-item-unregister": HaListItemRegistrationDetail;
|
||||
|
||||
@@ -23,7 +23,6 @@ import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityLocation } from "../../common/entity/get_entity_location";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
@@ -585,17 +584,18 @@ export class HaMap extends ReactiveElement {
|
||||
const customTitle = typeof entity !== "string" ? entity.name : undefined;
|
||||
const title = customTitle ?? computeStateName(stateObj);
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
passive,
|
||||
icon,
|
||||
radius,
|
||||
entity_picture: entityPicture,
|
||||
gps_accuracy: gpsAccuracy,
|
||||
} = stateObj.attributes;
|
||||
|
||||
const location = getEntityLocation(stateObj, hass.states);
|
||||
if (!location) {
|
||||
if (!(latitude && longitude)) {
|
||||
continue;
|
||||
}
|
||||
const { latitude, longitude, gpsAccuracy } = location;
|
||||
|
||||
if (computeStateDomain(stateObj) === "zone") {
|
||||
// DRAW ZONE
|
||||
|
||||
@@ -38,7 +38,7 @@ class MediaManageButton extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-button appearance="filled" size="s" @click=${this._manage}>
|
||||
<ha-button appearance="filled" size="small" @click=${this._manage}>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
|
||||
@@ -150,15 +150,8 @@ export class HaTracePathDetails extends LitElement {
|
||||
|
||||
parts.push(
|
||||
data.map((trace, idx) => {
|
||||
const {
|
||||
path,
|
||||
timestamp,
|
||||
result,
|
||||
error,
|
||||
template_errors,
|
||||
changed_variables,
|
||||
...rest
|
||||
} = trace as any;
|
||||
const { path, timestamp, result, error, changed_variables, ...rest } =
|
||||
trace as any;
|
||||
|
||||
if (result?.enabled === false) {
|
||||
return html`${this.hass!.localize(
|
||||
@@ -247,18 +240,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
${template_errors?.length
|
||||
? html`<div class="error">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.template_errors"
|
||||
)}
|
||||
<ul>
|
||||
${template_errors.map(
|
||||
(templateError: string) => html`<li>${templateError}</li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>`
|
||||
: nothing}
|
||||
${result
|
||||
? html`${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.result"
|
||||
@@ -425,11 +406,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.error ul {
|
||||
margin: var(--ha-space-1) 0;
|
||||
padding-left: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-tab-group {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DeviceRegistryEntry } from "../device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
@@ -39,7 +39,7 @@ export interface AreaRegistryEntryMutableParams {
|
||||
}
|
||||
|
||||
export const createAreaRegistryEntry = (
|
||||
hass: HomeAssistantApi,
|
||||
hass: HomeAssistant,
|
||||
values: AreaRegistryEntryMutableParams
|
||||
) =>
|
||||
hass.callWS<AreaRegistryEntry>({
|
||||
|
||||
@@ -4,8 +4,9 @@ import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export interface CategoryRegistryEntry {
|
||||
export interface CategoryRegistryEntry extends RegistryEntry {
|
||||
category_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
|
||||
@@ -26,13 +26,6 @@ export interface HomeFrontendSystemData {
|
||||
shortcuts?: ShortcutItem[];
|
||||
}
|
||||
|
||||
export interface EnergyFrontendSystemData {
|
||||
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
|
||||
// hidden. An absent key or array means nothing is hidden (all cards visible),
|
||||
// so cards added in the future are shown by default.
|
||||
hidden_cards?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
core: CoreFrontendUserData;
|
||||
@@ -41,7 +34,6 @@ declare global {
|
||||
interface FrontendSystemData {
|
||||
core: CoreFrontendSystemData;
|
||||
home: HomeFrontendSystemData;
|
||||
energy: EnergyFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,6 @@ export interface ControlButton {
|
||||
icon: string;
|
||||
// Used as key for action as well as tooltip and aria-label translation key
|
||||
action: keyof TranslationDict["ui"]["card"]["media_player"];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface MediaPlayerItem {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface RadioFrequencyTransmitter {
|
||||
entity_id: string;
|
||||
device_id: string | null;
|
||||
config_entry_id: string | null;
|
||||
supported_frequency_ranges: [number, number][];
|
||||
supported_modulations: string[];
|
||||
}
|
||||
|
||||
interface RadioFrequencyTransmitterList {
|
||||
transmitters: RadioFrequencyTransmitter[];
|
||||
}
|
||||
|
||||
export const fetchRadioFrequencyTransmitters = (
|
||||
hass: HomeAssistant
|
||||
): Promise<RadioFrequencyTransmitterList> =>
|
||||
hass.callWS({
|
||||
type: "radio_frequency/list",
|
||||
});
|
||||
@@ -11,7 +11,6 @@ interface BaseTraceStep {
|
||||
path: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
template_errors?: string[];
|
||||
changed_variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ class EntityPreviewRow extends LitElement {
|
||||
|
||||
if (domain === "button") {
|
||||
return html`
|
||||
<ha-button appearance="plain" size="s" .disabled=${disabled}>
|
||||
<ha-button appearance="plain" size="small" .disabled=${disabled}>
|
||||
${this.hass.localize("ui.card.button.press")}
|
||||
</ha-button>
|
||||
`;
|
||||
@@ -123,7 +123,8 @@ class EntityPreviewRow extends LitElement {
|
||||
const climateDomains = ["climate", "water_heater"];
|
||||
if (climateDomains.includes(domain)) {
|
||||
return html`
|
||||
<ha-climate-state .stateObj=${stateObj}> </ha-climate-state>
|
||||
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
|
||||
</ha-climate-state>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -132,11 +133,15 @@ class EntityPreviewRow extends LitElement {
|
||||
${isTiltOnly(stateObj)
|
||||
? html`
|
||||
<ha-cover-tilt-controls
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-cover-tilt-controls>
|
||||
`
|
||||
: html`
|
||||
<ha-cover-controls .stateObj=${stateObj}></ha-cover-controls>
|
||||
<ha-cover-controls
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-cover-controls>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
@@ -211,7 +216,8 @@ class EntityPreviewRow extends LitElement {
|
||||
|
||||
if (domain === "humidifier") {
|
||||
return html`
|
||||
<ha-humidifier-state .stateObj=${stateObj}> </ha-humidifier-state>
|
||||
<ha-humidifier-state .hass=${this.hass} .stateObj=${stateObj}>
|
||||
</ha-humidifier-state>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -231,7 +237,7 @@ class EntityPreviewRow extends LitElement {
|
||||
.disabled=${disabled}
|
||||
class="text-content"
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
>
|
||||
${stateObj.state === "locked"
|
||||
? this.hass!.localize("ui.card.lock.unlock")
|
||||
|
||||
@@ -182,6 +182,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
.device=${device.id}
|
||||
></ha-input>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.device=${device.id}
|
||||
.value=${this._deviceUpdate[device.id]?.area ??
|
||||
device.area_id ??
|
||||
|
||||
@@ -214,7 +214,7 @@ export class HaMoreInfoViewVacuumCleanAreas extends LitElement {
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
@click=${this._openSegmentMapping}
|
||||
>
|
||||
<ha-svg-icon
|
||||
|
||||
@@ -31,7 +31,7 @@ class MoreInfoAutomation extends LitElement {
|
||||
<div class="actions">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
@click=${this._runActions}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@ class MoreInfoCounter extends LitElement {
|
||||
<div class="actions">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
.action=${"increment"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled ||
|
||||
@@ -32,7 +32,7 @@ class MoreInfoCounter extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
.action=${"decrement"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled ||
|
||||
@@ -42,7 +42,7 @@ class MoreInfoCounter extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
.action=${"reset"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
|
||||
@@ -431,7 +431,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
? html`<ha-button
|
||||
variant="brand"
|
||||
appearance="filled"
|
||||
size="m"
|
||||
size="medium"
|
||||
action=${action}
|
||||
@click=${this._handleClick}
|
||||
class="center-control"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { getEntityLocation } from "../../../common/entity/get_entity_location";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/map/ha-map";
|
||||
import { showZoneEditor } from "../../../data/zone";
|
||||
@@ -22,13 +21,8 @@ class MoreInfoPerson extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const location = getEntityLocation(this.stateObj, this.hass.states);
|
||||
const hasOwnCoordinates =
|
||||
typeof this.stateObj.attributes.latitude === "number" &&
|
||||
typeof this.stateObj.attributes.longitude === "number";
|
||||
|
||||
return html`
|
||||
${location
|
||||
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
|
||||
? html`
|
||||
<ha-map
|
||||
.hass=${this.hass}
|
||||
@@ -37,12 +31,15 @@ class MoreInfoPerson extends LitElement {
|
||||
></ha-map>
|
||||
`
|
||||
: ""}
|
||||
${!__DEMO__ && this.hass.user?.is_admin && hasOwnCoordinates
|
||||
${!__DEMO__ &&
|
||||
this.hass.user?.is_admin &&
|
||||
this.stateObj.attributes.latitude &&
|
||||
this.stateObj.attributes.longitude
|
||||
? html`
|
||||
<div class="actions">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
@click=${this._handleAction}
|
||||
>
|
||||
${this.hass.localize(
|
||||
|
||||
@@ -52,7 +52,7 @@ class MoreInfoSiren extends LitElement {
|
||||
${allowAdvanced
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
size="small"
|
||||
@click=${this._showAdvancedControlsDialog}
|
||||
>
|
||||
${this.hass.localize("ui.components.siren.advanced_controls")}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user