diff --git a/shine-UI/js/components/palette-editor.js b/shine-UI/js/components/palette-editor.js
index 9b73c546..b7a43a80 100644
--- a/shine-UI/js/components/palette-editor.js
+++ b/shine-UI/js/components/palette-editor.js
@@ -1,4 +1,4 @@
-// Скрытый редактор палитры: открывается долгим нажатием на «День» или «Ночь» в настройках.
+// Редактор точной настройки палитры.
import {
PALETTE_PRESETS,
PALETTE_ROLES,
@@ -24,7 +24,8 @@ export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {})
Настраиваемая тема
@@ -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 стоит на , и 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();
diff --git a/shine-UI/js/pages/settings-view.js b/shine-UI/js/pages/settings-view.js
index 614160b4..8b10f964 100644
--- a/shine-UI/js/pages/settings-view.js
+++ b/shine-UI/js/pages/settings-view.js
@@ -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}) {
«Авто» — как на устройстве.
+
${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;
}
diff --git a/shine-UI/js/services/theme-service.js b/shine-UI/js/services/theme-service.js
index b9aa3f66..f16a0e73 100644
--- a/shine-UI/js/services/theme-service.js
+++ b/shine-UI/js/services/theme-service.js
@@ -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;
diff --git a/shine-UI/styles/components/palette-editor.css b/shine-UI/styles/components/palette-editor.css
index 217686a2..87990574 100644
--- a/shine-UI/styles/components/palette-editor.css
+++ b/shine-UI/styles/components/palette-editor.css
@@ -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; }
diff --git a/shine-UI/styles/features/settings.css b/shine-UI/styles/features/settings.css
index 9e350f96..5127bb6a 100644
--- a/shine-UI/styles/features/settings.css
+++ b/shine-UI/styles/features/settings.css
@@ -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); }