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
55 changes: 40 additions & 15 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clr, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=c5766a9d';
import { clr, el, focusFirst, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=9a9bfb54';
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=af772a1b';
Expand All @@ -22,10 +22,10 @@ import {
reveal,
setTogDisabled,
swapContent,
} from '/js/admin-shared.js?v=a81b9cbe';
import { createListbox } from '/js/listbox.js?v=9a8ae607';
} from '/js/admin-shared.js?v=f5551857';
import { createListbox } from '/js/listbox.js?v=30c1b9d1';
import { MAX_LABELS } from '/js/badge-logic.js?v=9e6d9d4b';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=0d4d1038';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=233683ad';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=f82486b2';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { iconSvg } from '/js/icon-set.js?v=34af798f';
Expand Down Expand Up @@ -65,7 +65,7 @@ function _wireFolderApps(apps, children) {
const row = el('folder-apps-row');
if (!row) return;
const box = createListbox({
label: t('folder.appsInFolder'),
label: t('folder.addApps'),
multiple: true,
options: apps.map(a => ({ value: a.id, label: a.label || a.id })),
value: children,
Expand Down Expand Up @@ -115,14 +115,14 @@ export function buildAppForm(body, item) {
${ier('ie-url', t('app.url'), 'f-href', item?.href, t('app.urlPh'), 'url')}
</div>

<p class="grp-hdr">${t('app.icon')}</p>
<p class="grp-hdr" role="heading" aria-level="2">${t('app.icon')}</p>
<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>${(item?.label || '?')[0]?.toUpperCase() || '?'}</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" aria-label="${t('app.upload')}" accept=".svg,.png,.ico,image/svg+xml,image/png,image/x-icon">
<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">
</div>
<div class="iprs" id="iprs" role="listbox" aria-label="${t('app.iconResults')}"></div>
</div>
Expand All @@ -136,18 +136,18 @@ export function buildAppForm(body, item) {
</div>
${dockBlocked ? html`<p class="grp-tip" id="dock-full-tip">${t('app.dockFull', { max: DOCK_MAX })}</p>` : ''}

<p class="grp-hdr">${t('app.badge')}</p>
<p class="grp-hdr" role="heading" aria-level="2">${t('app.badge')}</p>
<div class="grp">
<div class="row"><span class="rl">${t('app.healthCheck')}</span>${tog('hc-en', hc.enabled, t('app.healthCheck'))}</div>
<div id="hc-sub" class="reveal${hc.enabled ? ' open' : ''}"><div class="reveal-in">
<div class="row"><span class="rl">${t('app.type')}</span><div class="segr">
<div class="row"><span class="rl" id="hc-type-lbl">${t('app.type')}</span><div class="segr" role="group" aria-labelledby="hc-type-lbl">
<label class="segr-opt"><input type="radio" name="hc-type" id="hc-type-con" ${isPing ? '' : 'checked'}><span class="segr-dot"></span><span>${t('app.container')}</span></label>
<label class="segr-opt"><input type="radio" name="hc-type" id="hc-type-ping" ${isPing ? 'checked' : ''}><span class="segr-dot"></span><span>${t('app.ping')}</span></label>
</div></div>
<div id="hc-con-row" ${isPing ? 'hidden' : ''}>${ier('ie-hc-con', t('app.container'), 'hc-con', hc.container, t('app.containerPh'))}</div>
<div id="hc-ping-row" ${isPing ? '' : 'hidden'}>
${ier('ie-hc-ping', t('app.pingUrl'), 'hc-ping', hc.pingUrl, t('app.pingUrlPh'), 'url')}
<div class="row"><span class="rl"></span><span id="hc-ping-status" class="row-status"></span><button type="button" class="row-btn" id="hc-ping-test">${t('app.test')}</button></div>
<div class="row"><span class="rl"></span><span id="hc-ping-status" class="row-status" role="status"></span><button type="button" class="row-btn" id="hc-ping-test">${t('app.test')}</button></div>
</div>
</div></div>
</div>
Expand All @@ -165,7 +165,7 @@ export function buildAppForm(body, item) {
<div class="row"><span class="rl">${t('app.liveActivity')}</span>${tog('act-en', act.enabled, t('app.liveActivity'))}</div>
<div id="act-sub" class="reveal${act.enabled ? ' open' : ''}"><div class="reveal-in">
${ier('ie-burl', t('app.apiUrl'), 'f-burl', act.url, t('app.apiUrlPh'), 'url')}
<div class="row"><span class="rl"></span><span id="bst" class="row-status"></span><button type="button" class="row-btn" id="bfetch">${t('app.fetch')}</button></div>
<div class="row"><span class="rl"></span><span id="bst" class="row-status" role="status"></span><button type="button" class="row-btn" id="bfetch">${t('app.fetch')}</button></div>
<div id="auth-row-wrap">
<div class="row"><span class="rl">${t('app.authentication')}</span>${tog('auth-en', !!(act.params || act.headers), t('app.authentication'))}</div>
<div id="auth-sub" class="reveal${act.params?.length || act.headers?.length ? ' open' : ''}"><div class="reveal-in">
Expand Down Expand Up @@ -366,7 +366,7 @@ function renderActLabels(host) {
hdr.dataset.idx = String(i);
setHtml(
hdr,
html`<span class="albl-grip" aria-hidden="true">${raw(GRIP_SVG)}</span><span>${t('app.labelN', { n: i + 1 })}</span>`,
html`<span class="albl-grip" aria-hidden="true">${raw(GRIP_SVG)}</span><span role="heading" aria-level="3">${t('app.labelN', { n: i + 1 })}</span>`,
);
const ctl = document.createElement('span');
ctl.className = 'albl-ctl';
Expand All @@ -388,6 +388,12 @@ function renderActLabels(host) {
captureActLabels();
state.spaths.splice(i, 1);
syncActMode();
focusFirst(
actLabelBtns(i, '.grp-hdr-rm')[0],
actLabelBtns(i - 1, '.grp-hdr-rm')[0],
el('act-add-label'),
el('bfetch'),
);
}),
);
hdr.appendChild(ctl);
Expand Down Expand Up @@ -453,17 +459,29 @@ function addActLabel() {
toast(t('app.noValuesLeft'), 'err');
return;
}
const add = el('act-add-label');
const hadFocus = document.activeElement === add;
state.spaths.push(free.path);
syncActMode();
if (hadFocus && add?.hidden) focusFirst(actLabelBtns(state.spaths.length - 1, '.grp-hdr-rm')[0]);
}

function moveActLabel(from, delta) {
const to = from + delta;
if (to < 0 || to >= state.spaths.length) return;
captureActLabels();
const hadFocus = !!el('act-labels')?.contains(document.activeElement);
const [p] = state.spaths.splice(from, 1);
state.spaths.splice(to, 0, p);
syncActMode();
if (!hadFocus) return;
const [up, down] = actLabelBtns(to, '.albl-move');
focusFirst(delta < 0 ? up : down, delta < 0 ? down : up);
}

function actLabelBtns(i, sel) {
const hdr = el('act-labels')?.querySelector(`.albl-hdr[data-idx="${i}"]`);
return hdr ? qa(sel, hdr) : [];
}

/** Drag a label header to reorder. Keep the arrows: a pointer drag is
Expand Down Expand Up @@ -523,6 +541,7 @@ const VARIANT_LABEL = { base: 'app.iconVariantBase', light: 'app.iconVariantLigh

let ipList = [];
let ipActive = -1;
let ipOutsideWired = false;

function ipClose() {
const rs = el('iprs');
Expand Down Expand Up @@ -727,9 +746,12 @@ function wireIcon() {
};
}

document.addEventListener('click', e => {
if (!el('ipw')?.contains(/** @type {Node} */ (e.target))) ipClose();
});
if (!ipOutsideWired) {
ipOutsideWired = true;
document.addEventListener('click', e => {
if (!el('ipw')?.contains(/** @type {Node} */ (e.target))) ipClose();
});
}
loadIconVariants(state.siurl);
}

