mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-17 23:10:44 +00:00
Setup base animation styles, add fade out to launch screen (#27829)
* Setup base animation styles * Add fade out to launch screen * Cleanup * Set opacity before removing element * Remove * Final * Use computed duration for timeout * Add skip animation prop * Swap * Use common function and fix issue
This commit is contained in:
36
src/common/util/parse-animation-duration.ts
Normal file
36
src/common/util/parse-animation-duration.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Parses a CSS duration string (e.g., "300ms", "3s") and returns the duration in milliseconds.
|
||||||
|
*
|
||||||
|
* @param duration - A CSS duration string (e.g., "300ms", "3s", "0.5s")
|
||||||
|
* @returns The duration in milliseconds, or 0 if the input is invalid
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseAnimationDuration("300ms") // Returns 300
|
||||||
|
* parseAnimationDuration("3s") // Returns 3000
|
||||||
|
* parseAnimationDuration("0.5s") // Returns 500
|
||||||
|
* parseAnimationDuration("invalid") // Returns 0
|
||||||
|
*/
|
||||||
|
export const parseAnimationDuration = (duration: string): number => {
|
||||||
|
const trimmed = duration.trim();
|
||||||
|
|
||||||
|
let value: number;
|
||||||
|
let multiplier: number;
|
||||||
|
|
||||||
|
if (trimmed.endsWith("ms")) {
|
||||||
|
value = parseFloat(trimmed.slice(0, -2));
|
||||||
|
multiplier = 1;
|
||||||
|
} else if (trimmed.endsWith("s")) {
|
||||||
|
value = parseFloat(trimmed.slice(0, -1));
|
||||||
|
multiplier = 1000;
|
||||||
|
} else {
|
||||||
|
// No recognized unit, try parsing as number (assume ms)
|
||||||
|
value = parseFloat(trimmed);
|
||||||
|
multiplier = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFinite(value) || value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value * multiplier;
|
||||||
|
};
|
||||||
@@ -20,6 +20,21 @@
|
|||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<%= renderTemplate("_style_base.html.template") %>
|
<%= renderTemplate("_style_base.html.template") %>
|
||||||
<style>
|
<style>
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::view-transition-group(launch-screen) {
|
||||||
|
animation-duration: var(--ha-animation-base-duration, 350ms);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
::view-transition-old(launch-screen) {
|
||||||
|
animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out;
|
||||||
|
}
|
||||||
html {
|
html {
|
||||||
background-color: var(--primary-background-color, #fafafa);
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
color: var(--primary-text-color, #212121);
|
color: var(--primary-text-color, #212121);
|
||||||
@@ -32,11 +47,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ha-launch-screen {
|
#ha-launch-screen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
view-transition-name: launch-screen;
|
||||||
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#ha-launch-screen {
|
||||||
|
background-color: var(--primary-background-color, #111111);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ha-launch-screen.removing {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
#ha-launch-screen svg {
|
#ha-launch-screen svg {
|
||||||
width: 112px;
|
width: 112px;
|
||||||
|
|||||||
@@ -199,3 +199,23 @@ export const baseEntrypointStyles = css`
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const baseAnimationStyles = css`
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ export const coreStyles = css`
|
|||||||
--ha-space-18: 72px;
|
--ha-space-18: 72px;
|
||||||
--ha-space-19: 76px;
|
--ha-space-19: 76px;
|
||||||
--ha-space-20: 80px;
|
--ha-space-20: 80px;
|
||||||
|
|
||||||
|
--ha-animation-base-duration: 350ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
--ha-animation-base-duration: 0ms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,40 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { render } from "lit";
|
import { render } from "lit";
|
||||||
|
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
||||||
|
|
||||||
|
const removeElement = (
|
||||||
|
launchScreenElement: HTMLElement,
|
||||||
|
skipAnimation: boolean
|
||||||
|
) => {
|
||||||
|
if (skipAnimation) {
|
||||||
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchScreenElement.classList.add("removing");
|
||||||
|
|
||||||
|
const durationFromCss = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--ha-animation-base-duration")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
|
}, parseAnimationDuration(durationFromCss));
|
||||||
|
};
|
||||||
|
|
||||||
export const removeLaunchScreen = () => {
|
export const removeLaunchScreen = () => {
|
||||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||||
if (launchScreenElement) {
|
if (!launchScreenElement?.parentElement) {
|
||||||
launchScreenElement.parentElement!.removeChild(launchScreenElement);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
removeElement(launchScreenElement, false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: Direct removal without transition
|
||||||
|
removeElement(launchScreenElement, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
56
test/common/util/parse-animation-duration.test.ts
Normal file
56
test/common/util/parse-animation-duration.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { assert, describe, it } from "vitest";
|
||||||
|
|
||||||
|
import { parseAnimationDuration } from "../../../src/common/util/parse-animation-duration";
|
||||||
|
|
||||||
|
describe("parseAnimationDuration", () => {
|
||||||
|
it("Parses milliseconds with unit", () => {
|
||||||
|
assert.equal(parseAnimationDuration("300ms"), 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Parses seconds with unit", () => {
|
||||||
|
assert.equal(parseAnimationDuration("3s"), 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Parses decimal seconds", () => {
|
||||||
|
assert.equal(parseAnimationDuration("0.5s"), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Parses decimal milliseconds", () => {
|
||||||
|
assert.equal(parseAnimationDuration("250.5ms"), 250.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Handles whitespace", () => {
|
||||||
|
assert.equal(parseAnimationDuration(" 300ms "), 300);
|
||||||
|
assert.equal(parseAnimationDuration(" 3s "), 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Handles number without unit as milliseconds", () => {
|
||||||
|
assert.equal(parseAnimationDuration("300"), 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns 0 for invalid input", () => {
|
||||||
|
assert.equal(parseAnimationDuration("invalid"), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns 0 for empty string", () => {
|
||||||
|
assert.equal(parseAnimationDuration(""), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns 0 for negative values", () => {
|
||||||
|
assert.equal(parseAnimationDuration("-300ms"), 0);
|
||||||
|
assert.equal(parseAnimationDuration("-3s"), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns 0 for NaN", () => {
|
||||||
|
assert.equal(parseAnimationDuration("NaN"), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns 0 for Infinity", () => {
|
||||||
|
assert.equal(parseAnimationDuration("Infinity"), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Handles zero values", () => {
|
||||||
|
assert.equal(parseAnimationDuration("0ms"), 0);
|
||||||
|
assert.equal(parseAnimationDuration("0s"), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user