Skip to content
Open
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
25 changes: 23 additions & 2 deletions demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,17 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
<input type="text" id="name-input" value="" placeholder="Leave empty to hide" data-i18n-placeholder="placeholderEmpty">
</div>

<div class="control-group">
<label class="control-label">Layout</label>
<select id="layout-select">
<option value="default">Default</option>
<option value="minimal">Minimal (compact horizontal)</option>
</select>
</div>

<div class="control-group">
<label class="control-label" data-i18n="height">Height (px)</label>
<input type="number" id="height-input" value="200" min="150" max="400" step="10">
<input type="number" id="height-input" value="200" min="50" max="800" step="10">
</div>

<div class="control-group">
Expand Down Expand Up @@ -1099,7 +1107,8 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
showClock: document.getElementById('show-clock').checked,
clockPosition: document.getElementById('clock-position-select').value,
clockFormat: document.getElementById('clock-format-select').value,
language: document.getElementById('language-select').value
language: document.getElementById('language-select').value,
layout: document.getElementById('layout-select').value
};

card = document.createElement('dynamic-weather-card');
Expand Down Expand Up @@ -1143,6 +1152,7 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
clock_position: config.clockPosition,
clock_format: config.clockFormat,
language: config.language,
layout: config.layout,
};

if (config.name) {
Expand Down Expand Up @@ -1244,6 +1254,17 @@ <h2 class="panel-title" data-i18n="configuration">⚙️ Configuration</h2>
}
});

const layoutSelect = document.getElementById('layout-select');
const heightInput = document.getElementById('height-input');
layoutSelect?.addEventListener('change', () => {
if (layoutSelect.value === 'minimal') {
heightInput.value = '80';
} else {
heightInput.value = '200';
}
updateCard();
});

timeDisplay.textContent = formatTime(currentManualTime);
});

Expand Down
266 changes: 181 additions & 85 deletions dynamic-weather-card.js

Large diffs are not rendered by default.

