Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
i18n.setLanguage(lang);

window.i18n = i18n;
window.dispatchEvent(new CustomEvent('i18n-ready'));
</script>

<!-- Main application logic -->
Expand All @@ -788,14 +789,14 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
// ============================================================================
function applyTranslations() {
$$('[data-i18n]').forEach(el => {
el.textContent = i18n.t(`demo.${el.dataset.i18n}`);
el.textContent = window.i18n.t(`demo.${el.dataset.i18n}`);
});

$$('[data-i18n-placeholder]').forEach(el => {
el.placeholder = i18n.t(`demo.${el.dataset.i18nPlaceholder}`);
el.placeholder = window.i18n.t(`demo.${el.dataset.i18nPlaceholder}`);
});

document.title = i18n.t('demo.pageTitle');
document.title = window.i18n.t('demo.pageTitle');
}

// ============================================================================
Expand Down Expand Up @@ -925,7 +926,7 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
demoModeActive = true;
demoStep = 0;
const demoBtn = document.getElementById('demo-btn');
demoBtn.textContent = i18n.t('demo.stopDemo');
demoBtn.textContent = window.i18n.t('demo.stopDemo');
demoBtn.classList.add('active');

runDemoStep();
Expand All @@ -939,7 +940,7 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
demoInterval = null;
}
const demoBtn = document.getElementById('demo-btn');
demoBtn.textContent = i18n.t('demo.startDemo');
demoBtn.textContent = window.i18n.t('demo.startDemo');
demoBtn.classList.remove('active');
}

Expand Down Expand Up @@ -1064,7 +1065,7 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
container.innerHTML = '';

if (!window.cardLoaded) {
container.innerHTML = `<div class="loading-message">${i18n.t('loading')}</div>`;
container.innerHTML = `<div class="loading-message">${window.i18n.t('loading')}</div>`;
return;
}

Expand Down Expand Up @@ -1156,15 +1157,21 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
function setupLanguageSelect() {
const languageSelect = document.getElementById('language-select');

languageSelect.value = i18n.lang;
languageSelect.value = window.i18n.lang;

languageSelect.addEventListener('change', (e) => {
i18n.setLanguage(e.target.value);
window.i18n.setLanguage(e.target.value);
applyTranslations();
updateCard();
});
}

function initI18nDependentFeatures() {
setupLanguageSelect();
applyTranslations();
updateCard();
}

document.addEventListener('DOMContentLoaded', () => {
const timeSlider = document.getElementById('time-slider');
const timeDisplay = document.getElementById('time-display');
Expand All @@ -1184,8 +1191,12 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
updateCard();
});

setupLanguageSelect();
applyTranslations();
// Wait for i18n to be ready before using it
if (window.i18n) {
initI18nDependentFeatures();
} else {
window.addEventListener('i18n-ready', initI18nDependentFeatures, { once: true });
}

window.addEventListener('language-changed', () => {
applyTranslations();
Expand Down
162 changes: 162 additions & 0 deletions docs/plans/2026-01-25-card-decomposition-design-part2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Card.ts Decomposition Design

## Overview

Refactor `src/components/card.ts` (~727 lines) into smaller, focused modules to improve maintainability and testability.

## Target Structure

```
src/components/
├── card.ts
├── animation-manager.ts
├── forecast-service.ts
├── action-handler.ts
├── weather-data.ts
├── clock.ts
├── details.ts
├── daily-forecast.ts
├── hourly-forecast.ts
├── styles.ts
└── editor.ts
```

## Module Specifications

### AnimationManager

Manages canvas lifecycle and weather animations.

```typescript
export class AnimationManager {
private canvas: HTMLCanvasElement | null = null;
private ctx: CanvasRenderingContext2D | null = null;
private animationFrame: number | null = null;
private animations: Partial<Animations> = {};
private resizeObserver: ResizeObserver | null = null;
private width: number = 0;
private height: number = 0;

setup(container: Element): void;
destroy(): void;
draw(condition: string, timeOfDay: TimeOfDay): void;

private setupCanvas(): void;
private resizeCanvas(): void;
private initializeAnimations(): void;
private startAnimation(): void;
}
```

### ForecastService

Handles forecast subscriptions and data filtering.

```typescript
export class ForecastService {
private hourlyForecast: WeatherForecast[] = [];
private dailyForecast: WeatherForecast[] = [];
private hourlySubscription: Promise<(() => void)> | null = null;
private dailySubscription: Promise<(() => void)> | null = null;
private onUpdate: () => void;

constructor(onUpdate: () => void);

subscribe(hass: HomeAssistant, entityId: string, showDaily: boolean): void;
unsubscribe(): void;
getHourlyForecast(hours: number, fallbackData: WeatherForecast[]): WeatherForecast[];
getDailyForecast(days: number, fallbackData: WeatherForecast[]): WeatherForecast[];
}
```

### ActionHandler

Manages user interactions (tap, hold, double-tap).

```typescript
export class ActionHandler {
private holdTimer: number | null = null;
private lastTap: number | null = null;
private holdFired: boolean = false;
private readonly holdDelay = 500;

constructor(
getHass: () => HomeAssistant | undefined,
getConfig: () => WeatherCardConfigInternal,
fireEvent: (type: string, detail: Record<string, unknown>) => void
);

handleTap(e: MouseEvent): void;
handlePointerDown(e: PointerEvent): void;
handlePointerUp(e: PointerEvent): void;

private handleAction(actionConfig: ActionConfig): void;
private handleHold(): void;
private handleDoubleTap(): void;
}
```

### WeatherData

Pure functions for extracting weather data from Home Assistant.

```typescript
export function getWeatherState(hass: HomeAssistant, entityId: string): string | null;
export function getWeatherAttributes(hass: HomeAssistant, entityId: string): WeatherEntityAttributes;
export function getWeatherData(
hass: HomeAssistant,
entityId: string,
config: { templowAttribute?: string | null },
hourlyForecast: WeatherForecast[]
): WeatherData;
```

## Card.ts Responsibilities (After Refactor)

- Lit Element lifecycle management
- `setConfig()` and `getStubConfig()`
- `render()` template
- Coordination between modules
- Language/i18n updates

## Integration Pattern

```typescript
// card.ts
class AnimatedWeatherCard extends LitElement {
private animationManager = new AnimationManager();
private forecastService = new ForecastService(() => this.requestUpdate());
private actionHandler = new ActionHandler(
() => this.hass,
() => this.config,
(type, detail) => this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }))
);

connectedCallback() {
super.connectedCallback();
this.updateComplete.then(() => {
const container = this.shadowRoot?.querySelector('.canvas-container');
if (container) this.animationManager.setup(container);
});
}

disconnectedCallback() {
super.disconnectedCallback();
this.animationManager.destroy();
this.forecastService.unsubscribe();
}

updated(changedProperties) {
if (changedProperties.has('hass') || changedProperties.has('config')) {
this.forecastService.subscribe(this.hass, this.config.entity, this.config.showDailyForecast);
}
}
}
```

## Design Principles

1. **Single Responsibility** - Each module handles one concern
2. **No Cross-Dependencies** - Modules don't know about each other
3. **Card as Coordinator** - Card creates and orchestrates modules
4. **Testability** - Pure functions and injectable dependencies
Loading