Expand Down Expand Up @@ -914,6 +936,7 @@ function renderKvRows(host, rows, ph) {
add.onclick = () => {
rows.push({ key: '', value: '', secret: false, valueSet: false });
renderKvRows(host, rows, ph);
focusFirst(qa('.kv-row .kv-k', host).at(-1));
};
host.appendChild(add);
}
Expand Down Expand Up @@ -959,6 +982,8 @@ function kvRowEl(host, rows, row, ph) {
const idx = rows.indexOf(row);
if (idx >= 0) rows.splice(idx, 1);
renderKvRows(host, rows, ph);
const dels = qa('.kv-del', host);
focusFirst(dels[idx], dels[idx - 1], qSel('.kv-add', host));
};
return rowEl;
}
Expand Down
26 changes: 24 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { apiGet, apiPost } from '/js/admin-shared.js?v=a81b9cbe';
import { apiGet, apiPost } from '/js/admin-shared.js?v=f5551857';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { el, inp as inpById, qa } from '/js/utils.js?v=c5766a9d';
import { el, inp as inpById, qa } from '/js/utils.js?v=9a9bfb54';
import { blockingScreenFor } from '/js/config-recovery.js?v=706fc9a7';

export async function checkAuth(onLogin) {
Expand Down Expand Up @@ -29,19 +29,41 @@ export function requireLogin() {
});
}

