Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/img/admin/app-edit-labels-af3ec51a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/img/admin/app-edit-labels-b87badcc.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/img/admin/app-edit-labels-dark-7cf4b4cd.png
Binary file not shown.
Binary file removed public/img/admin/appearance-14c98840.png
Binary file not shown.
Binary file added public/img/admin/appearance-dark-449811e9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/img/admin/appearance-dark-ff2e7225.png
Binary file not shown.
Binary file added public/img/admin/appearance-e9d99c15.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=045df327';
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=55685187';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c8c550c';
import { state } from '/js/admin-state.js?v=5a5d655f';
import { state } from '/js/admin-state.js?v=831e219e';
import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import {
isDockBlocked,
Expand All @@ -21,10 +21,10 @@ import {
reveal,
setTogDisabled,
swapContent,
} from '/js/admin-shared.js?v=dc0e02b6';
import { createListbox } from '/js/listbox.js?v=96066369';
import { MAX_LABELS } from '/js/badge-logic.js?v=b3c8b6c2';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=a48fbc58';
} from '/js/admin-shared.js?v=52e149f5';
import { createListbox } from '/js/listbox.js?v=32f787c3';
import { MAX_LABELS } from '/js/badge-logic.js?v=ad283693';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=42c11a9b';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { iconSvg } from '/js/icon-set.js?v=606a68c6';
Expand Down Expand Up @@ -210,9 +210,9 @@ export function buildAppForm(body, item) {
const pv0 = el('ipv');
if (pv0) pv0.style.background = rc(state.scol);
renderColorControl(el('icon-color-slot'), {
value: state.scol || 'dark',
value: state.scol || 'auto',
idPrefix: 'icon-col',
semantic: true,
variant: 'tile',
label: t('common.color'),
onChange(v) {
state.scol = v;
Expand All @@ -223,7 +223,7 @@ export function buildAppForm(body, item) {
renderColorControl(el('static-color-slot'), {
value: staticBadge.color || BADGE_DEFAULT,
idPrefix: 'static-col',
swatchColors: BADGE_SWATCHES,
variant: 'badge',
label: t('common.color'),
});
state._bpar = normKvRows(act.params);
Expand Down Expand Up @@ -409,7 +409,7 @@ function renderActLabels(host) {
renderColorControl(el(`albl-col-slot-${i}`), {
value: l.color || BADGE_DEFAULT,
idPrefix: `albl-col-${i}`,
swatchColors: BADGE_SWATCHES,
variant: 'badge',
label: t('common.color'),
});
const pathRow = el(`albl-path-row-${i}`);
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ag, ap } from '/js/admin-shared.js?v=dc0e02b6';
import { ag, ap } from '/js/admin-shared.js?v=52e149f5';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { el, inp as inpById, qa } from '/js/utils.js?v=045df327';
import { el, inp as inpById, qa } from '/js/utils.js?v=55685187';

export async function checkAuth(onLogin) {
try {
Expand Down
115 changes: 75 additions & 40 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=dc0e02b6';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=52e149f5';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=045df327';
import { qa, q } from '/js/utils.js?v=55685187';
import { iconSvg } from '/js/icon-set.js?v=606a68c6';
import { HUE_NAMES, ROLE_NAMES, TILE_KEYWORDS, pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43';

const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'];
export const BADGE_DEFAULT = '#1e6ef4';
export const BADGE_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#1e6ef4', '#cb30df'];
export const BADGE_DEFAULT = 'info';
const _ccIco = {
hueLo: iconSvg('hue-lo', 16),
hueHi: iconSvg('hue-hi', 16),
Expand Down Expand Up @@ -111,32 +111,64 @@ function _hexToHsv(hex) {

export const _internals = { cssToHex: _cssToHex, hsvToRgb: _hsvToRgb, hexToHsv: _hexToHsv };

const COLOR_KEYS = {
dark: 'color.dark',
light: 'color.light',
auto: 'color.auto',
clear: 'color.clear',
accent: 'color.accent',
success: 'color.success',
warning: 'color.warning',
danger: 'color.danger',
info: 'color.info',
red: 'color.red',
orange: 'color.orange',
yellow: 'color.yellow',
green: 'color.green',
mint: 'color.mint',
teal: 'color.teal',
cyan: 'color.cyan',
blue: 'color.blue',
indigo: 'color.indigo',
purple: 'color.purple',
pink: 'color.pink',
brown: 'color.brown',
gray: 'color.gray',
};

/** @param {string} v */
const _swatchFill = v => tileColor(v, pageTheme());

/** @param {HTMLElement} container
@param {{ value?: string, idPrefix?: string,
onChange?: (value: string) => void, semantic?: boolean,
onChange?: (value: string) => void, variant?: 'tile'|'badge',
swatchColors?: string[], label?: string }} [opts] */
export function renderColorControl(
container,
{ value = '#0289ff', idPrefix, onChange, semantic = false, swatchColors = CC_SWATCHES, label = 'Color' } = {},
{ value = '#0289ff', idPrefix, onChange, variant, swatchColors = CC_SWATCHES, label = 'Color' } = {},
) {
const isSem = v => v === 'dark' || v === 'light';
const init = _hexToHsv(isSem(value) ? '#0289ff' : value) || { h: 212, s: 99, v: 100 };
const top = variant === 'tile' ? TILE_KEYWORDS : variant === 'badge' ? ROLE_NAMES : [];
const hues = variant ? HUE_NAMES : [];
const isKw = v => typeof v === 'string' && (top.includes(v) || hues.includes(v));
const kwName = v => t(COLOR_KEYS[v]);
const init = _hexToHsv(isKw(value) ? _swatchFill(value) || '#0289ff' : value) || { h: 212, s: 99, v: 100 };
const kwSwatch = v =>
html`<button type="button" class="cc-swatch cc-kw cc-kw-${v}" data-v="${v}" title="${kwName(v)}" aria-label="${kwName(v)}"></button>`;
const swatch = h => html`<button type="button" class="cc-swatch" data-v="${h}" aria-label="${h}"></button>`;
const swatches = semantic
? html`<button type="button" class="cc-swatch cc-sem cc-sem-dark" data-v="dark" title="${t('appearance.themeDark')}" aria-label="${t('appearance.displayDark')}"></button>
<button type="button" class="cc-swatch cc-sem cc-sem-light" data-v="light" title="${t('appearance.themeLight')}" aria-label="${t('appearance.displayLight')}"></button>
<button type="button" class="cc-swatch cc-rainbow" data-v="custom" aria-label="${t('appearance.customColor')}"></button>
${['#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'].map(swatch)}`
: html`<button type="button" class="cc-swatch cc-rainbow" data-v="custom" aria-label="${t('appearance.customColor')}"></button>
${swatchColors.map(swatch)}`;
const rainbow = html`<button type="button" class="cc-swatch cc-rainbow" data-v="custom" aria-label="${t('appearance.customColor')}"></button>`;
const swatches = variant ? html`${top.map(kwSwatch)}${rainbow}` : html`${rainbow}${swatchColors.map(swatch)}`;
const wrap = document.createElement('div');
const slider = (label, cls, id, max, val, lo, hi) => html`
<div class="row hsb-row"><span class="rl">${label}</span><div class="hsb-track"><span class="hsb-ico">${raw(lo)}</span><input type="range" class="${cls}" id="${id}" min="0" max="${max}" value="${val}" aria-label="${label}"><span class="hsb-ico">${raw(hi)}</span></div></div>`;
const hueRow = hues.length
? html`<div class="row cc-row cc-hues"><span class="rl">${t('color.system')}</span><div class="cc-sw">${hues.map(kwSwatch)}</div></div>`
: '';
setHtml(
wrap,
html`
<div class="row cc-row"><span class="rl">${label}</span><div class="cc-sw">${swatches}</div></div>
<div class="row-wrap reveal cc-tune"><div class="reveal-in">
${hueRow}
${slider('Hue', 'hsb-range hsb-hue', `${idPrefix}-h`, 360, init.h, _ccIco.hueLo, _ccIco.hueHi)}
${slider('Saturation', 'hsb-range', `${idPrefix}-s`, 100, init.s, _ccIco.satLo, _ccIco.satHi)}
${slider('Brightness', 'hsb-range', `${idPrefix}-v`, 100, init.v, _ccIco.brLo, _ccIco.brHi)}
Expand All @@ -146,8 +178,9 @@ export function renderColorControl(
/* The markup carries no style attribute, so the page keeps style-src without
'unsafe-inline'. A swatch paints itself from its own value. */
for (const b of wrap.querySelectorAll('.cc-swatch[data-v]')) {
const v = b.getAttribute('data-v');
if (v && v[0] === '#') /** @type {HTMLElement} */ (b).style.background = v;
const v = b.getAttribute('data-v') || '';
const fill = v[0] === '#' ? v : v === 'auto' || v === 'clear' ? '' : _swatchFill(v);
if (fill) /** @type {HTMLElement} */ (b).style.background = fill;
}
const rows = /** @type {HTMLElement[]} */ ([...wrap.children]);
rows.forEach(r => container.appendChild(r));
Expand All @@ -161,8 +194,8 @@ export function renderColorControl(
hidden.type = 'hidden';
hidden.id = `${idPrefix}-val`;
container.appendChild(hidden);
let mode = isSem(value) ? value : 'color';
let showTune = false;
let mode = isKw(value) ? value : 'color';
let showTune = hues.includes(mode);
/* The first paint restores stored state, so it must not animate. */
let _painted = false;
/* The value as it was stored. The sliders are integers, so reading a colour
Expand All @@ -179,6 +212,13 @@ export function renderColorControl(
rb = _rgb(b);
return ra && rb && ra.every((n, i) => Math.abs(n - rb[i]) <= 3);
};
const setSliders = hex => {
const hv = _hexToHsv(hex);
if (!hv) return;
hEl.value = String(hv.h);
sEl.value = String(hv.s);
vEl.value = String(hv.v);
};
function paint() {
const h = +hEl.value,
v = +vEl.value,
Expand All @@ -195,16 +235,15 @@ export function renderColorControl(
vEl.style.setProperty('--knob-fill', hex);
qa('.cc-swatch', container).forEach(b => {
let on = false;
if (mode === 'dark' || mode === 'light') on = b.dataset.v === mode;
else if (!showTune) on = b.dataset.v !== 'custom' && !b.classList.contains('cc-sem') && _near(b.dataset.v, hex);
if (mode !== 'color') on = b.dataset.v === mode;
else if (!showTune) on = b.dataset.v !== 'custom' && !b.classList.contains('cc-kw') && _near(b.dataset.v, hex);
b.classList.toggle('on', on);
});
const rb = q('.cc-rainbow', container);
if (rb) rb.classList.toggle('on', mode === 'color' && showTune);
if (rb) rb.classList.toggle('on', showTune && (mode === 'color' || hues.includes(mode)));
reveal(tune, showTune, !_painted);
if (!codeRv.closest('.editing')) {
codeRv.textContent =
mode === 'color' ? hex : t(mode === 'dark' ? 'appearance.displayDark' : 'appearance.displayLight');
codeRv.textContent = mode === 'color' ? hex : kwName(mode);
codeRv.classList.remove('is-ph');
}
hidden.value = mode === 'color' ? (pristine ?? hex) : mode;
Expand All @@ -224,48 +263,44 @@ export function renderColorControl(
);
qa('.cc-swatch', container).forEach(b =>
b.addEventListener('click', () => {
if (b.dataset.v === 'dark' || b.dataset.v === 'light') {
mode = b.dataset.v;
showTune = false;
const v = b.dataset.v || '';
if (v === 'custom') {
showTune = !(showTune && (mode === 'color' || hues.includes(mode)));
if (showTune && !hues.includes(mode)) mode = 'color';
commit();
return;
}
if (b.dataset.v === 'custom') {
mode = 'color';
showTune = true;
if (isKw(v)) {
mode = v;
showTune = hues.includes(v);
if (showTune) setSliders(_swatchFill(v));
commit();
return;
}
mode = 'color';
showTune = false;
const hv = _hexToHsv(b.dataset.v);
if (hv) {
hEl.value = String(hv.h);
sEl.value = String(hv.s);
vEl.value = String(hv.v);
}
setSliders(v);
commit();
}),
);
initInlineEdit(`${idPrefix}-code-row`, `${idPrefix}-hex`, {
root: container,
placeholder: '#rrggbb or any CSS color',
onCommit(val) {
if (mode !== 'color' && val === kwName(mode)) return;
const { value, ok } = normalizeColorInput(val);
const hv = ok ? _hexToHsv(value) : null;
if (hv) {
mode = 'color';
showTune = true;
hEl.value = String(hv.h);
sEl.value = String(hv.s);
vEl.value = String(hv.v);
setSliders(value);
} else if (val) toast(t('toast.colorInvalid'), 'err');
commit();
},
});
if (mode === 'color') {
const presets = qa('.cc-swatch', container)
.filter(b => b.dataset.v !== 'custom' && !b.classList.contains('cc-sem'))
.filter(b => b.dataset.v !== 'custom' && !b.classList.contains('cc-kw'))
.map(b => b.dataset.v);
showTune = !presets.some(pv => _near(pv, value));
}
Expand Down
6 changes: 3 additions & 3 deletions public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
it is near, and which classes say so. */

import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=5a5d655f';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { qa } from '/js/utils.js?v=045df327';
import { state } from '/js/admin-state.js?v=831e219e';
import { snapshotItems } from '/js/admin-save-logic.js?v=858f3f84';
import { qa } from '/js/utils.js?v=55685187';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
12 changes: 6 additions & 6 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
The filter and the collapsed folders are in admin-state.js, because the page
reads and writes them too. */

import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=831e219e';
import { snapshotItems } from '/js/admin-save-logic.js?v=858f3f84';
import { reorderItems } from '/js/admin-logic.js?v=e3673bd7';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=227e3153';
import { paintIcon } from '/js/admin-shared.js?v=dc0e02b6';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=045df327';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=a9bbc794';
import { paintIcon } from '/js/admin-shared.js?v=52e149f5';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=55685187';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { sizeLabel } from '/js/admin-widget-form.js?v=3237dd3e';
import { sizeLabel } from '/js/admin-widget-form.js?v=17a37df4';
import { widgetGlyph } from '/js/widget-glyphs.js?v=3adb57ad';
import { iconSvg } from '/js/icon-set.js?v=606a68c6';

Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-save-logic.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
/* Keep this module free of the DOM. The DOM reads stay in doSave. */

const BADGE_DEFAULT = '#1e6ef4';
const BADGE_DEFAULT = 'info';

/* Keep in step with MAX_LABELS in badge-logic.js. */
const MAX_LABELS = 5;
Expand Down
26 changes: 18 additions & 8 deletions public/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=dc0e02b6';
import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=52e149f5';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import {
Expand All @@ -9,15 +9,15 @@ import {
BLOCK,
} from '/js/admin-logic.js?v=e3673bd7';
import { confirmText } from '/js/modal.js?v=11fa1eff';
import { el, inp, setUserText } from '/js/utils.js?v=045df327';
import { el, inp, setUserText } from '/js/utils.js?v=55685187';

/* Mirrors the server's rule: auth cannot be switched on with no password. */
let _passwordSet = false;
let _authEnabled = false;

/** @type {{ dirty: () => boolean, reset: () => void } | null} */
/** @type {{ dirty: () => boolean, reset: (force?: boolean) => void } | null} */
let _srvTrack = null;
/** @type {{ dirty: () => boolean, reset: () => void } | null} */
/** @type {{ dirty: () => boolean, reset: (force?: boolean) => void } | null} */
let _bgTrack = null;

const _val = (...ids) => {
Expand Down Expand Up @@ -60,19 +60,29 @@ const readWallpaperForm = () =>
function trackSave(buttonId, read) {
const btn = /** @type {HTMLButtonElement|null} */ (el(buttonId));
const tr = createDirtyTracker(read);
let touched = false;
const sync = () => {
if (btn) btn.disabled = !tr.dirty();
};
/* On the document, not the section: a picker's option list is attached to
the body, and a choice made there must still enable Save. Deferred: pickers
and inline editors update their value after the event. */
for (const type of ['input', 'change', 'click', 'keyup', 'focusout'])
document.addEventListener(type, () => setTimeout(sync));
document.addEventListener(type, () =>
setTimeout(() => {
if (tr.dirty()) touched = true;
sync();
}),
);
sync();
return {
dirty: tr.dirty,
reset: () => {
tr.reset();
/* Do not re-baseline a touched form. The edit is swallowed and Save never
lights again.
@param {boolean} [force] */
reset: (force = true) => {
if (force || !touched) tr.reset();
if (force) touched = false;
sync();
},
};
Expand Down Expand Up @@ -251,7 +261,7 @@ export function loadSettings(c) {

_srvTrack = trackSave('srv-save', readServerForm);
_bgTrack = trackSave('bg-save', readWallpaperForm);
syncAuthFromServer().then(() => _srvTrack?.reset());
syncAuthFromServer().then(() => _srvTrack?.reset(false));
}

async function syncAuthFromServer() {
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-shared.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* Stateless helpers shared by the admin modules. Mutable state stays out. */
import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=e3673bd7';
import { el, q } from '/js/utils.js?v=045df327';
import { el, q } from '/js/utils.js?v=55685187';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { iconChain } from '/js/icons.js?v=9c8c550c';
import { iconSvg } from '/js/icon-set.js?v=606a68c6';
Expand Down
Loading
Loading