feat: size picker in the appearance menu and admin default (T5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* tonalmine: per-user appearance (mode, contrast, color palette).
|
||||
* tonalmine: per-user appearance (mode, contrast, size, color palette).
|
||||
*
|
||||
* Redmine loads this file in <head>, so the saved choice is applied before the page renders
|
||||
* (no flash of the default colors). The picker is added to the profile menu and the mobile
|
||||
@@ -18,9 +18,10 @@
|
||||
];
|
||||
var MODES = [['auto', 'Auto'], ['light', 'Light'], ['dark', 'Dark']];
|
||||
var CONTRASTS = [['standard', 'Standard'], ['medium', 'Medium'], ['high', 'High']];
|
||||
var SIZES = [['compact', 'Compact'], ['normal', 'Normal'], ['large', 'Large']];
|
||||
var KEY = 'tonalmine';
|
||||
var CUSTOM_SEL = ':root[data-palette="custom"]';
|
||||
var DEFAULT_KEYS = ['mode', 'contrast', 'palette', 'seed', 'css'];
|
||||
var DEFAULT_KEYS = ['mode', 'contrast', 'size', 'palette', 'seed', 'css'];
|
||||
var root = document.documentElement;
|
||||
|
||||
function load() {
|
||||
@@ -34,6 +35,8 @@
|
||||
else root.removeAttribute('data-mode');
|
||||
if (prefs.contrast === 'medium' || prefs.contrast === 'high') root.setAttribute('data-contrast', prefs.contrast);
|
||||
else root.removeAttribute('data-contrast');
|
||||
if (prefs.size === 'compact' || prefs.size === 'large') root.setAttribute('data-size', prefs.size);
|
||||
else root.removeAttribute('data-size');
|
||||
// The custom palette's rules are generated once (see withScheme) and kept with the prefs.
|
||||
if (prefs.palette === 'custom' && prefs.css) {
|
||||
var style = document.getElementById('nm-custom-palette');
|
||||
@@ -73,7 +76,7 @@
|
||||
// palette, seed and css form one part: a custom palette brings its own generated CSS.
|
||||
function merge(base, top) {
|
||||
var src = top.palette ? top : base;
|
||||
return { mode: top.mode || base.mode, contrast: top.contrast || base.contrast,
|
||||
return { mode: top.mode || base.mode, contrast: top.contrast || base.contrast, size: top.size || base.size,
|
||||
palette: src.palette, seed: src.seed, css: src.css };
|
||||
}
|
||||
|
||||
@@ -130,6 +133,7 @@
|
||||
function pickerHtml(prefs, isDefault) {
|
||||
var mode = prefs.mode || 'auto';
|
||||
var contrast = prefs.contrast || 'standard';
|
||||
var size = prefs.size || 'normal';
|
||||
var palette = prefs.palette || PALETTES[0][0];
|
||||
return '<div class="nm-appearance' + (isDefault ? ' nm-appearance--default" data-nm-default' : '"') + '>' +
|
||||
(isDefault ? '' : '<div class="nm-appearance__title">Appearance</div>') +
|
||||
@@ -140,6 +144,10 @@
|
||||
'<div class="nm-segmented" role="group" aria-label="Contrast">' +
|
||||
CONTRASTS.map(function (c) { return button('data-nm-contrast', c[0], c[1], c[0] === contrast); }).join('') +
|
||||
'</div>' +
|
||||
'<div class="nm-appearance__title">Size</div>' +
|
||||
'<div class="nm-segmented" role="group" aria-label="Size">' +
|
||||
SIZES.map(function (s) { return button('data-nm-size', s[0], s[1], s[0] === size); }).join('') +
|
||||
'</div>' +
|
||||
'<div class="nm-swatches" role="group" aria-label="Color">' +
|
||||
PALETTES.map(function (p) {
|
||||
return button('data-nm-palette', p[0], '', p[0] === palette,
|
||||
@@ -162,6 +170,9 @@
|
||||
picker.querySelectorAll('[data-nm-contrast]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-contrast') === (prefs.contrast || 'standard')));
|
||||
});
|
||||
picker.querySelectorAll('[data-nm-size]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-size') === (prefs.size || 'normal')));
|
||||
});
|
||||
picker.querySelectorAll('[data-nm-palette]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-palette') === (prefs.palette || PALETTES[0][0])));
|
||||
});
|
||||
@@ -190,13 +201,14 @@
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var target = event.target.closest('[data-nm-mode], [data-nm-contrast], [data-nm-palette], [data-nm-reset]');
|
||||
var target = event.target.closest('[data-nm-mode], [data-nm-contrast], [data-nm-size], [data-nm-palette], [data-nm-reset]');
|
||||
if (!target) return;
|
||||
var isDefault = !!target.closest('[data-nm-default]');
|
||||
var prefs = isDefault ? draft : own;
|
||||
if (target.hasAttribute('data-nm-reset')) own = {};
|
||||
else if (target.hasAttribute('data-nm-mode')) prefs.mode = target.getAttribute('data-nm-mode');
|
||||
else if (target.hasAttribute('data-nm-contrast')) prefs.contrast = target.getAttribute('data-nm-contrast');
|
||||
else if (target.hasAttribute('data-nm-size')) prefs.size = target.getAttribute('data-nm-size');
|
||||
// The custom swatch opens the color dialog; it only selects the palette once a color was picked.
|
||||
else if (target.getAttribute('data-nm-palette') !== 'custom' || prefs.css) prefs.palette = target.getAttribute('data-nm-palette');
|
||||
changed(isDefault, true);
|
||||
@@ -225,26 +237,26 @@
|
||||
// select. The hidden inputs are saved by the settings form as Setting.plugin_redmine_tonalmine.
|
||||
function defaultPicker(select) {
|
||||
draft = merge({}, defaults);
|
||||
// One labelled form row per group (mode, contrast, color), each in its own default picker
|
||||
// One labelled form row per group (mode, contrast, size, color), each in its own default picker
|
||||
// wrapper so sync() and the click handlers treat it like the whole picker.
|
||||
var picker = document.createElement('div');
|
||||
picker.innerHTML = pickerHtml(draft, true);
|
||||
var groups = picker.querySelectorAll('[role="group"]');
|
||||
var rows = ['Default appearance', 'Contrast', 'Color'].map(function (label, i) {
|
||||
var rows = ['Default appearance', 'Contrast', 'Size', 'Color'].map(function (label, i) {
|
||||
var row = document.createElement('p');
|
||||
row.innerHTML = '<label>' + label + '</label><div class="nm-appearance nm-appearance--default" data-nm-default></div>';
|
||||
row.lastChild.appendChild(groups[i]);
|
||||
return row;
|
||||
});
|
||||
rows[2].insertAdjacentHTML('beforeend', DEFAULT_KEYS.map(function (key) {
|
||||
rows[3].insertAdjacentHTML('beforeend', DEFAULT_KEYS.map(function (key) {
|
||||
return '<input type="hidden" name="settings[plugin_redmine_tonalmine][' + key + ']" data-nm-default-input="' + key + '">';
|
||||
}).join(''));
|
||||
select.closest('p').after(rows[0], rows[1], rows[2]);
|
||||
select.closest('p').after(rows[0], rows[1], rows[2], rows[3]);
|
||||
// Only shown, and only posted, while Tonalmine is selected; otherwise the saved default stays.
|
||||
function toggle() {
|
||||
var on = select.value === 'tonalmine';
|
||||
rows.forEach(function (row) { row.style.display = on ? '' : 'none'; });
|
||||
rows[2].querySelectorAll('[data-nm-default-input]').forEach(function (input) { input.disabled = !on; });
|
||||
rows[3].querySelectorAll('[data-nm-default-input]').forEach(function (input) { input.disabled = !on; });
|
||||
}
|
||||
select.addEventListener('change', toggle);
|
||||
toggle();
|
||||
|
||||
Reference in New Issue
Block a user