v0.14.8
This commit is contained in:
@@ -1649,6 +1649,26 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="outdoorHistoryDialog" class="dialog-wide outdoor-history-dialog">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="history.24h">24 hours</span><h2 data-i18n="history.allOutdoor">Outdoor temperatures</h2></div>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="outdoor-history-current"><small data-i18n="house.outdoor">Outdoor</small><strong id="outdoorHistoryCurrent">--</strong></div>
|
||||
<div id="outdoorHistoryChartHost"></div>
|
||||
<div class="outdoor-history-links" data-i18n-aria="house.outdoorHistoryMore" aria-label="More metrics and ranges in history">
|
||||
<span data-i18n="house.outdoorHistoryMore">More in history</span>
|
||||
<div>
|
||||
<a href="/history/overview?hours=168" data-history-route data-history-hours="168" data-i18n="history.range7d">7 days</a>
|
||||
<a href="/history/overview?hours=720" data-history-route data-history-hours="720" data-i18n="history.range30d">30 days</a>
|
||||
<a href="/history/overview?hours=8760" data-history-route data-history-hours="8760" data-i18n="history.range1y">1 year</a>
|
||||
<a href="/history/overview" data-history-route class="outdoor-history-all" data-i18n="house.outdoorHistoryFull">Full history</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="tokenDialog" class="auth-dialog">
|
||||
<form method="dialog" id="tokenForm" class="dialog-form">
|
||||
<div class="brand large">
|
||||
|
||||
+70
-5
@@ -66,29 +66,36 @@ function resetHistoryRangeControl(host) {
|
||||
if (range && toolbar && range.parentElement === host) toolbar.insertBefore(range, refresh);
|
||||
}
|
||||
|
||||
function configureHistoryRangeControl(tab, host) {
|
||||
function configureHistoryRangeControl(tab, host, hasEnergyTargets = true) {
|
||||
const range = $('#historyRangeControl');
|
||||
if (!range) return;
|
||||
const toolbar = range.closest('.chart-toolbar') || host?.closest('.chart-toolbar');
|
||||
const toolbarPanel = toolbar?.closest('.history-toolbar-panel');
|
||||
const refresh = $('#historyRefresh');
|
||||
const label = range.querySelector('span');
|
||||
const hint = $('#historyRangeHint');
|
||||
const sixHours = range.querySelector('option[value="6"]');
|
||||
const energy = tab === 'energy';
|
||||
const energyUnavailable = energy && !hasEnergyTargets;
|
||||
const rangeLabel = tr(energy ? 'energy.period' : 'history.range');
|
||||
if (label) label.textContent = rangeLabel;
|
||||
range.setAttribute('aria-label', rangeLabel);
|
||||
if (hint) { hint.textContent = energy ? tr('energy.periodHint') : ''; hint.hidden = !energy; }
|
||||
if (sixHours) { sixHours.hidden = energy; sixHours.disabled = energy; }
|
||||
if (energy && $('#historyHours')?.value === '6') $('#historyHours').value = '24';
|
||||
range.hidden = energyUnavailable;
|
||||
if (refresh) refresh.hidden = energyUnavailable;
|
||||
if (toolbarPanel) toolbarPanel.hidden = energyUnavailable;
|
||||
range.classList.toggle('energy-period-control', energy);
|
||||
if (toolbar) toolbar.classList.toggle('energy-toolbar', energy);
|
||||
if (energy && host) host.appendChild(range);
|
||||
if (energy && host && hasEnergyTargets) host.appendChild(range);
|
||||
}
|
||||
|
||||
function renderHistoryNavigation() {
|
||||
$$('#historyTabs [data-history-tab]').forEach(button => button.classList.toggle('active', button.dataset.historyTab === app.historyTab));
|
||||
const energyPickerWasOpen = $('#historyEnergyTargetPicker')?.open === true;
|
||||
const host = $('#historyContextControls'); if (!host) return;
|
||||
let hasEnergyTargets = true;
|
||||
resetHistoryRangeControl(host);
|
||||
host.classList.toggle('energy-context-controls', app.historyTab === 'energy');
|
||||
const options = historyEntityOptions();
|
||||
@@ -100,10 +107,11 @@ function renderHistoryNavigation() {
|
||||
const select = $('#historyDeviceSelect'); if ([...select.options].some(option => option.value === app.historyDevice)) select.value = app.historyDevice;
|
||||
} else if (app.historyTab === 'energy') {
|
||||
const targets = normalizeEnergyHistoryTargets();
|
||||
hasEnergyTargets = targets.length > 0;
|
||||
const targetOptions = targets.map(target => `<label class="history-energy-option"><input type="checkbox" data-history-energy-target="${esc(target.id)}" ${app.historyEnergyTargets.includes(target.id) ? 'checked' : ''}><span>${esc(target.label)}</span></label>`).join('');
|
||||
host.innerHTML = targets.length
|
||||
? `<div class="history-energy-targets"><span class="history-control-label">${esc(tr('energy.targets'))}</span><details class="history-energy-picker" id="historyEnergyTargetPicker"><summary><span>${esc(energyTargetPickerSummary(targets))}</span><b>${app.historyEnergyTargets.length}/8</b></summary><div class="history-energy-options">${targetOptions}</div></details><small>${esc(tr('energy.multiselectHint'))}</small></div><label><span>${esc(tr('history.bucket'))}</span><select id="historyEnergyInterval"><option value="hourly">${esc(tr('energy.hourly'))}</option><option value="daily">${esc(tr('energy.daily'))}</option><option value="weekly">${esc(tr('energy.weekly'))}</option><option value="monthly">${esc(tr('energy.monthly'))}</option></select></label><label><span>${esc(tr('energy.compare'))}</span><select id="historyEnergyCompare"><option value="none">${esc(tr('energy.compareNone'))}</option><option value="previous_day">${esc(tr('energy.comparePreviousDay'))}</option><option value="previous_period">${esc(tr('energy.comparePreviousPeriod'))}</option><option value="previous_year">${esc(tr('energy.comparePreviousYear'))}</option></select></label>`
|
||||
: `<span class="history-context-hint">${esc(tr('energy.noData'))}</span>`;
|
||||
: '';
|
||||
const intervalSelect = $('#historyEnergyInterval'); if (intervalSelect) intervalSelect.value = app.historyEnergyInterval;
|
||||
const compareSelect = $('#historyEnergyCompare'); if (compareSelect) compareSelect.value = app.historyEnergyCompare;
|
||||
if (energyPickerWasOpen && $('#historyEnergyTargetPicker')) $('#historyEnergyTargetPicker').open = true;
|
||||
@@ -121,7 +129,7 @@ function renderHistoryNavigation() {
|
||||
} else {
|
||||
host.innerHTML = `<span class="history-context-hint">${esc(tr('history.overviewHint'))}</span>`;
|
||||
}
|
||||
configureHistoryRangeControl(app.historyTab, host);
|
||||
configureHistoryRangeControl(app.historyTab, host, hasEnergyTargets);
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
@@ -164,6 +172,7 @@ async function loadHistory() {
|
||||
const HISTORY_COLORS = ['--accent', '--info', '--warning', '--purple', '--danger', '--teal', '--orange', '--blue'];
|
||||
const MAX_CHART_ZOOM = 16;
|
||||
const chartRuntime = new Map();
|
||||
const chartFullscreenState = new WeakMap();
|
||||
|
||||
function chartSeriesKey(item, index) {
|
||||
return item.key || `${index}:${item.label}`;
|
||||
@@ -251,6 +260,28 @@ function updateChartFullscreenButton(card) {
|
||||
button.setAttribute('aria-label', button.title);
|
||||
}
|
||||
|
||||
function restoreChartPreviewLayout(card) {
|
||||
const state = chartFullscreenState.get(card);
|
||||
if (!state) return null;
|
||||
const canvas = card.querySelector('canvas[id]');
|
||||
const wrap = canvas?.parentElement;
|
||||
if (canvas) {
|
||||
if (state.canvasStyleWidth) canvas.style.width = state.canvasStyleWidth; else canvas.style.removeProperty('width');
|
||||
if (state.canvasStyleHeight) canvas.style.height = state.canvasStyleHeight; else canvas.style.removeProperty('height');
|
||||
canvas.width = state.canvasWidth;
|
||||
canvas.height = state.canvasHeight;
|
||||
}
|
||||
if (wrap) {
|
||||
if (state.wrapStyleHeight) wrap.style.height = state.wrapStyleHeight; else wrap.style.removeProperty('height');
|
||||
if (state.wrapStyleMinHeight) wrap.style.minHeight = state.wrapStyleMinHeight; else wrap.style.removeProperty('min-height');
|
||||
if (state.wrapStyleMaxHeight) wrap.style.maxHeight = state.wrapStyleMaxHeight; else wrap.style.removeProperty('max-height');
|
||||
wrap.scrollLeft = state.scrollLeft;
|
||||
wrap.scrollTop = state.scrollTop;
|
||||
}
|
||||
chartFullscreenState.delete(card);
|
||||
return state;
|
||||
}
|
||||
|
||||
function closeChartPreview(card) {
|
||||
if (!card) return;
|
||||
card.classList.remove('chart-fullscreen-fallback');
|
||||
@@ -258,9 +289,31 @@ function closeChartPreview(card) {
|
||||
card.removeAttribute('aria-modal');
|
||||
card.removeAttribute('aria-label');
|
||||
document.body.classList.remove('chart-fullscreen-open');
|
||||
const state = restoreChartPreviewLayout(card);
|
||||
updateChartFullscreenButton(card);
|
||||
const canvas = card.querySelector('canvas[id]');
|
||||
if (canvas?.id) requestAnimationFrame(() => redrawHistoryChart(canvas.id));
|
||||
if (!canvas?.id) return;
|
||||
|
||||
// Force layout after dropping the fullscreen class before sizing the canvas again.
|
||||
// This prevents a fullscreen-sized canvas from keeping a dialog artificially tall.
|
||||
card.getBoundingClientRect();
|
||||
requestAnimationFrame(() => {
|
||||
redrawHistoryChart(canvas.id);
|
||||
const wrap = canvas.parentElement;
|
||||
if (wrap && state) {
|
||||
wrap.scrollLeft = state.scrollLeft;
|
||||
wrap.scrollTop = state.scrollTop;
|
||||
}
|
||||
const dialog = card.closest('dialog');
|
||||
if (dialog?.open) {
|
||||
dialog.style.removeProperty('height');
|
||||
dialog.style.removeProperty('max-height');
|
||||
requestAnimationFrame(() => {
|
||||
dialog.style.removeProperty('height');
|
||||
dialog.style.removeProperty('max-height');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleChartFullscreen(id) {
|
||||
@@ -278,6 +331,18 @@ function toggleChartFullscreen(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = canvas.parentElement;
|
||||
chartFullscreenState.set(card, {
|
||||
canvasStyleWidth: canvas.style.width,
|
||||
canvasStyleHeight: canvas.style.height,
|
||||
canvasWidth: canvas.width,
|
||||
canvasHeight: canvas.height,
|
||||
wrapStyleHeight: wrap?.style.height || '',
|
||||
wrapStyleMinHeight: wrap?.style.minHeight || '',
|
||||
wrapStyleMaxHeight: wrap?.style.maxHeight || '',
|
||||
scrollLeft: wrap?.scrollLeft || 0,
|
||||
scrollTop: wrap?.scrollTop || 0,
|
||||
});
|
||||
card.classList.add('chart-fullscreen-fallback');
|
||||
card.setAttribute('role', 'dialog');
|
||||
card.setAttribute('aria-modal', 'true');
|
||||
|
||||
+40
-15
@@ -261,11 +261,36 @@ function applyTranslations() {
|
||||
if (app.currentView === 'history' && app.zones.length) loadHistory();
|
||||
}
|
||||
|
||||
function setLanguage(language) {
|
||||
async function loadLanguagePack(language) {
|
||||
if (app.translations[language]) return;
|
||||
const item = app.languages.find(entry => entry.code === language);
|
||||
if (!item) throw new Error(`Unknown language: ${language}`);
|
||||
const response = await fetch(withBase(item.path || `/lang/${encodeURIComponent(item.code)}.json`));
|
||||
if (!response.ok) throw new Error(`Language ${item.code} HTTP ${response.status}`);
|
||||
const pack = await response.json();
|
||||
app.translations[item.code] = pack.translations || {};
|
||||
app.locales[item.code] = pack.meta?.locale || item.locale || item.code;
|
||||
}
|
||||
|
||||
async function setLanguage(language) {
|
||||
const available = app.languages.some(item => item.code === language);
|
||||
app.language = available ? language : DEFAULT_LANGUAGE;
|
||||
setCookie('gree_controller_language', app.language);
|
||||
applyTranslations();
|
||||
const nextLanguage = available ? language : DEFAULT_LANGUAGE;
|
||||
const previousLanguage = app.language;
|
||||
const select = $('#languageSelect');
|
||||
if (nextLanguage === previousLanguage && app.translations[nextLanguage]) return;
|
||||
if (select) select.disabled = true;
|
||||
try {
|
||||
await loadLanguagePack(nextLanguage);
|
||||
app.language = nextLanguage;
|
||||
setCookie('gree_controller_language', app.language);
|
||||
applyTranslations();
|
||||
} catch (error) {
|
||||
console.error(`Unable to load language ${nextLanguage}:`, error);
|
||||
if (select) select.value = previousLanguage;
|
||||
toast(error.message, true);
|
||||
} finally {
|
||||
if (select) select.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLanguageOptions() {
|
||||
@@ -286,21 +311,21 @@ async function loadLanguages() {
|
||||
const manifest = await response.json();
|
||||
const languages = Array.isArray(manifest.languages) ? manifest.languages : [];
|
||||
if (!languages.some(item => item.code === DEFAULT_LANGUAGE)) throw new Error('Default English language pack is missing');
|
||||
|
||||
const loaded = await Promise.all(languages.map(async item => {
|
||||
const packResponse = await fetch(withBase(item.path || `/lang/${encodeURIComponent(item.code)}.json`));
|
||||
if (!packResponse.ok) throw new Error(`Language ${item.code} HTTP ${packResponse.status}`);
|
||||
const pack = await packResponse.json();
|
||||
return { item, pack };
|
||||
}));
|
||||
|
||||
app.languages = loaded.map(({ item }) => item);
|
||||
app.translations = Object.fromEntries(loaded.map(({ item, pack }) => [item.code, pack.translations || {}]));
|
||||
app.locales = Object.fromEntries(loaded.map(({ item, pack }) => [item.code, pack.meta?.locale || item.locale || item.code]));
|
||||
app.languages = languages;
|
||||
app.translations = {};
|
||||
app.locales = Object.fromEntries(languages.map(item => [item.code, item.locale || item.code]));
|
||||
app.language = app.languages.some(item => item.code === preferredLanguage)
|
||||
? preferredLanguage
|
||||
: (manifest.default || DEFAULT_LANGUAGE);
|
||||
if (!app.languages.some(item => item.code === app.language)) app.language = DEFAULT_LANGUAGE;
|
||||
try {
|
||||
await loadLanguagePack(app.language);
|
||||
} catch (error) {
|
||||
if (app.language === DEFAULT_LANGUAGE) throw error;
|
||||
console.error(`Unable to load preferred language ${app.language}:`, error);
|
||||
app.language = DEFAULT_LANGUAGE;
|
||||
await loadLanguagePack(DEFAULT_LANGUAGE);
|
||||
}
|
||||
renderLanguageOptions();
|
||||
} catch (error) {
|
||||
console.error('Unable to load language packs:', error);
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ function renderHouseClimate() {
|
||||
|
||||
if (powerOn) { powerOn.classList.remove('active'); powerOn.removeAttribute('aria-pressed'); }
|
||||
if (powerOff) { powerOff.classList.remove('active'); powerOff.removeAttribute('aria-pressed'); }
|
||||
node.innerHTML = `<div class="house-climate-head"><div><span class="eyebrow">${esc(tr('house.seasonMode'))}</span><h3>${esc(tr('house.smartThermostat'))}</h3><p>${esc(tr('house.setpointStrategy'))}</p></div><div class="outside-pill"><small>${esc(tr('house.outdoor'))}</small><strong>${esc(outdoor)}</strong></div></div>
|
||||
node.innerHTML = `<div class="house-climate-head"><div><span class="eyebrow">${esc(tr('house.seasonMode'))}</span><h3>${esc(tr('house.smartThermostat'))}</h3><p>${esc(tr('house.setpointStrategy'))}</p></div><button type="button" class="outside-pill" data-action="open-outdoor-history" title="${esc(tr('house.outdoorHistoryOpen'))}" aria-label="${esc(tr('house.outdoorHistoryOpen'))}"><small>${esc(tr('house.outdoor'))}</small><strong>${esc(outdoor)}</strong></button></div>
|
||||
<div class="house-mode-row">
|
||||
<button class="${mode === 'cool' ? 'active' : ''}" data-action="house-mode" data-value="cool" aria-pressed="${mode === 'cool'}">${esc(tr('mode.cool'))}</button>
|
||||
<button class="${mode === 'heat' ? 'active' : ''}" data-action="house-mode" data-value="heat" aria-pressed="${mode === 'heat'}">${esc(tr('mode.heat'))}</button>
|
||||
|
||||
@@ -11,6 +11,19 @@ document.addEventListener('keydown', event => {
|
||||
document.addEventListener('click', async event => {
|
||||
const inlineTarget = event.target.closest('[data-temperature-kind]');
|
||||
if (inlineTarget && !event.target.closest('button')) { beginInlineTemperatureEdit(inlineTarget); return; }
|
||||
const historyRoute = event.target.closest('a[data-history-route]');
|
||||
if (historyRoute) {
|
||||
event.preventDefault();
|
||||
const hours = historyRoute.dataset.historyHours;
|
||||
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'));
|
||||
if (dialog?.open) dialog.close();
|
||||
app.historyTab = 'overview';
|
||||
showView('history');
|
||||
return;
|
||||
}
|
||||
const button = event.target.closest('button'); if (!button) return;
|
||||
if (button.dataset.settingsTab) { setSettingsTab(button.dataset.settingsTab); return; }
|
||||
if (button.dataset.debugFilter) { app.debugFilter = button.dataset.debugFilter; renderDebugOverlay(); return; }
|
||||
@@ -73,6 +86,7 @@ document.addEventListener('click', async event => {
|
||||
return;
|
||||
}
|
||||
const action = button.dataset.action; if (!action) return;
|
||||
if (action === 'open-outdoor-history') { await openOutdoorHistory(); return; }
|
||||
if (action === 'add-cloud-device') {
|
||||
const cloudId = button.dataset.cloudId; if (!cloudId) return;
|
||||
button.disabled = true;
|
||||
|
||||
+5
-5
@@ -816,16 +816,16 @@ function flowPresetRequirements(preset) {
|
||||
const haEntities = [...new Set(nodes.map(node => node.config?.entity_id).filter(value => typeof value === 'string' && value && !value.startsWith('$')))];
|
||||
const requirements = [];
|
||||
const missing = [];
|
||||
const addCount = (kind, count, available, key) => {
|
||||
const addCount = (count, available, key) => {
|
||||
if (!count) return;
|
||||
const label = tr(key, { count });
|
||||
requirements.push({ label, ok: available >= count });
|
||||
if (available < count) missing.push(label);
|
||||
};
|
||||
addCount('zone', placeholders.zone.size, (app.zones || []).length, 'flow.templateRequiresZone');
|
||||
addCount('device', placeholders.device.size, (app.devices || []).length, 'flow.templateRequiresDevice');
|
||||
addCount('group', placeholders.group.size, (app.groups || []).length, 'flow.templateRequiresGroup');
|
||||
addCount('shared', placeholders.shared.size, (app.flowSharedInputs || []).length, 'flow.templateRequiresShared');
|
||||
addCount(placeholders.zone.size, (app.zones || []).length, 'flow.templateRequiresZone');
|
||||
addCount(placeholders.device.size, (app.devices || []).length, 'flow.templateRequiresDevice');
|
||||
addCount(placeholders.group.size, (app.groups || []).length, 'flow.templateRequiresGroup');
|
||||
addCount(placeholders.shared.size, (app.flowSharedInputs || []).length, 'flow.templateRequiresShared');
|
||||
if (hasHa) {
|
||||
const ready = Boolean(app.settings?.home_assistant?.url && app.settings?.home_assistant?.token_configured);
|
||||
requirements.push({ label: tr('flow.templateRequiresHa'), ok: ready });
|
||||
|
||||
@@ -273,6 +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'));
|
||||
dialog.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,43 @@ function outdoorHistorySeries(devices, rows) {
|
||||
}));
|
||||
}
|
||||
|
||||
async function openOutdoorHistory() {
|
||||
const host = $('#outdoorHistoryChartHost');
|
||||
const current = $('#outdoorHistoryCurrent');
|
||||
if (!host || !current) return;
|
||||
current.textContent = app.outdoorTemperature == null ? tr('common.unavailable') : fmtTemp(app.outdoorTemperature);
|
||||
host.innerHTML = `<div class="panel outdoor-history-loading">${esc(tr('common.loading'))}</div>`;
|
||||
$$('#outdoorHistoryDialog [data-history-route]').forEach(link => {
|
||||
const hours = link.dataset.historyHours;
|
||||
link.href = withBase(`/history/overview${hours ? `?hours=${encodeURIComponent(hours)}` : ''}`);
|
||||
});
|
||||
openDialog('outdoorHistoryDialog');
|
||||
try {
|
||||
const data = await api('/api/history?scope=overview&hours=24&limit=20000');
|
||||
const deviceRows = data.devices || [];
|
||||
const outdoorDeviceSeries = outdoorHistorySeries(app.devices, deviceRows).map(item => ({
|
||||
...item,
|
||||
label: item.label.includes(' · ') ? item.label : `${item.label} · ${tr('history.greeOutdoor')}`,
|
||||
}));
|
||||
const sensorRows = (data.sensors || []).filter(row => row.kind === 'outdoor');
|
||||
const outdoorEntities = [...new Set(sensorRows.map(row => row.entity_id))];
|
||||
const outdoorHaSeries = outdoorEntities.map((entity, index) => ({
|
||||
label: `HA · ${haSensorLabel(entity)}`,
|
||||
color: historySeriesColor(index + outdoorDeviceSeries.length),
|
||||
dash: [6, 4],
|
||||
value: row => row.entity_id === entity ? historyNumber(row.temperature) : NaN,
|
||||
}));
|
||||
const series = [...outdoorDeviceSeries, ...outdoorHaSeries];
|
||||
const rows = [...deviceRows, ...sensorRows];
|
||||
host.innerHTML = historyChartMarkup('outdoorHistoryModalChart', tr('history.allOutdoor'), tr('house.outdoorHistoryHint'));
|
||||
drawLineChart($('#outdoorHistoryModalChart'), series, rows, { height: 350 });
|
||||
renderLegend($('#outdoorHistoryModalChartLegend'), series);
|
||||
} catch (error) {
|
||||
host.innerHTML = `<div class="empty"><strong>${esc(tr('history.noData'))}</strong><span>${esc(error.message)}</span></div>`;
|
||||
toast(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderHistorySummary() {
|
||||
const host = $('#historySummary'); if (!host) return;
|
||||
if (app.historyTab === 'energy') { renderEnergyHistorySummary(); return; }
|
||||
|
||||
+30
-24
@@ -738,18 +738,21 @@ async function openDeviceDetails(id) {
|
||||
const sensorSelect = form.ha_energy_entity_id;
|
||||
sensorSelect.disabled = groupOwnsEnergy;
|
||||
sensorSelect.innerHTML = '<option value="">—</option>';
|
||||
try {
|
||||
const response = await api('/api/integrations/home-assistant/energy-sensors');
|
||||
for (const sensor of (response.sensors || [])) {
|
||||
const option = document.createElement('option');
|
||||
option.value = sensor.entity_id;
|
||||
option.textContent = `${sensor.name || sensor.entity_id} · ${sensor.unit || ''}`;
|
||||
option.dataset.unit = sensor.unit || '';
|
||||
option.dataset.deviceClass = sensor.device_class || '';
|
||||
option.dataset.stateClass = sensor.state_class || '';
|
||||
sensorSelect.append(option);
|
||||
}
|
||||
} catch (_) { }
|
||||
const haConfigured = Boolean(String(app.settings?.home_assistant?.url || '').trim() && app.settings?.home_assistant?.token_configured);
|
||||
if (haConfigured) {
|
||||
try {
|
||||
const response = await api('/api/integrations/home-assistant/energy-sensors');
|
||||
for (const sensor of (response.sensors || [])) {
|
||||
const option = document.createElement('option');
|
||||
option.value = sensor.entity_id;
|
||||
option.textContent = `${sensor.name || sensor.entity_id} · ${sensor.unit || ''}`;
|
||||
option.dataset.unit = sensor.unit || '';
|
||||
option.dataset.deviceClass = sensor.device_class || '';
|
||||
option.dataset.stateClass = sensor.state_class || '';
|
||||
sensorSelect.append(option);
|
||||
}
|
||||
} catch (_) { }
|
||||
}
|
||||
const energyEntity = groupOwnsEnergy ? installation.ha_energy_entity_id : device.ha_energy_entity_id;
|
||||
const energyUnit = groupOwnsEnergy ? installation.ha_energy_unit : device.ha_energy_unit;
|
||||
const energyDeviceClass = groupOwnsEnergy ? installation.ha_energy_device_class : device.ha_energy_device_class;
|
||||
@@ -822,18 +825,21 @@ async function loadDeviceGroupEnergySensors(group = null) {
|
||||
const form = $('#deviceGroupForm'); if (!form) return;
|
||||
const select = form.ha_energy_entity_id;
|
||||
select.innerHTML = '<option value="">—</option>';
|
||||
try {
|
||||
const response = await api('/api/integrations/home-assistant/energy-sensors');
|
||||
for (const sensor of (response.sensors || [])) {
|
||||
const option = document.createElement('option');
|
||||
option.value = sensor.entity_id;
|
||||
option.textContent = `${sensor.name || sensor.entity_id} · ${sensor.unit || ''}`;
|
||||
option.dataset.unit = sensor.unit || '';
|
||||
option.dataset.deviceClass = sensor.device_class || '';
|
||||
option.dataset.stateClass = sensor.state_class || '';
|
||||
select.append(option);
|
||||
}
|
||||
} catch (_) { }
|
||||
const haConfigured = Boolean(String(app.settings?.home_assistant?.url || '').trim() && app.settings?.home_assistant?.token_configured);
|
||||
if (haConfigured) {
|
||||
try {
|
||||
const response = await api('/api/integrations/home-assistant/energy-sensors');
|
||||
for (const sensor of (response.sensors || [])) {
|
||||
const option = document.createElement('option');
|
||||
option.value = sensor.entity_id;
|
||||
option.textContent = `${sensor.name || sensor.entity_id} · ${sensor.unit || ''}`;
|
||||
option.dataset.unit = sensor.unit || '';
|
||||
option.dataset.deviceClass = sensor.device_class || '';
|
||||
option.dataset.stateClass = sensor.state_class || '';
|
||||
select.append(option);
|
||||
}
|
||||
} catch (_) { }
|
||||
}
|
||||
if (group?.ha_energy_entity_id && ![...select.options].some(option => option.value === group.ha_energy_entity_id)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = group.ha_energy_entity_id;
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
'use strict';
|
||||
|
||||
const customSelectRegistry = new WeakMap();
|
||||
let customSelectOpenState = null;
|
||||
let customSelectSequence = 0;
|
||||
let customSelectObserver = null;
|
||||
let customSelectPropertiesPatched = false;
|
||||
|
||||
function patchCustomSelectProperties() {
|
||||
if (customSelectPropertiesPatched) return;
|
||||
customSelectPropertiesPatched = true;
|
||||
['value', 'selectedIndex'].forEach(property => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, property);
|
||||
if (!descriptor?.get || !descriptor?.set || descriptor.configurable === false) return;
|
||||
try {
|
||||
Object.defineProperty(HTMLSelectElement.prototype, property, {
|
||||
configurable: descriptor.configurable,
|
||||
enumerable: descriptor.enumerable,
|
||||
get: descriptor.get,
|
||||
set(nextValue) {
|
||||
descriptor.set.call(this, nextValue);
|
||||
queueMicrotask(() => refreshCustomSelect(this));
|
||||
},
|
||||
});
|
||||
} catch (_) { }
|
||||
});
|
||||
}
|
||||
|
||||
function customSelectSelectedOption(select) {
|
||||
return select.options?.[select.selectedIndex] || null;
|
||||
}
|
||||
|
||||
function customSelectLabel(select) {
|
||||
const explicit = select.getAttribute('aria-label');
|
||||
if (explicit) return explicit;
|
||||
const labelledBy = select.getAttribute('aria-labelledby');
|
||||
if (labelledBy) {
|
||||
const text = labelledBy.split(/\s+/).map(id => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(' ');
|
||||
if (text) return text;
|
||||
}
|
||||
const label = select.closest('label');
|
||||
const labelSpan = label?.querySelector(':scope > span');
|
||||
return labelSpan?.textContent?.trim() || select.name || select.id || '';
|
||||
}
|
||||
|
||||
function customSelectOptionRows(select) {
|
||||
const rows = [];
|
||||
[...select.children].forEach(child => {
|
||||
if (child instanceof HTMLOptGroupElement) {
|
||||
rows.push({ type: 'group', label: child.label });
|
||||
[...child.children].forEach(option => {
|
||||
if (option instanceof HTMLOptionElement) rows.push({ type: 'option', option, index: [...select.options].indexOf(option) });
|
||||
});
|
||||
} else if (child instanceof HTMLOptionElement) {
|
||||
rows.push({ type: 'option', option: child, index: [...select.options].indexOf(child) });
|
||||
}
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function positionCustomSelectMenu(state) {
|
||||
if (!state?.open || !state.trigger?.isConnected || !state.menu?.isConnected) return;
|
||||
const rect = state.trigger.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const gap = 6;
|
||||
const toolbar = state.toolbar;
|
||||
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
||||
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
||||
const width = Math.min(Math.max(rect.width, toolbar ? 210 : 180), Math.max(180, viewportWidth - margin * 2));
|
||||
const below = Math.max(0, viewportHeight - rect.bottom - gap - margin);
|
||||
const above = Math.max(0, rect.top - gap - margin);
|
||||
|
||||
state.menu.style.width = `${Math.round(width)}px`;
|
||||
state.menu.style.maxHeight = '320px';
|
||||
|
||||
// Once the popover is visible, scrollHeight is the natural menu height.
|
||||
// Use that height to choose the side and, especially, to place short menus
|
||||
// directly above the trigger instead of at the top of a 320px allowance.
|
||||
const naturalHeight = Math.min(320, Math.max(0, state.menu.scrollHeight || state.menu.getBoundingClientRect().height || 0));
|
||||
const desiredHeight = naturalHeight || 180;
|
||||
const useBelow = below >= desiredHeight || (below >= above && above < desiredHeight);
|
||||
const available = Math.max(1, Math.min(320, useBelow ? below : above));
|
||||
state.menu.style.maxHeight = `${Math.round(available)}px`;
|
||||
|
||||
const renderedHeight = naturalHeight ? Math.min(naturalHeight, available) : available;
|
||||
const left = Math.min(Math.max(margin, rect.left), Math.max(margin, viewportWidth - width - margin));
|
||||
const rawTop = useBelow ? rect.bottom + gap : rect.top - gap - renderedHeight;
|
||||
const maxTop = Math.max(margin, viewportHeight - renderedHeight - margin);
|
||||
const top = Math.min(Math.max(margin, rawTop), maxTop);
|
||||
|
||||
state.menu.style.left = `${Math.round(left)}px`;
|
||||
state.menu.style.top = `${Math.round(top)}px`;
|
||||
state.menu.dataset.placement = useBelow ? 'bottom' : 'top';
|
||||
}
|
||||
|
||||
|
||||
function rebuildCustomSelectMenu(select, state) {
|
||||
const rows = customSelectOptionRows(select);
|
||||
state.menu.replaceChildren();
|
||||
|
||||
if (!rows.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'custom-select-empty';
|
||||
empty.textContent = '—';
|
||||
state.menu.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.type === 'group') {
|
||||
const group = document.createElement('div');
|
||||
group.className = 'custom-select-group';
|
||||
group.textContent = row.label;
|
||||
state.menu.appendChild(group);
|
||||
return;
|
||||
}
|
||||
|
||||
const { option, index } = row;
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'custom-select-option';
|
||||
button.dataset.index = String(index);
|
||||
button.setAttribute('role', 'option');
|
||||
button.setAttribute('aria-selected', option.selected ? 'true' : 'false');
|
||||
button.disabled = option.disabled || option.parentElement?.disabled || select.disabled;
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = option.textContent || option.label || option.value || '—';
|
||||
const check = document.createElement('b');
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = option.selected ? '✓' : '';
|
||||
button.append(text, check);
|
||||
|
||||
button.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (button.disabled) return;
|
||||
const nextIndex = Number(button.dataset.index);
|
||||
if (!Number.isInteger(nextIndex) || !select.options[nextIndex]) return;
|
||||
const changed = select.selectedIndex !== nextIndex;
|
||||
select.selectedIndex = nextIndex;
|
||||
refreshCustomSelect(select);
|
||||
closeCustomSelect();
|
||||
if (changed) {
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
state.trigger.focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
state.menu.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshCustomSelect(select) {
|
||||
const state = customSelectRegistry.get(select);
|
||||
if (!state) return;
|
||||
const selected = customSelectSelectedOption(select);
|
||||
const text = selected?.textContent?.trim() || selected?.label || selected?.value || '—';
|
||||
const label = customSelectLabel(select);
|
||||
const disabled = !!select.disabled;
|
||||
|
||||
if (state.toolbar) {
|
||||
state.trigger.classList.toggle('custom-select-disabled', disabled);
|
||||
state.trigger.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
||||
state.trigger.setAttribute('aria-label', label ? `${label}: ${text}` : text);
|
||||
state.trigger.tabIndex = disabled ? -1 : 0;
|
||||
} else {
|
||||
state.value.textContent = text;
|
||||
state.trigger.disabled = disabled;
|
||||
state.trigger.setAttribute('aria-label', label ? `${label}: ${text}` : text);
|
||||
state.wrapper.hidden = !!select.hidden;
|
||||
}
|
||||
|
||||
if (state.open) {
|
||||
rebuildCustomSelectMenu(select, state);
|
||||
requestAnimationFrame(() => positionCustomSelectMenu(state));
|
||||
}
|
||||
}
|
||||
|
||||
function closeCustomSelect(state = customSelectOpenState) {
|
||||
if (!state?.open) return;
|
||||
state.open = false;
|
||||
state.trigger.setAttribute('aria-expanded', 'false');
|
||||
state.trigger.classList.remove('custom-select-open');
|
||||
state.wrapper?.classList.remove('custom-select-open');
|
||||
|
||||
if (typeof state.menu.hidePopover === 'function') {
|
||||
try {
|
||||
if (state.menu.matches(':popover-open')) state.menu.hidePopover();
|
||||
} catch (_) { }
|
||||
}
|
||||
state.menu.classList.remove('custom-select-menu-fallback-open');
|
||||
state.menu.hidden = true;
|
||||
if (customSelectOpenState === state) customSelectOpenState = null;
|
||||
}
|
||||
|
||||
function openCustomSelect(select) {
|
||||
const state = customSelectRegistry.get(select);
|
||||
if (!state || select.disabled) return;
|
||||
if (customSelectOpenState === state && state.open) {
|
||||
closeCustomSelect(state);
|
||||
return;
|
||||
}
|
||||
closeCustomSelect();
|
||||
refreshCustomSelect(select);
|
||||
rebuildCustomSelectMenu(select, state);
|
||||
|
||||
state.open = true;
|
||||
customSelectOpenState = state;
|
||||
state.trigger.setAttribute('aria-expanded', 'true');
|
||||
state.trigger.classList.add('custom-select-open');
|
||||
state.wrapper?.classList.add('custom-select-open');
|
||||
state.menu.hidden = false;
|
||||
positionCustomSelectMenu(state);
|
||||
|
||||
if (typeof state.menu.showPopover === 'function') {
|
||||
try { state.menu.showPopover(); }
|
||||
catch (_) { state.menu.classList.add('custom-select-menu-fallback-open'); }
|
||||
} else {
|
||||
state.menu.classList.add('custom-select-menu-fallback-open');
|
||||
}
|
||||
|
||||
positionCustomSelectMenu(state);
|
||||
requestAnimationFrame(() => {
|
||||
positionCustomSelectMenu(state);
|
||||
state.menu.querySelector('.custom-select-option[aria-selected="true"]')?.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
}
|
||||
|
||||
function customSelectKeyboard(event, select) {
|
||||
if (select.disabled) return;
|
||||
const state = customSelectRegistry.get(select);
|
||||
if (!state) return;
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
if (!state.open) {
|
||||
openCustomSelect(select);
|
||||
return;
|
||||
}
|
||||
const enabled = [...state.menu.querySelectorAll('.custom-select-option:not(:disabled)')];
|
||||
if (!enabled.length) return;
|
||||
const active = document.activeElement;
|
||||
const current = enabled.indexOf(active);
|
||||
const delta = event.key === 'ArrowUp' ? -1 : 1;
|
||||
enabled[(current + delta + enabled.length) % enabled.length].focus();
|
||||
} else if (event.key === 'Escape' && state.open) {
|
||||
event.preventDefault();
|
||||
closeCustomSelect(state);
|
||||
state.trigger.focus({ preventScroll: true });
|
||||
} else if (event.key === 'Home' && state.open) {
|
||||
event.preventDefault();
|
||||
state.menu.querySelector('.custom-select-option:not(:disabled)')?.focus();
|
||||
} else if (event.key === 'End' && state.open) {
|
||||
event.preventDefault();
|
||||
[...state.menu.querySelectorAll('.custom-select-option:not(:disabled)')].at(-1)?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceCustomSelect(select) {
|
||||
if (!(select instanceof HTMLSelectElement) || customSelectRegistry.has(select) || select.multiple || select.size > 1 || select.dataset.nativeSelect === 'true') return;
|
||||
|
||||
const toolbarHost = select.classList.contains('toolbar-picker-select') ? select.closest('.toolbar-picker') : null;
|
||||
const originalSelectState = {
|
||||
tabIndex: select.getAttribute('tabindex'),
|
||||
ariaHidden: select.getAttribute('aria-hidden'),
|
||||
};
|
||||
const originalToolbarState = toolbarHost ? {
|
||||
role: toolbarHost.getAttribute('role'),
|
||||
ariaHaspopup: toolbarHost.getAttribute('aria-haspopup'),
|
||||
ariaControls: toolbarHost.getAttribute('aria-controls'),
|
||||
ariaExpanded: toolbarHost.getAttribute('aria-expanded'),
|
||||
ariaDisabled: toolbarHost.getAttribute('aria-disabled'),
|
||||
} : null;
|
||||
const menu = document.createElement('div');
|
||||
const menuId = `customSelectMenu${++customSelectSequence}`;
|
||||
menu.id = menuId;
|
||||
menu.className = 'custom-select-menu';
|
||||
menu.setAttribute('role', 'listbox');
|
||||
menu.setAttribute('popover', 'manual');
|
||||
menu.hidden = true;
|
||||
document.body.appendChild(menu);
|
||||
|
||||
let wrapper = null;
|
||||
let trigger = toolbarHost;
|
||||
let value = null;
|
||||
|
||||
select.classList.add('select-native-proxy');
|
||||
select.tabIndex = -1;
|
||||
select.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (toolbarHost) {
|
||||
toolbarHost.classList.add('custom-select-toolbar');
|
||||
toolbarHost.setAttribute('role', 'button');
|
||||
toolbarHost.setAttribute('aria-haspopup', 'listbox');
|
||||
toolbarHost.setAttribute('aria-controls', menuId);
|
||||
toolbarHost.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
wrapper = document.createElement('div');
|
||||
wrapper.className = 'custom-select';
|
||||
trigger = document.createElement('button');
|
||||
trigger.type = 'button';
|
||||
trigger.className = 'custom-select-trigger';
|
||||
trigger.setAttribute('aria-haspopup', 'listbox');
|
||||
trigger.setAttribute('aria-controls', menuId);
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
value = document.createElement('span');
|
||||
value.className = 'custom-select-value';
|
||||
const chevron = document.createElement('span');
|
||||
chevron.className = 'custom-select-chevron';
|
||||
chevron.setAttribute('aria-hidden', 'true');
|
||||
chevron.textContent = '⌄';
|
||||
trigger.append(value, chevron);
|
||||
wrapper.appendChild(trigger);
|
||||
select.insertAdjacentElement('afterend', wrapper);
|
||||
}
|
||||
|
||||
const state = {
|
||||
select,
|
||||
wrapper,
|
||||
trigger,
|
||||
value,
|
||||
menu,
|
||||
toolbar: !!toolbarHost,
|
||||
open: false,
|
||||
originalSelectState,
|
||||
originalToolbarState,
|
||||
};
|
||||
customSelectRegistry.set(select, state);
|
||||
|
||||
trigger.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openCustomSelect(select);
|
||||
});
|
||||
trigger.addEventListener('keydown', event => customSelectKeyboard(event, select));
|
||||
menu.addEventListener('keydown', event => customSelectKeyboard(event, select));
|
||||
|
||||
// Clicking an implicit <label> may synthesize a click on the original select.
|
||||
// Redirect that trusted interaction to the custom popup instead of opening the OS picker.
|
||||
select.addEventListener('click', event => {
|
||||
if (!event.isTrusted) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openCustomSelect(select);
|
||||
});
|
||||
select.addEventListener('change', () => refreshCustomSelect(select));
|
||||
select.addEventListener('input', () => refreshCustomSelect(select));
|
||||
|
||||
refreshCustomSelect(select);
|
||||
}
|
||||
|
||||
function cleanupCustomSelect(select) {
|
||||
const state = customSelectRegistry.get(select);
|
||||
if (!state) return;
|
||||
if (state.open) closeCustomSelect(state);
|
||||
state.menu.remove();
|
||||
state.wrapper?.remove();
|
||||
select.classList.remove('select-native-proxy');
|
||||
if (state.originalSelectState.tabIndex == null) select.removeAttribute('tabindex');
|
||||
else select.setAttribute('tabindex', state.originalSelectState.tabIndex);
|
||||
if (state.originalSelectState.ariaHidden == null) select.removeAttribute('aria-hidden');
|
||||
else select.setAttribute('aria-hidden', state.originalSelectState.ariaHidden);
|
||||
if (state.toolbar && state.trigger) {
|
||||
state.trigger.classList.remove('custom-select-toolbar', 'custom-select-open', 'custom-select-disabled');
|
||||
const restoreAttribute = (name, value) => value == null ? state.trigger.removeAttribute(name) : state.trigger.setAttribute(name, value);
|
||||
restoreAttribute('role', state.originalToolbarState?.role);
|
||||
restoreAttribute('aria-haspopup', state.originalToolbarState?.ariaHaspopup);
|
||||
restoreAttribute('aria-controls', state.originalToolbarState?.ariaControls);
|
||||
restoreAttribute('aria-expanded', state.originalToolbarState?.ariaExpanded);
|
||||
restoreAttribute('aria-disabled', state.originalToolbarState?.ariaDisabled);
|
||||
}
|
||||
customSelectRegistry.delete(select);
|
||||
}
|
||||
|
||||
function refreshCustomSelects(root = document) {
|
||||
if (root instanceof HTMLSelectElement) refreshCustomSelect(root);
|
||||
root.querySelectorAll?.('select').forEach(select => refreshCustomSelect(select));
|
||||
}
|
||||
|
||||
function enhanceCustomSelects(root = document) {
|
||||
if (root instanceof HTMLSelectElement) enhanceCustomSelect(root);
|
||||
root.querySelectorAll?.('select').forEach(enhanceCustomSelect);
|
||||
}
|
||||
|
||||
function initCustomSelects() {
|
||||
patchCustomSelectProperties();
|
||||
enhanceCustomSelects(document);
|
||||
if (customSelectObserver) return;
|
||||
|
||||
customSelectObserver = new MutationObserver(records => {
|
||||
const toRefresh = new Set();
|
||||
records.forEach(record => {
|
||||
record.addedNodes.forEach(node => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
enhanceCustomSelects(node);
|
||||
const select = node.closest?.('select');
|
||||
if (select) toRefresh.add(select);
|
||||
});
|
||||
record.removedNodes.forEach(node => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node instanceof HTMLSelectElement && !node.isConnected) cleanupCustomSelect(node);
|
||||
node.querySelectorAll?.('select').forEach(select => {
|
||||
// A DOM move is reported as a removal followed by an addition. Keep
|
||||
// the existing custom-select state when the select is still attached
|
||||
// to the document; otherwise every move would leave an old wrapper
|
||||
// behind and create another one on the next enhancement pass.
|
||||
if (!select.isConnected) cleanupCustomSelect(select);
|
||||
});
|
||||
});
|
||||
const targetElement = record.target.nodeType === Node.ELEMENT_NODE ? record.target : record.target.parentElement;
|
||||
const select = targetElement?.closest?.('select');
|
||||
if (select) toRefresh.add(select);
|
||||
});
|
||||
toRefresh.forEach(select => {
|
||||
if (!select.isConnected) {
|
||||
cleanupCustomSelect(select);
|
||||
return;
|
||||
}
|
||||
if (!customSelectRegistry.has(select)) enhanceCustomSelect(select);
|
||||
refreshCustomSelect(select);
|
||||
});
|
||||
});
|
||||
|
||||
customSelectObserver.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['disabled', 'hidden', 'selected', 'label', 'value', 'aria-label', 'aria-labelledby'],
|
||||
});
|
||||
|
||||
document.addEventListener('pointerdown', event => {
|
||||
const state = customSelectOpenState;
|
||||
if (!state?.open) return;
|
||||
if (state.menu.contains(event.target) || state.trigger.contains(event.target)) return;
|
||||
closeCustomSelect(state);
|
||||
}, true);
|
||||
|
||||
document.addEventListener('reset', event => {
|
||||
requestAnimationFrame(() => refreshCustomSelects(event.target));
|
||||
}, true);
|
||||
|
||||
document.addEventListener('invalid', event => {
|
||||
const select = event.target;
|
||||
if (!(select instanceof HTMLSelectElement)) return;
|
||||
const state = customSelectRegistry.get(select);
|
||||
if (!state) return;
|
||||
requestAnimationFrame(() => state.trigger.focus({ preventScroll: true }));
|
||||
}, true);
|
||||
|
||||
window.addEventListener('resize', () => positionCustomSelectMenu(customSelectOpenState));
|
||||
document.addEventListener('scroll', event => {
|
||||
const state = customSelectOpenState;
|
||||
if (!state?.open || state.menu.contains(event.target)) return;
|
||||
closeCustomSelect(state);
|
||||
}, true);
|
||||
}
|
||||
|
||||
initCustomSelects();
|
||||
+328
-79
@@ -4,7 +4,6 @@
|
||||
--canvas: #181818;
|
||||
--surface: #1c1c1c;
|
||||
--surface-2: #222222;
|
||||
--surface-3: #282828;
|
||||
--surface-muted: #202020;
|
||||
--input-bg: #181818;
|
||||
--nav-bg: #171717;
|
||||
@@ -33,11 +32,7 @@
|
||||
--orange: #fb923c;
|
||||
--backdrop: rgba(0, 0, 0, .68);
|
||||
--grid: rgba(255, 255, 255, .07);
|
||||
--scroll-thumb: #444444;
|
||||
--scroll-thumb-hover: #575757;
|
||||
--radius: 16px;
|
||||
--radius-small: 10px;
|
||||
--radius-medium: 14px;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
@@ -47,7 +42,6 @@
|
||||
--canvas: #f8faf9;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f5f6f6;
|
||||
--surface-3: #eef0ef;
|
||||
--surface-muted: #f6f7f7;
|
||||
--input-bg: #ffffff;
|
||||
--nav-bg: #ffffff;
|
||||
@@ -76,8 +70,6 @@
|
||||
--orange: #c56b21;
|
||||
--backdrop: rgba(23, 23, 23, .32);
|
||||
--grid: rgba(24, 24, 27, .08);
|
||||
--scroll-thumb: #c8ccca;
|
||||
--scroll-thumb-hover: #aeb3b0;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -119,7 +111,6 @@ button:disabled {
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 72px;
|
||||
@@ -184,7 +175,6 @@ button:disabled {
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 13px;
|
||||
padding: 11px 15px;
|
||||
color: var(--text);
|
||||
background: var(--surface-2);
|
||||
@@ -285,9 +275,7 @@ h3 {
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 690px;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.hero {
|
||||
@@ -350,7 +338,6 @@ h3 {
|
||||
.metric {
|
||||
padding: 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
@@ -392,7 +379,6 @@ h3 {
|
||||
.panel,
|
||||
.list-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
@@ -840,7 +826,6 @@ h3 {
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
@@ -885,6 +870,31 @@ select:focus {
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
select:not(.toolbar-picker-select) {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
padding-right: 38px;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--muted) 50%),
|
||||
linear-gradient(135deg, var(--muted) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 17px) 50%,
|
||||
calc(100% - 12px) 50%;
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s ease, background-color .15s ease, outline-color .15s ease;
|
||||
}
|
||||
|
||||
select:not(.toolbar-picker-select):hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--accent) 32%, var(--line));
|
||||
background-color: color-mix(in srgb, var(--input-bg) 90%, var(--surface-muted));
|
||||
}
|
||||
|
||||
select:not(.toolbar-picker-select):disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
grid-auto-flow: column;
|
||||
@@ -1077,9 +1087,6 @@ select:focus {
|
||||
}
|
||||
|
||||
.log-row {
|
||||
display: grid;
|
||||
grid-template-columns: 78px 150px 1fr;
|
||||
gap: 12px;
|
||||
padding: 11px 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-size: 12px;
|
||||
@@ -1550,11 +1557,27 @@ legend {
|
||||
flex: 0 0 auto;
|
||||
min-width: 94px;
|
||||
padding: 10px 13px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 14px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
button.outside-pill {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.outside-pill:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 28%, var(--line));
|
||||
background: color-mix(in srgb, var(--surface-muted) 86%, var(--accent-soft));
|
||||
}
|
||||
|
||||
button.outside-pill:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.outside-pill small,
|
||||
.outside-pill strong {
|
||||
display: block;
|
||||
@@ -1570,6 +1593,92 @@ legend {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog {
|
||||
width: min(920px, calc(100% - 24px));
|
||||
}
|
||||
|
||||
.outdoor-history-current {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.outdoor-history-current small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.outdoor-history-current strong {
|
||||
font-size: 24px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog .history-chart-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog .history-chart-card:not(.chart-fullscreen-fallback) .chart-wrap {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.outdoor-history-dialog .history-chart-card:not(.chart-fullscreen-fallback) canvas {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.outdoor-history-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px 14px;
|
||||
flex-wrap: wrap;
|
||||
padding: 11px 2px 0;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.outdoor-history-links > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.outdoor-history-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-soft);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.outdoor-history-links a:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 36%, var(--line));
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.outdoor-history-links .outdoor-history-all {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.outdoor-history-loading {
|
||||
padding: 28px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.automation-plan-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr));
|
||||
@@ -1908,7 +2017,6 @@ legend {
|
||||
|
||||
.chart-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
@@ -2096,9 +2204,7 @@ legend {
|
||||
}
|
||||
|
||||
.history-context-controls {
|
||||
display: flex;
|
||||
flex: 1 1 260px;
|
||||
min-width: min(100%, 220px);
|
||||
}
|
||||
|
||||
.history-context-controls label {
|
||||
@@ -2883,7 +2989,6 @@ legend {
|
||||
.settings-block-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 13px;
|
||||
}
|
||||
|
||||
.settings-block-head h3 {
|
||||
@@ -3298,7 +3403,6 @@ legend {
|
||||
.device-capability-panel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px 0 2px;
|
||||
}
|
||||
|
||||
.device-capability-panel>small {
|
||||
@@ -3311,22 +3415,12 @@ legend {
|
||||
.device-capability-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.device-capability-buttons button {
|
||||
min-height: 34px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.device-capability-buttons button.active {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.device-capability-panel {
|
||||
justify-items: center;
|
||||
@@ -3544,7 +3638,6 @@ legend {
|
||||
|
||||
.group-custom-temperature-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
@@ -4022,9 +4115,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
background: var(--warning-soft);
|
||||
}
|
||||
|
||||
.quick-thermostat-control .thermostat-main {
|
||||
margin: 8px 0 7px;
|
||||
}
|
||||
|
||||
.zone-config-card .zone-enable-toggle {
|
||||
min-height: 28px;
|
||||
@@ -4529,7 +4619,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
}
|
||||
|
||||
#controlPlan .plan-card-head .eyebrow {
|
||||
font-size: 8px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@@ -4542,10 +4631,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#controlPlan .plan-card>p {
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
#controlPlan .plan-temp {
|
||||
gap: 5px;
|
||||
@@ -4587,8 +4672,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
#controlPlan .plan-events li {
|
||||
grid-template-columns: 62px minmax(0, 1fr);
|
||||
gap: 5px;
|
||||
font-size: 9px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
#controlPlan .plan-events li span {
|
||||
@@ -4626,14 +4709,12 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
|
||||
#controlPlan .plan-rule-summary strong {
|
||||
color: var(--text);
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
#controlPlan .plan-rule-summary small {
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@@ -4684,9 +4765,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#controlPlan.automation-plan-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
[data-view="dashboard"] .dashboard-summary-card {
|
||||
@@ -5510,10 +5588,6 @@ textarea[aria-invalid="true"] {
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 16%, transparent);
|
||||
}
|
||||
|
||||
.zone-thermostat.lockout-waiting,
|
||||
.zone-config-card.lockout-waiting {
|
||||
background-image: linear-gradient(color-mix(in srgb, var(--warning-soft) 42%, transparent), color-mix(in srgb, var(--warning-soft) 42%, transparent));
|
||||
}
|
||||
|
||||
.group-custom-temperature-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
||||
@@ -5816,9 +5890,6 @@ textarea[aria-invalid="true"] {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
body.flow-editor-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flow-editor {
|
||||
position: fixed;
|
||||
@@ -6028,8 +6099,6 @@ body.flow-editor-open {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.flow-palette-group button:hover {
|
||||
@@ -6151,9 +6220,6 @@ body.flow-editor-open {
|
||||
.flow-editor .flow-port {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-top: -7px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--surface);
|
||||
background: var(--muted-2);
|
||||
@@ -6161,13 +6227,6 @@ body.flow-editor-open {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.flow-editor .flow-port-in {
|
||||
left: -8px;
|
||||
}
|
||||
|
||||
.flow-editor .flow-port-out {
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
.flow-editor .flow-port:hover,
|
||||
.flow-editor .flow-port.armed {
|
||||
@@ -6364,16 +6423,13 @@ body.flow-editor-open {
|
||||
}
|
||||
|
||||
.flow-template-card {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
text-align: left;
|
||||
align-content: start;
|
||||
min-height: 118px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.flow-template-card:hover {
|
||||
@@ -6494,12 +6550,6 @@ body.flow-editor-open {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:760px) {
|
||||
.flow-editor-actions {
|
||||
max-width: none;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-device-options {
|
||||
margin: 10px 0;
|
||||
@@ -7754,7 +7804,6 @@ button {
|
||||
}
|
||||
|
||||
.automation-tabs {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
@@ -9064,7 +9113,6 @@ body.flow-editor-open {
|
||||
}
|
||||
|
||||
#controlPlan .plan-rules .plan-events li {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
@@ -9643,3 +9691,204 @@ body.flow-editor-open {
|
||||
.history-network-controls { grid-template-columns: 1fr; }
|
||||
.history-jitter-toggle { width: 100%; }
|
||||
}
|
||||
|
||||
/* Unified custom selects -------------------------------------------------- */
|
||||
.select-native-proxy {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
min-width: 1px !important;
|
||||
height: 1px !important;
|
||||
min-height: 1px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
clip-path: inset(50%) !important;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.custom-select[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-select-trigger {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 45px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
outline: 0;
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s ease, background-color .15s ease, outline-color .15s ease;
|
||||
}
|
||||
|
||||
.custom-select-trigger:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--accent) 32%, var(--line));
|
||||
background: color-mix(in srgb, var(--input-bg) 90%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.custom-select-trigger:focus-visible,
|
||||
.custom-select.custom-select-open .custom-select-trigger {
|
||||
border-color: var(--accent);
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.custom-select-trigger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.custom-select-value {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-select-chevron {
|
||||
margin-top: -2px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
|
||||
.custom-select-open .custom-select-chevron,
|
||||
.custom-select-toolbar.custom-select-open .toolbar-picker-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.custom-select-menu {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
margin: 0;
|
||||
padding: 7px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
outline: 0;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
box-shadow: 0 14px 34px rgba(0, 0, 0, .22);
|
||||
}
|
||||
|
||||
.custom-select-menu[popover] {
|
||||
inset: auto;
|
||||
}
|
||||
|
||||
.custom-select-menu.custom-select-menu-fallback-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-select-option {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 20px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 7px 8px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--text-soft);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-select-option:hover:not(:disabled),
|
||||
.custom-select-option:focus-visible {
|
||||
background: var(--surface-muted);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.custom-select-option[aria-selected="true"] {
|
||||
background: color-mix(in srgb, var(--accent) 11%, var(--surface));
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.custom-select-option b {
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-select-option:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.custom-select-group {
|
||||
padding: 9px 8px 5px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.custom-select-empty {
|
||||
padding: 9px 8px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toolbar-picker.custom-select-toolbar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-picker.custom-select-toolbar.custom-select-open {
|
||||
border-color: var(--accent);
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.toolbar-picker.custom-select-toolbar.custom-select-disabled {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chart-toolbar .custom-select,
|
||||
.custom-chart-add .custom-select {
|
||||
flex: 1 1 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.flow-inspector .custom-select-trigger {
|
||||
border-radius: 10px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.has-error > .custom-select .custom-select-trigger,
|
||||
select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-trigger {
|
||||
border-color: color-mix(in srgb, var(--danger) 68%, var(--line));
|
||||
outline: 2px solid color-mix(in srgb, var(--danger) 13%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.custom-select-menu {
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user