144 changes: 100 additions & 44 deletions src/components/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export class AnimatedWeatherCard extends LitElement {
overlayOpacity: config.overlay_opacity !== undefined ? config.overlay_opacity : DEFAULT_CONFIG.overlayOpacity,
language: config.language || DEFAULT_CONFIG.language,
windSpeedUnit: config.wind_speed_unit || DEFAULT_CONFIG.windSpeedUnit,
layout: config.layout || DEFAULT_CONFIG.layout,
sunriseEntity: config.sunrise_entity || null,
sunsetEntity: config.sunset_entity || null,
templowAttribute: config.templow_attribute || null,
Expand Down Expand Up @@ -225,7 +226,9 @@ export class AnimatedWeatherCard extends LitElement {
const timeOfDay = this._testTimeOfDay || getTimeOfDayWithSunData(sunData);
const cardClasses = `weather-card ${timeOfDay.type}`;

const minHeight = this.config.height ? `${this.config.height}px` : '200px';
const isMinimal = this.config.layout === 'minimal';
const defaultHeight = isMinimal ? '56px' : '200px';
const minHeight = this.config.height ? `${this.config.height}px` : defaultHeight;

const bgGradient: BackgroundGradient | null = getBackgroundGradient(timeOfDay);
const bgStyle = bgGradient
Expand All @@ -251,60 +254,113 @@ export class AnimatedWeatherCard extends LitElement {
)
: [];

const cardStyle = `min-height: ${minHeight}; ${bgStyle}; ${overlayStyle} cursor: pointer;`;
const hass = this.hass;

return html`
<ha-card
@click=${(e: MouseEvent) => this.actionHandler.handleTap(e)}
@pointerdown=${() => this.actionHandler.handlePointerDown()}
@pointerup=${(e: PointerEvent) => this.actionHandler.handlePointerUp(e)}
@pointercancel=${(e: PointerEvent) => this.actionHandler.handlePointerUp(e)}
>
<div class="${cardClasses}" style="min-height: ${minHeight}; ${bgStyle}; ${overlayStyle} cursor: pointer;">
<div class="canvas-container"></div>
<div class="content">
${this.config.name && this.config.name.trim() !== '' ? html`
<div class="header">
<div class="location">${this.config.name}</div>
</div>
` : ''}
<div class="primary">
<div class="primary-left">
<div class="condition">${i18n.t(weather.condition)}</div>
<div class="temperature">${weather.temperature != null ? Math.round(weather.temperature) + '°' : i18n.t('no_data')}</div>
${this.config.showMinTemp ? html`
<div class="temp-range">
<span class="temp-min">↓ ${weather.templow != null ? `${Math.round(weather.templow)}°` : i18n.t('no_data')}</span>
</div>
` : ''}
${this.config.showFeelsLike ? html`
<div class="feels-like">${i18n.t('feels_like')} ${weather.apparentTemperature != null ? `${Math.round(weather.apparentTemperature)}°` : i18n.t('no_data')}</div>
` : ''}
</div>
<weather-clock
.format=${this.config.showClock && this.config.clockPosition === 'top' ? this.config.clockFormat : null}
></weather-clock>
${isMinimal ? this.renderMinimal(weather, sunData, hass, cardClasses, cardStyle) : this.renderDefault(weather, sunData, hourlyForecast, dailyForecast, hass, cardClasses, cardStyle)}
</ha-card>
`;
}

private renderDefault(
weather: ReturnType<typeof getWeatherData>,
sunData: SunData,
hourlyForecast: import('../types.js').WeatherForecast[],
dailyForecast: import('../types.js').WeatherForecast[],
hass: HomeAssistant,
cardClasses: string,
cardStyle: string
): TemplateResult {
return html`
<div class="${cardClasses}" style="${cardStyle}">
<div class="canvas-container"></div>
<div class="content">
${this.config.name && this.config.name.trim() !== '' ? html`
<div class="header">
<div class="location">${this.config.name}</div>
</div>
<div class="details ${this.config.showClock && this.config.clockPosition === 'details' ? 'details--clock' : ''}">
<weather-details
.weather=${weather}
.sunData=${sunData}
.config=${this.getDetailsConfig()}
.entityAttributes=${getWeatherAttributes(this.hass, this.config.entity)}
></weather-details>
<weather-clock
.format=${this.config.showClock && this.config.clockPosition === 'details' ? this.config.clockFormat : null}
></weather-clock>
` : ''}
<div class="primary">
<div class="primary-left">
<div class="condition">${i18n.t(weather.condition)}</div>
<div class="temperature">${weather.temperature != null ? Math.round(weather.temperature) + '°' : i18n.t('no_data')}</div>
${this.config.showMinTemp ? html`
<div class="temp-range">
<span class="temp-min">↓ ${weather.templow != null ? `${Math.round(weather.templow)}°` : i18n.t('no_data')}</span>
</div>
` : ''}
${this.config.showFeelsLike ? html`
<div class="feels-like">${i18n.t('feels_like')} ${weather.apparentTemperature != null ? `${Math.round(weather.apparentTemperature)}°` : i18n.t('no_data')}</div>
` : ''}
</div>
<hourly-forecast
.forecast=${hourlyForecast}
.clockFormat=${this.config.clockFormat ?? '24h'}
></hourly-forecast>
<daily-forecast
.forecast=${dailyForecast}
.lang=${i18n.lang}
></daily-forecast>
<weather-clock
.format=${this.config.showClock && this.config.clockPosition === 'top' ? this.config.clockFormat : null}
></weather-clock>
</div>
<div class="details ${this.config.showClock && this.config.clockPosition === 'details' ? 'details--clock' : ''}">
<weather-details
.weather=${weather}
.sunData=${sunData}
.config=${this.getDetailsConfig()}
.entityAttributes=${getWeatherAttributes(hass, this.config.entity)}
></weather-details>
<weather-clock
.format=${this.config.showClock && this.config.clockPosition === 'details' ? this.config.clockFormat : null}
></weather-clock>
</div>
<hourly-forecast
.forecast=${hourlyForecast}
.clockFormat=${this.config.clockFormat ?? '24h'}
></hourly-forecast>
<daily-forecast
.forecast=${dailyForecast}
.lang=${i18n.lang}
></daily-forecast>
</div>
</ha-card>
</div>
`;
}

private renderMinimal(
weather: ReturnType<typeof getWeatherData>,
sunData: SunData,
hass: HomeAssistant,
cardClasses: string,
cardStyle: string
): TemplateResult {
const tempStr = weather.temperature != null ? Math.round(weather.temperature) + '°' : i18n.t('no_data');
const templowStr = weather.templow != null ? `↓ ${Math.round(weather.templow)}°` : null;

return html`
<div class="${cardClasses} layout--minimal" style="${cardStyle}">
<div class="canvas-container"></div>
<div class="content">
<div class="mini-primary">
<div class="mini-temp">${tempStr}</div>
${this.config.showMinTemp && templowStr ? html`<div class="mini-temp-low">${templowStr}</div>` : ''}
</div>
<div class="mini-details">
<div class="mini-condition">${i18n.t(weather.condition)}</div>
<weather-details
.weather=${weather}
.sunData=${sunData}
.config=${this.getDetailsConfig()}
.entityAttributes=${getWeatherAttributes(hass, this.config.entity)}
.compact=${true}
></weather-details>
</div>
${this.config.showClock ? html`
<weather-clock .format=${this.config.clockFormat}></weather-clock>
` : ''}
</div>
</div>
`;
}
}
26 changes: 24 additions & 2 deletions src/components/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class WeatherDetails extends LitElement {
@property({ type: Object }) sunData: SunData | null = null;
@property({ type: Object }) config: DetailsConfig | null = null;
@property({ type: Object }) entityAttributes: WeatherEntityAttributes | null = null;
@property({ type: Boolean, reflect: true }) compact = false;

static styles = css`
:host {
Expand All @@ -28,6 +29,14 @@ export class WeatherDetails extends LitElement {
opacity: 0.9;
}

:host([compact]) .info-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 4px 12px;
font-size: 12px;
}

.info-item {
display: flex;
align-items: center;
Expand All @@ -53,6 +62,12 @@ export class WeatherDetails extends LitElement {
height: 20px;
display: block;
}

:host([compact]) .sun-group {
display: flex;
flex-direction: row;
gap: 12px;
}
`;

private hasContent(): boolean {
Expand Down Expand Up @@ -130,12 +145,19 @@ export class WeatherDetails extends LitElement {
render(): TemplateResult {
if (!this.hasContent()) return html``;

const hasSun = this.config?.showSunriseSunset && this.sunData?.hasSunData;
const sunItems = hasSun ? html`
<div class="sun-group">
${this.renderSunrise()}
${this.renderSunset()}
</div>
` : html``;

return html`
<div class="info-grid">
${this.renderHumidity()}
${this.renderSunrise()}
${this.renderWind()}
${this.renderSunset()}
${this.compact ? sunItems : html`${this.renderSunrise()}${this.renderSunset()}`}
</div>
`;
}
Expand Down
14 changes: 13 additions & 1 deletion src/components/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class DynamicWeatherCardEditor extends LitElement {
setConfig(config: WeatherCardEditorConfig): void {
this._config = {
name: '',
layout: DEFAULT_CONFIG.layout,
height: DEFAULT_CONFIG.height,
show_feels_like: DEFAULT_CONFIG.showFeelsLike,
show_wind: DEFAULT_CONFIG.showWind,
Expand Down Expand Up @@ -59,7 +60,18 @@ export class DynamicWeatherCardEditor extends LitElement {
return [
{ name: 'entity', required: true, selector: { entity: { domain: ['weather'] } } },
{ name: 'name', selector: { text: {} } },
{ name: 'height', selector: { number: { min: 200, max: 800, step: 10, mode: 'box' } } },
{
name: 'layout',
selector: {
select: {
options: [
{ label: i18n.t('editor.layout_default'), value: 'default' },
{ label: i18n.t('editor.layout_minimal'), value: 'minimal' }
]
}
}
},
{ name: 'height', selector: { number: { min: 50, max: 800, step: 10, mode: 'box' } } },
{ name: 'show_feels_like', selector: { boolean: {} } },
{ name: 'show_wind', selector: { boolean: {} } },
{ name: 'show_wind_gust', selector: { boolean: {} } },
Expand Down
53 changes: 53 additions & 0 deletions src/components/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,57 @@ export const cardStyles = css`
margin-bottom: 0;
}
}

/* ---- Minimal layout ---- */
.weather-card.layout--minimal {
min-height: 56px;
}

.weather-card.layout--minimal .content {
flex-direction: row;
align-items: center;
padding: 4px 12px;
gap: 12px;
min-height: inherit;
}

.mini-primary {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-shrink: 0;
gap: 0;
}

.mini-condition {
font-size: 11px;
opacity: 0.85;
font-weight: 400;
white-space: nowrap;
}

.mini-temp {
font-size: 44px;
font-weight: 100;
line-height: 1;
}

.mini-temp-low {
font-size: 11px;
opacity: 0.7;
margin-top: 1px;
}

.mini-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
min-width: 0;
}

.weather-card.layout--minimal weather-clock .clock {
font-size: 26px;
}
`;
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ export const DEFAULT_CONFIG: Required<Omit<WeatherCardConfig, 'entity' | 'type'>
overlayOpacity: 0.1,
language: 'auto',
height: null,
windSpeedUnit: 'ms'
windSpeedUnit: 'ms',
layout: 'default' as 'default' | 'minimal'
};
3 changes: 3 additions & 0 deletions src/internationalization/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"editor": {
"entity": "Wetter-Entität",
"name": "Kartentitel",
"layout": "Layout",
"layout_default": "Default",
"layout_minimal": "Minimal",
"height": "Kartenhöhe",
"show_feels_like": "Gefühlte Temperatur anzeigen",
"show_wind": "Windgeschwindigkeit anzeigen",
Expand Down
Loading