Files
houseplan-card/docs/design/600-settings-dialogs/reference/general-settings.js
T
Claude 94c9d8addf docs: положить дизайн-референс диалогов настроек в репозиторий (#600)
AC0 задачи: пока референс лежит вложением в issue, ревью сравнивает продукт с
макетом «на глаз», и ровно это дало выпущенный в 1.77.0-beta.2 ложный паритет —
карточки снаружи, старая разметка внутри.

Содержимое архива `houseplan-settings-design-2026-09-18.zip`
(SHA-256 06f07fc212947ee9da1a39aaab2d14cbd41154cc9effe39bc48e1c2e1517255b,
сверен побайтово с вложением #591) разложено по образцу #505:

- `SPEC.md`, `IMPLEMENTATION-GUIDE.md`, `OPEN-POINTS.md`, карта полей Space —
  с этого коммита часть ТЗ: расхождение называется ссылкой на пункт документа;
- `reference/` — свёрстанный прототип четырёх форм;
- `screenshots/` — четыре формы целиком, 560 px, 2x;
- `README.md` — происхождение, отпечаток, границы использования;
- `ACCEPTANCE.md` — место для парных кадров «референс / продукт» по AC1.

Автор — сотрудник владельца (подтверждено 20.09 в #600), внешний грант не нужен.

Windows-лаунчеры `START.cmd` и `START.ps1` не переносились: `node serve.mjs`
делает то же самое. `room-fill.test.mjs` перенесён как документ намерения, CI
его не запускает — под `docs/**` не попадает ни один тестовый glob.

В бандл отсюда не попадает ничего; `repo-hygiene` и полный набор зелёные
(2793 pass).

Issue: #600
User-Visible: no
2026-09-20 09:07:53 +03:00

259 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* General settings: design prototype of the house-wide settings dialog.
Same visual system as Space settings (cards in one flow, "?" help, inline color fields).
Field keys mirror the product's SettingsDialogState; data stays in this origin's localStorage. */
import {registerDemoForm, notifyDemoFormClosed} from './demo-shell.js';
import {$, $$, escape, clone, icon, hydrateIcons, showToast, attachHelp, closeHelp, helpOpen} from './ui.js';
const STORAGE_KEY = 'houseplan-general-settings-design-v1';
/* DEFAULT_FILL_COLORS from src/logic.ts; opacity kept as a fraction like the product. */
const DEFAULT_COLORS = {
light_on:{c:'#ffd45c',a:0.18}, light_off:{c:'#9aa0a6',a:0.14}, light_none:{c:'#6b7480',a:0},
temp_cold:{c:'#4fc3f7',a:0.18}, temp_ok:{c:'#66d17a',a:0.18}, temp_hot:{c:'#ffd45c',a:0.18},
lqi_low:{c:'#f25a4a',a:0.18}, lqi_high:{c:'#4bd28f',a:0.18},
glow_base:{c:'#0d1b2a',a:0.5}, glow_light:{c:'#ffd9a0',a:0.85},
wall_fill:{c:'#ffffff',a:1},
};
const defaults = {
showRoomTooltip:true, radarShowLive:true,
zigbeeTopology:{enabled:false, z2mTopics:'zigbee2mqtt'},
colors:clone(DEFAULT_COLORS), glowRadius:3,
bgMode:'daynight', bgColor:null, northDeg:null, sunRays:true, sunRayOrigin:'inner',
};
/* Visual example for a fresh browser: matches the current stage (north 165°, Zigbee links on). */
const example = {...clone(defaults), northDeg:165, zigbeeTopology:{enabled:true, z2mTopics:'zigbee2mqtt'}};
const THEME_BG = '#edf1f2';
function load() {
try {
const data = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (!data || typeof data !== 'object') return clone(example);
const ok = Object.keys(defaults).every(k => k in data) && Object.keys(DEFAULT_COLORS).every(k => data.colors?.[k]?.c);
return ok ? data : clone(example);
} catch { return clone(example); }
}
let saved = load();
let draft = clone(saved);
const dirty = () => JSON.stringify(draft) !== JSON.stringify(saved);
/* Dialog shell: same structure and classes as the Space settings dialog. */
const dialog = document.createElement('dialog');
dialog.id = 'general-settings'; dialog.className = 'settings-dialog'; dialog.setAttribute('aria-labelledby', 'gs-title');
dialog.innerHTML = `
<header class="dialog-header">
<button type="button" id="gs-close" class="icon-button" aria-label="Close general settings" title="Close" data-icon="close"></button>
<h1 id="gs-title" tabindex="-1">General settings</h1>
</header>
<div class="dialog-scroll" id="gs-scroll"><div class="dialog-body"><div id="gs-content" class="settings-content"></div></div></div>
<footer class="dialog-footer">
<div class="footer-tools"><button class="button" id="gs-reset" type="button"><span data-icon="reset"></span>Reset to defaults</button></div>
<div class="save-status" id="gs-status" role="status" hidden></div>
<div class="footer-commit"><button class="button" id="gs-cancel" type="button">Cancel</button><button class="button primary" id="gs-save" type="button" disabled>Save</button></div>
</footer>`;
$('#demo-stage').append(dialog);
const subDialog = document.createElement('dialog');
subDialog.id = 'gs-sub-dialog'; subDialog.className = 'sub-dialog'; subDialog.setAttribute('aria-labelledby', 'gs-sub-title');
document.body.append(subDialog);
const content = $('#gs-content');
const scroller = $('#gs-scroll');
/* Help texts: product keys where they exist (gs.*.help, topology help). */
const helpCopy = {
zigbee:['Zigbee links','Shows which Zigbee devices each device talks to directly. Line colour is link quality (LQI) on the same scale as the device’s LQI reading: red at 40 and below, green at 180 and above. A dashed line means quality was not reported. An arrow points to the next device on the way to the coordinator; a line without an arrow is a spare neighbour. Data comes from the last load and goes stale; links are shown with a mouse only.'],
fills:['Room fill colors','Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space’s dialog.'],
glowRadius:['Glow radius','Sets the default glow radius in metres or feet; a device-specific radius overrides it.'],
bgMode:['Plan background','“Follows the Sun” uses sun.sun, falling back to the browser’s local clock; Static always shows the selected colour.'],
north:['North on the plan','The angle is measured clockwise from the plan’s upward vertical; north is required for window rays, not for the “Follows the Sun” background.'],
};
const help = attachHelp(content, scroller, helpCopy);
/* Rendering helpers (same markup as Space settings). */
const card = (id, title, body, extra='') => `<section id="gs-section-${id}" class="settings-card" aria-labelledby="gs-heading-${id}"><div class="card-heading"><div class="card-title"><h2 id="gs-heading-${id}">${title}</h2>${extra}</div></div><div class="panel-scroll">${body}</div></section>`;
const hint = text => `<p class="field-hint">${text}</p>`;
const callout = (text, warning=false) => `<div class="callout ${warning?'warning':''}">${icon('info')}<p>${text}</p></div>`;
const subsectionHeading = (text, key='') => `<div class="subsection-heading"><h3>${text}</h3>${key?help.button(key):''}</div>`;
function toggle(key, title, description, name, disabled=false) {
return `<div class="setting-item"><div class="setting-row"><span class="row-icon">${icon(name)}</span><div class="row-copy"><div class="row-title"><label for="gs-toggle-${key}"><strong>${title}</strong></label></div>${description?`<small>${description}</small>`:''}</div><span class="switch"><input id="gs-toggle-${key}" type="checkbox" role="switch" aria-label="${title}" data-field="${key}" ${draft[key]?'checked':''} ${disabled?'disabled':''}><span class="switch-track"></span></span></div></div>`;
}
function segmented(key, values, label) {
return `<div class="segmented" role="radiogroup" aria-label="${label}">${values.map(([value,title])=>`<label class="segment"><input type="radio" name="gs-${key}" data-field="${key}" value="${value}" ${draft[key]===value?'checked':''}><span>${title}</span></label>`).join('')}</div>`;
}
/* Color tiles: one card per color with a large swatch (system picker), name, hex and opacity. Groups of colors sit in one row. */
const onDark = hex => { const n = parseInt(hex.slice(1), 16); const r = n >> 16, g = (n >> 8) & 255, b = n & 255; return (0.299*r + 0.587*g + 0.114*b) < 150; };
function colorTile(key, label) {
const {c, a} = draft.colors[key];
return `<div class="color-tile"><span class="tile-swatch-wrap ${onDark(c)?'on-dark':''}"><input id="gs-color-${key}" class="color-input tile-swatch" type="color" data-color="${key}" value="${c}" aria-label="${label}"><span class="tile-name">${label}</span></span><div class="tile-meta"><span class="hex-code" data-hex="${key}">${c.toUpperCase()}</span><label class="unit-inside tile-opacity" title="Opacity"><input type="number" aria-label="${label} opacity" min="0" max="100" step="1" data-opacity="${key}" value="${Math.round(a*100)}"><span>%</span></label></div></div>`;
}
const colorTiles = items => `<div class="color-tiles">${items.map(([k,l])=>colorTile(k,l)).join('')}</div>`;
/* Single color: same plate as the Space settings color fields (label above, swatch, hex, optional opacity and Reset). */
function colorPlate(key, label, {opacity=true, reset=''}={}) {
const {c, a} = draft.colors[key];
return `<div class="field-stack"><label class="field-label" for="gs-color-${key}">${label}</label><div class="color-field"><input id="gs-color-${key}" class="color-input" type="color" data-color="${key}" value="${c}" aria-label="${label}"><span class="hex-code" data-hex="${key}">${c.toUpperCase()}</span>${opacity?`<label class="color-opacity"><span>Opacity</span><span class="unit-inside"><input type="number" aria-label="${label} opacity" min="0" max="100" step="1" data-opacity="${key}" value="${Math.round(a*100)}"><span>%</span></span></label>`:''}${reset?`<button class="text-link" type="button" data-action="${reset}">Reset</button>`:''}</div></div>`;
}
const northNeedle = '<svg class="dial-arrow compass-needle" id="gs-dial-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21V3m-6 6 6-6 6 6"/></svg>';
function generalCard() {
return card('general','Display',
toggle('showRoomTooltip','Show the room information window on hover','Hover a room on the plan to see its summary.','tooltip')+
toggle('radarShowLive','Show live presence on the plan','Hides or shows all live radar targets. This does not start or stop observation recording.','radar')
);
}
function zigbeeCard() {
const z = draft.zigbeeTopology, savedEnabled = saved.zigbeeTopology.enabled;
const providers = z.enabled ? `
${!savedEnabled?callout('Save this setting before loading topology data.'):''}
<div class="subsection">${subsectionHeading('ZHA')}${hint('Reads ZHA’s cached topology; it does not start a radio scan.')}<div class="action-line"><button class="button" type="button" data-action="zha" ${savedEnabled?'':'disabled'}>${icon('wifi')}Read ZHA data</button><span class="action-status">Data not loaded</span></div></div>
<div class="subsection">${subsectionHeading('Zigbee2MQTT')}<div class="field"><label class="field-label" for="gs-z2m-topics">Base topics (one per line)</label><textarea id="gs-z2m-topics" class="text-input textarea" rows="2" data-field="z2mTopics" spellcheck="false">${escape(z.z2mTopics)}</textarea></div>${callout('A network-map scan can take 10 seconds to 2 minutes and may temporarily reduce Zigbee responsiveness.',true)}<div class="action-line"><button class="button" type="button" data-action="z2m" ${savedEnabled?'':'disabled'}>${icon('refresh')}Update map · ${escape(z.z2mTopics.split('\n')[0].trim()||'zigbee2mqtt')}</button><span class="action-status">Data not loaded</span></div></div>` : '';
return card('zigbee','Zigbee links',
`<div class="setting-item"><div class="setting-row"><span class="row-icon">${icon('zigbee')}</span><div class="row-copy"><div class="row-title"><label for="gs-toggle-zigbee"><strong>Show Zigbee links when hovering over a device</strong></label></div><small>Shows observed direct neighbours only. Nothing is fetched while you hover.</small></div><span class="switch"><input id="gs-toggle-zigbee" type="checkbox" role="switch" aria-label="Show Zigbee links when hovering over a device" data-field="zigbeeEnabled" ${z.enabled?'checked':''}><span class="switch-track"></span></span></div></div>`+providers,
help.button('zigbee')
);
}
function fillsCard() {
return card('fills','Room fill colors',
`<div class="subsection first">${subsectionHeading('Lights')}${colorTiles([['light_on','Lights on'],['light_off','All lights off'],['light_none','No light sources']])}</div>`+
`<div class="subsection">${subsectionHeading('Temperature')}${colorTiles([['temp_cold','Cold'],['temp_ok','Comfortable'],['temp_hot','Hot']])}</div>`+
`<div class="subsection">${subsectionHeading('Zigbee signal')}${colorTiles([['lqi_low','Weak signal'],['lqi_high','Strong signal']])}</div>`,
help.button('fills')
);
}
function glowCard() {
return card('glow','Light-source glow',
colorTiles([['glow_base','House darkness'],['glow_light','Default light color / intensity']])+
`<div class="field-stack"><div class="field-heading-with-help"><label class="field-label" for="gs-glow-radius">Glow radius</label>${help.button('glowRadius')}</div><div class="field-unit"><span class="unit-inside"><input id="gs-glow-radius" type="number" min="0.5" step="0.5" data-field="glowRadius" value="${draft.glowRadius}" aria-describedby="gs-error-glowRadius"><span>m</span></span></div>${errorSlot('glowRadius')}</div>`
);
}
function planCard() {
const bgDetail = draft.bgMode==='static'
? `<div class="field-stack"><label class="field-label" for="gs-bg-color">Background around the plan</label><div class="color-field"><input id="gs-bg-color" class="color-input" type="color" data-field="bgColor" value="${draft.bgColor||THEME_BG}" aria-label="Background around the plan"><span class="hex-code" data-hex="bg">${(draft.bgColor||THEME_BG).toUpperCase()}</span>${draft.bgColor?`<button class="text-link" type="button" data-action="bg-theme">Reset</button>`:''}</div>${hint(draft.bgColor?'Custom background color.':'Using the theme background.')}</div>`
: '';
return card('plan','Plan',
colorPlate('wall_fill','Wall fill')+
`<div class="subsection"><div class="field-heading-with-help"><span class="field-label">Plan background</span>${help.button('bgMode')}</div>${segmented('bgMode',[['static','Static color'],['daynight','Follows the Sun']],'Plan background')}${bgDetail}</div>`
);
}
function sunCard() {
return card('sun','Sun',
`<div class="north-setting"><div class="north-controls"><div class="field-heading-with-help"><label class="field-label" for="gs-north">North on the plan</label>${help.button('north')}</div><div class="field-unit north-input"><span class="unit-inside"><input id="gs-north" type="number" min="0" max="359" step="1" data-field="northDeg" value="${draft.northDeg??''}" placeholder="not set" aria-describedby="gs-error-northDeg"><span>°</span></span>${draft.northDeg!==null?`<button class="text-link" type="button" data-action="clear-north">Clear</button>`:''}</div></div><div class="north-indicator" aria-hidden="true"><span>N</span><div class="north-dial">${northNeedle}</div></div></div>${errorSlot('northDeg')}`+
`<div class="subsection">${toggle('sunRays','Sunlight through windows','Draw sun rays through window openings.','sun')}</div>`+
`<div class="subsection"><span class="field-label">Sun rays</span>${segmented('sunRayOrigin',[['inner','From the inner window corners'],['outer','From the outer window corners']],'Sun rays')}</div>`
);
}
function dataCard() {
return card('data','Data',
`<div class="subsection first">${subsectionHeading('Backup and transfer')}${hint('Download a portable JSON backup or preview and import a backup made by House Plan.')}<div class="action-line"><button class="button" type="button" data-action="export">${icon('download')}Export</button><button class="button" type="button" data-action="import">${icon('upload')}Import</button></div></div>`+
`<div class="subsection">${subsectionHeading('Plan maintenance')}${hint('Updates data models, aligns plan elements to the grid and merges redundant wall fragments. An exact report is shown before anything is stored.')}<div class="action-line"><button class="button" type="button" data-action="optimize">${icon('broom')}Optimize plans</button></div></div>`
);
}
/* Validation, chrome and state. */
function errorFor(key) {
if (key==='glowRadius' && (draft.glowRadius==='' || !Number.isFinite(Number(draft.glowRadius)) || Number(draft.glowRadius)<=0)) return 'Enter a radius greater than 0.';
if (key==='northDeg' && draft.northDeg!==null && (draft.northDeg==='' || !Number.isInteger(Number(draft.northDeg)) || draft.northDeg<0 || draft.northDeg>359)) return 'Enter a whole number between 0° and 359°.';
return '';
}
const errorFields = ['glowRadius','northDeg'];
const allErrors = () => errorFields.map(k=>[k,errorFor(k)]).filter(([,m])=>m);
const errorSlot = key => `<div class="error" id="gs-error-${key}" aria-live="polite" ${errorFor(key)?'':'hidden'}>${errorFor(key)}</div>`;
function renderMain() {
closeHelp();
content.innerHTML = generalCard()+zigbeeCard()+fillsCard()+glowCard()+planCard()+sunCard()+dataCard();
}
function updateChrome() {
const errors = allErrors();
$('#gs-save').disabled = !dirty() || errors.length>0;
const status = $('#gs-status');
status.classList.toggle('dirty', dirty());
status.hidden = !dirty() && !errors.length;
status.textContent = dirty() ? 'Unsaved changes' : '';
if (errors.length) status.innerHTML = `<button class="text-link" data-action="review-errors">Review ${errors.length} ${errors.length===1?'field':'fields'}</button>`;
errorFields.forEach(key => {
const el = $(`#gs-error-${key}`), message = errorFor(key);
if (el) { el.hidden = !message; el.textContent = message; }
$$(`[data-field="${key}"]`, content).forEach(input => input.setAttribute('aria-invalid', String(!!message)));
});
$$('[data-hex]', content).forEach(el => { const k = el.dataset.hex; el.textContent = (k==='bg' ? (draft.bgColor||THEME_BG) : draft.colors[k].c).toUpperCase(); });
const arrow = $('#gs-dial-arrow'); if (arrow) arrow.style.transform = `rotate(${draft.northDeg??0}deg)`;
$('#gs-dial-arrow')?.parentElement.classList.toggle('unset', draft.northDeg===null);
}
const render = () => { renderMain(); updateChrome(); };
function rerenderKeepingScroll() { const top = scroller.scrollTop; renderMain(); updateChrome(); scroller.scrollTop = top; }
function updateField(el) {
const key = el.dataset.field;
if (el.dataset.color) { draft.colors[el.dataset.color].c = el.value; el.parentElement.classList.toggle('on-dark', onDark(el.value)); updateChrome(); return; }
if (el.dataset.opacity) { const n = Math.min(100, Math.max(0, Number(el.value))); if (Number.isFinite(n)) draft.colors[el.dataset.opacity].a = n/100; updateChrome(); return; }
if (!key) return;
if (key==='zigbeeEnabled') { draft.zigbeeTopology.enabled = el.checked; rerenderKeepingScroll(); return; }
if (key==='z2mTopics') { draft.zigbeeTopology.z2mTopics = el.value; $('[data-action="z2m"]', content).lastChild.textContent = `Update map · ${el.value.split('\n')[0].trim()||'zigbee2mqtt'}`; updateChrome(); return; }
if (key==='bgColor') { draft.bgColor = el.value; rerenderKeepingScroll(); return; }
if (key==='northDeg') { draft.northDeg = el.value==='' ? null : Number(el.value); updateChrome(); if ((el.value==='')!==!$('[data-action="clear-north"]', content)) rerenderKeepingScroll(); return; }
if (el.type==='checkbox') draft[key] = el.checked;
else if (el.type==='number') draft[key] = el.value==='' ? '' : Number(el.value);
else draft[key] = el.value;
if (key==='bgMode') rerenderKeepingScroll(); else updateChrome();
}
function handleAction(action) {
if (action==='review-errors') { const first = allErrors()[0]; if (!first) return; const field = $(`[data-field="${first[0]}"]`, content); field?.scrollIntoView({block:'center'}); field?.focus({preventScroll:true}); return; }
if (action==='bg-theme') draft.bgColor = null;
if (action==='clear-north') draft.northDeg = null;
if (action==='zha' || action==='z2m') { showToast('Topology data is loaded from Home Assistant in the product; this preview only shows the controls.'); return; }
if (action==='export') { showToast('The product downloads a JSON backup here.'); return; }
if (action==='import') { showToast('The product opens a file picker and previews the backup here.'); return; }
if (action==='optimize') { showToast('The product shows an exact optimization report before storing anything.'); return; }
rerenderKeepingScroll();
}
/* Open, close, save. */
let closeDecision, lastSubTrigger;
function openSettings() {
saved = load(); draft = clone(saved); render();
if (!dialog.open) dialog.show();
scroller.scrollTop = 0; $('#gs-title').focus({preventScroll:true});
}
function closeSettings() { dialog.close(); notifyDemoFormClosed('general-settings'); }
function showSub(title, body, actions) {
lastSubTrigger = document.activeElement;
subDialog.innerHTML = `<div class="sub-heading"><h2 id="gs-sub-title">${title}</h2><button class="icon-button" aria-label="Close dialog" data-sub="close">${icon('close')}</button></div>${body}<div class="sub-actions">${actions}</div>`;
subDialog.showModal();
}
function closeSub() { subDialog.close(); lastSubTrigger?.focus({preventScroll:true}); if (closeDecision) { const r = closeDecision; closeDecision = undefined; r(false); } }
function requestClose() {
if (!dirty()) { closeSettings(); return Promise.resolve(true); }
showSub('Discard your changes?', '<p>Your changes to the general settings haven’t been saved.</p>', '<button class="button" data-sub="discard">Discard changes</button><button class="button primary" data-sub="close">Keep editing</button>');
$('[data-sub="close"].primary', subDialog).focus();
return new Promise(resolve => { closeDecision = resolve; });
}
function saveSettings() {
if (!dirty() || allErrors().length) return;
const normalized = clone(draft);
normalized.zigbeeTopology.z2mTopics = normalized.zigbeeTopology.z2mTopics.split('\n').map(s=>s.trim()).filter(Boolean).join('\n') || 'zigbee2mqtt';
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized)); }
catch { showToast('Could not save locally. Enable browser storage and try again.'); return; }
saved = clone(normalized); draft = clone(saved); closeSettings();
showToast('General settings saved in this browser.');
}
content.addEventListener('input', e => { if (['checkbox','radio'].includes(e.target.type) || e.target.tagName==='SELECT') return; updateField(e.target); });
content.addEventListener('change', e => { if (['checkbox','radio'].includes(e.target.type) || e.target.tagName==='SELECT') updateField(e.target); });
content.addEventListener('click', e => { const b = e.target.closest('button[data-action]'); if (b) handleAction(b.dataset.action); });
$('#gs-status').addEventListener('click', e => { const b = e.target.closest('button[data-action]'); if (b) handleAction(b.dataset.action); });
$('#gs-close').addEventListener('click', requestClose);
$('#gs-cancel').addEventListener('click', requestClose);
$('#gs-save').addEventListener('click', saveSettings);
$('#gs-reset').addEventListener('click', () => { draft = clone(defaults); draft.zigbeeTopology.enabled = saved.zigbeeTopology.enabled; rerenderKeepingScroll(); showToast('Defaults restored. Save to keep them.'); });
dialog.addEventListener('cancel', e => { e.preventDefault(); requestClose(); });
dialog.addEventListener('keydown', e => {
if (e.key==='Escape' && helpOpen()) { e.preventDefault(); e.stopPropagation(); closeHelp(); return; }
if (e.key==='Escape' && !subDialog.open) { e.preventDefault(); e.stopPropagation(); requestClose(); }
});
subDialog.addEventListener('cancel', e => { e.preventDefault(); closeSub(); });
subDialog.addEventListener('click', e => {
const b = e.target.closest('button[data-sub]'); if (!b) return;
if (b.dataset.sub==='close') { closeSub(); return; }
if (b.dataset.sub==='discard') { const r = closeDecision; closeDecision = undefined; draft = clone(saved); closeSub(); closeSettings(); r?.(true); }
});
window.addEventListener('beforeunload', e => { if (dialog.open && dirty()) { e.preventDefault(); e.returnValue=''; } });
hydrateIcons(dialog);
registerDemoForm({id:'general-settings', title:'General settings', description:'House-wide display, fill colors, glow, background, sun and maintenance.', open:openSettings, requestClose});