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
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/' },
Expand Down
2 changes: 1 addition & 1 deletion public/api/widget-data/gh-github-prs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"view": "prs",
"totalCount": 4,
"label": "created",
"filters": ["created"],
"allUrl": "https://github.com/pulls",
"items": [
{
Expand Down
2 changes: 1 addition & 1 deletion public/api/widget-data/gh-github-prs-md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"view": "prs",
"totalCount": 4,
"label": "created",
"filters": ["created"],
"allUrl": "https://github.com/pulls",
"items": [
{
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
19 changes: 15 additions & 4 deletions public/js/admin-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@ export const CHEV_SVG =
'<svg class="dd-chev" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 10.5 12 6.5 16 10.5"/><path d="M8 13.5 12 17.5 16 13.5"/></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));
Expand All @@ -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?.();
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 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=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,
Expand Down
16 changes: 8 additions & 8 deletions public/js/admin.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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 ? '↑' : '↓';
Expand Down Expand Up @@ -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');
Expand Down
25 changes: 22 additions & 3 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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';
Expand Down
12 changes: 10 additions & 2 deletions public/js/ui.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions public/js/widget-config-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions public/js/widget-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} 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;
}
38 changes: 24 additions & 14 deletions public/widgets/books/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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)}
Expand All @@ -73,7 +76,8 @@
<path d="M6 4h12a1 1 0 0 1 1 1v15l-7-4-7 4V5a1 1 0 0 1 1-1z"/></svg>
</div>
<div class="shelf" id="shelf">
<div class="ledge-top"></div><div class="ledge"></div>
<ul class="books" id="books"></ul>
<div class="ledge-top" aria-hidden="true"></div><div class="ledge" aria-hidden="true"></div>
</div>
<div class="info" id="info" aria-hidden="true">
<div class="t" id="i-t"></div><div class="a" id="i-a"></div><div class="p" id="i-p"></div>
Expand All @@ -92,6 +96,7 @@
const SRC_LABEL={recently:'RECENTLY ADDED',unread:'ON DECK',list:'READING'};

const card=document.getElementById('card'), shelf=document.getElementById('shelf'), lbl=document.getElementById('lbl');
const list=document.getElementById('books');
const info=document.getElementById('info'), iT=document.getElementById('i-t'), iA=document.getElementById('i-a'), iP=document.getElementById('i-p'), sr=document.getElementById('sr');
let href='';

Expand All @@ -112,12 +117,17 @@
function hideInfo(){ info.classList.remove('show'); info.setAttribute('aria-hidden','true'); }

function render(books){
shelf.querySelectorAll('.bk,.empty').forEach(n=>n.remove());
list.innerHTML='';
shelf.querySelectorAll('.empty').forEach(n=>n.remove());
/* Removing the bullets drops the list semantics in one engine, so the role
is declared even on a real list. It is set only while there are rows: an
empty list that declares the role reports as missing its children. */
list.removeAttribute('role');
if(!books.length){ const e=document.createElement('div'); e.className='empty'; e.textContent=wt('ui.noBooks','No books'); shelf.appendChild(e); return; }
list.setAttribute('role','list');
const tallestIdx=books.reduce((mi,b,i,a)=>(b._h>a[mi]._h?i:mi),0);
const ledgeTop=shelf.querySelector('.ledge-top');
books.forEach((b,i)=>{
const el=document.createElement('div'); el.className='bk';
const el=document.createElement('li'); el.className='bk';
el.style.width=b._w+'px'; el.style.height=b._h+'px'; el.style.background=b._color;
const dark = lum(b._color) > 0.62;
const sp=document.createElement('span'); sp.className='spine';
Expand All @@ -126,14 +136,14 @@
if(i===tallestIdx){ const rb=document.createElement('div'); rb.className='ribbon'; el.appendChild(rb); }
if(b._w>=17){ const d=document.createElement('div'); d.className='deco'; d.innerHTML='<span></span><span></span>'; el.appendChild(d); }
el.appendChild(sp);
el.tabIndex=0; el.setAttribute('role','listitem');
el.tabIndex=0;
const [ptxt]=progressText(b);
el.setAttribute('aria-label',(b.title||wt('ui.untitled','Untitled'))+(b.author?' '+wt('ui.by','by')+' '+b.author:'')+', '+ptxt);
const on=()=>{ el.classList.add('up'); showInfo(b); };
const off=()=>{ el.classList.remove('up'); hideInfo(); };
el.addEventListener('mouseenter',on); el.addEventListener('mouseleave',off);
el.addEventListener('focus',on); el.addEventListener('blur',off);
shelf.insertBefore(el, ledgeTop);
list.appendChild(el);
});
}

Expand Down
4 changes: 2 additions & 2 deletions public/widgets/clock/digital.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
white-space: nowrap; line-height: 1; flex-shrink: 0;
padding-top: 2px;
}
.mode-dark .date-line { color: rgba(255,255,255,0.38); }
.mode-light .date-line { color: rgba(0,0,0,0.33); }
.mode-dark .date-line { color: rgba(255,255,255,0.55); }
.mode-light .date-line { color: rgba(0,0,0,0.55); }
@media (prefers-reduced-motion: reduce){ *{transition:none !important;animation:none !important} }
</style>
</head>
Expand Down
2 changes: 1 addition & 1 deletion public/widgets/connections/connections-map.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
dot.style.backgroundColor=dotColor;
dot.style.boxShadow='0 0 5px '+dotColor;
it.append(dot,document.createTextNode(l.name));
it.setAttribute('aria-label', l.name+(clickable?' — open admin':''));
it.setAttribute('aria-label', l.name+(clickable?', '+wt('ui.openAdmin','open admin'):''));
const emph=()=>{emphasis=l.role;it.style.background='rgba(255,255,255,.10)';legendEl.querySelectorAll('button').forEach(b=>{if(b!==it)b.style.opacity='.45';});kick();};
const clear=()=>{emphasis=null;legendEl.querySelectorAll('button').forEach(b=>{b.style.background='none';b.style.opacity='1';});kick();};
it.addEventListener('pointerenter',emph);
Expand Down
Loading