Compare commits

...

4 Commits

Author SHA1 Message Date
Paul Bottein
bfc5ae4a4f Add labs feature toggle, translations, and random tips
- Subscribe to frontend.windows_98 lab feature to enable/disable
- Move tips to lazy-loaded translation fragment (ui.panel.windows_98)
- Randomize tip selection
- Sort windows_98 with other fun features in labs page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:38:47 +01:00
Paul Bottein
1b237e744f Add Clippy-style home automation tip
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:14:02 +01:00
Paul Bottein
2139df49c5 Remove outdated right-click tip
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:16:31 +01:00
Paul Bottein
892f8f605f Add Windows 98 Easter egg with Casita assistant
Adds a Clippy-inspired interactive house character (Casita) that applies
a full Windows 98 theme. Features draggable positioning, speech bubble
with fun tips, idle/sleep animations, and a dismiss button. Theme is
enforced via MutationObserver to survive theme mixin overwrites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:16:01 +01:00
5 changed files with 988 additions and 5 deletions

View File

@@ -0,0 +1,412 @@
import type { Theme } from "../data/ws-themes";
export const WINDOWS_98_THEME: Theme = {
// Sharp corners
"ha-border-radius-sm": "0",
"ha-border-radius-md": "0",
"ha-border-radius-lg": "0",
"ha-border-radius-xl": "0",
"ha-border-radius-2xl": "0",
"ha-border-radius-3xl": "0",
"ha-border-radius-4xl": "0",
"ha-border-radius-5xl": "0",
"ha-border-radius-6xl": "0",
"ha-border-radius-pill": "0",
"ha-border-radius-circle": "0",
// Fonts
"ha-font-family-body":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
"ha-font-family-heading":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
"ha-font-family-code": "'Courier New', Courier, monospace",
// No transparency
"ha-dialog-scrim-backdrop-filter": "none",
// Disable animations
"ha-animation-duration-fast": "1ms",
"ha-animation-duration-normal": "1ms",
"ha-animation-duration-slow": "1ms",
modes: {
light: {
"primary-color": "#000080",
"dark-primary-color": "#00006B",
"light-primary-color": "#4040C0",
"accent-color": "#000080",
"primary-text-color": "#000000",
"secondary-text-color": "#404040",
"text-primary-color": "#ffffff",
"text-light-primary-color": "#000000",
"disabled-text-color": "#808080",
"primary-background-color": "#008080",
"secondary-background-color": "#C0C0C0",
"card-background-color": "#C0C0C0",
"clear-background-color": "#C0C0C0",
"rgb-primary-color": "0, 0, 128",
"rgb-accent-color": "0, 0, 128",
"rgb-primary-text-color": "0, 0, 0",
"rgb-secondary-text-color": "64, 64, 64",
"rgb-text-primary-color": "255, 255, 255",
"rgb-card-background-color": "192, 192, 192",
"divider-color": "#808080",
"outline-color": "#808080",
"outline-hover-color": "#404040",
"shadow-color": "rgba(0, 0, 0, 0.5)",
"scrollbar-thumb-color": "#808080",
"disabled-color": "#C0C0C0",
"ha-card-border-width": "1px",
"ha-card-border-color": "#808080",
"ha-card-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-card-border-radius": "0",
"ha-dialog-border-radius": "0",
"ha-dialog-surface-background": "#C0C0C0",
"dialog-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-s": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-m": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-l": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"sidebar-background-color": "#C0C0C0",
"sidebar-text-color": "#000000",
"sidebar-selected-text-color": "#ffffff",
"sidebar-selected-icon-color": "#000080",
"sidebar-icon-color": "#000000",
"dark-divider-opacity": "0.3",
"app-header-background-color": "#000080",
"app-header-text-color": "#ffffff",
"app-header-border-bottom": "2px outset #C0C0C0",
"switch-checked-color": "#000080",
"switch-checked-button-color": "#C0C0C0",
"switch-checked-track-color": "#000080",
"switch-unchecked-button-color": "#C0C0C0",
"switch-unchecked-track-color": "#808080",
"slider-color": "#000080",
"table-row-background-color": "#C0C0C0",
"table-row-alternative-background-color": "#D4D0C8",
"table-header-background-color": "#D4D0C8",
"data-table-background-color": "#C0C0C0",
"data-table-border-width": "0",
"label-badge-background-color": "#C0C0C0",
"label-badge-text-color": "#000000",
"input-fill-color": "#ffffff",
"input-disabled-fill-color": "#C0C0C0",
"input-ink-color": "#000000",
"input-label-ink-color": "#000000",
"input-disabled-ink-color": "#808080",
"input-idle-line-color": "#808080",
"input-hover-line-color": "#000000",
"input-disabled-line-color": "#C0C0C0",
"input-outlined-idle-border-color": "#808080",
"input-outlined-hover-border-color": "#000000",
"input-outlined-disabled-border-color": "#C0C0C0",
"input-dropdown-icon-color": "#000000",
"error-color": "#FF0000",
"warning-color": "#FF8000",
"success-color": "#008000",
"info-color": "#000080",
"state-icon-color": "#000080",
"state-active-color": "#000080",
"state-inactive-color": "#808080",
"mdc-theme-primary": "#000080",
"mdc-theme-secondary": "#000080",
"mdc-theme-background": "#008080",
"mdc-theme-surface": "#C0C0C0",
"mdc-theme-on-primary": "#ffffff",
"mdc-theme-on-secondary": "#ffffff",
"mdc-theme-on-surface": "#000000",
"mdc-theme-error": "#FF0000",
"mdc-checkbox-unchecked-color": "#808080",
"mdc-radio-unchecked-color": "#808080",
"mdc-tab-text-label-color-default": "#000000",
"mdc-button-outline-color": "#808080",
"mdc-dialog-heading-ink-color": "#000000",
"mdc-dialog-content-ink-color": "#000000",
"mdc-text-field-fill-color": "#ffffff",
"mdc-text-field-ink-color": "#000000",
"mdc-text-field-label-ink-color": "#000000",
"mdc-text-field-idle-line-color": "#808080",
"mdc-text-field-hover-line-color": "#000000",
"mdc-select-fill-color": "#ffffff",
"mdc-select-ink-color": "#000000",
"mdc-select-label-ink-color": "#000000",
"mdc-select-idle-line-color": "#808080",
"mdc-select-hover-line-color": "#000000",
"mdc-select-dropdown-icon-color": "#000000",
"ha-color-primary-05": "#00003A",
"ha-color-primary-10": "#000050",
"ha-color-primary-20": "#000066",
"ha-color-primary-30": "#00007A",
"ha-color-primary-40": "#000080",
"ha-color-primary-50": "#0000AA",
"ha-color-primary-60": "#4040C0",
"ha-color-primary-70": "#6060D0",
"ha-color-primary-80": "#8080E0",
"ha-color-primary-90": "#C0C0F0",
"ha-color-primary-95": "#E0E0FF",
"ha-color-neutral-05": "#000000",
"ha-color-neutral-10": "#404040",
"ha-color-neutral-20": "#606060",
"ha-color-neutral-30": "#808080",
"ha-color-neutral-40": "#808080",
"ha-color-neutral-50": "#A0A0A0",
"ha-color-neutral-60": "#B0B0B0",
"ha-color-neutral-70": "#C0C0C0",
"ha-color-neutral-80": "#D4D0C8",
"ha-color-neutral-90": "#DFDFDF",
"ha-color-neutral-95": "#F0F0F0",
"ha-color-text-primary": "#000000",
"ha-color-text-secondary": "#404040",
"ha-color-text-disabled": "#808080",
"ha-color-text-link": "#000080",
"ha-color-surface-default": "#C0C0C0",
"ha-color-on-surface-default": "#000000",
"ha-color-border-neutral-quiet": "#C0C0C0",
"ha-color-border-neutral-normal": "#808080",
"ha-color-border-neutral-loud": "#404040",
"ha-color-fill-primary-quiet-resting": "#C0C0F0",
"ha-color-fill-primary-quiet-hover": "#A0A0E0",
"ha-color-fill-primary-quiet-active": "#8080D0",
"ha-color-fill-primary-normal-resting": "#8080D0",
"ha-color-fill-primary-normal-hover": "#6060C0",
"ha-color-fill-primary-normal-active": "#4040B0",
"ha-color-fill-primary-loud-resting": "#000080",
"ha-color-fill-primary-loud-hover": "#00006B",
"ha-color-fill-primary-loud-active": "#000060",
"ha-color-fill-neutral-quiet-resting": "#D4D0C8",
"ha-color-fill-neutral-quiet-hover": "#C0C0C0",
"ha-color-fill-neutral-quiet-active": "#B0B0B0",
"ha-color-fill-neutral-normal-resting": "#C0C0C0",
"ha-color-fill-neutral-normal-hover": "#B0B0B0",
"ha-color-fill-neutral-normal-active": "#A0A0A0",
"ha-color-fill-neutral-loud-resting": "#808080",
"ha-color-fill-neutral-loud-hover": "#606060",
"ha-color-fill-neutral-loud-active": "#404040",
"ha-color-on-primary-quiet": "#000080",
"ha-color-on-primary-normal": "#000080",
"ha-color-on-primary-loud": "#ffffff",
"ha-color-on-neutral-quiet": "#808080",
"ha-color-on-neutral-normal": "#606060",
"ha-color-on-neutral-loud": "#ffffff",
"ha-color-fill-danger-loud-resting": "#FF0000",
"ha-color-fill-danger-loud-hover": "#CC0000",
"ha-color-fill-danger-loud-active": "#AA0000",
"ha-color-fill-warning-loud-resting": "#FF8000",
"ha-color-fill-warning-loud-hover": "#CC6600",
"ha-color-fill-warning-loud-active": "#AA5500",
"ha-color-fill-success-loud-resting": "#008000",
"ha-color-fill-success-loud-hover": "#006600",
"ha-color-fill-success-loud-active": "#005500",
"ha-assist-chip-filled-container-color": "#D4D0C8",
"chip-background-color": "#D4D0C8",
"markdown-code-background-color": "#D4D0C8",
"codemirror-keyword": "#000080",
"codemirror-operator": "#000000",
"codemirror-variable": "#008080",
"codemirror-variable-2": "#000080",
"codemirror-variable-3": "#808000",
"codemirror-builtin": "#800080",
"codemirror-atom": "#008080",
"codemirror-number": "#FF0000",
"codemirror-def": "#000080",
"codemirror-string": "#008000",
"codemirror-string-2": "#808000",
"codemirror-comment": "#808080",
"codemirror-tag": "#800000",
"codemirror-meta": "#000080",
"codemirror-attribute": "#FF0000",
"codemirror-property": "#000080",
"codemirror-qualifier": "#808000",
"codemirror-type": "#000080",
},
dark: {
"primary-color": "#4040C0",
"dark-primary-color": "#000080",
"light-primary-color": "#6060D0",
"accent-color": "#4040C0",
"primary-text-color": "#C0C0C0",
"secondary-text-color": "#A0A0A0",
"text-primary-color": "#ffffff",
"text-light-primary-color": "#C0C0C0",
"disabled-text-color": "#606060",
"primary-background-color": "#003030",
"secondary-background-color": "#2A2A2A",
"card-background-color": "#3A3A3A",
"clear-background-color": "#2A2A2A",
"rgb-primary-color": "64, 64, 192",
"rgb-accent-color": "64, 64, 192",
"rgb-primary-text-color": "192, 192, 192",
"rgb-secondary-text-color": "160, 160, 160",
"rgb-text-primary-color": "255, 255, 255",
"rgb-card-background-color": "58, 58, 58",
"divider-color": "#606060",
"outline-color": "#606060",
"outline-hover-color": "#808080",
"shadow-color": "rgba(0, 0, 0, 0.7)",
"scrollbar-thumb-color": "#606060",
"disabled-color": "#606060",
"ha-card-border-width": "1px",
"ha-card-border-color": "#606060",
"ha-card-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-card-border-radius": "0",
"ha-dialog-border-radius": "0",
"ha-dialog-surface-background": "#3A3A3A",
"dialog-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-s": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-m": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-l": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"sidebar-background-color": "#2A2A2A",
"sidebar-text-color": "#C0C0C0",
"sidebar-selected-text-color": "#ffffff",
"sidebar-selected-icon-color": "#4040C0",
"sidebar-icon-color": "#A0A0A0",
"dark-divider-opacity": "0.3",
"app-header-background-color": "#000060",
"app-header-text-color": "#ffffff",
"app-header-border-bottom": "2px outset #3A3A3A",
"switch-checked-color": "#4040C0",
"switch-checked-button-color": "#808080",
"switch-checked-track-color": "#4040C0",
"switch-unchecked-button-color": "#808080",
"switch-unchecked-track-color": "#404040",
"slider-color": "#4040C0",
"table-row-background-color": "#3A3A3A",
"table-row-alternative-background-color": "#2A2A2A",
"table-header-background-color": "#4A4A4A",
"data-table-background-color": "#3A3A3A",
"data-table-border-width": "0",
"label-badge-background-color": "#3A3A3A",
"label-badge-text-color": "#C0C0C0",
"input-fill-color": "#2A2A2A",
"input-disabled-fill-color": "#3A3A3A",
"input-ink-color": "#C0C0C0",
"input-label-ink-color": "#A0A0A0",
"input-disabled-ink-color": "#606060",
"input-idle-line-color": "#606060",
"input-hover-line-color": "#808080",
"input-disabled-line-color": "#404040",
"input-outlined-idle-border-color": "#606060",
"input-outlined-hover-border-color": "#808080",
"input-outlined-disabled-border-color": "#404040",
"input-dropdown-icon-color": "#A0A0A0",
"error-color": "#FF4040",
"warning-color": "#FFA040",
"success-color": "#40C040",
"info-color": "#4040C0",
"state-icon-color": "#4040C0",
"state-active-color": "#4040C0",
"state-inactive-color": "#606060",
"mdc-theme-primary": "#4040C0",
"mdc-theme-secondary": "#4040C0",
"mdc-theme-background": "#003030",
"mdc-theme-surface": "#3A3A3A",
"mdc-theme-on-primary": "#ffffff",
"mdc-theme-on-secondary": "#ffffff",
"mdc-theme-on-surface": "#C0C0C0",
"mdc-theme-error": "#FF4040",
"mdc-checkbox-unchecked-color": "#606060",
"mdc-radio-unchecked-color": "#606060",
"mdc-tab-text-label-color-default": "#C0C0C0",
"mdc-button-outline-color": "#606060",
"mdc-dialog-heading-ink-color": "#C0C0C0",
"mdc-dialog-content-ink-color": "#C0C0C0",
"mdc-text-field-fill-color": "#2A2A2A",
"mdc-text-field-ink-color": "#C0C0C0",
"mdc-text-field-label-ink-color": "#A0A0A0",
"mdc-text-field-idle-line-color": "#606060",
"mdc-text-field-hover-line-color": "#808080",
"mdc-select-fill-color": "#2A2A2A",
"mdc-select-ink-color": "#C0C0C0",
"mdc-select-label-ink-color": "#A0A0A0",
"mdc-select-idle-line-color": "#606060",
"mdc-select-hover-line-color": "#808080",
"mdc-select-dropdown-icon-color": "#A0A0A0",
"ha-color-primary-05": "#00002A",
"ha-color-primary-10": "#000040",
"ha-color-primary-20": "#000060",
"ha-color-primary-30": "#000080",
"ha-color-primary-40": "#4040C0",
"ha-color-primary-50": "#6060D0",
"ha-color-primary-60": "#8080E0",
"ha-color-primary-70": "#A0A0F0",
"ha-color-primary-80": "#C0C0FF",
"ha-color-primary-90": "#E0E0FF",
"ha-color-primary-95": "#F0F0FF",
"ha-color-neutral-05": "#1A1A1A",
"ha-color-neutral-10": "#2A2A2A",
"ha-color-neutral-20": "#3A3A3A",
"ha-color-neutral-30": "#4A4A4A",
"ha-color-neutral-40": "#606060",
"ha-color-neutral-50": "#808080",
"ha-color-neutral-60": "#A0A0A0",
"ha-color-neutral-70": "#B0B0B0",
"ha-color-neutral-80": "#C0C0C0",
"ha-color-neutral-90": "#D4D0C8",
"ha-color-neutral-95": "#E0E0E0",
"ha-color-text-primary": "#C0C0C0",
"ha-color-text-secondary": "#A0A0A0",
"ha-color-text-disabled": "#606060",
"ha-color-text-link": "#8080E0",
"ha-color-surface-default": "#3A3A3A",
"ha-color-on-surface-default": "#C0C0C0",
"ha-color-border-neutral-quiet": "#4A4A4A",
"ha-color-border-neutral-normal": "#606060",
"ha-color-border-neutral-loud": "#808080",
"ha-color-fill-primary-quiet-resting": "#00002A",
"ha-color-fill-primary-quiet-hover": "#000040",
"ha-color-fill-primary-quiet-active": "#000060",
"ha-color-fill-primary-normal-resting": "#000040",
"ha-color-fill-primary-normal-hover": "#000060",
"ha-color-fill-primary-normal-active": "#000080",
"ha-color-fill-primary-loud-resting": "#4040C0",
"ha-color-fill-primary-loud-hover": "#3030A0",
"ha-color-fill-primary-loud-active": "#2020A0",
"ha-color-fill-neutral-quiet-resting": "#2A2A2A",
"ha-color-fill-neutral-quiet-hover": "#3A3A3A",
"ha-color-fill-neutral-quiet-active": "#2A2A2A",
"ha-color-fill-neutral-normal-resting": "#3A3A3A",
"ha-color-fill-neutral-normal-hover": "#4A4A4A",
"ha-color-fill-neutral-normal-active": "#3A3A3A",
"ha-color-fill-neutral-loud-resting": "#606060",
"ha-color-fill-neutral-loud-hover": "#4A4A4A",
"ha-color-fill-neutral-loud-active": "#606060",
"ha-color-on-primary-quiet": "#8080E0",
"ha-color-on-primary-normal": "#6060D0",
"ha-color-on-primary-loud": "#ffffff",
"ha-color-on-neutral-quiet": "#A0A0A0",
"ha-color-on-neutral-normal": "#808080",
"ha-color-on-neutral-loud": "#ffffff",
"ha-color-fill-danger-loud-resting": "#FF4040",
"ha-color-fill-danger-loud-hover": "#CC3030",
"ha-color-fill-danger-loud-active": "#AA2020",
"ha-color-fill-warning-loud-resting": "#FFA040",
"ha-color-fill-warning-loud-hover": "#CC8030",
"ha-color-fill-warning-loud-active": "#AA6020",
"ha-color-fill-success-loud-resting": "#40C040",
"ha-color-fill-success-loud-hover": "#30A030",
"ha-color-fill-success-loud-active": "#208020",
"ha-assist-chip-filled-container-color": "#4A4A4A",
"chip-background-color": "#4A4A4A",
"markdown-code-background-color": "#2A2A2A",
"codemirror-keyword": "#8080E0",
"codemirror-operator": "#C0C0C0",
"codemirror-variable": "#40C0C0",
"codemirror-variable-2": "#8080E0",
"codemirror-variable-3": "#C0C040",
"codemirror-builtin": "#C040C0",
"codemirror-atom": "#40C0C0",
"codemirror-number": "#FF6060",
"codemirror-def": "#8080E0",
"codemirror-string": "#40C040",
"codemirror-string-2": "#C0C040",
"codemirror-comment": "#808080",
"codemirror-tag": "#C04040",
"codemirror-meta": "#8080E0",
"codemirror-attribute": "#FF6060",
"codemirror-property": "#8080E0",
"codemirror-qualifier": "#C0C040",
"codemirror-type": "#8080E0",
"map-filter":
"invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3)",
},
},
};

