diff --git a/src/components/ha-grid-size-picker.ts b/src/components/ha-grid-size-picker.ts
index f3b5607c21..49a293e33d 100644
--- a/src/components/ha-grid-size-picker.ts
+++ b/src/components/ha-grid-size-picker.ts
@@ -2,9 +2,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
-
import { mdiRestore } from "@mdi/js";
-import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
@@ -20,7 +18,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public rows = 8;
- @property({ attribute: false }) public columns = 4;
+ @property({ attribute: false }) public columns = 12;
@property({ attribute: false }) public rowMin?: number;
@@ -32,6 +30,8 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean;
+ @property({ attribute: false }) public step: number = 1;
+
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) {
@@ -51,8 +51,9 @@ export class HaGridSizeEditor extends LitElement {
const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows;
- const columnMin = this.columnMin ?? 1;
- const columnMax = this.columnMax ?? this.columns;
+ const columnMin = Math.ceil((this.columnMin ?? 1) / this.step) * this.step;
+ const columnMax =
+ Math.ceil((this.columnMax ?? this.columns) / this.step) * this.step;
const rowValue = autoHeight ? rowMin : this._localValue?.rows;
const columnValue = this._localValue?.columns;
@@ -67,9 +68,11 @@ export class HaGridSizeEditor extends LitElement {
.max=${columnMax}
.range=${this.columns}
.value=${fullWidth ? this.columns : this.value?.columns}
+ .step=${this.step}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
+ tooltip-mode="always"
>
${!this.isDefault
? html`
@@ -102,34 +106,44 @@ export class HaGridSizeEditor extends LitElement {
`
: nothing}
-
-
- ${Array(this.rows * this.columns)
+
+
+ ${Array(this.rows)
.fill(0)
.map((_, index) => {
- const row = Math.floor(index / this.columns) + 1;
- const column = (index % this.columns) + 1;
+ const row = index + 1;
return html`
-
+
+ ${Array(this.columns)
+ .fill(0)
+ .map((__, columnIndex) => {
+ const column = columnIndex + 1;
+ if (
+ column % this.step !== 0 ||
+ (this.columns > 24 && column % 3 !== 0)
+ ) {
+ return nothing;
+ }
+ return html`
+ |
+ `;
+ })}
+
`;
})}
-
-
+
+
`;
@@ -223,42 +237,40 @@ export class HaGridSizeEditor extends LitElement {
}
.reset {
grid-area: reset;
+ --mdc-icon-button-size: 36px;
}
.preview {
position: relative;
grid-area: preview;
}
- .preview > div {
- position: relative;
- display: grid;
- grid-template-columns: repeat(var(--total-columns), 1fr);
- grid-template-rows: repeat(var(--total-rows), 25px);
- gap: 4px;
+ .preview table,
+ .preview tr,
+ .preview td {
+ border: 2px dotted var(--divider-color);
+ border-collapse: collapse;
}
- .preview .cell {
- background-color: var(--disabled-color);
- grid-column: span 1;
- grid-row: span 1;
- border-radius: 4px;
- opacity: 0.2;
- cursor: pointer;
- }
- .preview .selected {
- position: absolute;
- pointer-events: none;
- top: 0;
- left: 0;
- height: 100%;
+ .preview table {
width: 100%;
}
- .selected .cell {
- background-color: var(--primary-color);
- grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
- grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
- opacity: 0.5;
+ .preview tr {
+ height: 30px;
}
- .preview.full-width .selected .cell {
- grid-column: 1 / -1;
+ .preview td {
+ cursor: pointer;
+ }
+ .preview-card {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-color: var(--primary-color);
+ opacity: 0.3;
+ border-radius: 8px;
+ height: calc(var(--rows, 1) * 30px);
+ width: calc(var(--columns, 1) * 100% / var(--total-columns, 12));
+ pointer-events: none;
+ transition:
+ width ease-in-out 180ms,
+ height ease-in-out 180ms;
}
`,
];
diff --git a/src/panels/lovelace/common/compute-card-grid-size.ts b/src/panels/lovelace/common/compute-card-grid-size.ts
index cc0be5ac76..49bc4946ca 100644
--- a/src/panels/lovelace/common/compute-card-grid-size.ts
+++ b/src/panels/lovelace/common/compute-card-grid-size.ts
@@ -3,16 +3,11 @@ import type { LovelaceGridOptions, LovelaceLayoutOptions } from "../types";
export const GRID_COLUMN_MULTIPLIER = 3;
-export const multiplyBy =
(
+const multiplyBy = (
value: T,
multiplier: number
): T => (typeof value === "number" ? ((value * multiplier) as T) : value);
-export const divideBy = (
- value: T,
- divider: number
-): T => (typeof value === "number" ? (Math.ceil(value / divider) as T) : value);
-
export const migrateLayoutToGridOptions = (
options: LovelaceLayoutOptions
): LovelaceGridOptions => {
@@ -42,6 +37,9 @@ export type CardGridSize = {
columns: number | "full";
};
+export const isPreciseMode = (options: LovelaceGridOptions) =>
+ typeof options.columns === "number" && options.columns % 3 !== 0;
+
export const computeCardGridSize = (
options: LovelaceGridOptions
): CardGridSize => {
diff --git a/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts b/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
index 206436ce95..e133be40c1 100644
--- a/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
+++ b/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
@@ -23,6 +23,8 @@ const A11Y_KEY_CODES = new Set([
"End",
]);
+type TooltipMode = "never" | "always" | "interaction";
+
@customElement("ha-grid-layout-slider")
export class HaGridLayoutSlider extends LitElement {
@property({ type: Boolean, reflect: true })
@@ -34,6 +36,9 @@ export class HaGridLayoutSlider extends LitElement {
@property({ attribute: "touch-action" })
public touchAction?: string;
+ @property({ attribute: "tooltip-mode" })
+ public tooltipMode: TooltipMode = "interaction";
+
@property({ type: Number })
public value?: number;
@@ -52,6 +57,9 @@ export class HaGridLayoutSlider extends LitElement {
@state()
public pressed = false;
+ @state()
+ public tooltipVisible = false;
+
private _mc?: HammerManager;
private get _range() {
@@ -135,11 +143,13 @@ export class HaGridLayoutSlider extends LitElement {
this._mc.on("panstart", () => {
if (this.disabled) return;
this.pressed = true;
+ this._showTooltip();
savedValue = this.value;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this.pressed = false;
+ this._hideTooltip();
this.value = savedValue;
});
this._mc.on("panmove", (e) => {
@@ -152,6 +162,7 @@ export class HaGridLayoutSlider extends LitElement {
this._mc.on("panend", (e) => {
if (this.disabled) return;
this.pressed = false;
+ this._hideTooltip();
const percentage = this._getPercentageFromEvent(e);
const value = this._percentageToValue(percentage);
this.value = this._steppedValue(this._boundedValue(value));
@@ -223,6 +234,23 @@ export class HaGridLayoutSlider extends LitElement {
fireEvent(this, "value-changed", { value: this.value });
}
+ private _tooltipTimeout?: number;
+
+ _showTooltip() {
+ if (this._tooltipTimeout != null) window.clearTimeout(this._tooltipTimeout);
+ this.tooltipVisible = true;
+ }
+
+ _hideTooltip(delay?: number) {
+ if (!delay) {
+ this.tooltipVisible = false;
+ return;
+ }
+ this._tooltipTimeout = window.setTimeout(() => {
+ this.tooltipVisible = false;
+ }, delay);
+ }
+
private _getPercentageFromEvent = (e: HammerInput) => {
if (this.vertical) {
const y = e.center.y;
@@ -236,6 +264,30 @@ export class HaGridLayoutSlider extends LitElement {
return Math.max(Math.min(1, (x - offset) / total), 0);
};
+ private _renderTooltip() {
+ if (this.tooltipMode === "never") return nothing;
+
+ const position = this.vertical ? "left" : "top";
+
+ const visible =
+ this.tooltipMode === "always" ||
+ (this.tooltipVisible && this.tooltipMode === "interaction");
+
+ const value = this._boundedValue(this._steppedValue(this.value ?? 0));
+
+ return html`
+
+ ${value}
+
+ `;
+ }
+
protected render(): TemplateResult {
return html`
+
+ ${Array(this._range / this.step)
+ .fill(0)
+ .map((_, i) => {
+ const percentage = i / (this._range / this.step);
+ const disabled =
+ this.min >= i * this.step || i * this.step > this.max;
+ if (disabled) {
+ return nothing;
+ }
+ return html`
+
+ `;
+ })}
${this.value !== undefined
? html``
: nothing}
+ ${this._renderTooltip()}
`;
@@ -269,7 +341,7 @@ export class HaGridLayoutSlider extends LitElement {
return css`
:host {
display: block;
- --grid-layout-slider: 48px;
+ --grid-layout-slider: 36px;
height: var(--grid-layout-slider);
width: 100%;
outline: none;
@@ -297,6 +369,7 @@ export class HaGridLayoutSlider extends LitElement {
}
.slider * {
pointer-events: none;
+ user-select: none;
}
.track {
position: absolute;
@@ -315,12 +388,11 @@ export class HaGridLayoutSlider extends LitElement {
position: absolute;
inset: 0;
background: var(--disabled-color);
- opacity: 0.2;
+ opacity: 0.4;
}
.active {
position: absolute;
- background: grey;
- opacity: 0.7;
+ background: var(--primary-color);
top: 0;
right: calc(var(--max) * 100%);
bottom: 0;
@@ -351,6 +423,27 @@ export class HaGridLayoutSlider extends LitElement {
height: 16px;
width: 100%;
}
+ .dot {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ opacity: 0.6;
+ margin: auto;
+ width: 4px;
+ height: 4px;
+ flex-shrink: 0;
+ transform: translate(-50%, 0);
+ background: var(--card-background-color);
+ left: calc(var(--value, 0%) * 100%);
+ border-radius: 2px;
+ }
+ :host([vertical]) .dot {
+ transform: translate(0, -50%);
+ left: 0;
+ right: 0;
+ bottom: inherit;
+ top: calc(var(--value, 0%) * 100%);
+ }
.handle::after {
position: absolute;
inset: 0;
@@ -358,7 +451,7 @@ export class HaGridLayoutSlider extends LitElement {
border-radius: 2px;
height: 100%;
margin: auto;
- background: grey;
+ background: var(--primary-color);
content: "";
}
:host([vertical]) .handle::after {
@@ -374,9 +467,88 @@ export class HaGridLayoutSlider extends LitElement {
:host(:disabled) .active {
background: var(--disabled-color);
}
+
+ .tooltip {
+ position: absolute;
+ background-color: var(--clear-background-color);
+ color: var(--primary-text-color);
+ font-size: var(--control-slider-tooltip-font-size);
+ border-radius: 0.8em;
+ padding: 0.2em 0.4em;
+ opacity: 0;
+ white-space: nowrap;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ transition:
+ opacity 180ms ease-in-out,
+ left 180ms ease-in-out,
+ bottom 180ms ease-in-out;
+ --handle-spacing: calc(2 * var(--handle-margin) + var(--handle-size));
+ --slider-tooltip-margin: 0px;
+ --slider-tooltip-range: 100%;
+ --slider-tooltip-offset: 0px;
+ --slider-tooltip-position: calc(
+ min(
+ max(
+ var(--value) * var(--slider-tooltip-range) +
+ var(--slider-tooltip-offset),
+ 0%
+ ),
+ 100%
+ )
+ );
+ }
+ .tooltip.start {
+ --slider-tooltip-offset: calc(-0.5 * (var(--handle-spacing)));
+ }
+ .tooltip.end {
+ --slider-tooltip-offset: calc(0.5 * (var(--handle-spacing)));
+ }
+ .tooltip.cursor {
+ --slider-tooltip-range: calc(100% - var(--handle-spacing));
+ --slider-tooltip-offset: calc(0.5 * (var(--handle-spacing)));
+ }
+ .tooltip.show-handle {
+ --slider-tooltip-range: calc(100% - var(--handle-spacing));
+ --slider-tooltip-offset: calc(0.5 * (var(--handle-spacing)));
+ }
+ .tooltip.visible {
+ opacity: 1;
+ }
+ .tooltip.top {
+ transform: translate3d(-50%, -100%, 0);
+ top: var(--slider-tooltip-margin);
+ left: 50%;
+ }
+ .tooltip.bottom {
+ transform: translate3d(-50%, 100%, 0);
+ bottom: var(--slider-tooltip-margin);
+ left: 50%;
+ }
+ .tooltip.left {
+ transform: translate3d(-100%, -50%, 0);
+ top: 50%;
+ left: var(--slider-tooltip-margin);
+ }
+ .tooltip.right {
+ transform: translate3d(100%, -50%, 0);
+ top: 50%;
+ right: var(--slider-tooltip-margin);
+ }
+ :host(:not([vertical])) .tooltip.top,
+ :host(:not([vertical])) .tooltip.bottom {
+ left: var(--slider-tooltip-position);
+ }
+ :host([vertical]) .tooltip.right,
+ :host([vertical]) .tooltip.left {
+ top: var(--slider-tooltip-position);
+ }
+
.pressed .handle {
transition: none;
}
+ .pressed .tooltip {
+ transition: opacity 180ms ease-in-out;
+ }
`;
}
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
index 494dcd9edb..a79e6eeccd 100644
--- a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
@@ -26,16 +26,12 @@ import type { HuiCard } from "../../cards/hui-card";
import type { CardGridSize } from "../../common/compute-card-grid-size";
import {
computeCardGridSize,
- divideBy,
GRID_COLUMN_MULTIPLIER,
+ isPreciseMode,
migrateLayoutToGridOptions,
- multiplyBy,
} from "../../common/compute-card-grid-size";
import type { LovelaceGridOptions } from "../../types";
-const computePreciseMode = (columns?: number | string) =>
- typeof columns === "number" && columns % 3 !== 0;
-
@customElement("hui-card-layout-editor")
export class HuiCardLayoutEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -63,22 +59,6 @@ export class HuiCardLayoutEditor extends LitElement {
private _computeCardGridSize = memoizeOne(computeCardGridSize);
- private _simplifyOptions = (
- options: LovelaceGridOptions
- ): LovelaceGridOptions => ({
- ...options,
- columns: divideBy(options.columns, GRID_COLUMN_MULTIPLIER),
- max_columns: divideBy(options.max_columns, GRID_COLUMN_MULTIPLIER),
- min_columns: divideBy(options.min_columns, GRID_COLUMN_MULTIPLIER),
- });
-
- private _standardizeOptions = (options: LovelaceGridOptions) => ({
- ...options,
- columns: multiplyBy(options.columns, GRID_COLUMN_MULTIPLIER),
- max_columns: multiplyBy(options.max_columns, GRID_COLUMN_MULTIPLIER),
- min_columns: multiplyBy(options.min_columns, GRID_COLUMN_MULTIPLIER),
- });
-
private _isDefault = memoizeOne(
(options?: LovelaceGridOptions) =>
options?.columns === undefined && options?.rows === undefined
@@ -101,14 +81,11 @@ export class HuiCardLayoutEditor extends LitElement {
this._defaultGridOptions
);
- const gridOptions = this._preciseMode
- ? options
- : this._simplifyOptions(options);
+ const gridOptions = options;
const gridValue = this._computeCardGridSize(gridOptions);
const columnSpan = this.sectionConfig.column_span ?? 1;
- const gridTotalColumns =
- (12 * columnSpan) / (this._preciseMode ? 1 : GRID_COLUMN_MULTIPLIER);
+ const gridTotalColumns = 12 * columnSpan;
return html`