/** @type {Element[]} */
let madeInert = [];
/** @type {HTMLElement|null} */
let focusBefore = null;

/** @param {HTMLElement|null} screen */
function blockPageBehind(screen) {
if (madeInert.length) return;
focusBefore = /** @type {HTMLElement|null} */ (document.activeElement);
madeInert = [...document.body.children].filter(c => c !== screen && !c.hasAttribute('inert'));
madeInert.forEach(c => c.setAttribute('inert', ''));
}

function unblockPageBehind() {
madeInert.forEach(c => c.removeAttribute('inert'));
madeInert = [];
if (focusBefore?.isConnected && focusBefore !== document.body) focusBefore.focus();
focusBefore = null;
}

function showLoginScreen(onLogin) {
const s = el('login-screen');
const btn = inpById('login-btn');
const pw = inpById('login-pw');
const err = el('login-err');
if (s) s.style.display = 'flex';
blockPageBehind(s);

async function doLogin() {
if (btn) btn.disabled = true;
if (err) err.style.display = 'none';
try {
await apiPost('/api/auth/login', { password: pw?.value || '' });
if (s) s.style.display = 'none';
unblockPageBehind();
onLogin?.();
} catch (e) {
if (err) {
Expand Down
10 changes: 8 additions & 2 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=a81b9cbe';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=f5551857';
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=c5766a9d';
import { qa, q } from '/js/utils.js?v=9a9bfb54';
import { iconSvg } from '/js/icon-set.js?v=34af798f';
import { HUE_NAMES, ROLE_NAMES, TILE_KEYWORDS, pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43';

Expand Down Expand Up @@ -236,6 +236,12 @@ export function renderColorControl(
});
const rb = q('.cc-rainbow', container);
if (rb) rb.classList.toggle('on', 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));
reveal(tune, showTune, !_painted);
if (!codeRv.closest('.editing')) {
codeRv.textContent = mode === 'color' ? hex : kwName(mode);
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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=8389782f';
import { qa } from '/js/utils.js?v=c5766a9d';
import { qa } from '/js/utils.js?v=9a9bfb54';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
35 changes: 31 additions & 4 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=8389782f';
import { reorderItems } from '/js/admin-logic.js?v=fc7f0836';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=18af71d8';
import { paintIcon } from '/js/admin-shared.js?v=a81b9cbe';
import { clr, el, qa, setUserText } from '/js/utils.js?v=c5766a9d';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=444456dd';
import { paintIcon } from '/js/admin-shared.js?v=f5551857';
import { clr, el, focusFirst, qa, setUserText } from '/js/utils.js?v=9a9bfb54';
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=ef53bce9';
import { sizeLabel } from '/js/admin-widget-form.js?v=53ceafd3';
import { widgetGlyph } from '/js/widget-glyphs.js?v=648cc374';
import { iconSvg } from '/js/icon-set.js?v=34af798f';

Expand Down Expand Up @@ -112,6 +112,7 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
if (isFolderRow) {
const collapsed = collapsedFolders.has(item.id);
nm.setAttribute('type', 'button');
nm.dataset.act = 'toggle';
nm.style.cssText = 'display:flex;align-items:center;gap:6px;';
nm.setAttribute('aria-expanded', String(!collapsed));
nm.setAttribute('aria-label', t(collapsed ? 'folder.expandAria' : 'folder.collapseAria', { name: item.label }));
Expand Down Expand Up @@ -165,6 +166,7 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
b.title = lbl;
b.setAttribute('aria-label', lbl + ': ' + (item.label || item.id || t('type.app')));
b.textContent = dir < 0 ? '↑' : '↓';
b.dataset.act = dir < 0 ? 'up' : 'down';
b.disabled = !can;
b.onclick = () => moveRow(item, dir, { folderId, childIdx });
return b;
Expand All @@ -174,6 +176,7 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
const hb = document.createElement('button');
hb.className = 'btn bg sm';
hb.textContent = t(item.hidden ? 'common.show' : 'common.hide');
hb.dataset.act = 'hide';
const lbl = t(item.hidden ? 'general.showSettingsAria' : 'general.hideSettingsAria');
hb.title = lbl;
hb.setAttribute('aria-label', lbl);
Expand All @@ -187,6 +190,7 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
const ed = document.createElement('button');
ed.className = 'btn bg sm';
ed.textContent = t('common.edit');
ed.dataset.act = 'edit';
ed.onclick = () => _page.openModal(idx);
ac.append(ed);
}
Expand All @@ -195,8 +199,29 @@ export function mkRow(item, idx, { indent = false, childIdx = null, folderId = n
return row;
}

const FALLBACK = { up: ['up', 'down'], down: ['down', 'up'] };

/** @param {string|null|undefined} itemId @param {string} [act] @returns {boolean} */
export function focusRow(itemId, act = 'edit') {
const row = qa('.drow', el('al')).find(r => r.dataset.itemId === itemId);
if (!row) return false;
const order = [...(FALLBACK[act] || [act]), 'edit', 'hide', 'toggle'];
return focusFirst(...order.map(a => row.querySelector(`[data-act="${a}"]`)));
}

export function render() {
const l = el('al');
const was = /** @type {HTMLElement|null} */ (document.activeElement);
const inList = !!was && !!l?.contains(was);
const row = /** @type {HTMLElement|null} */ (inList ? was.closest('.drow') : null);
const act = inList ? was.dataset.act : undefined;
const id = row ? row.dataset.itemId : was?.dataset.folderId;
draw(l);
if (act === 'add-to') focusFirst(qa('.fp-add', l).find(b => b.dataset.folderId === id));
else if (act) focusRow(id, act);
}

function draw(l) {
const bar = el('al-filter');
const grp = el('al-grp');
if (bar) {
Expand Down Expand Up @@ -248,6 +273,8 @@ export function render() {
const addRow = document.createElement('button');
addRow.type = 'button';
addRow.className = 'fp-add';
addRow.dataset.act = 'add-to';
addRow.dataset.folderId = item.id;
setHtml(addRow, html`<span>+</span> ${t('folder.addAppToFolder')}`);
addRow.onclick = () => _page.openFolderPicker(null, item.id);
l.appendChild(addRow);
Expand Down
Loading
Loading