View File

@@ -0,0 +1,538 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
applyThemesOnElement,
invalidateThemeCache,
} from "../common/dom/apply_themes_on_element";
import type { LocalizeKeys } from "../common/translations/localize";
import { subscribeLabFeature } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import { WINDOWS_98_THEME } from "./ha-windows-98-theme";
const TIP_COUNT = 25;
type CasitaExpression =
| "hi"
| "ok-nabu"
| "heart"
| "sleep"
| "great-job"
| "error";
const STORAGE_KEY = "windows-98-position";
const DRAG_THRESHOLD = 5;
const BUBBLE_TIMEOUT = 8000;
const SLEEP_TIMEOUT = 30000;
@customElement("ha-windows-98")
export class HaWindows98 extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _enabled = false;
public hassSubscribe() {
return [
subscribeLabFeature(
this.hass!.connection,
"frontend",
"windows_98",
(feature) => {
this._enabled = feature.enabled;
}
),
];
}
@state() private _casitaVisible = true;
@state() private _showBubble = false;
@state() private _bubbleText = "";
@state() private _expression: CasitaExpression = "hi";
@state() private _position: { x: number; y: number } | null = null;
private _dragging = false;
private _dragStartX = 0;
private _dragStartY = 0;
private _dragOffsetX = 0;
private _dragOffsetY = 0;
private _dragMoved = false;
private _bubbleTimer?: ReturnType<typeof setTimeout>;
private _sleepTimer?: ReturnType<typeof setTimeout>;
private _boundPointerMove = this._onPointerMove.bind(this);
private _boundPointerUp = this._onPointerUp.bind(this);
private _themeApplied = false;
private _isApplyingTheme = false;
private _themeObserver?: MutationObserver;
connectedCallback(): void {
super.connectedCallback();
this._loadPosition();
this._resetSleepTimer();
this._applyWin98Theme();
this._startThemeObserver();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._clearTimers();
this._stopThemeObserver();
this._revertTheme();
document.removeEventListener("pointermove", this._boundPointerMove);
document.removeEventListener("pointerup", this._boundPointerUp);
}
protected willUpdate(changedProps: Map<string, unknown>): void {
if (changedProps.has("_enabled")) {
if (this._enabled) {
this.hass!.loadFragmentTranslation("windows_98");
this._applyWin98Theme();
this._startThemeObserver();
} else {
this._stopThemeObserver();
this._revertTheme();
}
}
if (changedProps.has("hass") && this._enabled) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
// Re-apply if darkMode changed
if (oldHass && oldHass.themes.darkMode !== this.hass!.themes.darkMode) {
this._themeApplied = false;
this._applyWin98Theme();
}
}
}
private _startThemeObserver(): void {
if (this._themeObserver) return;
this._themeObserver = new MutationObserver(() => {
if (this._isApplyingTheme || !this._enabled || !this.hass) return;
// Check if our theme was overwritten by the themes mixin
const el = document.documentElement as HTMLElement & {
__themes?: { cacheKey?: string };
};
if (!el.__themes?.cacheKey?.startsWith("Windows 98")) {
this._themeApplied = false;
this._applyWin98Theme();
}
});
this._themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["style"],
});
}
private _stopThemeObserver(): void {
this._themeObserver?.disconnect();
this._themeObserver = undefined;
}
private _applyWin98Theme(): void {
if (!this.hass || this._themeApplied) return;
this._isApplyingTheme = true;
const themes = {
...this.hass.themes,
themes: {
...this.hass.themes.themes,
"Windows 98": WINDOWS_98_THEME,
},
};
invalidateThemeCache();
applyThemesOnElement(
document.documentElement,
themes,
"Windows 98",
{ dark: this.hass.themes.darkMode },
true
);
this._themeApplied = true;
this._isApplyingTheme = false;
}
private _revertTheme(): void {
if (!this.hass || !this._themeApplied) return;
this._isApplyingTheme = true;
invalidateThemeCache();
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme?.theme || "default",
{
dark: this.hass.themes.darkMode,
primaryColor: this.hass.selectedTheme?.primaryColor,
accentColor: this.hass.selectedTheme?.accentColor,
},
true
);
this._themeApplied = false;
this._isApplyingTheme = false;
}
private _loadPosition(): void {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const pos = JSON.parse(stored);
if (typeof pos.x === "number" && typeof pos.y === "number") {
this._position = this._clampPosition(pos.x, pos.y);
}
}
} catch {
// Ignore invalid stored position
}
}
private _savePosition(): void {
if (this._position) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._position));
} catch {
// Ignore storage errors
}
}
}
private _clampPosition(x: number, y: number): { x: number; y: number } {
const size = 80;
return {
x: Math.max(0, Math.min(window.innerWidth - size, x)),
y: Math.max(0, Math.min(window.innerHeight - size, y)),
};
}
private _onPointerDown(ev: PointerEvent): void {
if (ev.button !== 0) return;
this._dragging = true;
this._dragMoved = false;
this._dragStartX = ev.clientX;
this._dragStartY = ev.clientY;
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
this._dragOffsetX = ev.clientX - rect.left;
this._dragOffsetY = ev.clientY - rect.top;
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
document.addEventListener("pointermove", this._boundPointerMove);
document.addEventListener("pointerup", this._boundPointerUp);
ev.preventDefault();
}
private _onPointerMove(ev: PointerEvent): void {
if (!this._dragging) return;
const dx = ev.clientX - this._dragStartX;
const dy = ev.clientY - this._dragStartY;
if (!this._dragMoved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
return;
}
this._dragMoved = true;
const x = ev.clientX - this._dragOffsetX;
const y = ev.clientY - this._dragOffsetY;
this._position = this._clampPosition(x, y);
}
private _onPointerUp(ev: PointerEvent): void {
document.removeEventListener("pointermove", this._boundPointerMove);
document.removeEventListener("pointerup", this._boundPointerUp);
this._dragging = false;
if (this._dragMoved) {
this._savePosition();
} else {
this._toggleBubble();
}
ev.preventDefault();
}
private _stopPropagation(ev: Event): void {
ev.stopPropagation();
}
private _dismiss(ev: Event): void {
ev.stopPropagation();
this._casitaVisible = false;
this._clearTimers();
}
private _toggleBubble(): void {
if (this._showBubble) {
this._hideBubble();
} else {
this._showTip();
}
}
private _showTip(): void {
const tipIndex = Math.floor(Math.random() * TIP_COUNT) + 1;
this._bubbleText = this.hass!.localize(
`ui.panel.windows_98.tip_${tipIndex}` as LocalizeKeys
);
this._showBubble = true;
this._expression = "ok-nabu";
this._resetSleepTimer();
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
}
this._bubbleTimer = setTimeout(() => {
this._hideBubble();
}, BUBBLE_TIMEOUT);
}
private _hideBubble(): void {
this._showBubble = false;
this._expression = "hi";
this._resetSleepTimer();
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
this._bubbleTimer = undefined;
}
}
private _closeBubble(ev: Event): void {
ev.stopPropagation();
this._hideBubble();
}
private _resetSleepTimer(): void {
if (this._sleepTimer) {
clearTimeout(this._sleepTimer);
}
this._sleepTimer = setTimeout(() => {
if (!this._showBubble) {
this._expression = "sleep";
}
}, SLEEP_TIMEOUT);
}
private _clearTimers(): void {
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
this._bubbleTimer = undefined;
}
if (this._sleepTimer) {
clearTimeout(this._sleepTimer);
this._sleepTimer = undefined;
}
}
protected render() {
if (!this._enabled || !this._casitaVisible) {
return nothing;
}
const size = 80;
const posStyle = this._position
? `left: ${this._position.x}px; top: ${this._position.y}px;`
: `right: 16px; bottom: 16px;`;
return html`
<div
class="casita-container ${this._dragging ? "dragging" : ""}"
style="width: ${size}px; ${posStyle}"
aria-hidden="true"
@pointerdown=${this._onPointerDown}
>
${this._showBubble
? html`
<div class="speech-bubble">
<span class="bubble-text">${this._bubbleText}</span>
<button
class="bubble-close"
@pointerdown=${this._stopPropagation}
@click=${this._closeBubble}
>
</button>
<button
class="bubble-dismiss"
@pointerdown=${this._stopPropagation}
@click=${this._dismiss}
>
${this.hass!.localize("ui.panel.windows_98.dismiss")}
</button>
<div class="bubble-arrow"></div>
</div>
`
: nothing}
<img
class="casita-image"
src="/static/images/voice-assistant/${this._expression}.png"
alt="Casita"
draggable="false"
/>
</div>
`;
}
static readonly styles = css`
:host {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
z-index: 9999;
}
.casita-container {
position: fixed;
pointer-events: auto;
cursor: grab;
user-select: none;
touch-action: none;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
}
.casita-container.dragging {
cursor: grabbing;
}
.casita-image {
width: 100%;
height: auto;
animation: bob 3s ease-in-out infinite;
pointer-events: none;
}
.dragging .casita-image {
animation: none;
}
.speech-bubble {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
background: #ffffe1;
color: #000000;
border-radius: 12px;
border: 2px solid #000000;
padding: 12px 28px 12px 12px;
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
width: 300px;
box-sizing: border-box;
word-wrap: break-word;
overflow-wrap: break-word;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
animation: bubble-in 200ms ease-out;
pointer-events: auto;
}
.bubble-close {
position: absolute;
top: 4px;
right: 4px;
background: none;
border: none;
cursor: pointer;
color: #000000;
font-size: 14px;
padding: 2px 6px;
line-height: 1;
border-radius: 50%;
}
.bubble-close:hover {
background: #e0e0c0;
}
.bubble-dismiss {
display: block;
margin-top: 8px;
background: none;
border: none;
cursor: pointer;
color: #808080;
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
font-size: 12px;
padding: 0;
text-decoration: underline;
}
.bubble-dismiss:hover {
color: #000000;
}
.bubble-arrow {
position: absolute;
bottom: -8px;
right: 32px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #ffffe1;
}
@keyframes bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
@keyframes bubble-in {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.casita-image {
animation: none;
}
.speech-bubble {
animation: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-windows-98": HaWindows98;
}
}

View File

@@ -52,6 +52,7 @@ export class HomeAssistantMain extends LitElement {
return html`
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
<ha-windows-98 .hass=${this.hass} .narrow=${this.narrow}></ha-windows-98>
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}
@@ -79,6 +80,7 @@ export class HomeAssistantMain extends LitElement {
protected firstUpdated() {
import(/* webpackPreload: true */ "../components/ha-sidebar");
import("../components/ha-snowflakes");
import("../components/ha-windows-98");
if (this.hass.auth.external) {
this._externalSidebar =

View File

@@ -46,11 +46,14 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
const featuresToSort = [...features];
return featuresToSort.sort((a, b) => {
// Place frontend.winter_mode at the bottom
if (a.domain === "frontend" && a.preview_feature === "winter_mode")
return 1;
if (b.domain === "frontend" && b.preview_feature === "winter_mode")
return -1;
// Place frontend fun features at the bottom
const funFeatures = ["winter_mode", "windows_98"];
const aIsFun =
a.domain === "frontend" && funFeatures.includes(a.preview_feature);
const bIsFun =
b.domain === "frontend" && funFeatures.includes(b.preview_feature);
if (aIsFun && !bIsFun) return 1;
if (bIsFun && !aIsFun) return -1;
// Sort everything else alphabetically
return domainToName(localize, a.domain).localeCompare(

View File

@@ -10397,6 +10397,34 @@
"add_card": "Add current view as card",
"add_card_error": "Unable to add card",
"error_no_data": "You need to select some data sources first."
},
"windows_98": {
"tip_1": "Try turning your house off and on again.",
"tip_2": "If your automation doesn't work, just add more YAML.",
"tip_3": "Talk to your devices. They won't answer, but it helps.",
"tip_4": "The best way to secure your smart home is to go back to candles.",
"tip_5": "Rebooting fixes everything. Everything.",
"tip_6": "Naming your vacuum 'DJ Roomba' increases cleaning efficiency by 200%.",
"tip_7": "Your automations run better when you're not looking.",
"tip_8": "Every time you restart Home Assistant, a smart bulb loses its pairing.",
"tip_9": "The cloud is just someone else's Raspberry Pi.",
"tip_10": "You can automate your coffee machine, but you still have to drink it yourself.",
"tip_11": "You can save energy by not having a home.",
"tip_12": "Psst... you can drag me anywhere you want!",
"tip_13": "Did you know? I never sleep. Well, sometimes I do. Zzz...",
"tip_14": "Zigbee, Z-Wave, Wi-Fi, Thread... so many protocols, so little time.",
"tip_15": "The sun can trigger your automations. Nature is the best sensor.",
"tip_16": "It looks like you're trying to automate your home! Would you like help?",
"tip_17": "My previous job was a paperclip. I got promoted.",
"tip_18": "I run entirely on YAML and good vibes.",
"tip_19": "Somewhere, a smart plug is blinking and nobody knows why.",
"tip_20": "Home Assistant runs on a Raspberry Pi. I run on hopes and dreams.",
"tip_21": "Behind every great home, there's someone staring at logs at 2am.",
"tip_22": "404: Motivation not found. Try again after coffee.",
"tip_23": "There are two types of people: those who back up, and those who will.",
"tip_24": "My favorite color is #008080. Don't ask me why.",
"tip_25": "Automations are just spicy if-then statements.",
"dismiss": "Dismiss me"
}
},
"tips": {