From 99acb19a3a307f1033e7ef83212771275f69b4a7 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:19:52 -0400 Subject: [PATCH] Docs: sync widget previews and add an accessibility page Refresh public/widgets/ and public/js/ from the application repository. Replace the GitHub pull request stubs' "label" key with "filters", matching the field the widget now reads. Add /docs/accessibility/, generated from ACCESSIBILITY.md in the application repository at build time. Gate it in scripts/check-dist.mjs alongside the development and changelog pages, and link it from the sidebar and the landing footer. --- astro.config.mjs | 1 + public/api/widget-data/gh-github-prs | 2 +- public/api/widget-data/gh-github-prs-md | 2 +- public/js/admin-app-form.js | 4 +- public/js/admin-auth.js | 2 +- public/js/admin-color-control.js | 2 +- public/js/admin-settings.js | 2 +- public/js/admin-shared.js | 19 ++++++++-- public/js/admin-widget-form.js | 4 +- public/js/admin.js | 16 ++++---- public/js/dashboard.js | 25 ++++++++++-- public/js/ui.js | 12 +++++- public/js/widget-config-form.js | 4 +- public/js/widget-types.js | 10 +++++ public/widgets/books/index.html | 38 ++++++++++++------- public/widgets/clock/digital.html | 4 +- .../widgets/connections/connections-map.html | 2 +- public/widgets/connections/i18n/de.json | 3 +- public/widgets/connections/i18n/en.json | 3 +- public/widgets/connections/i18n/es.json | 3 +- public/widgets/connections/i18n/fa.json | 3 +- public/widgets/connections/i18n/fr.json | 3 +- public/widgets/connections/i18n/zh-Hans.json | 3 +- public/widgets/dashboard-switch/i18n/de.json | 3 +- public/widgets/dashboard-switch/i18n/en.json | 3 +- public/widgets/dashboard-switch/i18n/es.json | 3 +- public/widgets/dashboard-switch/i18n/fa.json | 3 +- public/widgets/dashboard-switch/i18n/fr.json | 3 +- .../dashboard-switch/i18n/zh-Hans.json | 3 +- public/widgets/dashboard-switch/index.html | 2 +- public/widgets/disk-health/i18n/de.json | 4 +- public/widgets/disk-health/i18n/en.json | 4 +- public/widgets/disk-health/i18n/es.json | 4 +- public/widgets/disk-health/i18n/fa.json | 4 +- public/widgets/disk-health/i18n/fr.json | 4 +- public/widgets/disk-health/i18n/zh-Hans.json | 4 +- public/widgets/disk-health/index.html | 4 +- public/widgets/dns/index.html | 2 +- public/widgets/github/contributions.html | 3 +- public/widgets/github/data.js | 11 ++---- public/widgets/github/i18n/de.json | 4 +- public/widgets/github/i18n/en.json | 4 +- public/widgets/github/i18n/es.json | 4 +- public/widgets/github/i18n/fa.json | 4 +- public/widgets/github/i18n/fr.json | 4 +- public/widgets/github/i18n/zh-Hans.json | 4 +- public/widgets/github/pullrequests.html | 8 ++-- public/widgets/nowplaying/i18n/de.json | 3 +- public/widgets/nowplaying/i18n/en.json | 3 +- public/widgets/nowplaying/i18n/es.json | 3 +- public/widgets/nowplaying/i18n/fa.json | 3 +- public/widgets/nowplaying/i18n/fr.json | 3 +- public/widgets/nowplaying/i18n/zh-Hans.json | 3 +- public/widgets/nowplaying/index.html | 4 +- public/widgets/system-summary/index.html | 2 +- public/widgets/weather/index.html | 2 +- scripts/check-dist.mjs | 3 +- src/pages/docs/accessibility.astro | 29 ++++++++++++++ src/pages/index.astro | 3 +- 59 files changed, 227 insertions(+), 97 deletions(-) create mode 100644 src/pages/docs/accessibility.astro diff --git a/astro.config.mjs b/astro.config.mjs index ca1bd2d..ef7af29 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -97,6 +97,7 @@ export default defineConfig({ ], }, { label: 'Security', link: '/docs/security/' }, + { label: 'Accessibility', link: '/docs/accessibility/' }, { label: 'Troubleshooting', link: '/docs/troubleshooting/' }, { label: 'Support', link: '/docs/support/' }, { label: 'Development', link: '/docs/development/' }, diff --git a/public/api/widget-data/gh-github-prs b/public/api/widget-data/gh-github-prs index c60ab5b..d4cf7a1 100644 --- a/public/api/widget-data/gh-github-prs +++ b/public/api/widget-data/gh-github-prs @@ -1,7 +1,7 @@ { "view": "prs", "totalCount": 4, - "label": "created", + "filters": ["created"], "allUrl": "https://github.com/pulls", "items": [ { diff --git a/public/api/widget-data/gh-github-prs-md b/public/api/widget-data/gh-github-prs-md index c60ab5b..d4cf7a1 100644 --- a/public/api/widget-data/gh-github-prs-md +++ b/public/api/widget-data/gh-github-prs-md @@ -1,7 +1,7 @@ { "view": "prs", "totalCount": 4, - "label": "created", + "filters": ["created"], "allUrl": "https://github.com/pulls", "items": [ { diff --git a/public/js/admin-app-form.js b/public/js/admin-app-form.js index a00f059..3363dc5 100644 --- a/public/js/admin-app-form.js +++ b/public/js/admin-app-form.js @@ -13,8 +13,8 @@ import { initInlineEdit, setTogDisabled, wireChecklist, -} from '/js/admin-shared.js?v=d96fc091'; -import { renderColorControl, BADGE_SWATCHES } from '/js/admin-color-control.js?v=89eee5e8'; +} from '/js/admin-shared.js?v=8f69dad6'; +import { renderColorControl, BADGE_SWATCHES } from '/js/admin-color-control.js?v=984a0b69'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; export function buildFolderForm(body, item) { diff --git a/public/js/admin-auth.js b/public/js/admin-auth.js index c322a86..1719f33 100644 --- a/public/js/admin-auth.js +++ b/public/js/admin-auth.js @@ -1,4 +1,4 @@ -import { ag, ap } from '/js/admin-shared.js?v=d96fc091'; +import { ag, ap } from '/js/admin-shared.js?v=8f69dad6'; 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'; diff --git a/public/js/admin-color-control.js b/public/js/admin-color-control.js index 2737025..495e695 100644 --- a/public/js/admin-color-control.js +++ b/public/js/admin-color-control.js @@ -1,4 +1,4 @@ -import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=d96fc091'; +import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=8f69dad6'; 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'; diff --git a/public/js/admin-settings.js b/public/js/admin-settings.js index a199ae9..24170ef 100644 --- a/public/js/admin-settings.js +++ b/public/js/admin-settings.js @@ -1,4 +1,4 @@ -import { toast, ag, ap } from '/js/admin-shared.js?v=d96fc091'; +import { toast, ag, ap } from '/js/admin-shared.js?v=8f69dad6'; 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'; diff --git a/public/js/admin-shared.js b/public/js/admin-shared.js index 607beb5..3c64877 100644 --- a/public/js/admin-shared.js +++ b/public/js/admin-shared.js @@ -129,10 +129,13 @@ export const CHEV_SVG = ''; /* `root` lets a caller wire a subtree that is not in the document yet. */ -/** @param {string} rowId @param {string} inputId - @param {{ type?: string, placeholder?: string, +/** A function placeholder is resolved on use, not at wiring time: this runs + before the catalog is fetched, so a translated one has to be read later. + @param {string} rowId @param {string} inputId + @param {{ type?: string, placeholder?: string | (() => string), onCommit?: (value: string) => void, root?: ParentNode }} [opts] */ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '', onCommit, root = document } = {}) { + const ph = () => (typeof placeholder === 'function' ? placeholder() : placeholder); const byId = id => (root === document ? el(id) : root.querySelector('#' + CSS.escape(id))); const row = byId(rowId); const inp = /** @type {HTMLInputElement} */ (byId(inputId)); @@ -142,15 +145,23 @@ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '' if (!valEl || !pen) return; inp.type = type; - inp.placeholder = placeholder; + inp.placeholder = ph(); inp.className = 'row-inp'; inp.style.display = ''; inp.style.cssText = ''; + /* The row's own label carries the translation. A placeholder is a hint, not a + name, and several readers drop it once the field holds a value. */ + const labelEl = q('.rl', row); + if (labelEl) { + if (!labelEl.id) labelEl.id = `${rowId}-rl`; + inp.setAttribute('aria-labelledby', labelEl.id); + } row.insertBefore(inp, pen); function open() { if (row.classList.contains('editing')) return; row.classList.add('editing'); + inp.placeholder = ph(); inp.value = valEl.classList.contains('is-ph') ? '' : valEl.textContent; inp.focus(); inp.select?.(); @@ -163,7 +174,7 @@ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '' valEl.textContent = v; valEl.classList.remove('is-ph'); } else { - valEl.textContent = placeholder || ''; + valEl.textContent = ph() || ''; valEl.classList.add('is-ph'); } onCommit?.(v); diff --git a/public/js/admin-widget-form.js b/public/js/admin-widget-form.js index 588ae88..505734b 100644 --- a/public/js/admin-widget-form.js +++ b/public/js/admin-widget-form.js @@ -1,6 +1,6 @@ import { state } from '/js/admin-state.js?v=b7731aa4'; -import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=d96fc091'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=71fe9a70'; +import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=8f69dad6'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=45bbb2b7'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, diff --git a/public/js/admin.js b/public/js/admin.js index 44a77eb..aadefa1 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1,5 +1,5 @@ -import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=f87415c7'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=589ad7b9'; +import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=f236bd68'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=5e6450f0'; import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=ebe3e806'; import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=ddfc6f80'; import { @@ -10,10 +10,10 @@ import { snapshotItems, upsertItem, } from '/js/admin-save-logic.js?v=52a970d3'; -import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=b2b67c47'; -import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=d96fc091'; +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 { state } from '/js/admin-state.js?v=b7731aa4'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=55e0396a'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=15aad905'; 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'; @@ -30,7 +30,7 @@ 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=89eee5e8'; +import { normalizeColorInput } from '/js/admin-color-control.js?v=984a0b69'; import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=cceca788'; /* A class rather than a bare media query. Some phones report a wider CSS @@ -319,7 +319,7 @@ function mkRow(item, idx, { indent = false, childIdx = null, folderId = null } = const mkMove = (dir, can) => { const b = document.createElement('button'); b.className = 'btn bg sm ic'; - const lbl = dir < 0 ? 'Move up' : 'Move down'; + const lbl = t(dir < 0 ? 'common.moveUp' : 'common.moveDown'); b.title = lbl; b.setAttribute('aria-label', lbl + ': ' + (item.label || item.id || 'item')); b.textContent = dir < 0 ? '↑' : '↓'; @@ -1043,7 +1043,7 @@ function initAllInlineEdits() { initInlineEdit('ie-pw', 'sec-pw', { type: 'password', - placeholder: t('general.passwordPh'), + placeholder: () => t('general.passwordPh'), onCommit() { const bars = el('sec-pw-bars'); const hint = el('sec-pw-hint'); diff --git a/public/js/dashboard.js b/public/js/dashboard.js index f965368..ab5bb34 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -7,7 +7,8 @@ import { WIDGET_COST, widgetSrc, cardPreset, -} from '/js/widget-types.js?v=b81ceeea'; + uniqueTitle, +} from '/js/widget-types.js?v=6a5e1619'; import { mk, mkWrap as _mkWrap, @@ -25,7 +26,7 @@ import { html, setHtml, raw } from '/js/html.js?v=c71f8903'; import { initI18n, t, currentLang } from '/js/i18n.js?v=d056c9c5'; import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=42f45ac7'; import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f'; -import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=371827cc'; +import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=14202a72'; import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=771f3230'; import { configChanged, @@ -271,8 +272,10 @@ function mkIcon(item) { return a; } +/* Reset per build: the names must stay stable across a rebuild. */ +let usedWidgetTitles = new Set(); function widgetTitle(item) { - return item.label || widgetReg[item.widgetType]?.label || t('type.widget'); + return uniqueTitle(item.label || widgetReg[item.widgetType]?.label || t('type.widget'), usedWidgetTitles); } function mkWidget(item) { const sz = item.widgetSize || 'medium'; @@ -331,6 +334,7 @@ function buildDesktop() { widgets started. */ teardownWidgets(); BEL.clear(); + usedWidgetTitles = new Set(); /* Before paginate() and before any tile is built: both size against it. */ gm = gridMetrics(); const dock = items.filter(i => i.type === 'app' && i.dock && !i.hidden).slice(0, 4); @@ -392,6 +396,20 @@ function announcePage(index, total) { live.textContent = t('home.pageAnnounce', { page: index + 1, total }); } +/* A page that has scrolled off is still in the DOM and still focusable, so Tab + walks out of the visible page into tiles nobody can see and the pager does + not follow. inert takes them out of the tab order and the accessibility tree + together. */ +/** @param {number} current */ +function syncPageInert(current) { + const strip = el('pages'); + if (!strip) return; + [...strip.children].forEach((page, i) => { + if (i === current) page.removeAttribute('inert'); + else page.setAttribute('inert', ''); + }); +} + function goTo(n, dotEls, announce = true) { const total = dotEls ? dotEls.length : totalPages; const was = pg; @@ -400,6 +418,7 @@ function goTo(n, dotEls, announce = true) { if (_stateRef) _stateRef.pg = pg; storeSet(PAGE_STORE, String(pg)); const strip = el('pages'); + syncPageInert(pg); const t = `translateX(-${pg * 100}vw)`; strip.style.transform = strip.style.webkitTransform = t; strip.style.willChange = 'transform'; diff --git a/public/js/ui.js b/public/js/ui.js index 027733b..f541456 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -1,5 +1,5 @@ import { iconChain } from '/js/icons.js?v=69c2b9bd'; -import { widgetSrc, cardPreset, WIDGET_DESIGN } from '/js/widget-types.js?v=b81ceeea'; +import { widgetSrc, cardPreset, uniqueTitle, WIDGET_DESIGN } from '/js/widget-types.js?v=6a5e1619'; import { mk, clr, @@ -415,6 +415,12 @@ export function openFolderMobile(folder, isz, _ir, _im, sc) { curPage = Math.max(0, Math.min(pages.length - 1, n)); strip.style.transform = strip.style.webkitTransform = `translateX(-${curPage * pageW}px)`; dotEls.forEach((d, j) => d.classList.toggle('on', j === curPage)); + /* A page that has scrolled off stays focusable, so Tab would leave the + visible page for tiles nobody can see. */ + [...strip.children].forEach((page, j) => { + if (j === curPage) page.removeAttribute('inert'); + else page.setAttribute('inert', ''); + }); } function buildPage(apps) { @@ -645,8 +651,10 @@ export function buildMobile() { } const pages = packMobile(gridItems); + /* Scoped to this build, so the names stay stable across a rebuild. */ + const usedWidgetTitles = new Set(); function widgetTitle(item) { - return item.label || widgetReg()[item.widgetType]?.label || t('type.widget'); + return uniqueTitle(item.label || widgetReg()[item.widgetType]?.label || t('type.widget'), usedWidgetTitles); } function mIcon(item) { diff --git a/public/js/widget-config-form.js b/public/js/widget-config-form.js index abe930a..3fe95e8 100644 --- a/public/js/widget-config-form.js +++ b/public/js/widget-config-form.js @@ -4,8 +4,8 @@ import { t } from '/js/i18n.js?v=d056c9c5'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { wireChecklist } from '/js/admin-shared.js?v=d96fc091'; -import { renderColorControl } from '/js/admin-color-control.js?v=89eee5e8'; +import { wireChecklist } from '/js/admin-shared.js?v=8f69dad6'; +import { renderColorControl } from '/js/admin-color-control.js?v=984a0b69'; import { seedCarried, applyOptionSet, diff --git a/public/js/widget-types.js b/public/js/widget-types.js index 803749a..fab01fa 100644 --- a/public/js/widget-types.js +++ b/public/js/widget-types.js @@ -60,3 +60,13 @@ export function widgetSrc(item, reg, opts) { if (opts?.lang) parts.push('lang=' + encodeURIComponent(opts.lang)); return `/widgets/${type}/${file}?${parts.join('&')}`; } + +/* Two widgets of the same type carry the same manifest label, and a frame list + then holds several entries with one name. */ +/** @param {string} base @param {Set} used */ +export function uniqueTitle(base, used) { + let title = base; + for (let n = 2; used.has(title); n++) title = `${base} ${n}`; + used.add(title); + return title; +} diff --git a/public/widgets/books/index.html b/public/widgets/books/index.html index 2c553f7..e1159c8 100644 --- a/public/widgets/books/index.html +++ b/public/widgets/books/index.html @@ -20,8 +20,8 @@ white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .head svg{width:13px;height:13px;color:#636366;flex:none} - .shelf{flex:1;position:relative;display:flex;align-items:flex-end;justify-content:center; - gap:2px;padding-bottom:9px;margin-top:6px;perspective:520px} + .shelf{flex:1;position:relative;padding-bottom:9px;margin-top:6px;perspective:520px} + .books{list-style:none;height:100%;display:flex;align-items:flex-end;justify-content:center;gap:2px} .empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; color:#636366;font-size:11px} @@ -46,15 +46,18 @@ .ledge{position:absolute;left:0;right:0;bottom:0;height:7px;background:#4a3018} .ledge-top{position:absolute;left:0;right:0;bottom:7px;height:1.5px;background:#6b4a28} - .info{position:absolute;left:0;right:0;top:0;padding:9px 12px 10px; - background:linear-gradient(180deg,rgba(18,18,18,.97),rgba(18,18,18,.86)); - border-bottom:.5px solid rgba(255,255,255,.06); + /* Inset to the card's own padding. At the edge it sat under the rounded + corner and read as part of the tile rather than a layer over it. */ + .info{position:absolute;left:14px;right:14px;top:14px;padding:10px 12px 11px;border-radius:10px; + background:linear-gradient(180deg,rgba(18,18,18,.97),rgba(18,18,18,.90)); + border:.5px solid rgba(255,255,255,.08); + box-shadow:0 6px 18px rgba(0,0,0,.34); opacity:0;transform:translateY(-4px);transition:opacity .16s,transform .16s;pointer-events:none} .info.show{opacity:1;transform:translateY(0)} - .info .t{font-size:11px;font-weight:600;color:#FFFFFF;line-height:1.25; + .info .t{font-size:13px;font-weight:600;color:#FFFFFF;line-height:1.25; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} - .info .a{font-size:10px;color:#8E8E93;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px} - .info .p{font-size:9px;font-weight:600;letter-spacing:.03em;color:#0091FF;margin-top:3px} + .info .a{font-size:11px;color:#8E8E93;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px} + .info .p{font-size:10px;font-weight:600;letter-spacing:.03em;color:#0091FF;margin-top:3px} .info .p.done{color:#30D158}.info .p.none{color:#8E8E93} .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)} @@ -73,7 +76,8 @@
-
+
    +
    -
    +