SHA256
Добавил больше цветовых схем
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user