mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-08 14:15:54 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4189b61cda | ||
|
|
1d2b8a9c84 |
@@ -0,0 +1,53 @@
|
||||
export interface DebugSafeArea {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
highlight: boolean;
|
||||
}
|
||||
|
||||
export const DEBUG_SAFE_AREA_TOOL_STORAGE_KEY = "debugSafeAreaTool";
|
||||
|
||||
export const DEBUG_SAFE_AREA_STORAGE_KEY = "debugSafeArea";
|
||||
|
||||
const STYLE_ID = "ha-debug-safe-area";
|
||||
|
||||
export const applyDebugSafeArea = (config?: DebugSafeArea): void => {
|
||||
let style = document.getElementById(STYLE_ID);
|
||||
if (
|
||||
!config ||
|
||||
!(config.top || config.right || config.bottom || config.left)
|
||||
) {
|
||||
style?.remove();
|
||||
return;
|
||||
}
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
document.head.append(style);
|
||||
}
|
||||
const { top, right, bottom, left, highlight } = config;
|
||||
// Important wins over the insets the companion app sets
|
||||
style.textContent = `
|
||||
:root {
|
||||
--app-safe-area-inset-top: ${top}px !important;
|
||||
--app-safe-area-inset-right: ${right}px !important;
|
||||
--app-safe-area-inset-bottom: ${bottom}px !important;
|
||||
--app-safe-area-inset-left: ${left}px !important;
|
||||
}
|
||||
${
|
||||
highlight
|
||||
? `html::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
border: solid rgba(255, 0, 0, 0.25);
|
||||
border-width: ${top}px ${right}px ${bottom}px ${left}px;
|
||||
pointer-events: none;
|
||||
z-index: 2147483646;
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
};
|
||||
@@ -0,0 +1,343 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiClose, mdiCropFree } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import type { DebugSafeArea } from "../common/util/debug-safe-area";
|
||||
import {
|
||||
applyDebugSafeArea,
|
||||
DEBUG_SAFE_AREA_STORAGE_KEY,
|
||||
} from "../common/util/debug-safe-area";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-switch";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
const SIDES: Side[] = ["top", "bottom", "left", "right"];
|
||||
|
||||
const PRESETS = {
|
||||
none: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
portrait: { top: 62, right: 0, bottom: 34, left: 0 },
|
||||
landscape: { top: 0, right: 62, bottom: 21, left: 62 },
|
||||
} as const;
|
||||
|
||||
type Preset = keyof typeof PRESETS;
|
||||
|
||||
const BUBBLE_SIZE = 48;
|
||||
|
||||
const BUBBLE_MARGIN = 16;
|
||||
|
||||
const DRAG_THRESHOLD = 4;
|
||||
|
||||
const DEFAULT_SAFE_AREA: DebugSafeArea = {
|
||||
...PRESETS.none,
|
||||
highlight: true,
|
||||
};
|
||||
|
||||
interface DragStart {
|
||||
pointerX: number;
|
||||
pointerY: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-debug-safe-area-tool")
|
||||
export class HaDebugSafeAreaTool extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@storage({ key: DEBUG_SAFE_AREA_STORAGE_KEY, state: true, subscribe: false })
|
||||
private _config: DebugSafeArea = DEFAULT_SAFE_AREA;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _position = { right: BUBBLE_MARGIN, bottom: BUBBLE_MARGIN };
|
||||
|
||||
private _dragStart?: DragStart;
|
||||
|
||||
private _dragged = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
applyDebugSafeArea(this._config);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
applyDebugSafeArea(undefined);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._i18n) {
|
||||
return nothing;
|
||||
}
|
||||
const { localize } = this._i18n;
|
||||
const { right, bottom } = this._position;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
tool: true,
|
||||
above: bottom < window.innerHeight / 2,
|
||||
end: right < window.innerWidth / 2,
|
||||
})}
|
||||
style=${styleMap({
|
||||
right: `clamp(0px, ${right}px, calc(100vw - ${BUBBLE_SIZE}px))`,
|
||||
bottom: `clamp(0px, ${bottom}px, calc(100vh - ${BUBBLE_SIZE}px))`,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
class="bubble"
|
||||
type="button"
|
||||
aria-label=${localize("ui.components.safe_area_tool.title")}
|
||||
aria-expanded=${this._open}
|
||||
@pointerdown=${this._pointerDown}
|
||||
@pointermove=${this._pointerMove}
|
||||
@pointerup=${this._pointerUp}
|
||||
@click=${this._toggleOpen}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCropFree}></ha-svg-icon>
|
||||
</button>
|
||||
${
|
||||
this._open
|
||||
? html`
|
||||
<div class="panel">
|
||||
<div class="header">
|
||||
<span
|
||||
>${localize("ui.components.safe_area_tool.title")}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._toggleOpen}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="presets">
|
||||
${(Object.keys(PRESETS) as Preset[]).map(
|
||||
(preset) => html`
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="outlined"
|
||||
data-preset=${preset}
|
||||
@click=${this._applyPreset}
|
||||
>
|
||||
${localize(
|
||||
`ui.components.safe_area_tool.presets.${preset}`
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="sides">
|
||||
<div class="screen" aria-hidden="true"></div>
|
||||
${SIDES.map(
|
||||
(side) => html`
|
||||
<ha-input
|
||||
class=${side}
|
||||
appearance="outlined"
|
||||
type="number"
|
||||
min="0"
|
||||
without-spin-buttons
|
||||
.label=${localize(
|
||||
`ui.components.safe_area_tool.${side}`
|
||||
)}
|
||||
data-side=${side}
|
||||
.value=${String(this._config[side])}
|
||||
@change=${this._sideChanged}
|
||||
></ha-input>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<label class="highlight">
|
||||
${localize("ui.components.safe_area_tool.highlight")}
|
||||
<ha-switch
|
||||
.checked=${this._config.highlight}
|
||||
@change=${this._highlightChanged}
|
||||
></ha-switch>
|
||||
</label>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _update(config: DebugSafeArea) {
|
||||
this._config = config;
|
||||
applyDebugSafeArea(config);
|
||||
}
|
||||
|
||||
private _applyPreset(ev: Event) {
|
||||
const preset = (ev.currentTarget as HTMLElement).dataset.preset as Preset;
|
||||
this._update({ ...this._config, ...PRESETS[preset] });
|
||||
}
|
||||
|
||||
private _sideChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HaInput;
|
||||
const side = input.dataset.side as Side;
|
||||
const value = Math.max(0, Number(input.value) || 0);
|
||||
this._update({ ...this._config, [side]: value });
|
||||
}
|
||||
|
||||
private _highlightChanged(ev: Event) {
|
||||
this._update({
|
||||
...this._config,
|
||||
highlight: (ev.currentTarget as HaSwitch).checked,
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleOpen() {
|
||||
if (this._dragged) {
|
||||
this._dragged = false;
|
||||
return;
|
||||
}
|
||||
this._open = !this._open;
|
||||
}
|
||||
|
||||
private _pointerDown(ev: PointerEvent) {
|
||||
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
|
||||
this._dragStart = {
|
||||
pointerX: ev.clientX,
|
||||
pointerY: ev.clientY,
|
||||
...this._position,
|
||||
moved: false,
|
||||
};
|
||||
}
|
||||
|
||||
private _pointerMove(ev: PointerEvent) {
|
||||
const start = this._dragStart;
|
||||
if (!start) {
|
||||
return;
|
||||
}
|
||||
const dx = ev.clientX - start.pointerX;
|
||||
const dy = ev.clientY - start.pointerY;
|
||||
if (!start.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
start.moved = true;
|
||||
this._position = {
|
||||
right: Math.min(
|
||||
Math.max(start.right - dx, 0),
|
||||
window.innerWidth - BUBBLE_SIZE
|
||||
),
|
||||
bottom: Math.min(
|
||||
Math.max(start.bottom - dy, 0),
|
||||
window.innerHeight - BUBBLE_SIZE
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private _pointerUp() {
|
||||
this._dragged = !!this._dragStart?.moved;
|
||||
this._dragStart = undefined;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.tool {
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
.bubble {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${BUBBLE_SIZE}px;
|
||||
height: ${BUBBLE_SIZE}px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
box-shadow: var(--ha-box-shadow-m);
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.bubble:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--ha-space-2));
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
width: 300px;
|
||||
padding: var(--ha-space-2) var(--ha-space-3) var(--ha-space-3);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
box-shadow: var(--ha-box-shadow-l);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.above .panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + var(--ha-space-2));
|
||||
}
|
||||
.end .panel {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.presets {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.sides {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-areas:
|
||||
". top ."
|
||||
"left screen right"
|
||||
". bottom .";
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.screen {
|
||||
grid-area: screen;
|
||||
align-self: stretch;
|
||||
min-height: 64px;
|
||||
border: 2px dashed var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
.top {
|
||||
grid-area: top;
|
||||
}
|
||||
.bottom {
|
||||
grid-area: bottom;
|
||||
}
|
||||
.left {
|
||||
grid-area: left;
|
||||
}
|
||||
.right {
|
||||
grid-area: right;
|
||||
}
|
||||
.highlight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-debug-safe-area-tool": HaDebugSafeAreaTool;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import { DEBUG_SAFE_AREA_TOOL_STORAGE_KEY } from "../common/util/debug-safe-area";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import "../components/ha-drawer";
|
||||
import { narrowViewportContext } from "../data/context";
|
||||
@@ -37,6 +39,13 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
|
||||
@storage({
|
||||
key: DEBUG_SAFE_AREA_TOOL_STORAGE_KEY,
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _debugSafeAreaTool = false;
|
||||
|
||||
private _narrowViewportProvider = new ContextProvider(this, {
|
||||
context: narrowViewportContext,
|
||||
initialValue: this.narrow,
|
||||
@@ -80,6 +89,11 @@ export class HomeAssistantMain extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</ha-drawer>
|
||||
${
|
||||
this._debugSafeAreaTool
|
||||
? html`<ha-debug-safe-area-tool></ha-debug-safe-area-tool>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -135,6 +149,10 @@ export class HomeAssistantMain extends LitElement {
|
||||
if (changedProps.has("route") && this._sidebarNarrow) {
|
||||
this._drawerOpen = false;
|
||||
}
|
||||
|
||||
if (this._debugSafeAreaTool) {
|
||||
import("../components/ha-debug-safe-area-tool");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consume } from "../../../../common/decorators/consume";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { DEBUG_SAFE_AREA_TOOL_STORAGE_KEY } from "../../../../common/util/debug-safe-area";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
|
||||
@customElement("ha-debug-safe-area-tool-row")
|
||||
class HaDebugSafeAreaToolRow extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@storage({ key: DEBUG_SAFE_AREA_TOOL_STORAGE_KEY, state: true })
|
||||
private _enabled = false;
|
||||
|
||||
protected render() {
|
||||
if (!this._i18n) {
|
||||
return nothing;
|
||||
}
|
||||
const { localize } = this._i18n;
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${localize(
|
||||
"ui.panel.config.tools.tabs.debug.safe_area_tool.title"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${localize(
|
||||
"ui.panel.config.tools.tabs.debug.safe_area_tool.description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${this._enabled}
|
||||
@change=${this._checkedChanged}
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _checkedChanged(ev: Event) {
|
||||
this._enabled = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-debug-safe-area-tool-row": HaDebugSafeAreaToolRow;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import "./ha-debug-connection-row";
|
||||
import "./ha-debug-disable-view-transition-row";
|
||||
import "./ha-debug-safe-area-tool-row";
|
||||
import "./ha-debug-viewport-environment-card";
|
||||
|
||||
@customElement("tools-debug")
|
||||
@@ -35,6 +36,7 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
>
|
||||
<ha-list-base>
|
||||
<ha-debug-safe-area-tool-row></ha-debug-safe-area-tool-row>
|
||||
<ha-debug-connection-row
|
||||
.hass=${this.hass}
|
||||
></ha-debug-connection-row>
|
||||
|
||||
@@ -529,6 +529,19 @@
|
||||
"last": "Last"
|
||||
},
|
||||
"components": {
|
||||
"safe_area_tool": {
|
||||
"title": "Safe area",
|
||||
"presets": {
|
||||
"none": "None",
|
||||
"portrait": "Portrait",
|
||||
"landscape": "Landscape"
|
||||
},
|
||||
"top": "Top",
|
||||
"bottom": "Bottom",
|
||||
"left": "Left",
|
||||
"right": "Right",
|
||||
"highlight": "Highlight"
|
||||
},
|
||||
"input": {
|
||||
"clear": "Clear",
|
||||
"show_password": "Show password",
|
||||
@@ -4055,6 +4068,10 @@
|
||||
"title": "Debug connection",
|
||||
"description": "Observe requests to the server and responses from the server in browser console."
|
||||
},
|
||||
"safe_area_tool": {
|
||||
"title": "Debug safe area",
|
||||
"description": "Show a floating tool to simulate the parts of the screen a device can't use, like a notch or a camera cutout. Only applies to this browser."
|
||||
},
|
||||
"disable_view_transition": {
|
||||
"title": "Disable view transitions",
|
||||
"description": "Disable animated view transitions to prevent browser crash when using browser developer tools."
|
||||
|
||||
Reference in New Issue
Block a user