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
33 changes: 21 additions & 12 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=14b18b8c';
import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=705ca896';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c7b5111';
import {
loadLocalIcons,
resolveIcon,
iconChain,
cdnIconRef,
splitIconRef,
throughServer,
} from '/js/icons.js?v=5ac01d72';
import { state } from '/js/admin-state.js?v=af772a1b';
import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import { formatNumber } from '/js/format-number.js?v=abcd4ae4';
Expand All @@ -11,7 +18,7 @@ import {
failureIsMissingApiPath,
nextActiveIndex,
sameIconName,
} from '/js/admin-logic.js?v=fc7f0836';
} from '/js/admin-logic.js?v=1cca36f5';
import { t } from '/js/i18n.js?v=ed7c5565';
import {
toast,
Expand All @@ -24,10 +31,10 @@ import {
reveal,
setTogDisabled,
swapContent,
} from '/js/admin-shared.js?v=36dc5cc1';
import { createListbox } from '/js/listbox.js?v=23e87940';
} from '/js/admin-shared.js?v=1c01be9a';
import { createListbox } from '/js/listbox.js?v=f814170f';
import { MAX_LABELS } from '/js/badge-logic.js?v=5c72bd4c';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=e80f4a68';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=4083ea3b';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { iconSvg } from '/js/icon-set.js?v=628341d4';
Expand Down Expand Up @@ -121,7 +128,7 @@ export function buildAppForm(body, item) {
<div class="grp" id="ipw">
<div class="icon-src-anchor">
<div class="row icon-src-row">
<span class="icon-prev" id="ipv">${state.siurl ? html`<img src="${resolveIcon(state.siurl)}" alt="" id="ipv-img">` : html`<span>${initial(item?.label)}</span>`}</span>
<span class="icon-prev" id="ipv">${state.siurl ? html`<img src="${throughServer(resolveIcon(state.siurl))}" alt="" id="ipv-img">` : html`<span>${initial(item?.label)}</span>`}</span>
<input class="icon-srch" id="ip-in" type="text" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="iprs" aria-autocomplete="list" aria-describedby="ip-status" aria-label="${t('app.icon')}" placeholder="${t('app.iconPh')}" value="${state.siurl}">
<button type="button" class="row-btn" id="ip-upload-lbl">${t('app.upload')}</button>
<input type="file" id="ip-upload" class="file-hidden" tabindex="-1" aria-hidden="true" accept=".svg,.png,.ico,image/svg+xml,image/png,image/x-icon">
Expand Down Expand Up @@ -667,12 +674,14 @@ function wireIcon() {
const d = await apiGet(`/api/icons/search?q=${encodeURIComponent(q)}`);
if (mine !== run) return;
showIPRes(d.results || [], v);
} catch {
} catch (e) {
if (mine !== run) return;
/* No catalogue to confirm against, so the typed name is all that is left. */
updPrev();
ipMessage(t('app.iconSearchFailed'), t('app.iconTryUpload'));
ipSay(t('app.iconSearchFailed'));
const demo = /** @type {{ code?: string }} */ (e)?.code === 'blocked.demo';
const why = t(demo ? 'app.iconSearchDemo' : 'app.iconSearchFailed');
ipMessage(why, demo ? '' : t('app.iconTryUpload'));
ipSay(why);
}
};
inp.oninput = () => {
Expand Down Expand Up @@ -767,7 +776,7 @@ function ipRow(i, iconRef, name, source, urls = [], variants = []) {
const img = document.createElement('img');
img.alt = '';
/* Ordered by the catalogue, whose first entry is the file it actually holds. */
const srcs = [...urls, ...variants.flatMap(v => v.urls || [])].filter(Boolean);
const srcs = [...urls, ...variants.flatMap(v => v.urls || [])].filter(Boolean).map(throughServer);
if (!srcs.length) srcs.push(...iconChain(iconRef));
let step = 0;
img.src = srcs[0] || '';
Expand Down Expand Up @@ -857,7 +866,7 @@ function updPrev(explicit) {
setInitialGlyph(p);
return;
}
const fallbacks = explicit?.length ? explicit : iconChain(state.siurl);
const fallbacks = explicit?.length ? explicit.map(throughServer) : iconChain(state.siurl);
if (!fallbacks.length) {
setInitialGlyph(p);
return;
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiGet, apiPost } from '/js/admin-shared.js?v=36dc5cc1';
import { apiGet, apiPost } from '/js/admin-shared.js?v=1c01be9a';
import { initI18n, t } from '/js/i18n.js?v=ed7c5565';
import { loginErrorKey } from '/js/admin-error.js?v=b0881957';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { el, inp as inpById, qa } from '/js/utils.js?v=14b18b8c';
import { el, inp as inpById, qa } from '/js/utils.js?v=705ca896';
import { blockingScreenFor } from '/js/config-recovery.js?v=85138697';

export async function checkAuth(onLogin) {
Expand Down
68 changes: 55 additions & 13 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=36dc5cc1';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=1c01be9a';
import { t } from '/js/i18n.js?v=ed7c5565';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=14b18b8c';
import { qa, q } from '/js/utils.js?v=705ca896';
import { iconSvg } from '/js/icon-set.js?v=628341d4';
import { createListbox } from '/js/listbox.js?v=f814170f';
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'];
Expand Down Expand Up @@ -147,9 +148,11 @@ export function renderColorControl(
container,
{ value = '#0289ff', idPrefix, onChange, variant, swatchColors = CC_SWATCHES, label = t('common.color') } = {},
) {
const top = variant === 'tile' ? TILE_KEYWORDS : variant === 'badge' ? ROLE_NAMES : [];
const styled = variant === 'tile';
const styles = styled ? TILE_KEYWORDS : [];
const top = variant === 'badge' ? ROLE_NAMES : [];
const hues = variant ? HUE_NAMES : [];
const isKw = v => typeof v === 'string' && (top.includes(v) || hues.includes(v));
const isKw = v => typeof v === 'string' && (styles.includes(v) || 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 =>
Expand All @@ -160,13 +163,18 @@ export function renderColorControl(
const wrap = document.createElement('div');
const slider = (name, cls, id, max, val, lo, hi) => html`
<div class="row hsb-row"><span class="rl">${name}</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="${name}"><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>`
: '';
const hueRow = !hues.length
? ''
: styled
? html`<div class="row cc-row cc-hues"><span class="rl">${label}</span><div class="cc-sw">${hues.map(kwSwatch)}${rainbow}</div></div>`
: html`<div class="row cc-row cc-hues"><span class="rl">${t('color.system')}</span><div class="cc-sw">${hues.map(kwSwatch)}</div></div>`;
const topRow = styled
? html`<div class="row cc-style-row"><span class="rl">${t('color.style')}</span></div>`
: html`<div class="row cc-row"><span class="rl">${label}</span><div class="cc-sw">${swatches}</div></div>`;
setHtml(
wrap,
html`
<div class="row cc-row"><span class="rl">${label}</span><div class="cc-sw">${swatches}</div></div>
${topRow}
<div class="row-wrap reveal cc-tune"><div class="reveal-in">
${hueRow}
${slider(t('appearance.hue'), 'hsb-range hsb-hue', `${idPrefix}-h`, 360, init.h, _ccIco.hueLo, _ccIco.hueHi)}
Expand All @@ -179,7 +187,7 @@ export function renderColorControl(
'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') || '';
const fill = v[0] === '#' ? v : v === 'auto' || v === 'clear' ? '' : _swatchFill(v);
const fill = v[0] === '#' ? v : _swatchFill(v);
if (fill) /** @type {HTMLElement} */ (b).style.background = fill;
}
const rows = /** @type {HTMLElement[]} */ ([...wrap.children]);
Expand All @@ -196,6 +204,33 @@ export function renderColorControl(
container.appendChild(hidden);
let mode = isKw(value) ? value : 'color';
let showTune = hues.includes(mode);
/* The colour a Style other than Color replaced, so choosing Color brings it back. */
let resume = null;
const styleOf = () => (styles.includes(mode) ? mode : 'color');
const styleBox = styled
? createListbox({
id: `${idPrefix}-style`,
label: t('color.style'),
options: [...styles, 'color'].map(v => ({ value: v, label: v === 'color' ? label : kwName(v) })),
value: styleOf(),
onChange: next => {
if (next !== 'color') {
if (!styles.includes(mode)) resume = { mode, hex: curHex(), pristine };
mode = next;
showTune = false;
commit();
return;
}
if (styles.includes(mode)) {
mode = resume?.mode ?? 'blue';
setSliders(resume?.hex ?? _swatchFill('blue'));
}
showTune = true;
commit(resume?.pristine);
},
})
: null;
if (styleBox) rows[0].appendChild(styleBox.el);
/* 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 Down Expand Up @@ -235,13 +270,15 @@ export function renderColorControl(
b.classList.toggle('on', on);
});
const rb = q('.cc-rainbow', container);
if (rb) rb.classList.toggle('on', showTune && (mode === 'color' || hues.includes(mode)));
if (rb)
rb.classList.toggle('on', styled ? mode === 'color' : showTune && (mode === 'color' || hues.includes(mode)));
const sw = qa('.cc-swatch', container);
const preset = sw.some(b => b !== rb && b.classList.contains('on'));
sw.forEach(b =>
b.setAttribute('aria-pressed', String(b === rb ? mode === 'color' && !preset : b.classList.contains('on'))),
);
rb?.setAttribute('aria-expanded', String(showTune));
if (!styled) rb?.setAttribute('aria-expanded', String(showTune));
styleBox?.setValue(styleOf());
reveal(tune, showTune, !_painted);
if (!codeRv.closest('.editing')) {
codeRv.textContent = mode === 'color' ? hex : kwName(mode);
Expand All @@ -250,8 +287,8 @@ export function renderColorControl(
hidden.value = mode === 'color' ? (pristine ?? hex) : mode;
_painted = true;
}
const commit = () => {
pristine = null;
const commit = (keep = null) => {
pristine = keep;
paint();
onChange?.(hidden.value);
};
Expand All @@ -265,6 +302,11 @@ export function renderColorControl(
qa('.cc-swatch', container).forEach(b =>
b.addEventListener('click', () => {
const v = b.dataset.v || '';
if (v === 'custom' && styled) {
mode = 'color';
commit();
return;
}
if (v === 'custom') {
showTune = !(showTune && (mode === 'color' || hues.includes(mode)));
if (showTune && !hues.includes(mode)) mode = 'color';
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=5811620e';
import { qa } from '/js/utils.js?v=14b18b8c';
import { snapshotItems } from '/js/admin-save-logic.js?v=5613a9fa';
import { qa } from '/js/utils.js?v=705ca896';

/** @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 @@ -12,14 +12,14 @@
reads and writes them too. */

import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=5811620e';
import { reorderItems } from '/js/admin-logic.js?v=fc7f0836';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=b2422767';
import { paintIcon } from '/js/admin-shared.js?v=36dc5cc1';
import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=14b18b8c';
import { snapshotItems } from '/js/admin-save-logic.js?v=5613a9fa';
import { reorderItems } from '/js/admin-logic.js?v=1cca36f5';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=8fa6fbae';
import { paintIcon } from '/js/admin-shared.js?v=1c01be9a';
import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=705ca896';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { t } from '/js/i18n.js?v=ed7c5565';
import { sizeLabel } from '/js/admin-widget-form.js?v=8a6be17f';
import { sizeLabel } from '/js/admin-widget-form.js?v=1cb745cf';
import { widgetGlyph } from '/js/widget-glyphs.js?v=b383b7c8';
import { iconSvg } from '/js/icon-set.js?v=628341d4';

Expand Down
10 changes: 10 additions & 0 deletions public/js/admin-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,13 @@ export function createDirtyTracker(read) {
},
};
}

/** What a single-choice picker shows as chosen. An empty string is a choice
only when an option carries it.
@param {unknown} value @param {{ value?: unknown, group?: unknown }[]} options
@returns {string[]} */
export function singleChoice(value, options) {
if (value == null) return [];
const v = String(value);
return v !== '' || options.some(o => o.group == null && String(o.value) === '') ? [v] : [];
}
19 changes: 19 additions & 0 deletions public/js/admin-save-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,25 @@ function sameData(a, b) {
);
}

/** What restoring `incoming` over `current` does to the item list. Compare
against the server's export, not the editor's items: the editor keeps row
shapes and secrets the export does not.
@param {any[]} current @param {any[]} incoming
@returns {{ added: number, updated: number, deleted: number }} */
export function importCounts(current, incoming) {
const cur = new Map(current.map(i => [i?.id, i]));
const inc = new Map(incoming.map(i => [i?.id, i]));
let added = 0,
updated = 0,
deleted = 0;
for (const [id, it] of inc) {
if (!cur.has(id)) added++;
else if (!sameData(cur.get(id), it)) updated++;
}
for (const id of cur.keys()) if (!inc.has(id)) deleted++;
return { added, updated, deleted };
}

/** The settings a backup restores. The server owns `auth` and drops it from
every write.
@param {unknown} settings @returns {Record<string, unknown> | null} */
Expand Down
Loading
Loading