Добавил больше цветовых схем

This commit is contained in:
AidarKC
2026-09-26 20:32:37 +03:00
parent f8900e531a
commit 451280edd2
5 changed files with 85 additions and 34 deletions
+14 -13
View File
@@ -1,4 +1,4 @@
// Скрытый редактор палитры: открывается долгим нажатием на «День» или «Ночь» в настройках.
// Редактор точной настройки палитры.
import {
PALETTE_PRESETS,
PALETTE_ROLES,
@@ -24,7 +24,8 @@ export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {})
</div>
<div class="palette-editor__section">
<span class="palette-editor__label">Основа</span>
<div class="tabs tabs--auto palette-editor__presets" role="radiogroup" aria-label="Готовая палитра"></div>
<select class="input palette-editor__presets" aria-label="Готовая цветовая тема"></select>
<div class="palette-editor__preview" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
</div>
<div class="palette-editor__section">
<span class="palette-editor__label">Настраиваемая тема</span>
@@ -59,13 +60,10 @@ export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {})
const errorEl = modal.querySelector('.palette-editor__error');
for (const [id, preset] of Object.entries(PALETTE_PRESETS)) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'tab-btn';
btn.setAttribute('role', 'radio');
btn.dataset.preset = id;
btn.textContent = preset.label;
presetsEl.append(btn);
const option = document.createElement('option');
option.value = id;
option.textContent = preset.label;
presetsEl.append(option);
}
const update = (mutate) => {
@@ -80,8 +78,9 @@ export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {})
const settings = getPaletteSettings();
const colors = resolvePalette(editTheme, settings);
const custom = settings.custom[editTheme];
modal.querySelectorAll('[data-preset]').forEach((btn) => {
btn.setAttribute('aria-checked', String(btn.dataset.preset === settings.preset));
presetsEl.value = settings.preset;
modal.querySelectorAll('.palette-editor__preview span').forEach((swatch, index) => {
swatch.style.backgroundColor = [colors.background, colors.surface, colors.accent, colors['surface-selected']][index];
});
modal.querySelectorAll('[data-edit-theme]').forEach((btn) => {
btn.setAttribute('aria-checked', String(btn.dataset.editTheme === editTheme));
@@ -117,8 +116,6 @@ export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {})
modal.addEventListener('click', async (event) => {
if (event.target === modal) { close(); return; }
const presetBtn = event.target.closest('[data-preset]');
if (presetBtn) { update((next) => { next.preset = presetBtn.dataset.preset; }); return; }
// data-edit-theme, а не data-theme: data-theme стоит на <html>, и closest() находил бы его при любом клике.
const themeBtn = event.target.closest('[data-edit-theme]');
if (themeBtn) { editTheme = themeBtn.dataset.editTheme; render(); return; }
@@ -143,6 +140,10 @@ export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {})
}
}
});
presetsEl.addEventListener('change', () => update((next) => {
next.preset = presetsEl.value;
next.custom = { dark: {}, light: {} };
}));
document.addEventListener('keydown', onKeydown);
render();
+6 -21
View File
@@ -2,7 +2,7 @@ import { confirmDialog } from '../components/confirm-dialog.js';
import { createTopBar } from '../components/topbar.js';
import { addAppLogEntry, authService, closeSavedProfile, state } from '../state.js';
import { isDeveloperToolsEnabled } from '../services/feature-settings.js';
import { getThemeMode, setThemeMode } from '../services/theme-service.js';
import { getThemeMode, resolveThemeMode, setThemeMode } from '../services/theme-service.js';
import { openPaletteEditor } from '../components/palette-editor.js';
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
@@ -69,6 +69,7 @@ export function render({navigate, chrome}) {
<button type="button" class="tab-btn" role="radio" data-mode="dark">Ночь</button>
</div>
<span class="meta-muted">«Авто» — как на устройстве.</span>
<button type="button" class="secondary-btn settings-theme-editor" id="settings-theme-editor">Настроить цвета</button>
</div>
<div class="nav-list">
${row('settings-device', 'Устройства', 'Сеансы, подключение устройств, ключи')}
@@ -92,30 +93,15 @@ export function render({navigate, chrome}) {
};
syncThemeSwitch();
const LONG_PRESS_MS = 600;
let longPressTimer = 0;
let longPressFired = false;
const cancelLongPress = () => { window.clearTimeout(longPressTimer); longPressTimer = 0; };
themeSwitch.addEventListener('pointerdown', (event) => {
const mode = event.target.closest('[data-mode]')?.dataset.mode;
longPressFired = false;
if (mode !== 'light' && mode !== 'dark') return;
longPressTimer = window.setTimeout(() => {
longPressFired = true;
setThemeMode(mode);
syncThemeSwitch();
openPaletteEditor({ theme: mode });
}, LONG_PRESS_MS);
});
['pointerup', 'pointerleave', 'pointercancel'].forEach((type) => themeSwitch.addEventListener(type, cancelLongPress));
// На телефоне долгое нажатие иначе вызывает системное меню выделения.
themeSwitch.addEventListener('contextmenu', (event) => event.preventDefault());
themeSwitch.addEventListener('click', (event) => {
const mode = event.target.closest('[data-mode]')?.dataset.mode;
if (!mode || longPressFired) return;
if (!mode) return;
setThemeMode(mode);
syncThemeSwitch();
});
card.querySelector('#settings-theme-editor').addEventListener('click', () => {
openPaletteEditor({ theme: resolveThemeMode() });
});
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
@@ -209,7 +195,6 @@ export function render({navigate, chrome}) {
screen.cleanup = () => {
isDisposed = true;
cancelLongPress();
};
return screen;
}
+60
View File
@@ -78,6 +78,66 @@ export const PALETTE_PRESETS = {
},
};
function mixHex(first, second, amount) {
const a = first.match(/[0-9a-f]{2}/gi).map((part) => parseInt(part, 16));
const b = second.match(/[0-9a-f]{2}/gi).map((part) => parseInt(part, 16));
return `#${a.map((value, index) => Math.round(value * (1 - amount) + b[index] * amount).toString(16).padStart(2, '0')).join('')}`;
}
const EXTRA_PRESETS = [
['ocean', 'Океан', '#101c2b', '#182a3d', '#213c55', '#edf5ff', '#a6b9cf', '#75c8f0', '#102b40', '#f9fcff', '#e4f2fb', '#173247', '#536f83', '#087eae', '#ffffff'],
['lavender', 'Лаванда', '#211b2b', '#30263d', '#473658', '#f5efff', '#bcb0cf', '#c5a0f5', '#f1ebf7', '#fffaff', '#e9def3', '#31283a', '#6d6076', '#7950a8', '#ffffff'],
['graphite', 'Графит', '#17191c', '#222529', '#34383d', '#f2f4f5', '#adb4ba', '#c4cbd1', '#eef0f1', '#ffffff', '#e0e4e6', '#202427', '#5b646a', '#46535b', '#ffffff'],
['sakura', 'Сакура', '#291b25', '#382430', '#503140', '#fff0f5', '#c9aab7', '#f39abb', '#fff0f4', '#fffafd', '#f7dce6', '#39232d', '#795967', '#b73565', '#ffffff'],
['emerald', 'Изумруд', '#10231e', '#19332b', '#24483a', '#e8f8ef', '#a0c5ae', '#7fe0aa', '#eaf5ed', '#fbfffc', '#d8eddf', '#20372a', '#587363', '#187747', '#ffffff'],
['sunrise', 'Рассвет', '#2b2017', '#3b2b1c', '#58402a', '#fff4df', '#d0b894', '#ffc36b', '#fff3df', '#fffaf0', '#f5e4c4', '#3a2b1b', '#78664c', '#a45c12', '#ffffff'],
['cyberpunk', 'Киберпанк', '#171329', '#241b3b', '#382657', '#f5f0ff', '#b9a9d8', '#ff70dc', '#f6edfb', '#fffbff', '#eddcf5', '#30233b', '#705b7b', '#a12b91', '#ffffff'],
['arctic', 'Арктика', '#14242a', '#1d333a', '#294750', '#edfaff', '#a7c4cc', '#82e2e5', '#e8f7f8', '#fbffff', '#d6eef0', '#20373a', '#587477', '#087f83', '#ffffff'],
['coral', 'Коралл', '#2b1d1b', '#3a2925', '#563b34', '#fff1ec', '#d1b2a8', '#ff9d83', '#fff0e9', '#fffaf7', '#f9dfd4', '#3b2923', '#7b6258', '#bc4931', '#ffffff'],
['grape', 'Виноград', '#211829', '#302238', '#493151', '#f8efff', '#c1a9cd', '#d39afa', '#f4eafa', '#fffaff', '#e9d8f2', '#35253d', '#725d7a', '#8145a9', '#ffffff'],
['forest', 'Лес', '#19231b', '#263328', '#394b39', '#f0f7ed', '#b1c1a7', '#b0d17c', '#f0f4e8', '#fcfff7', '#e4ebd7', '#303828', '#69735d', '#52751f', '#ffffff'],
['ruby', 'Рубин', '#28181e', '#382128', '#512e39', '#fff0f2', '#ceabb3', '#fa8095', '#fff0f1', '#fffafb', '#f4dce0', '#3c242b', '#795861', '#b52d4d', '#ffffff'],
['lagoon', 'Лагуна', '#102323', '#193333', '#254949', '#e9f8f4', '#a1c4bc', '#67d8c2', '#e7f5ef', '#fbfffc', '#d3eee4', '#1e3932', '#56746b', '#087e70', '#ffffff'],
['copper', 'Медь', '#281e19', '#392a22', '#533d30', '#fff2e8', '#ceb5a2', '#e9a879', '#f8eee5', '#fffaf5', '#eddfd1', '#392a21', '#786657', '#a65e2b', '#ffffff'],
['neon', 'Неон', '#141f1b', '#1e3028', '#2b4939', '#edfff2', '#a6c6ae', '#a7f36b', '#eff7e9', '#fcfff8', '#dff0d0', '#253923', '#5b7352', '#4c861d', '#ffffff'],
['paper', 'Бумага', '#20201e', '#2e2e2b', '#454540', '#f6f5ef', '#bcbcb2', '#d6c49b', '#f3f0e8', '#fffdf7', '#e8e2d3', '#302e28', '#716d61', '#76613b', '#ffffff'],
['cosmos', 'Космос', '#171a30', '#222744', '#323b62', '#f0f2ff', '#aeb6d6', '#9eacff', '#edf0fc', '#fbfcff', '#dfe4fa', '#252b46', '#5c6687', '#4f61c5', '#ffffff'],
['mint', 'Мята', '#14231f', '#1f332c', '#2e4940', '#ebfbf4', '#a9c6b9', '#8fe2c0', '#eaf5ee', '#fbfffc', '#d7eee1', '#21392f', '#597568', '#16805b', '#ffffff'],
['sandstone', 'Песчаник', '#272019', '#382e23', '#514333', '#fff5e8', '#c9b69c', '#f0bd78', '#f6efe3', '#fffdf8', '#e9deca', '#3a3023', '#776b58', '#96601b', '#ffffff'],
['dusk', 'Сумерки', '#211b2a', '#30263c', '#493658', '#f5efff', '#b8acc8', '#e0a6c8', '#f2edf4', '#fffaff', '#e8dfe9', '#352a39', '#706174', '#98577d', '#ffffff'],
['lemon', 'Лимон', '#202216', '#30331e', '#454a2b', '#f8f9e9', '#bfc39c', '#e2e96d', '#f2f3e3', '#fdfff5', '#e6e9cc', '#353822', '#71755a', '#737c16', '#ffffff'],
['obsidian', 'Обсидиан', '#151719', '#202326', '#30363b', '#f2f5f7', '#aab5bc', '#8db7c7', '#e9eef0', '#fbfeff', '#dce5e8', '#252c30', '#5c6a70', '#3b778c', '#ffffff'],
];
function makePreset(label, dark, light) {
const toColors = (palette, theme) => {
const [background, surface, ...rest] = palette;
const [selected, text, secondary, accent, onAccent] = rest.length === 5
? rest
: [mixHex(surface, rest[0], 0.08), ...rest];
return {
background,
surface,
'surface-selected': selected,
'text-primary': text,
'text-secondary': secondary,
'border-subtle': mixHex(surface, text, 0.14),
'border-control': mixHex(surface, text, 0.4),
accent,
'on-accent': onAccent,
'reaction-active': theme === 'dark' ? '#ff9bb3' : '#c02a55',
danger: theme === 'dark' ? '#ffa7a3' : '#b3261e',
success: theme === 'dark' ? '#8fdab4' : '#2c6b4a',
warning: theme === 'dark' ? '#e8c585' : '#7d5a10',
};
};
return { label, dark: toColors(dark, 'dark'), light: toColors(light, 'light') };
}
for (const [id, label, ...colors] of EXTRA_PRESETS) {
PALETTE_PRESETS[id] = makePreset(label, colors.slice(0, 7), colors.slice(7));
}
const DEFAULT_PRESET = 'club';
const HEX_COLOR = /^#[0-9a-f]{6}$/i;
@@ -1,5 +1,9 @@
/* Скрытый редактор палитры (js/components/palette-editor.js). */
.palette-editor__card { display: grid; gap: 16px; }
.palette-editor__presets { width: 100%; min-height: 48px; padding: 0 40px 0 14px; color: var(--text-primary); background-color: var(--surface); }
.palette-editor__presets option { color: var(--text-primary); background: var(--surface); }
.palette-editor__preview { display: flex; gap: 8px; }
.palette-editor__preview span { width: 30px; height: 20px; border: 1px solid var(--border-control); border-radius: 4px; }
.palette-editor__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.palette-editor__close { width: 44px; min-width: 44px; padding: 0; font-size: 18px; color: var(--text-secondary); }
.palette-editor__section { display: grid; gap: 8px; }
+1
View File
@@ -389,5 +389,6 @@
.settings-shine-logo-button { min-height: 0; padding: 0; margin: 0 auto; }
.settings-sections { gap: var(--space-4); }
.settings-theme-card { gap: var(--space-2); }
.settings-theme-editor { width: 100%; }
.settings-intro { padding-top: var(--space-3); }
.settings-intro > p { margin: 0; padding-inline: var(--space-1); font-size: var(--text-sm); line-height: var(--leading-sm); }