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 modified public/img/dashboard/badge-labels.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 modified public/img/dashboard/badge-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=b18c93ed';
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=26566e09';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconName } from '/js/icons.js?v=69c2b9bd';
import { state } from '/js/admin-state.js?v=c23e6346';
Expand All @@ -13,9 +13,9 @@ import {
initInlineEdit,
setTogDisabled,
wireChecklist,
} from '/js/admin-shared.js?v=8f69dad6';
import { MAX_LABELS } from '/js/badge-logic.js?v=be6330d6';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=9fded679';
} from '/js/admin-shared.js?v=e9afcefb';
import { MAX_LABELS } from '/js/badge-logic.js?v=c6430afc';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=32111fda';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';

export function buildFolderForm(body, item) {
Expand Down Expand Up @@ -176,6 +176,7 @@ export function buildAppForm(body, item) {
</div>
<div id="act-labels-wrap" class="bprow-hidden">
<div id="act-labels"></div>
<p class="grp-tip" id="act-label-order">${t('app.labelOrderTip')}</p>
<div class="albl-add-wrap"><button type="button" class="albl-add" id="act-add-label">${t('app.addLabel')}</button></div>
<p class="grp-tip" id="act-label-max" hidden>${t('app.labelMax', { n: MAX_LABELS })}</p>
<div id="act-combine-row" class="bprow-hidden">
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=8f69dad6';
import { ag, ap } from '/js/admin-shared.js?v=e9afcefb';
import { t } from '/js/i18n.js?v=d056c9c5';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { el, inp as inpById, qa } from '/js/utils.js?v=b18c93ed';
import { el, inp as inpById, qa } from '/js/utils.js?v=26566e09';

export async function checkAuth(onLogin) {
try {
Expand Down
4 changes: 2 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 } from '/js/admin-shared.js?v=8f69dad6';
import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=e9afcefb';
import { t } from '/js/i18n.js?v=d056c9c5';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=b18c93ed';
import { qa, q } from '/js/utils.js?v=26566e09';

const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'];
export const BADGE_DEFAULT = '#1e6ef4';
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toast, ag, ap } from '/js/admin-shared.js?v=8f69dad6';
import { toast, ag, ap } from '/js/admin-shared.js?v=e9afcefb';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { t } from '/js/i18n.js?v=d056c9c5';
import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=ddfc6f80';
import { el, inp, q, qa, setUserText } from '/js/utils.js?v=b18c93ed';
import { el, inp, q, qa, setUserText } from '/js/utils.js?v=26566e09';

/* Mirrors the server's rule: auth cannot be switched on with no password. */
let _passwordSet = false;
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 { nextActiveIndex, recoversSession, toastHoldMs } from '/js/admin-logic.js?v=ddfc6f80';
import { el, qa, q } from '/js/utils.js?v=b18c93ed';
import { el, qa, q } from '/js/utils.js?v=26566e09';
import { t } from '/js/i18n.js?v=d056c9c5';

export const API = '';
Expand Down
6 changes: 3 additions & 3 deletions public/js/admin-widget-form.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { state } from '/js/admin-state.js?v=c23e6346';
import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=8f69dad6';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=63dccc19';
import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=e9afcefb';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=a68e6a57';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import {
sizesForView,
Expand All @@ -10,7 +10,7 @@ import {
carriesTypedValues,
} from '/js/admin-logic.js?v=ddfc6f80';
import { t } from '/js/i18n.js?v=d056c9c5';
import { q, qi, qa } from '/js/utils.js?v=b18c93ed';
import { q, qi, qa } from '/js/utils.js?v=26566e09';

const SIZE_ICONS = {
small:
Expand Down
21 changes: 12 additions & 9 deletions public/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=d54926e8';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=5e6450f0';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=aa32f5ed';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=2191f31e';
import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=ebe3e806';
import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=ddfc6f80';
import {
Expand All @@ -10,10 +10,10 @@ import {
snapshotItems,
upsertItem,
} from '/js/admin-save-logic.js?v=48a9e055';
import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=f7078bc0';
import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=8f69dad6';
import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=455deefb';
import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=e9afcefb';
import { state } from '/js/admin-state.js?v=c23e6346';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=c4b1a7b4';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=5a2dc8e8';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=d056c9c5';
import { iconChain, loadLocalIcons, resolveIcon } from '/js/icons.js?v=69c2b9bd';
Expand All @@ -28,9 +28,10 @@ import {
} from '/js/import-foreign.js?v=ef4f3d44';
import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=28416a75';
import { confirmModal, openModal as openDialog, promptModal } from '/js/modal.js?v=ff76dc56';
import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=fbd2d2ef';
import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=b18c93ed';
import { normalizeColorInput } from '/js/admin-color-control.js?v=9fded679';
import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=db4192cd';
import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=26566e09';
import { widgetGlyph } from '/js/widget-glyphs.js?v=b5036986';
import { normalizeColorInput } from '/js/admin-color-control.js?v=32111fda';
import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=cceca788';

/* A class rather than a bare media query. Some phones report a wider CSS
Expand Down Expand Up @@ -235,7 +236,9 @@ function mkRow(item, idx, { indent = false, childIdx = null, folderId = null } =
if (item.type === 'folder') {
ico.appendChild(svgNode(FOLDER_ICON));
} else if (item.type === 'widget') {
ico.appendChild(svgNode(SIZE_ICONS[item.widgetSize] || SIZE_ICONS.medium));
/* The type when the widget declares one, the size otherwise. */
const glyph = widgetGlyph(state._widgetReg?.[item.widgetType]?.glyph);
ico.appendChild(svgNode(glyph || SIZE_ICONS[item.widgetSize] || SIZE_ICONS.medium));
} else if (item.iconUrl) {
const img = document.createElement('img');
img.alt = item.label || '';
Expand Down
25 changes: 18 additions & 7 deletions public/js/badge-logic.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-check
/* Keep this file free of imports and of module state. Translation arrives
through an injected `translate`; a caller that passes none gets English. */
through an injected `translate`, and the digit shape through an injected
`format`; a caller that passes neither gets English and Latin digits. */
const EN = {
'status.needsAttention': 'Status: needs attention',
'status.healthy': 'Status: healthy',
Expand Down Expand Up @@ -85,8 +86,14 @@ export function healthReason(detail, translate) {
parts.push(_clip(detail.status, REASON_MAX));
}

if (detail.pingError) parts.push(_clip(tr('status.pingFailed', { error: detail.pingError }), REASON_MAX));
else if (detail.pingStatus >= 400) parts.push(tr('status.pingReturned', { status: detail.pingStatus }));
/* Give the room to the translated words and trim the upstream ones. The error
text comes from the network layer and is never translated, so clipping the
composed sentence cuts the half a reader can actually use. */
if (detail.pingError) {
const around = tr('status.pingFailed', { error: '' }).length;
const room = Math.max(8, REASON_MAX - around);
parts.push(_clip(tr('status.pingFailed', { error: _clip(detail.pingError, room) }), REASON_MAX));
} else if (detail.pingStatus >= 400) parts.push(tr('status.pingReturned', { status: detail.pingStatus }));

return parts.join(' \u2022 ');
}
Expand Down Expand Up @@ -147,6 +154,7 @@ export function firingLabels(labels, values) {
badgesStale?: boolean, healthStale?: boolean, activityStale?: boolean,
healthDetail?: Record<string, unknown>,
translate?: (key: string, vars?: Record<string, unknown>) => string,
format?: (value: number) => string,
}} opts */
export function computeBadgeVisual({
health,
Expand All @@ -162,8 +170,11 @@ export function computeBadgeVisual({
activityStale,
healthDetail,
translate,
format,
}) {
const tr = typeof translate === 'function' ? translate : (k, v) => _fallback(k, v);
/* Digit shape is the reader's, not the interface language's. */
const fmt = typeof format === 'function' ? format : v => String(v);
/* Below this the item is treated as having no activity at all. One keeps the
original behaviour of badging any count above zero. */
const min = badgeMinimum(custom);
Expand All @@ -182,12 +193,12 @@ export function computeBadgeVisual({
num = '!';
} else if (top) {
cls = 'badge on blue';
num = top.value > 99 ? '99+' : String(top.value);
num = top.value > 99 ? `${fmt(99)}+` : fmt(top.value);
unit = top.unit ? top.unit.slice(0, 8) : '';
bg = top.color;
} else if (active) {
cls = 'badge on blue';
num = activity > 99 ? '99+' : String(activity);
num = activity > 99 ? `${fmt(99)}+` : fmt(activity);
unit = custom.unit ? custom.unit.slice(0, 8) : '';
bg = safeColor(custom.color);
} else if (fixed) {
Expand All @@ -206,10 +217,10 @@ export function computeBadgeVisual({
/* Status text, so meaning is not carried by colour alone. */
let aria = '';
if (health) aria = tr('status.needsAttention');
else if (top) aria = top.name ? `${top.name}: ${top.value}` : tr('status.pending', { count: String(top.value) });
else if (top) aria = top.name ? `${top.name}: ${fmt(top.value)}` : tr('status.pending', { count: fmt(top.value) });
else if (active)
aria = tr('status.pending', {
count: (activity > 99 ? '99+' : String(activity)) + (custom.unit ? ' ' + custom.unit : ''),
count: (activity > 99 ? `${fmt(99)}+` : fmt(activity)) + (custom.unit ? ' ' + custom.unit : ''),
});
else if (fixed) aria = staticBdg.label;
else if (cls.includes('green')) aria = tr('status.healthy');
Expand Down
32 changes: 30 additions & 2 deletions public/js/badge-popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

const HOVER_IN_MS = 320;
const COMPAT_CLICK_MS = 500;
/* SC 1.4.13 asks that hover content be hoverable. The pointer has to cross the
gap between the badge and the popover without it closing underneath. */
const HOVER_OUT_MS = 200;

/** @type {HTMLElement|null} */
let pop = null;
Expand All @@ -12,6 +15,7 @@ let openFor = null;
/** @type {HTMLElement|null} */
let describedEl = null;
let openTimer = 0;
let closeTimer = 0;
let openedAt = 0;

const ROWS = new WeakMap();
Expand All @@ -23,6 +27,10 @@ function ensurePop() {
pop.id = 'badge-pop';
pop.setAttribute('role', 'tooltip');
pop.hidden = true;
/* Interactive now, so a click on it must not reach the tile underneath. */
pop.addEventListener('pointerenter', cancelClose);
pop.addEventListener('pointerleave', scheduleClose);
pop.addEventListener('click', e => e.stopPropagation());
document.body.appendChild(pop);
return pop;
}
Expand All @@ -42,7 +50,10 @@ function place(badge) {
const gap = 8;
let top = r.bottom + gap;
if (top + pr.height > window.innerHeight - 8) top = Math.max(8, r.top - gap - pr.height);
let left = r.right - pr.width;
/* Aligned with the badge's trailing edge, which is the other side of the
screen in a right-to-left language. */
const rtl = (document.documentElement.getAttribute('dir') || 'ltr') === 'rtl';
let left = rtl ? r.left : r.right - pr.width;
left = Math.min(Math.max(8, left), window.innerWidth - pr.width - 8);
p.style.top = Math.round(top) + 'px';
p.style.left = Math.round(left) + 'px';
Expand Down Expand Up @@ -84,6 +95,8 @@ function open(badge) {
export function closeBadgePopover() {
clearTimeout(openTimer);
openTimer = 0;
clearTimeout(closeTimer);
closeTimer = 0;
if (pop) pop.hidden = true;
openFor = null;
if (describedEl) {
Expand All @@ -94,10 +107,25 @@ export function closeBadgePopover() {

/** @param {HTMLElement} badge */
function schedule(badge, delay) {
clearTimeout(closeTimer);
closeTimer = 0;
clearTimeout(openTimer);
openTimer = window.setTimeout(() => open(badge), delay);
}

/* Cancelled when the pointer arrives on the popover. */
function scheduleClose() {
clearTimeout(openTimer);
openTimer = 0;
clearTimeout(closeTimer);
closeTimer = window.setTimeout(closeBadgePopover, HOVER_OUT_MS);
}

function cancelClose() {
clearTimeout(closeTimer);
closeTimer = 0;
}

let globalsWired = false;
function wireGlobals() {
if (globalsWired) return;
Expand Down Expand Up @@ -138,7 +166,7 @@ export function wireBadgePopover(badge, rows) {
});
badge.addEventListener('pointerleave', e => {
if (/** @type {PointerEvent} */ (e).pointerType === 'touch') return;
closeBadgePopover();
scheduleClose();
});

/* Keep the tap. iOS answers a long press on a link with its own callout and
Expand Down
Loading