v0.12.1
This commit is contained in:
+20
-14
@@ -280,10 +280,10 @@
|
||||
</select></label><label><span data-i18n="simulation.show">Show</span><select
|
||||
id="simulationTarget"></select></label></div>
|
||||
<div class="simulation-summary-grid" id="simulationSummary"></div>
|
||||
<div class="panel simulation-flow-shell">
|
||||
<div class="simulation-flow-head">
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationFlowTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.flowEyebrow">Live flow</span>
|
||||
<h2 data-i18n="simulation.flowTitle">Automation flow board</h2>
|
||||
<h2 id="simulationFlowTitle" data-i18n="simulation.flowTitle">Automation flow board</h2>
|
||||
</div>
|
||||
<div class="flow-legend"><span><i class="flow-legend-dot input"></i><b
|
||||
data-i18n="simulation.legendInput">Input</b></span><span><i class="flow-legend-dot logic"></i><b
|
||||
@@ -293,21 +293,27 @@
|
||||
<div class="simulation-flow-scroll">
|
||||
<div class="simulation-flow-board" id="simulationFlowBoard"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel simulation-timeline-panel">
|
||||
<div class="simulation-section-head">
|
||||
</section>
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationTimelineTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.timelineEyebrow">Timeline</span>
|
||||
<h2 data-i18n="simulation.timelineTitle">What will happen next</h2>
|
||||
<h2 id="simulationTimelineTitle" data-i18n="simulation.timelineTitle">What will happen next</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-timeline" id="simulationTimeline"></div>
|
||||
</div>
|
||||
<div class="simulation-section-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.rulesEyebrow">Rules</span>
|
||||
<h2 data-i18n="simulation.rulesTitle">Automation rules</h2>
|
||||
<div class="simulation-panel-body">
|
||||
<div class="simulation-timeline" id="simulationTimeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-rules-grid" id="simulationRules"></div>
|
||||
</section>
|
||||
<section class="panel simulation-section-panel" aria-labelledby="simulationRulesTitle">
|
||||
<div class="simulation-panel-head">
|
||||
<div><span class="eyebrow" data-i18n="simulation.rulesEyebrow">Rules</span>
|
||||
<h2 id="simulationRulesTitle" data-i18n="simulation.rulesTitle">Automation rules</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simulation-panel-body">
|
||||
<div class="simulation-rules-grid" id="simulationRules"></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="history">
|
||||
|
||||
+30
-106
@@ -151,6 +151,9 @@ function updateChartFullscreenButton(card) {
|
||||
function closeChartPreview(card) {
|
||||
if (!card) return;
|
||||
card.classList.remove('chart-fullscreen-fallback');
|
||||
card.removeAttribute('role');
|
||||
card.removeAttribute('aria-modal');
|
||||
card.removeAttribute('aria-label');
|
||||
document.body.classList.remove('chart-fullscreen-open');
|
||||
updateChartFullscreenButton(card);
|
||||
const canvas = card.querySelector('canvas[id]');
|
||||
@@ -158,6 +161,7 @@ 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');
|
||||
if (!card) return;
|
||||
@@ -172,12 +176,10 @@ function toggleChartFullscreen(id) {
|
||||
}
|
||||
|
||||
card.classList.add('chart-fullscreen-fallback');
|
||||
card.setAttribute('role', 'dialog');
|
||||
card.setAttribute('aria-modal', 'true');
|
||||
card.setAttribute('aria-label', card.querySelector('h3')?.textContent || tr('history.fullscreen'));
|
||||
document.body.classList.add('chart-fullscreen-open');
|
||||
if (window.matchMedia('(max-width: 760px)').matches) {
|
||||
app.chartZooms[id] = 1;
|
||||
canvas.dataset.chartZoom = '1';
|
||||
if (canvas.parentElement) canvas.parentElement.scrollLeft = 0;
|
||||
}
|
||||
updateChartFullscreenButton(card);
|
||||
requestAnimationFrame(() => redrawHistoryChart(id));
|
||||
}
|
||||
@@ -186,15 +188,12 @@ function prepareCanvas(canvas, height) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrap = canvas.parentElement;
|
||||
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
|
||||
const card = canvas.closest('.history-chart-card');
|
||||
const fullscreen = chartCardIsFullscreen(card);
|
||||
const mobileFullscreen = fullscreen && window.matchMedia('(max-width: 760px)').matches;
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card'));
|
||||
const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0;
|
||||
const naturalMobileHeight = Math.round(clamp(wrapWidth * .72, 260, 360));
|
||||
const actualHeight = mobileFullscreen ? Math.min(wrapHeight || naturalMobileHeight, naturalMobileHeight) : (wrapHeight || height);
|
||||
const actualHeight = wrapHeight || height;
|
||||
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM);
|
||||
// Normal charts may scroll horizontally. Fullscreen 100% always fits the viewport width.
|
||||
const baseWidth = fullscreen ? Math.max(1, wrapWidth) : Math.max(720, wrapWidth);
|
||||
// Keep the original chart geometry on narrow screens and let the wrapper scroll horizontally.
|
||||
const baseWidth = Math.max(720, wrapWidth);
|
||||
const width = Math.max(1, Math.floor(baseWidth * zoom));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr; canvas.height = actualHeight * dpr;
|
||||
@@ -262,10 +261,7 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
.filter(point => Number.isFinite(point.ts) && Number.isFinite(point.value))
|
||||
.sort((a, b) => a.ts - b.ts)
|
||||
}));
|
||||
const touches = new Map();
|
||||
let drag = null;
|
||||
let pinch = null;
|
||||
let touchPan = null;
|
||||
|
||||
const hide = () => { tooltip.hidden = true; line.hidden = true; };
|
||||
const hideSelection = () => { if (selection) selection.hidden = true; };
|
||||
@@ -323,114 +319,42 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
};
|
||||
|
||||
canvas.onpointerdown = event => {
|
||||
if (event.pointerType === 'mouse') {
|
||||
if (event.button !== 0) return;
|
||||
const startX = canvasX(event.clientX);
|
||||
drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false };
|
||||
hide(); hideSelection();
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch') {
|
||||
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
if (touches.size === 1) {
|
||||
touchPan = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, scrollLeft: wrap.scrollLeft, moved: false };
|
||||
showFromClientX(event.clientX);
|
||||
}
|
||||
if (touches.size === 2) {
|
||||
touchPan = null;
|
||||
const points = [...touches.values()];
|
||||
const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
|
||||
const midX = (points[0].x + points[1].x) / 2;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrapRect = wrap.getBoundingClientRect();
|
||||
pinch = {
|
||||
startDistance: Math.max(1, distance),
|
||||
startZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
baseWidth: (rect.width || width) / clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
anchorRatio: clamp((midX - rect.left) / Math.max(1, rect.width), 0, 1),
|
||||
viewportX: clamp(midX - wrapRect.left, 0, wrap.clientWidth),
|
||||
previewZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
};
|
||||
hide();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
||||
const startX = canvasX(event.clientX);
|
||||
drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false };
|
||||
hide(); hideSelection();
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
canvas.onpointermove = event => {
|
||||
if (event.pointerType === 'touch') return;
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) {
|
||||
updateSelection(canvasX(event.clientX));
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch' && touches.has(event.pointerId)) {
|
||||
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
if (pinch && touches.size >= 2) {
|
||||
const points = [...touches.values()].slice(0, 2);
|
||||
const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
|
||||
pinch.previewZoom = clamp(pinch.startZoom * (distance / pinch.startDistance), 1, MAX_CHART_ZOOM);
|
||||
const previewWidth = pinch.baseWidth * pinch.previewZoom;
|
||||
canvas.style.width = `${previewWidth}px`;
|
||||
wrap.scrollLeft = Math.max(0, pinch.anchorRatio * previewWidth - pinch.viewportX);
|
||||
const reset = canvas.closest('.history-chart-card')?.querySelector('[data-chart-zoom="reset"]');
|
||||
if (reset) reset.textContent = `${Math.round(pinch.previewZoom * 100)}%`;
|
||||
event.preventDefault();
|
||||
} else if (touchPan?.pointerId === event.pointerId && touches.size === 1) {
|
||||
const dx = event.clientX - touchPan.startX;
|
||||
const dy = event.clientY - touchPan.startY;
|
||||
if (Math.abs(dx) > 6 && Math.abs(dx) > Math.abs(dy)) {
|
||||
touchPan.moved = true;
|
||||
hide();
|
||||
wrap.scrollLeft = Math.max(0, touchPan.scrollLeft - dx);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
showFromClientX(event.clientX);
|
||||
};
|
||||
|
||||
const finishPointer = event => {
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) {
|
||||
const currentX = canvasX(event.clientX);
|
||||
const selectedWidth = Math.abs(currentX - drag.startX);
|
||||
const center = (currentX + drag.startX) / 2;
|
||||
const moved = drag.moved && selectedWidth >= 12;
|
||||
drag = null; hideSelection();
|
||||
try { canvas.releasePointerCapture?.(event.pointerId); } catch (_) { }
|
||||
if (moved) {
|
||||
const currentZoom = clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth));
|
||||
setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2);
|
||||
} else showFromClientX(event.clientX);
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch') {
|
||||
const wasPan = touchPan?.pointerId === event.pointerId ? touchPan : null;
|
||||
touches.delete(event.pointerId);
|
||||
if (pinch && touches.size < 2) {
|
||||
const completed = pinch;
|
||||
pinch = null;
|
||||
setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX);
|
||||
} else if (wasPan && !wasPan.moved) showFromClientX(event.clientX);
|
||||
if (touches.size === 0) touchPan = null;
|
||||
}
|
||||
if (event.pointerType !== 'mouse' || drag?.pointerId !== event.pointerId) return;
|
||||
const currentX = canvasX(event.clientX);
|
||||
const selectedWidth = Math.abs(currentX - drag.startX);
|
||||
const center = (currentX + drag.startX) / 2;
|
||||
const moved = drag.moved && selectedWidth >= 12;
|
||||
drag = null; hideSelection();
|
||||
try { canvas.releasePointerCapture?.(event.pointerId); } catch (_) { }
|
||||
if (moved) {
|
||||
const currentZoom = clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth));
|
||||
setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2);
|
||||
} else showFromClientX(event.clientX);
|
||||
};
|
||||
|
||||
canvas.onpointerup = finishPointer;
|
||||
canvas.onpointercancel = event => {
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) { drag = null; hideSelection(); }
|
||||
if (event.pointerType === 'touch') {
|
||||
touches.delete(event.pointerId);
|
||||
if (touchPan?.pointerId === event.pointerId) touchPan = null;
|
||||
if (pinch && touches.size < 2) {
|
||||
const completed = pinch;
|
||||
pinch = null;
|
||||
setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX);
|
||||
} else hide();
|
||||
}
|
||||
};
|
||||
canvas.onpointerleave = event => { if (event.pointerType !== 'touch' && !drag) hide(); };
|
||||
canvas.onkeydown = event => {
|
||||
|
||||
@@ -307,11 +307,6 @@ function setTheme(theme) {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function cycleTheme() {
|
||||
const modes = ['system', 'light', 'dark'];
|
||||
const index = modes.indexOf(app.theme);
|
||||
setTheme(modes[(index + 1) % modes.length]);
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
|
||||
+1
-1
@@ -406,7 +406,7 @@ function renderSimulationPage() {
|
||||
}).join('') : `<div class="empty">${esc(tr('plan.noEvents'))}</div>`;
|
||||
|
||||
const activeRules = (plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones));
|
||||
rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="panel simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`;
|
||||
rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`;
|
||||
}
|
||||
|
||||
function scheduleControlPlanLoad() {
|
||||
|
||||
@@ -10,9 +10,6 @@ function deviceFeaturePanel(device) {
|
||||
return `<div class="device-capability-panel"><small>${esc(tr('devices.features'))}</small><div class="device-capability-buttons">${features.map(([field, , label]) => `<button class="${device[field] ? 'active' : ''}" data-action="toggle" data-field="${field}" data-device="${esc(device.id)}">${esc(label)}</button>`).join('')}</div></div>`;
|
||||
}
|
||||
|
||||
function deviceZoneDisabled(deviceId) {
|
||||
return app.zones.some(zone => zone.device_id === deviceId && zone.enabled === false);
|
||||
}
|
||||
|
||||
function zoneForDevice(deviceId) {
|
||||
return app.zones.find(zone => zone.device_id === deviceId) || null;
|
||||
|
||||
+106
-637
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user