v0.14.18
This commit is contained in:
+14
-3
@@ -12,12 +12,23 @@
|
||||
</head>
|
||||
<body class="public-custom-chart-page">
|
||||
<main class="public-custom-chart-shell">
|
||||
<section class="panel chart-panel history-chart-card public-custom-chart-card" aria-live="polite">
|
||||
<div class="chart-title-row">
|
||||
<div>
|
||||
<section class="panel chart-panel chart-card public-custom-chart-card" aria-live="polite">
|
||||
<div class="chart-title-row public-chart-title-row">
|
||||
<div class="public-chart-heading">
|
||||
<h3 id="publicChartTitle">Custom chart</h3>
|
||||
<p id="publicChartHint">Loading…</p>
|
||||
</div>
|
||||
<label class="public-chart-range-control">
|
||||
<span id="publicChartRangeLabel">Range</span>
|
||||
<select id="publicChartHours" aria-label="Chart range">
|
||||
<option value="6">6 hours</option>
|
||||
<option value="24">24 hours</option>
|
||||
<option value="168">7 days</option>
|
||||
<option value="720">30 days</option>
|
||||
<option value="2160">90 days</option>
|
||||
<option value="8760">1 year</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="chart-wrap public-custom-chart-wrap">
|
||||
<canvas id="publicCustomChart" width="1200" height="520" tabindex="0" aria-label="Custom chart"></canvas>
|
||||
|
||||
+86
-5
@@ -3,6 +3,7 @@
|
||||
const PUBLIC_CHART_COLORS = ['--accent', '--info', '--warning', '--purple', '--danger', '--teal', '--orange', '--blue'];
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
const PUBLIC_CHART_RANGES = [6, 24, 168, 720, 2160, 8760];
|
||||
|
||||
function basePathFromScript() {
|
||||
const src = document.currentScript?.src || '';
|
||||
@@ -41,7 +42,7 @@ function preciseTime(timestamp, locale) {
|
||||
function prepareCanvas(canvas, height = 520) {
|
||||
const wrap = canvas.parentElement;
|
||||
const width = Math.max(720, Math.floor(wrap?.clientWidth || canvas.getBoundingClientRect().width || 720));
|
||||
const actualHeight = Math.max(320, Math.floor(wrap?.clientHeight || height));
|
||||
const actualHeight = Math.max(180, Math.floor(wrap?.clientHeight || height));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.floor(width * dpr);
|
||||
canvas.height = Math.floor(actualHeight * dpr);
|
||||
@@ -194,12 +195,64 @@ function drawChart(canvas, payload, locale) {
|
||||
bindTooltip(canvas, series, { pad, width, height, firstTs, lastTs }, locale);
|
||||
}
|
||||
|
||||
async function loadPublicChart() {
|
||||
const token = decodeURIComponent(location.pathname.split('/').filter(Boolean).pop() || '');
|
||||
let publicChartPayload = null;
|
||||
let publicChartLocale = 'en-GB';
|
||||
let publicChartResizeFrame = 0;
|
||||
|
||||
function publicChartToken() {
|
||||
return decodeURIComponent(location.pathname.split('/').filter(Boolean).pop() || '');
|
||||
}
|
||||
|
||||
function requestedPublicChartHours() {
|
||||
const value = Number(new URLSearchParams(location.search).get('hours'));
|
||||
return Number.isInteger(value) && value >= 1 && value <= 87600 ? value : null;
|
||||
}
|
||||
|
||||
function setPublicRangeLanguage(lang) {
|
||||
const pl = lang === 'pl';
|
||||
const label = $('#publicChartRangeLabel');
|
||||
const select = $('#publicChartHours');
|
||||
if (label) label.textContent = pl ? 'Zakres' : 'Range';
|
||||
if (!select) return;
|
||||
const labels = pl
|
||||
? ['6 godzin', '24 godziny', '7 dni', '30 dni', '90 dni', '1 rok']
|
||||
: ['6 hours', '24 hours', '7 days', '30 days', '90 days', '1 year'];
|
||||
[...select.options].forEach((option, index) => { if (labels[index]) option.textContent = labels[index]; });
|
||||
select.setAttribute('aria-label', pl ? 'Zakres wykresu' : 'Chart range');
|
||||
}
|
||||
|
||||
function syncPublicRangeControl(hours) {
|
||||
const select = $('#publicChartHours');
|
||||
if (!select) return;
|
||||
const value = String(hours);
|
||||
if (![...select.options].some(option => option.value === value)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = `${value} h`;
|
||||
select.append(option);
|
||||
}
|
||||
select.value = value;
|
||||
}
|
||||
|
||||
function publicChartApiPath(token, hours) {
|
||||
const value = Number(hours);
|
||||
const hasHours = hours !== null && hours !== undefined && Number.isInteger(value) && value >= 1 && value <= 87600;
|
||||
const query = hasHours ? `?hours=${encodeURIComponent(String(value))}` : '';
|
||||
return `/api/public/charts/custom/${encodeURIComponent(token)}${query}`;
|
||||
}
|
||||
|
||||
function redrawPublicChart() {
|
||||
if (!publicChartPayload) return;
|
||||
drawChart($('#publicCustomChart'), publicChartPayload, publicChartLocale);
|
||||
}
|
||||
|
||||
async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
const token = publicChartToken();
|
||||
const title = $('#publicChartTitle');
|
||||
const hint = $('#publicChartHint');
|
||||
const error = $('#publicChartError');
|
||||
const canvas = $('#publicCustomChart');
|
||||
const range = $('#publicChartHours');
|
||||
|
||||
if (!token.startsWith('chart_')) {
|
||||
title.textContent = 'Custom chart';
|
||||
@@ -210,7 +263,9 @@ async function loadPublicChart() {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(withBase(`/api/public/charts/custom/${encodeURIComponent(token)}`), { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (range) range.disabled = true;
|
||||
error.hidden = true;
|
||||
const response = await fetch(withBase(publicChartApiPath(token, hours)), { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
let message = `${response.status}`;
|
||||
try { message = (await response.json()).error || message; } catch (_) { }
|
||||
@@ -223,14 +278,40 @@ async function loadPublicChart() {
|
||||
title.textContent = payload.title || (lang === 'pl' ? 'Wykres niestandardowy' : 'Custom chart');
|
||||
hint.textContent = payload.hint || '';
|
||||
canvas.setAttribute('aria-label', title.textContent);
|
||||
setPublicRangeLanguage(lang);
|
||||
syncPublicRangeControl(payload.hours);
|
||||
publicChartPayload = payload;
|
||||
publicChartLocale = locale;
|
||||
drawChart(canvas, payload, locale);
|
||||
window.addEventListener('resize', () => drawChart(canvas, payload, locale));
|
||||
if (range) range.disabled = false;
|
||||
}
|
||||
|
||||
$('#publicChartHours')?.addEventListener('change', async event => {
|
||||
const hours = Number(event.currentTarget.value);
|
||||
if (!Number.isInteger(hours) || hours < 1 || hours > 87600) return;
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set('hours', String(hours));
|
||||
history.replaceState(null, '', url);
|
||||
try {
|
||||
await loadPublicChart(hours);
|
||||
} catch (error) {
|
||||
const host = $('#publicChartError');
|
||||
host.hidden = false;
|
||||
host.textContent = `${document.documentElement.lang === 'pl' ? 'Nie udało się wczytać wykresu' : 'Unable to load chart'}: ${error.message}`;
|
||||
event.currentTarget.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
cancelAnimationFrame(publicChartResizeFrame);
|
||||
publicChartResizeFrame = requestAnimationFrame(redrawPublicChart);
|
||||
});
|
||||
|
||||
loadPublicChart().catch(error => {
|
||||
const host = $('#publicChartError');
|
||||
host.hidden = false;
|
||||
host.textContent = `Unable to load chart: ${error.message}`;
|
||||
$('#publicChartHint').textContent = '';
|
||||
$('#publicChartHours').disabled = false;
|
||||
drawEmpty($('#publicCustomChart'), host.textContent);
|
||||
});
|
||||
|
||||
+78
-78
@@ -189,7 +189,7 @@
|
||||
data-i18n="flow.import">Import Flow</button><button class="primary" type="button" data-action="new-flow"
|
||||
data-i18n="flow.new">New Flow</button></div>
|
||||
</div>
|
||||
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<nav class="segmented-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<button type="button" class="active" data-go="flows" data-i18n="nav.flows">Flow</button>
|
||||
<button type="button" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
|
||||
<button type="button" data-go="automations" data-i18n="nav.automations">Automations</button>
|
||||
@@ -215,7 +215,7 @@
|
||||
<h1 data-i18n="nav.schedules">Schedules</h1>
|
||||
</div><button class="secondary" data-open="scheduleDialog" data-i18n="actions.add">Add</button>
|
||||
</div>
|
||||
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<nav class="segmented-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<button type="button" data-go="flows" data-i18n="nav.flows">Flow</button>
|
||||
<button type="button" class="active" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
|
||||
<button type="button" data-go="automations" data-i18n="nav.automations">Automations</button>
|
||||
@@ -248,7 +248,7 @@
|
||||
<h1 data-i18n="nav.automations">Automations</h1>
|
||||
</div><button class="secondary" data-open="automationDialog" data-i18n="actions.add">Add</button>
|
||||
</div>
|
||||
<nav class="automation-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<nav class="segmented-tabs" data-i18n-aria="automationHub.navigation" aria-label="Automation navigation">
|
||||
<button type="button" data-go="flows" data-i18n="nav.flows">Flow</button>
|
||||
<button type="button" data-go="schedules" data-i18n="nav.schedules">Schedules</button>
|
||||
<button type="button" class="active" data-go="automations" data-i18n="nav.automations">Automations</button>
|
||||
@@ -322,15 +322,15 @@
|
||||
<h1 data-i18n="history.title">Climate history</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-tabs" id="historyTabs" role="tablist">
|
||||
<button data-history-tab="overview" data-i18n="history.tabOverview">Overview</button>
|
||||
<button data-history-tab="zones" data-i18n="history.tabZones">Zones</button>
|
||||
<button data-history-tab="devices" data-i18n="history.tabDevices">GREE devices</button>
|
||||
<button data-history-tab="energy" data-i18n="energy.title">Energy</button>
|
||||
<button data-history-tab="network" data-i18n="history.tabNetwork">Pings</button>
|
||||
<button data-history-tab="sensors" data-i18n="history.tabSensors">HA sensors</button>
|
||||
<button data-history-tab="custom" data-i18n="history.tabCustom">Custom chart</button>
|
||||
</div>
|
||||
<nav class="segmented-tabs" id="historyTabs" role="tablist" aria-label="History navigation">
|
||||
<button type="button" data-history-tab="overview" data-i18n="history.tabOverview">Overview</button>
|
||||
<button type="button" data-history-tab="zones" data-i18n="history.tabZones">Zones</button>
|
||||
<button type="button" data-history-tab="devices" data-i18n="history.tabDevices">GREE devices</button>
|
||||
<button type="button" data-history-tab="energy" data-i18n="energy.title">Energy</button>
|
||||
<button type="button" data-history-tab="network" data-i18n="history.tabNetwork">Pings</button>
|
||||
<button type="button" data-history-tab="sensors" data-i18n="history.tabSensors">HA sensors</button>
|
||||
<button type="button" data-history-tab="custom" data-i18n="history.tabCustom">Custom chart</button>
|
||||
</nav>
|
||||
<div class="panel chart-panel history-toolbar-panel">
|
||||
<div class="chart-toolbar">
|
||||
<div id="historyContextControls" class="history-context-controls"></div>
|
||||
@@ -361,16 +361,16 @@
|
||||
</div>
|
||||
<p class="lead" data-i18n="night.description">Configure quieter thermostat behavior for sleeping hours without
|
||||
mixing it with application settings.</p>
|
||||
<form class="settings-form standalone-settings-form" id="nightModeForm">
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<form class="config-form full-width-form" id="nightModeForm">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.nightMode">Night mode</h3>
|
||||
<p data-i18n="settings.nightModeHint">During selected hours the thermostat limits fan speed and can
|
||||
request Quiet on supported units.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="night_mode_enabled"> <span
|
||||
data-i18n="settings.nightModeEnabled">Enable night mode</span></label>
|
||||
<label><span data-i18n="settings.nightStart">Start</span><input type="time" name="night_mode_start"
|
||||
@@ -391,7 +391,7 @@
|
||||
data-i18n="settings.nightNativeSleep">Use native Sleep when supported</span></label>
|
||||
</div>
|
||||
</section>
|
||||
<div class="settings-save-bar"><span data-i18n="night.saveHint">Save night-mode changes.</span><button
|
||||
<div class="sticky-form-actions"><span data-i18n="night.saveHint">Save night-mode changes.</span><button
|
||||
class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -404,16 +404,16 @@
|
||||
</div>
|
||||
<p class="lead" data-i18n="homeAssistant.description">Connection, temperature sensors, friendly aliases and access
|
||||
tokens for the Home Assistant integration.</p>
|
||||
<form class="settings-form standalone-settings-form" id="homeAssistantForm">
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<form class="config-form full-width-form" id="homeAssistantForm">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.haConnection">Połączenie z Home Assistant</h3>
|
||||
<p data-i18n="settings.haConnectionHint">Adres serwera i dane dostępu używane przez wszystkie funkcje Home
|
||||
Assistant.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<div id="haSupervisorStatus" class="ha-supervisor-status wide" role="status" aria-live="polite" hidden></div>
|
||||
<label class="wide" data-ha-manual-field><span>URL</span><input type="url" name="ha_url"
|
||||
placeholder="http://homeassistant.local:8123"></label>
|
||||
@@ -430,15 +430,15 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.haThermostatSources">Źródła temperatury sterowania</h3>
|
||||
<p data-i18n="settings.haThermostatSourcesHint">Te ustawienia należą do logiki termostatów i źródeł
|
||||
temperatury, nie do wspólnych wejść Flow.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="wide"><span data-i18n="settings.outdoorEntity">Global outdoor temperature entity_id</span><input
|
||||
name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label>
|
||||
<p class="field-note wide" data-i18n="settings.outdoorEntityHint">Global Home Assistant outdoor temperature sensor. Zones use it by default and may override it with another outdoor sensor in zone settings.</p>
|
||||
@@ -452,8 +452,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block flow-shared-settings" id="flowSharedInputsSettings">
|
||||
<div class="settings-block-head flow-shared-settings-head">
|
||||
<section class="panel form-section flow-shared-settings" id="flowSharedInputsSettings">
|
||||
<div class="form-section-head flow-shared-settings-head">
|
||||
<div>
|
||||
<h3 data-i18n="flow.sharedInputsTitle">Wspólne wejścia Flow</h3>
|
||||
<p data-i18n="flow.sharedInputsHint">Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow.
|
||||
@@ -465,8 +465,8 @@
|
||||
<div id="flowSharedInputList" class="flow-shared-input-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.sensorAliases">Sensor aliases</h3>
|
||||
<p data-i18n="settings.sensorAliasesHint">Badges show whether an entity is collected as metrics, used by
|
||||
@@ -481,8 +481,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.haIntegrationAccess">Home Assistant integration access</h3>
|
||||
<p data-i18n="settings.haIntegrationHint">Create a controller token and paste it into the GREE Controller
|
||||
@@ -495,7 +495,7 @@
|
||||
data-i18n="settings.newToken">Create new token</button></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="settings-save-bar"><span data-i18n="homeAssistant.saveHint">Save Home Assistant and sensor
|
||||
<div class="sticky-form-actions"><span data-i18n="homeAssistant.saveHint">Save Home Assistant and sensor
|
||||
changes.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -514,31 +514,31 @@
|
||||
data-i18n="settings.greeSettings">GREE</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-settings-tab="cloud">GREE Cloud</button>
|
||||
</div>
|
||||
<form class="settings-form" id="settingsForm" novalidate>
|
||||
<form class="config-form" id="settingsForm" novalidate>
|
||||
<div class="settings-pane" data-settings-pane="app">
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.applicationRuntime">Application runtime</h3>
|
||||
<p data-i18n="settings.applicationRuntimeHint">Application-wide behavior that does not change GREE
|
||||
transport or discovery.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="simulator_enabled"> <span
|
||||
data-i18n="settings.simulationMode">Simulation mode</span></label>
|
||||
<p class="field-note wide warning-note" data-i18n="settings.simulationModeHint">When enabled, a prominent
|
||||
warning is shown throughout the application.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="notifications.title">Notifications</h3>
|
||||
<p data-i18n="notifications.hint">Send important events or only problems/anomalies.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="notifications_enabled"> <span
|
||||
data-i18n="notifications.enable">Enable notifications</span></label>
|
||||
<label><span data-i18n="notifications.mode">Mode</span><select name="notifications_mode">
|
||||
@@ -600,15 +600,15 @@
|
||||
data-i18n="notifications.test">Send test notification</button></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.metricsAndLogs">Metrics and logs</h3>
|
||||
<p data-i18n="settings.compactionHint">SQLite keeps recent data locally and compacts older samples to
|
||||
the resolution used by charts.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label><span data-i18n="settings.retentionDays">Local metric retention (days)</span><input type="number"
|
||||
name="history_retention_days" min="1" max="3650" required></label>
|
||||
<label><span data-i18n="settings.eventRetentionDays">Event/log retention (days)</span><input type="number"
|
||||
@@ -617,15 +617,15 @@
|
||||
data-i18n="settings.compaction">Compact old metrics</span></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.influx">Long-term InfluxDB history</h3>
|
||||
<p data-i18n="settings.influxHint">Optional archive for older history. InfluxDB 1.x and 2.x are
|
||||
supported.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="influx_enabled"> <span
|
||||
data-i18n="settings.influxEnabled">Enable InfluxDB archive</span></label>
|
||||
<label><span data-i18n="settings.influxVersion">InfluxDB version</span><select name="influx_version">
|
||||
@@ -654,14 +654,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.debug">On-screen debug</h3>
|
||||
<p data-i18n="settings.debugHint">Live diagnostics displayed on every application page.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check"><input type="checkbox" name="debug_overlay_enabled"> <span
|
||||
data-i18n="settings.debugOverlay">Show debug window on every page</span></label>
|
||||
<label class="check"><input type="checkbox" name="debug_gree_frames"> <span
|
||||
@@ -672,8 +672,8 @@
|
||||
data-i18n="settings.debugCloudMqtt">Trace GREE Cloud MQTT</span></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.backup">Configuration backup</h3>
|
||||
<p class="warning-note" data-i18n="settings.backupHint">Export/import application configuration.
|
||||
@@ -689,15 +689,15 @@
|
||||
<div class="panel system-panel" id="systemInfo"></div>
|
||||
</div>
|
||||
<div class="settings-pane" data-settings-pane="gree" hidden>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.greeConnection">GREE connection</h3>
|
||||
<p data-i18n="settings.greeConnectionHint">Controller identity, polling intervals and LAN discovery
|
||||
transport.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label><span data-i18n="settings.clientId">Client identifier</span><input name="controller_id"
|
||||
required></label>
|
||||
<label><span data-i18n="settings.pollInterval">Poll interval (s)</span><input type="number"
|
||||
@@ -710,29 +710,29 @@
|
||||
name="discovery_timeout_ms" min="300" max="30000" required></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.connectivityTitle">Connectivity metrics</h3>
|
||||
<p data-i18n="settings.connectivityHint">Periodic GREE UDP round-trip measurements for Local/LAN units. Results are stored in History.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="ping_metrics_enabled"> <span data-i18n="settings.connectivityEnable">Enable local unit ping measurements</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="ping_interval_seconds" min="10" max="3600" value="60"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="ping_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.connectivityLocalOnly">Only Local/LAN installations are probed. Cloud devices are not pinged directly.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.greeCommands">GREE commands</h3>
|
||||
<p data-i18n="settings.suppressBeepHint">Only changed properties are sent. Buzzer suppression is also
|
||||
requested when supported by the unit firmware.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="suppress_device_beep"> <span
|
||||
data-i18n="settings.suppressBeep">Try to suppress command beeps</span></label>
|
||||
<label class="check wide"><input type="checkbox" name="compressor_protection_enabled"> <span
|
||||
@@ -744,8 +744,8 @@
|
||||
Thermostats.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.greeTraffic">GREE traffic</h3>
|
||||
<p data-i18n="settings.greeTrafficHint">Live counters of UDP frames received from configured air
|
||||
@@ -756,9 +756,9 @@
|
||||
</section>
|
||||
</div>
|
||||
<div class="settings-pane" data-settings-pane="cloud" hidden>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.cloudAccountTitle">GREE Cloud account</h3><p data-i18n="settings.cloudAccountHint">REST login and device discovery use the selected regional GREE service. The password is never returned by the API after it is saved.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head"><div><h3 data-i18n="settings.cloudAccountTitle">GREE Cloud account</h3><p data-i18n="settings.cloudAccountHint">REST login and device discovery use the selected regional GREE service. The password is never returned by the API after it is saved.</p></div></div>
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_enabled"> <span data-i18n="settings.cloudEnable">Enable GREE Cloud</span></label>
|
||||
<label><span data-i18n="settings.cloudRegion">Region</span><select name="gree_cloud_region">
|
||||
<option>Australia</option><option>China Mainland</option><option>East South Asia</option><option selected>Europe</option><option>India</option><option>Latin American</option><option>Middle East</option><option>North American</option><option>Russia</option><option>South American</option>
|
||||
@@ -787,14 +787,14 @@
|
||||
<div id="greeCloudTestResult" class="inline-alert wide" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.cloudConnectivityTitle">Cloud connectivity metrics</h3>
|
||||
<p data-i18n="settings.cloudConnectivityHint">Optional. Disabled by default. Measures REST login response and MQTT PINGRESP round-trip.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_connectivity_metrics_enabled"> <span data-i18n="settings.cloudConnectivityEnable">Measure Cloud REST/MQTT connectivity</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="gree_cloud_connectivity_metrics_interval_seconds" min="30" max="3600" value="300"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="gree_cloud_connectivity_metrics_sample_count" min="1" max="10" value="3"></label>
|
||||
@@ -802,7 +802,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="settings-save-bar"><span data-i18n="settings.saveHint">Save changes made in the blocks
|
||||
<div class="sticky-form-actions"><span data-i18n="settings.saveHint">Save changes made in the blocks
|
||||
above.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -878,16 +878,16 @@
|
||||
<aside class="flow-palette">
|
||||
<div class="flow-palette-head"><strong data-i18n="flow.blocks">Blocks</strong><small
|
||||
data-i18n="flow.paletteHint">Add a block, then connect its output to the next block.</small></div>
|
||||
<div class="flow-palette-group flow-palette-trigger"><span data-i18n="flow.triggers">Wyzwalacze</span>
|
||||
<div class="flow-palette-group block-category-trigger"><span data-i18n="flow.triggers">Wyzwalacze</span>
|
||||
<button type="button" data-flow-add="cron_trigger" data-i18n="flow.node.cronTrigger">CRON</button>
|
||||
</div>
|
||||
<div class="flow-palette-group flow-palette-time"><span data-i18n="flow.time">Czas</span>
|
||||
<div class="flow-palette-group block-category-time"><span data-i18n="flow.time">Czas</span>
|
||||
<button type="button" data-flow-add="weekday" data-i18n="flow.node.weekday">Dni tygodnia</button><button
|
||||
type="button" data-flow-add="time_range" data-i18n="flow.node.timeRange">Przedział godzin</button><button
|
||||
type="button" data-flow-add="date_range" data-i18n="flow.node.dateRange">Zakres dat</button><button
|
||||
type="button" data-flow-add="night_mode" data-i18n="flow.node.nightMode">Tryb nocny</button>
|
||||
</div>
|
||||
<div class="flow-palette-group flow-palette-timeop"><span data-i18n="flow.timeOps">Operacje czasu</span>
|
||||
<div class="flow-palette-group block-category-timeop"><span data-i18n="flow.timeOps">Operacje czasu</span>
|
||||
<button type="button" data-flow-add="stable_for" data-i18n="flow.node.stableFor">Utrzymuje się
|
||||
przez…</button><button type="button" data-flow-add="state_duration" data-i18n="flow.node.stateDuration">Czas
|
||||
trwania stanu</button><button type="button" data-flow-add="on_change" data-i18n="flow.node.onChange">Tylko
|
||||
@@ -895,7 +895,7 @@
|
||||
X razy / okres</button><button type="button" data-flow-add="delay"
|
||||
data-i18n="flow.node.delay">Odczekaj…</button>
|
||||
</div>
|
||||
<div class="flow-palette-group flow-palette-sensor"><span data-i18n="flow.sensors">Sensory / wartości</span>
|
||||
<div class="flow-palette-group block-category-sensor"><span data-i18n="flow.sensors">Sensory / wartości</span>
|
||||
<button type="button" data-flow-add="outdoor_temperature" data-i18n="flow.node.outdoorTemperature">Temperatura
|
||||
zewn.</button><button type="button" data-flow-add="device_temperature"
|
||||
data-i18n="flow.node.deviceTemperature">Temperatura urządzenia</button><button type="button"
|
||||
@@ -913,17 +913,17 @@
|
||||
data-flow-add="rolling_stat" data-i18n="flow.node.rollingStat">Średnia / mediana</button><button
|
||||
type="button" data-flow-add="oscillates" data-i18n="flow.node.oscillates">Wartość oscyluje</button>
|
||||
</div>
|
||||
<div class="flow-palette-group flow-palette-logic"><span data-i18n="flow.logic">Logika</span><button
|
||||
<div class="flow-palette-group block-category-logic"><span data-i18n="flow.logic">Logika</span><button
|
||||
type="button" data-flow-add="logic_and" data-i18n="flow.node.and">AND</button><button type="button"
|
||||
data-flow-add="logic_or" data-i18n="flow.node.or">OR</button><button type="button" data-flow-add="logic_not"
|
||||
data-i18n="flow.node.not">NOT</button></div>
|
||||
<div class="flow-palette-group flow-palette-action"><span data-i18n="flow.actions">Akcje</span>
|
||||
<div class="flow-palette-group block-category-action"><span data-i18n="flow.actions">Akcje</span>
|
||||
<button type="button" data-flow-add="zone_thermostat"
|
||||
data-i18n="flow.node.thermostat">Termostat</button><button type="button" data-flow-add="device_action"
|
||||
data-i18n="flow.node.greeDevice">Urządzenie GREE</button><button type="button" data-flow-add="group_action"
|
||||
data-i18n="flow.node.group">Grupa</button>
|
||||
</div>
|
||||
<div class="flow-palette-group flow-palette-haaction"><span data-i18n="flow.haActions">Home Assistant</span>
|
||||
<div class="flow-palette-group block-category-haaction"><span data-i18n="flow.haActions">Home Assistant</span>
|
||||
<button type="button" data-flow-add="ha_service_action" data-i18n="flow.node.haServiceAction">Home Assistant:
|
||||
usługa</button>
|
||||
</div>
|
||||
@@ -1233,9 +1233,9 @@
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="technical-device-grid" id="deviceDetailsMeta"></div>
|
||||
<section class="settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="energy.title">Energy</h3><p data-i18n="energy.sourceHint">Choose the cumulative energy source used for charts and period totals.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
<section class="form-section">
|
||||
<div class="form-section-head"><div><h3 data-i18n="energy.title">Energy</h3><p data-i18n="energy.sourceHint">Choose the cumulative energy source used for charts and period totals.</p></div></div>
|
||||
<div class="form-grid">
|
||||
<label><span data-i18n="energy.source">Energy source</span><select name="energy_source">
|
||||
<option value="auto" data-i18n="energy.auto">Auto</option>
|
||||
<option value="gree_cloud">GREE Cloud</option>
|
||||
@@ -1257,7 +1257,7 @@
|
||||
<div id="deviceGroupsList" class="installation-list"></div>
|
||||
<form id="deviceGroupForm" class="installation-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="settings-grid">
|
||||
<div class="form-grid">
|
||||
<label><span data-i18n="devices.installationName">Installation name</span><input name="name" maxlength="80" required></label>
|
||||
<label><span data-i18n="devices.installationType">Installation type</span><select name="kind"><option value="split" data-i18n="devices.split">Split</option><option value="multisplit" data-i18n="devices.multisplit">Multisplit</option></select></label>
|
||||
<div class="wide"><span class="field-label" data-i18n="devices.installationDevices">Indoor units</span><div id="deviceGroupDeviceChoices" class="installation-device-choices"></div></div>
|
||||
|
||||
+6
-6
@@ -191,7 +191,7 @@ function setChartSeriesHidden(chartId, item, index, hidden) {
|
||||
|
||||
function updateChartZoomControls(id) {
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
const card = canvas?.closest('.chart-card');
|
||||
if (!card) return;
|
||||
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const reset = card.querySelector('[data-chart-zoom="reset"]');
|
||||
@@ -319,11 +319,11 @@ function closeChartPreview(card) {
|
||||
function toggleChartFullscreen(id) {
|
||||
if (!window.matchMedia('(min-width: 761px)').matches) return;
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
const card = canvas?.closest('.chart-card');
|
||||
if (!card) return;
|
||||
|
||||
const active = chartCardIsFullscreen(card);
|
||||
const opened = document.querySelector('.history-chart-card.chart-fullscreen-fallback');
|
||||
const opened = document.querySelector('.chart-card.chart-fullscreen-fallback');
|
||||
if (opened && opened !== card) closeChartPreview(opened);
|
||||
|
||||
if (active) {
|
||||
@@ -356,7 +356,7 @@ function prepareCanvas(canvas, height) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrap = canvas.parentElement;
|
||||
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card'));
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.chart-card'));
|
||||
const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0;
|
||||
const actualHeight = wrapHeight || height;
|
||||
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM);
|
||||
@@ -724,7 +724,7 @@ function renderLegend(host, series) {
|
||||
function historyChartMarkup(id, title, hint, compact = false) {
|
||||
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const percent = Math.round(zoom * 100);
|
||||
return `<div class="panel chart-panel history-chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
return `<div class="panel chart-panel chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
}
|
||||
|
||||
function historySeriesColor(index) {
|
||||
@@ -733,6 +733,6 @@ function historySeriesColor(index) {
|
||||
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') closeChartPreview(document.querySelector('.history-chart-card.chart-fullscreen-fallback'));
|
||||
if (event.key === 'Escape') closeChartPreview(document.querySelector('.chart-card.chart-fullscreen-fallback'));
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ const app = {
|
||||
languages: [], translations: {}, locales: {},
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyNetworkTarget: 'all', historyNetworkShowJitter: true, historyNetwork: [], historyNetworkTargets: [], historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
|
||||
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
customChartSeries: [], savedCharts: [], customChartEditingId: null, customChartNameDraft: null, chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
haManualFallbackVisible: false, haSupervisorTestState: null, haEntityCatalog: [], haEntityCatalogConfigured: false, haEntityCatalogLoadedAt: 0, haEntityCatalogLoading: false,
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ function renderSimulationPage() {
|
||||
return `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax + bend} ${ay}, ${bx - bend} ${by}, ${bx} ${by}"/>`;
|
||||
};
|
||||
const verticalLink = (ax, ay, bx, by, cls = 'bus') => `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax} ${ay + 35}, ${bx} ${by - 35}, ${bx} ${by}"/>`;
|
||||
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="flow-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="flow-port in"></i><i class="flow-port out"></i></article>`;
|
||||
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="diagram-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="diagram-port in"></i><i class="diagram-port out"></i></article>`;
|
||||
|
||||
const globalHouseLeft = 305, globalNightLeft = 565, globalTop = 28;
|
||||
nodes.push(node({ left: globalHouseLeft, top: globalTop, kind: 'logic global', eyebrow: tr('simulation.globalInput'), title: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), meta: tr('simulation.strategy', { strategy: (plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint') }), badge: tr('simulation.house') }));
|
||||
|
||||
+1
-1
@@ -537,7 +537,7 @@ function zoneCard(zone, detailed = true) {
|
||||
const groupText = groupNames.length ? groupNames.join(' · ') : tr('zones.noGroup');
|
||||
const policy = zone.inherit_house_mode ? tr('zones.followHouse') : tr(zone.mode === 'heat' ? 'zones.heatOnly' : 'zones.coolOnly');
|
||||
return `<article class="list-card zone-config-card ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
|
||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
|
||||
<div class="zone-config-status">
|
||||
<div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span>${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div></div>
|
||||
<div class="zone-config-runtime"><span class="badge ${workingDemand ? 'active' : ''}">${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div>
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ document.addEventListener('click', async event => {
|
||||
if (hours && $('#historyHours')) $('#historyHours').value = hours;
|
||||
else if ($('#historyHours')) $('#historyHours').value = '24';
|
||||
const dialog = historyRoute.closest('dialog');
|
||||
closeChartPreview(dialog?.querySelector('.history-chart-card.chart-fullscreen-fallback'));
|
||||
closeChartPreview(dialog?.querySelector('.chart-card.chart-fullscreen-fallback'));
|
||||
if (dialog?.open) dialog.close();
|
||||
app.historyTab = 'overview';
|
||||
showView('history');
|
||||
|
||||
+5
-5
@@ -98,7 +98,7 @@ function renderFlows() {
|
||||
const host = $('#flowList'); if (!host) return;
|
||||
const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length });
|
||||
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled && !flow.draft ? '' : 'is-disabled'} ${flow.draft ? 'is-draft' : ''}" data-flow-card-id="${esc(flow.id)}" tabindex="0" aria-label="${esc(`${tr('flow.openEditor')}: ${flow.name}`)}">
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="zone-enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="zone-enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('flow.blocks'))}</small><strong>${flow.nodes?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.schedules'))}</small><strong>${flow.compiled_schedule_ids?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.automations'))}</small><strong>${flow.compiled_automation_ids?.length || 0}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
|
||||
@@ -426,7 +426,7 @@ function renderFlowBlockLibrary(filter = '') {
|
||||
const title = group.querySelector(':scope > span')?.textContent?.trim() || '';
|
||||
const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query));
|
||||
if (!buttons.length) return '';
|
||||
const categoryClass = [...group.classList].find(name => name.startsWith('flow-palette-') && name !== 'flow-palette-group') || '';
|
||||
const categoryClass = [...group.classList].find(name => name.startsWith('block-category-')) || '';
|
||||
return `<section class="flow-block-library-group ${esc(categoryClass)}"><h3>${esc(title)}</h3><div>${buttons.map(button => `<button type="button" class="secondary" data-flow-add="${esc(button.dataset.flowAdd)}">${esc(button.textContent.trim())}</button>`).join('')}</div></section>`;
|
||||
}).join('') || `<div class="empty compact"><strong>${esc(tr('flow.noBlocksFound'))}</strong><span>${esc(tr('flow.noBlocksFoundHint'))}</span></div>`;
|
||||
}
|
||||
@@ -443,11 +443,11 @@ function renderFlowEditor() {
|
||||
const nodesHost = $('#flowNodes');
|
||||
nodesHost.innerHTML = draft.nodes.map(node => {
|
||||
const meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' };
|
||||
return `<article class="flow-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
<button class="flow-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
|
||||
return `<article class="diagram-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
<button class="diagram-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
|
||||
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div>
|
||||
<div class="flow-node-body">${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `<div class="flow-node-current" data-flow-shared-current="${esc(node.config.input_id)}"><span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>—</strong></div>` : ''}</div>
|
||||
<button class="flow-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
|
||||
<button class="diagram-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
|
||||
</article>`;
|
||||
}).join('');
|
||||
$('#flowEmptyHint').hidden = draft.nodes.length > 0;
|
||||
|
||||
+3
-3
@@ -66,7 +66,7 @@ function showFormError(form, message) {
|
||||
summary.className = 'inline-alert error form-error-summary';
|
||||
summary.setAttribute('role', 'alert');
|
||||
summary.innerHTML = `<span>${esc(tr('devices.lastError'))}</span><strong>${esc(message)}</strong>`;
|
||||
const anchor = $('.settings-save-bar', form) || $('.form-actions', form);
|
||||
const anchor = $('.sticky-form-actions', form) || $('.form-actions', form);
|
||||
if (anchor?.parentElement) anchor.parentElement.insertBefore(summary, anchor);
|
||||
else form.appendChild(summary);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ function confirmDiscardForm(form) {
|
||||
function activeDirtySettingsForm() {
|
||||
const active = $('.view.active');
|
||||
if (!active) return null;
|
||||
const form = $('form.settings-form', active);
|
||||
const form = $('form.config-form', active);
|
||||
return form && isFormDirty(form) ? form : null;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ function requestDialogClose(dialog) {
|
||||
if (!dialog) return true;
|
||||
const form = $('form', dialog);
|
||||
if (form && !confirmDiscardForm(form)) return false;
|
||||
closeChartPreview(dialog.querySelector?.('.history-chart-card.chart-fullscreen-fallback'));
|
||||
closeChartPreview(dialog.querySelector?.('.chart-card.chart-fullscreen-fallback'));
|
||||
dialog.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
+38
-4
@@ -272,17 +272,31 @@ function persistSavedCharts() {
|
||||
localStorage.setItem('gree_controller_saved_charts', JSON.stringify(app.savedCharts.slice(0, 30)));
|
||||
}
|
||||
|
||||
function savedChartRangeLabel(hours) {
|
||||
const value = Number(hours || 24);
|
||||
if (value === 168) return tr('history.range7d');
|
||||
if (value === 720) return tr('history.range30d');
|
||||
if (value === 2160) return tr('history.range90d');
|
||||
if (value === 8760) return tr('history.range1y');
|
||||
return `${value} h`;
|
||||
}
|
||||
|
||||
function renderCustomBuilder() {
|
||||
const host = $('#historyCustomBuilder'); if (!host) return;
|
||||
if (app.historyTab !== 'custom') { host.innerHTML = ''; host.classList.remove('active'); return; }
|
||||
host.classList.add('active');
|
||||
const options = customSeriesOptions();
|
||||
const selected = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
|
||||
const editing = app.savedCharts.find(item => item.id === app.customChartEditingId) || null;
|
||||
const draftName = app.customChartNameDraft !== null ? app.customChartNameDraft : (editing?.name || '');
|
||||
const editNote = editing ? `<span class="custom-chart-edit-note">${esc(tr('history.editingChart'))}: <strong>${esc(editing.name)}</strong></span>` : '';
|
||||
const cancelEdit = editing ? `<button class="secondary" data-history-action="cancel-chart-edit">${esc(tr('actions.cancel'))}</button>` : '';
|
||||
host.innerHTML = `<div class="panel custom-chart-panel"><div class="chart-title-row"><div><h3>${esc(tr('history.customTitle'))}</h3><p>${esc(tr('history.customDescription'))}</p></div></div>
|
||||
<div class="custom-chart-add"><select id="customSeriesSelect">${options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('')}</select><button class="secondary" data-history-action="add-series">${esc(tr('history.addSeries'))}</button></div>
|
||||
<div class="custom-series-list">${selected.length ? selected.map((item, index) => `<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="${esc(tr('actions.remove'))}">${uiIcon('close')}</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
|
||||
<div class="custom-chart-save"><input id="customChartName" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(tr('actions.save'))}</button><button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||||
<div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">${uiIcon('close')}</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
|
||||
${editNote}
|
||||
<div class="custom-chart-save"><input id="customChartName" value="${esc(draftName)}" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(editing ? tr('history.saveChanges') : tr('actions.save'))}</button>${cancelEdit}<button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||||
<div class="saved-chart-section"><div class="saved-chart-heading"><strong>${esc(tr('history.savedCharts'))}</strong><small>${app.savedCharts.length}</small></div><div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row${item.id === app.customChartEditingId ? ' is-editing' : ''}"><button class="saved-chart-open" data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))} · ${esc(savedChartRangeLabel(item.hours))}</small></button><div class="saved-chart-actions"><button class="secondary" data-history-action="edit-chart" data-id="${esc(item.id)}" title="${esc(tr('actions.edit'))}" aria-label="${esc(tr('actions.edit'))}">${uiIcon('edit')}</button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}" title="${esc(tr('actions.delete'))}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -327,26 +341,46 @@ function publicCustomChartUrl(path) {
|
||||
async function handleHistoryAction(button) {
|
||||
const action = button.dataset.historyAction;
|
||||
if (action === 'add-series') {
|
||||
if ($('#customChartName')) app.customChartNameDraft = $('#customChartName').value;
|
||||
const value = $('#customSeriesSelect')?.value;
|
||||
if (value && !app.customChartSeries.includes(value)) app.customChartSeries.push(value);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||||
}
|
||||
if (action === 'remove-series') {
|
||||
if ($('#customChartName')) app.customChartNameDraft = $('#customChartName').value;
|
||||
app.customChartSeries.splice(Number(button.dataset.index), 1);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||||
}
|
||||
if (action === 'save-chart') {
|
||||
if (!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
|
||||
const name = $('#customChartName')?.value.trim() || tr('history.customChart');
|
||||
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours: $('#historyHours')?.value || '24' };
|
||||
app.savedCharts.unshift(item); persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
|
||||
const hours = $('#historyHours')?.value || '24';
|
||||
const editingIndex = app.savedCharts.findIndex(entry => entry.id === app.customChartEditingId);
|
||||
if (editingIndex >= 0) {
|
||||
app.savedCharts[editingIndex] = { ...app.savedCharts[editingIndex], name, series: [...app.customChartSeries], hours };
|
||||
app.customChartEditingId = null; app.customChartNameDraft = null;
|
||||
persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartUpdated')); return;
|
||||
}
|
||||
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours };
|
||||
app.savedCharts.unshift(item); app.customChartNameDraft = null; persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
|
||||
}
|
||||
if (action === 'load-chart') {
|
||||
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
|
||||
app.customChartEditingId = null; app.customChartNameDraft = item.name || '';
|
||||
app.customChartSeries = [...item.series]; if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); return;
|
||||
}
|
||||
if (action === 'edit-chart') {
|
||||
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
|
||||
app.customChartEditingId = item.id; app.customChartNameDraft = item.name || ''; app.customChartSeries = [...item.series];
|
||||
if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); $('#customChartName')?.focus(); return;
|
||||
}
|
||||
if (action === 'cancel-chart-edit') {
|
||||
app.customChartEditingId = null; app.customChartNameDraft = null; renderCustomHistory(); return;
|
||||
}
|
||||
if (action === 'delete-chart') {
|
||||
if (app.customChartEditingId === button.dataset.id) { app.customChartEditingId = null; app.customChartNameDraft = null; }
|
||||
app.savedCharts = app.savedCharts.filter(entry => entry.id !== button.dataset.id); persistSavedCharts(); renderCustomHistory(); return;
|
||||
}
|
||||
if (action === 'copy-chart-link') {
|
||||
|
||||
+170
-138
@@ -736,7 +736,7 @@ h3 {
|
||||
padding: 7px 11px;
|
||||
}
|
||||
|
||||
.zone-enable-toggle {
|
||||
.enable-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -749,13 +749,13 @@ h3 {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.zone-enable-toggle.active {
|
||||
.enable-toggle.active {
|
||||
border-color: color-mix(in srgb, var(--accent) 34%, var(--line));
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
||||
}
|
||||
|
||||
.zone-enable-toggle span {
|
||||
.enable-toggle span {
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -1639,16 +1639,16 @@ button.outside-pill:focus-visible {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog .history-chart-card {
|
||||
.outdoor-history-dialog .chart-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog .history-chart-card:not(.chart-fullscreen-fallback) .chart-wrap {
|
||||
.outdoor-history-dialog .chart-card:not(.chart-fullscreen-fallback) .chart-wrap {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog .history-chart-card:not(.chart-fullscreen-fallback) canvas {
|
||||
.outdoor-history-dialog .chart-card:not(.chart-fullscreen-fallback) canvas {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
@@ -2031,7 +2031,7 @@ button.outside-pill:focus-visible {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.history-chart-card {
|
||||
.chart-card {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@@ -2188,35 +2188,6 @@ button.outside-pill:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
.history-tabs {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 14px;
|
||||
padding: 3px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.history-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.history-tabs button {
|
||||
flex: 0 0 auto;
|
||||
padding: 9px 13px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.history-tabs button.active {
|
||||
color: var(--accent-text);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.history-toolbar-panel {
|
||||
margin-bottom: 14px;
|
||||
@@ -2309,23 +2280,49 @@ button.outside-pill:focus-visible {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.custom-chart-edit-note {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.custom-chart-edit-note strong { color: var(--text-soft); }
|
||||
|
||||
.saved-chart-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.saved-chart-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.saved-chart-heading small { color: var(--muted); }
|
||||
|
||||
.saved-chart-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding-top: 3px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.saved-chart-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.saved-chart-row>button:first-child {
|
||||
.saved-chart-row.is-editing .saved-chart-open {
|
||||
border-color: color-mix(in srgb, var(--accent) 65%, var(--line));
|
||||
}
|
||||
|
||||
.saved-chart-open {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -2344,7 +2341,12 @@ button.outside-pill:focus-visible {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.saved-chart-row>.danger {
|
||||
.saved-chart-actions {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.saved-chart-actions button {
|
||||
width: 42px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
@@ -2679,7 +2681,7 @@ button.outside-pill:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
.history-chart-card canvas {
|
||||
.chart-card canvas {
|
||||
width: 100%;
|
||||
min-width: 720px;
|
||||
}
|
||||
@@ -2960,7 +2962,7 @@ button.outside-pill:focus-visible {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
.config-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
@@ -3001,39 +3003,39 @@ button.outside-pill:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-block {
|
||||
.form-section {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.settings-block-head {
|
||||
.form-section-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.settings-block-head h3 {
|
||||
.form-section-head h3 {
|
||||
margin: 1px 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.settings-block-head p {
|
||||
.form-section-head p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.settings-grid .wide {
|
||||
.form-grid .wide {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.settings-save-bar {
|
||||
.sticky-form-actions {
|
||||
position: sticky;
|
||||
bottom: 18px;
|
||||
z-index: 9;
|
||||
@@ -3049,7 +3051,7 @@ button.outside-pill:focus-visible {
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.settings-save-bar span {
|
||||
.sticky-form-actions span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -3240,7 +3242,7 @@ button.outside-pill:focus-visible {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.flow-node {
|
||||
.diagram-node {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
display: grid;
|
||||
@@ -3253,31 +3255,31 @@ button.outside-pill:focus-visible {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.flow-node.input {
|
||||
.diagram-node.input {
|
||||
border-left: 3px solid var(--teal);
|
||||
}
|
||||
|
||||
.flow-node.logic {
|
||||
.diagram-node.logic {
|
||||
border-left: 3px solid var(--purple);
|
||||
}
|
||||
|
||||
.flow-node.action {
|
||||
.diagram-node.action {
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.flow-node.event {
|
||||
.diagram-node.event {
|
||||
border-left: 3px solid var(--info);
|
||||
}
|
||||
|
||||
.flow-node.decision.demand {
|
||||
.diagram-node.decision.demand {
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent), 0 8px 24px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.flow-node.global {
|
||||
.diagram-node.global {
|
||||
min-height: 106px;
|
||||
}
|
||||
|
||||
.flow-node.night-active {
|
||||
.diagram-node.night-active {
|
||||
border-color: color-mix(in srgb, var(--info) 45%, var(--line-strong));
|
||||
}
|
||||
|
||||
@@ -3293,7 +3295,7 @@ button.outside-pill:focus-visible {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.flow-node h3 {
|
||||
.diagram-node h3 {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
@@ -3302,13 +3304,13 @@ button.outside-pill:focus-visible {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flow-node>strong {
|
||||
.diagram-node>strong {
|
||||
color: var(--text);
|
||||
font-size: 17px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.flow-node p {
|
||||
.diagram-node p {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
@@ -3336,7 +3338,7 @@ button.outside-pill:focus-visible {
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
||||
}
|
||||
|
||||
.flow-port {
|
||||
.diagram-port {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 9px;
|
||||
@@ -3347,27 +3349,27 @@ button.outside-pill:focus-visible {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.flow-port.in {
|
||||
.diagram-port.in {
|
||||
left: -6px;
|
||||
}
|
||||
|
||||
.flow-port.out {
|
||||
.diagram-port.out {
|
||||
right: -6px;
|
||||
}
|
||||
|
||||
.flow-node.input .flow-port {
|
||||
.diagram-node.input .diagram-port {
|
||||
background: var(--teal);
|
||||
}
|
||||
|
||||
.flow-node.logic .flow-port {
|
||||
.diagram-node.logic .diagram-port {
|
||||
background: var(--purple);
|
||||
}
|
||||
|
||||
.flow-node.action .flow-port {
|
||||
.diagram-node.action .diagram-port {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.flow-node.event .flow-port {
|
||||
.diagram-node.event .diagram-port {
|
||||
background: var(--info);
|
||||
}
|
||||
|
||||
@@ -3384,15 +3386,15 @@ button.outside-pill:focus-visible {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.settings-grid .wide {
|
||||
.form-grid .wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.settings-save-bar {
|
||||
.sticky-form-actions {
|
||||
bottom: 76px;
|
||||
}
|
||||
|
||||
@@ -3416,7 +3418,7 @@ button.outside-pill:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
.settings-block-head {
|
||||
.form-section-head {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@@ -3480,7 +3482,7 @@ button.outside-pill:focus-visible {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
}
|
||||
|
||||
.standalone-settings-form {
|
||||
.full-width-form {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
@@ -4135,7 +4137,7 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
}
|
||||
|
||||
|
||||
.zone-config-card .zone-enable-toggle {
|
||||
.zone-config-card .enable-toggle {
|
||||
min-height: 28px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
@@ -4588,7 +4590,7 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.quick-thermostat-control .zone-enable-toggle {
|
||||
.quick-thermostat-control .enable-toggle {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -5326,7 +5328,7 @@ textarea[aria-invalid="true"] {
|
||||
}
|
||||
|
||||
|
||||
.settings-form>.form-error-summary {
|
||||
.config-form>.form-error-summary {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@@ -5334,7 +5336,7 @@ textarea[aria-invalid="true"] {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.settings-form.is-saving,
|
||||
.config-form.is-saving,
|
||||
.dialog-form.is-saving {
|
||||
cursor: progress;
|
||||
}
|
||||
@@ -5417,7 +5419,7 @@ textarea[aria-invalid="true"] {
|
||||
}
|
||||
|
||||
@media (hover:hover) and (pointer:fine) {
|
||||
.history-chart-card .chart-wrap canvas {
|
||||
.chart-card .chart-wrap canvas {
|
||||
cursor: crosshair;
|
||||
}
|
||||
}
|
||||
@@ -5492,7 +5494,7 @@ textarea[aria-invalid="true"] {
|
||||
}
|
||||
}
|
||||
|
||||
.settings-form.has-unsaved-changes .settings-save-bar {
|
||||
.config-form.has-unsaved-changes .sticky-form-actions {
|
||||
border-color: color-mix(in srgb, var(--accent) 48%, var(--line-strong));
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, .18), 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
@@ -6172,7 +6174,7 @@ textarea[aria-invalid="true"] {
|
||||
stroke-width: 4;
|
||||
}
|
||||
|
||||
.flow-editor .flow-node {
|
||||
.flow-editor .diagram-node {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: block;
|
||||
@@ -6188,7 +6190,7 @@ textarea[aria-invalid="true"] {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.flow-editor .flow-node.selected {
|
||||
.flow-editor .diagram-node.selected {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-soft), 0 12px 30px rgba(0, 0, 0, .22);
|
||||
}
|
||||
@@ -6236,7 +6238,7 @@ textarea[aria-invalid="true"] {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.flow-editor .flow-port {
|
||||
.flow-editor .diagram-port {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
border-radius: 50%;
|
||||
@@ -6247,8 +6249,8 @@ textarea[aria-invalid="true"] {
|
||||
}
|
||||
|
||||
|
||||
.flow-editor .flow-port:hover,
|
||||
.flow-editor .flow-port.armed {
|
||||
.flow-editor .diagram-port:hover,
|
||||
.flow-editor .diagram-port.armed {
|
||||
background: var(--accent);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
@@ -7230,31 +7232,31 @@ textarea[aria-invalid="true"] {
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.flow-palette-trigger {
|
||||
.block-category-trigger {
|
||||
border-left-color: var(--warning);
|
||||
}
|
||||
|
||||
.flow-palette-time {
|
||||
.block-category-time {
|
||||
border-left-color: var(--blue);
|
||||
}
|
||||
|
||||
.flow-palette-timeop {
|
||||
.block-category-timeop {
|
||||
border-left-color: var(--info);
|
||||
}
|
||||
|
||||
.flow-palette-sensor {
|
||||
.block-category-sensor {
|
||||
border-left-color: var(--teal);
|
||||
}
|
||||
|
||||
.flow-palette-logic {
|
||||
.block-category-logic {
|
||||
border-left-color: var(--purple);
|
||||
}
|
||||
|
||||
.flow-palette-action {
|
||||
.block-category-action {
|
||||
border-left-color: var(--orange);
|
||||
}
|
||||
|
||||
.flow-palette-haaction {
|
||||
.block-category-haaction {
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -7380,7 +7382,7 @@ button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.automation-tabs {
|
||||
.segmented-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
@@ -7395,11 +7397,11 @@ button {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.automation-tabs::-webkit-scrollbar {
|
||||
.segmented-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.automation-tabs button {
|
||||
.segmented-tabs button {
|
||||
flex: 0 0 auto;
|
||||
min-height: 38px;
|
||||
padding: 8px 12px;
|
||||
@@ -7409,7 +7411,7 @@ button {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.automation-tabs button.active {
|
||||
.segmented-tabs button.active {
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||
@@ -7614,31 +7616,31 @@ button {
|
||||
border-left: 3px solid var(--line-strong);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-trigger {
|
||||
.flow-block-library-group.block-category-trigger {
|
||||
border-left-color: var(--warning);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-time {
|
||||
.flow-block-library-group.block-category-time {
|
||||
border-left-color: var(--blue);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-timeop {
|
||||
.flow-block-library-group.block-category-timeop {
|
||||
border-left-color: var(--info);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-sensor {
|
||||
.flow-block-library-group.block-category-sensor {
|
||||
border-left-color: var(--teal);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-logic {
|
||||
.flow-block-library-group.block-category-logic {
|
||||
border-left-color: var(--purple);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-action {
|
||||
.flow-block-library-group.block-category-action {
|
||||
border-left-color: var(--orange);
|
||||
}
|
||||
|
||||
.flow-block-library-group.flow-palette-haaction {
|
||||
.flow-block-library-group.block-category-haaction {
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -7661,7 +7663,7 @@ button {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.flow-editor .flow-port {
|
||||
.flow-editor .diagram-port {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
@@ -7675,7 +7677,7 @@ button {
|
||||
right: -9px;
|
||||
}
|
||||
|
||||
.flow-editor .flow-port::after {
|
||||
.flow-editor .diagram-port::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -11px;
|
||||
@@ -7822,12 +7824,12 @@ button {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.automation-tabs {
|
||||
.segmented-tabs {
|
||||
margin-bottom: 12px;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.automation-tabs button {
|
||||
.segmented-tabs button {
|
||||
flex: 1 0 auto;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
@@ -7917,17 +7919,6 @@ button {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.history-tabs {
|
||||
margin-inline: -12px;
|
||||
padding-inline: 12px;
|
||||
scroll-padding-inline: 12px;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.history-tabs button {
|
||||
min-height: 40px;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.history-toolbar-panel {
|
||||
padding: 12px;
|
||||
@@ -7942,17 +7933,17 @@ button {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.settings-save-bar {
|
||||
.sticky-form-actions {
|
||||
bottom: calc(74px + env(safe-area-inset-bottom));
|
||||
margin-inline: -4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.settings-save-bar span {
|
||||
.sticky-form-actions span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-save-bar button {
|
||||
.sticky-form-actions button {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
@@ -8332,15 +8323,15 @@ button {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.settings-block {
|
||||
.form-section {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.settings-block-head h3 {
|
||||
.form-section-head h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.settings-block-head p,
|
||||
.form-section-head p,
|
||||
.field-note {
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -8531,7 +8522,7 @@ body.flow-editor-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.history-chart-card.chart-fullscreen-fallback {
|
||||
.chart-card.chart-fullscreen-fallback {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 140;
|
||||
@@ -8551,16 +8542,16 @@ body.flow-editor-open {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row {
|
||||
.chart-card.chart-fullscreen-fallback .chart-title-row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-title-row p,
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-hover-hint {
|
||||
.chart-card.chart-fullscreen-fallback .chart-title-row p,
|
||||
.chart-card.chart-fullscreen-fallback .chart-hover-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.history-chart-card.chart-fullscreen-fallback .chart-wrap {
|
||||
.chart-card.chart-fullscreen-fallback .chart-wrap {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
@@ -8569,7 +8560,7 @@ body.flow-editor-open {
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.history-chart-card.chart-fullscreen-fallback .legend {
|
||||
.chart-card.chart-fullscreen-fallback .legend {
|
||||
max-height: 88px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
@@ -8611,7 +8602,7 @@ body.flow-editor-open {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.automation-tabs {
|
||||
.segmented-tabs {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -9981,35 +9972,73 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
|
||||
/* Standalone public custom chart */
|
||||
.public-custom-chart-page {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.public-custom-chart-shell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.public-custom-chart-card {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 32px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.public-chart-title-row {
|
||||
flex: 0 0 auto;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.public-chart-heading { min-width: 0; }
|
||||
|
||||
.public-chart-range-control {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.public-chart-range-control select { min-width: 126px; }
|
||||
|
||||
.public-custom-chart-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-height: 360px;
|
||||
flex: 1 1 0;
|
||||
min-height: 180px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#publicCustomChart {
|
||||
width: 100%;
|
||||
min-width: 720px;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.public-custom-chart-card .legend {
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.public-custom-chart-card .legend::-webkit-scrollbar { display: none; }
|
||||
|
||||
.public-chart-legend-item {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -10028,5 +10057,8 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.public-custom-chart-shell { padding: 8px; }
|
||||
.public-custom-chart-card { min-height: calc(100vh - 16px); padding: 14px; }
|
||||
.public-custom-chart-card { padding: 12px; }
|
||||
.public-chart-title-row { align-items: center; }
|
||||
.public-chart-range-control select { min-width: 108px; }
|
||||
.public-custom-chart-wrap { min-height: 140px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user