diff --git a/astro.config.mjs b/astro.config.mjs
index 5719d5a..6f57d75 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -49,12 +49,12 @@ export default defineConfig({
{ tag: 'link', attrs: { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' } },
{ tag: 'link', attrs: { rel: 'manifest', href: '/manifest.webmanifest' } },
{ tag: 'meta', attrs: { name: 'theme-color', content: '#0d1117' } },
- { tag: 'meta', attrs: { property: 'og:image', content: `${SITE}/img/og-7da9dc1a.jpg` } },
+ { tag: 'meta', attrs: { property: 'og:image', content: `${SITE}/img/og-64722d2b.jpg` } },
{ tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } },
{ tag: 'meta', attrs: { property: 'og:image:height', content: '630' } },
{ tag: 'meta', attrs: { property: 'og:site_name', content: 'Stackyard' } },
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } },
- { tag: 'meta', attrs: { name: 'twitter:image', content: `${SITE}/img/og-7da9dc1a.jpg` } },
+ { tag: 'meta', attrs: { name: 'twitter:image', content: `${SITE}/img/og-64722d2b.jpg` } },
],
expressiveCode: {
diff --git a/public/img/dashboard-b3d50cc8.jpg b/public/img/dashboard-b3d50cc8.jpg
new file mode 100644
index 0000000..c1d2170
Binary files /dev/null and b/public/img/dashboard-b3d50cc8.jpg differ
diff --git a/public/img/dashboard-c6746175.jpg b/public/img/dashboard-c6746175.jpg
deleted file mode 100644
index fe3fdc3..0000000
Binary files a/public/img/dashboard-c6746175.jpg and /dev/null differ
diff --git a/public/img/og-64722d2b.jpg b/public/img/og-64722d2b.jpg
new file mode 100644
index 0000000..8f50d38
Binary files /dev/null and b/public/img/og-64722d2b.jpg differ
diff --git a/public/img/og-7da9dc1a.jpg b/public/img/og-7da9dc1a.jpg
deleted file mode 100644
index ab3eddc..0000000
Binary files a/public/img/og-7da9dc1a.jpg and /dev/null differ
diff --git a/public/js/admin-app-form.js b/public/js/admin-app-form.js
index ff74f44..a16f367 100644
--- a/public/js/admin-app-form.js
+++ b/public/js/admin-app-form.js
@@ -1,16 +1,16 @@
-import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=70cbc405';
+import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=5d2b6f16';
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=5a5d655f';
+import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import {
isDockBlocked,
- DOCK_MAX,
clearsStoredSecret,
isBareHostUrl,
failureIsMissingApiPath,
nextActiveIndex,
sameIconName,
-} from '/js/admin-logic.js?v=69e57d35';
+} from '/js/admin-logic.js?v=e3673bd7';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import {
toast,
@@ -21,10 +21,10 @@ import {
reveal,
setTogDisabled,
swapContent,
-} from '/js/admin-shared.js?v=fd784739';
-import { createListbox } from '/js/listbox.js?v=bedd1104';
+} from '/js/admin-shared.js?v=69f5c7f9';
+import { createListbox } from '/js/listbox.js?v=2b633751';
import { MAX_LABELS } from '/js/badge-logic.js?v=b3c8b6c2';
-import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=837961fa';
+import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=d0c89032';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
@@ -80,7 +80,7 @@ function _wireFolderApps(apps, children) {
}
export function buildAppForm(body, item) {
- const dockBlocked = isDockBlocked(state.items, item);
+ const dockBlocked = isDockBlocked(state.items, item, DOCK_MAX);
const globalHealthOn = !!inpById('srv-docker-en')?.checked;
const mon = item?.monitoring || {};
const hc = mon.healthcheck || {
diff --git a/public/js/admin-auth.js b/public/js/admin-auth.js
index 5d083e1..6add72f 100644
--- a/public/js/admin-auth.js
+++ b/public/js/admin-auth.js
@@ -1,7 +1,7 @@
-import { ag, ap } from '/js/admin-shared.js?v=fd784739';
+import { ag, ap } from '/js/admin-shared.js?v=69f5c7f9';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
-import { el, inp as inpById, qa } from '/js/utils.js?v=70cbc405';
+import { el, inp as inpById, qa } from '/js/utils.js?v=5d2b6f16';
export async function checkAuth(onLogin) {
try {
diff --git a/public/js/admin-color-control.js b/public/js/admin-color-control.js
index 7486796..26ba049 100644
--- a/public/js/admin-color-control.js
+++ b/public/js/admin-color-control.js
@@ -1,7 +1,7 @@
-import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=fd784739';
+import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=69f5c7f9';
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=70cbc405';
+import { qa, q } from '/js/utils.js?v=5d2b6f16';
const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'];
export const BADGE_DEFAULT = '#1e6ef4';
diff --git a/public/js/admin-drag.js b/public/js/admin-drag.js
index d439eee..bc9d2d1 100644
--- a/public/js/admin-drag.js
+++ b/public/js/admin-drag.js
@@ -12,7 +12,7 @@
import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=5a5d655f';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
-import { qa } from '/js/utils.js?v=70cbc405';
+import { qa } from '/js/utils.js?v=5d2b6f16';
/** @type {(before: unknown) => void} */
let _save = () => {};
diff --git a/public/js/admin-list.js b/public/js/admin-list.js
index be890c2..3c08fe1 100644
--- a/public/js/admin-list.js
+++ b/public/js/admin-list.js
@@ -13,13 +13,13 @@
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
-import { reorderItems } from '/js/admin-logic.js?v=69e57d35';
-import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=85ce4de5';
-import { paintIcon } from '/js/admin-shared.js?v=fd784739';
-import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=70cbc405';
+import { reorderItems } from '/js/admin-logic.js?v=e3673bd7';
+import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=dbf4e7e6';
+import { paintIcon } from '/js/admin-shared.js?v=69f5c7f9';
+import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=5d2b6f16';
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=3e1f15dd';
+import { sizeLabel } from '/js/admin-widget-form.js?v=c4b11d2a';
import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947';
/** @type {{ openModal: (idx: number|null) => void,
diff --git a/public/js/admin-logic.js b/public/js/admin-logic.js
index a54df69..626d7bf 100644
--- a/public/js/admin-logic.js
+++ b/public/js/admin-logic.js
@@ -132,8 +132,6 @@ export function nextActiveIndex(key, active, len) {
}
}
-/* dashboard.js slices the dock to DOCK_MAX, so the toggle must refuse beyond
- it. */
/* The server never refills a row that arrives non-secret, so unticking always
loses the stored value. Say so before the save. */
export function clearsStoredSecret(row, checked) {
@@ -203,14 +201,15 @@ export function resolveAdminSection(requested, available) {
return list.includes(String(requested ?? '')) ? String(requested) : list[0];
}
-export const DOCK_MAX = 4;
-
-export function isDockBlocked(items, editing) {
+/* The limit is a parameter. This module must not import. An import here breaks
+ every test that loads it without the path hook.
+ @param {any[]} items @param {any} editing @param {number} max */
+export function isDockBlocked(items, editing, max) {
if (editing?.dock) return false;
const docked = (Array.isArray(items) ? items : []).filter(
i => i?.type === 'app' && i.dock && i.id !== editing?.id,
).length;
- return docked >= DOCK_MAX;
+ return docked >= max;
}
export function groupBounds(field, size) {
diff --git a/public/js/admin-settings.js b/public/js/admin-settings.js
index 23bd7c3..708635d 100644
--- a/public/js/admin-settings.js
+++ b/public/js/admin-settings.js
@@ -1,4 +1,4 @@
-import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=fd784739';
+import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=69f5c7f9';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import {
@@ -7,9 +7,9 @@ import {
clearsStoredPassword,
createDirtyTracker,
BLOCK,
-} from '/js/admin-logic.js?v=69e57d35';
+} from '/js/admin-logic.js?v=e3673bd7';
import { confirmText } from '/js/modal.js?v=11fa1eff';
-import { el, inp, setUserText } from '/js/utils.js?v=70cbc405';
+import { el, inp, setUserText } from '/js/utils.js?v=5d2b6f16';
/* Mirrors the server's rule: auth cannot be switched on with no password. */
let _passwordSet = false;
diff --git a/public/js/admin-shared.js b/public/js/admin-shared.js
index 004193b..571ab88 100644
--- a/public/js/admin-shared.js
+++ b/public/js/admin-shared.js
@@ -1,6 +1,6 @@
/* Stateless helpers shared by the admin modules. Mutable state stays out. */
-import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=69e57d35';
-import { el, q } from '/js/utils.js?v=70cbc405';
+import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=e3673bd7';
+import { el, q } from '/js/utils.js?v=5d2b6f16';
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { iconChain } from '/js/icons.js?v=9c8c550c';
diff --git a/public/js/admin-widget-form.js b/public/js/admin-widget-form.js
index a056cc9..1b6d1f8 100644
--- a/public/js/admin-widget-form.js
+++ b/public/js/admin-widget-form.js
@@ -1,11 +1,11 @@
import { state } from '/js/admin-state.js?v=5a5d655f';
-import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=fd784739';
-import { createListbox } from '/js/listbox.js?v=bedd1104';
-import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=40e30a39';
+import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=69f5c7f9';
+import { createListbox } from '/js/listbox.js?v=2b633751';
+import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=253fcdcd';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
-import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=69e57d35';
+import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=e3673bd7';
import { t } from '/js/i18n.js?v=1f1ea9c1';
-import { q, qi, qa } from '/js/utils.js?v=70cbc405';
+import { q, qi, qa } from '/js/utils.js?v=5d2b6f16';
const SIZE_ICONS = {
small:
diff --git a/public/js/admin.js b/public/js/admin.js
index 1de6f87..ac0c351 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -1,7 +1,7 @@
-import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=47949906';
-import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=c70fb722';
-import { initList, render, syncFilterUI } from '/js/admin-list.js?v=94b420e9';
-import { resolveAdminSection } from '/js/admin-logic.js?v=69e57d35';
+import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=7da1ff10';
+import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=e2646986';
+import { initList, render, syncFilterUI } from '/js/admin-list.js?v=c5d8bff8';
+import { resolveAdminSection } from '/js/admin-logic.js?v=e3673bd7';
import {
buildAppItem,
claimFolderChildren,
@@ -10,13 +10,13 @@ import {
snapshotItems,
upsertItem,
} from '/js/admin-save-logic.js?v=4f71ef6c';
-import { loadSettings, settingsDirty, showBgFields, showWallpaperFile } from '/js/admin-settings.js?v=2d1a8a29';
-import { ag, ap, initInlineEdit, paintIcon, reveal, setReauthHandler, toast } from '/js/admin-shared.js?v=fd784739';
+import { loadSettings, settingsDirty, showBgFields, showWallpaperFile } from '/js/admin-settings.js?v=4d5e9919';
+import { ag, ap, initInlineEdit, paintIcon, reveal, setReauthHandler, toast } from '/js/admin-shared.js?v=69f5c7f9';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f';
-import { buildWidgetForm } from '/js/admin-widget-form.js?v=3e1f15dd';
+import { buildWidgetForm } from '/js/admin-widget-form.js?v=c4b11d2a';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0';
-import { createListbox } from '/js/listbox.js?v=bedd1104';
+import { createListbox } from '/js/listbox.js?v=2b633751';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=1f1ea9c1';
import { loadLocalIcons } from '/js/icons.js?v=9c8c550c';
@@ -32,8 +32,8 @@ import {
import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607';
import { confirmModal, confirmText, openModal as openDialog, promptModal } from '/js/modal.js?v=11fa1eff';
import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=787bfdff';
-import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=70cbc405';
-import { normalizeColorInput } from '/js/admin-color-control.js?v=837961fa';
+import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=5d2b6f16';
+import { normalizeColorInput } from '/js/admin-color-control.js?v=d0c89032';
import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=6ebb564c';
import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6';
diff --git a/public/js/dashboard.js b/public/js/dashboard.js
index 68a4d9f..7cc608e 100644
--- a/public/js/dashboard.js
+++ b/public/js/dashboard.js
@@ -23,13 +23,14 @@ import {
setUserText,
teardownWidgets,
titleWhenTruncated,
-} from '/js/utils.js?v=70cbc405';
+} from '/js/utils.js?v=5d2b6f16';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
-import { initSpotlight } from '/js/spotlight.js?v=12d9a895';
+import { initSpotlight } from '/js/spotlight.js?v=50bd3af5';
import { html, setHtml, raw } from '/js/html.js?v=c71f8903';
import { initI18n, t, currentLang } from '/js/i18n.js?v=1f1ea9c1';
import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=42f45ac7';
import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f';
+import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import {
initUI,
mkFolder,
@@ -38,7 +39,7 @@ import {
buildMobile,
resetMobileChrome,
mkFolderGlyph,
-} from '/js/ui.js?v=b530cbb6';
+} from '/js/ui.js?v=9317d147';
import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=b3c8b6c2';
import { formatNumber } from '/js/format-number.js?v=4a5ccef4';
import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=aa52b1a3';
@@ -394,7 +395,7 @@ function buildDesktop() {
/* Before paginate() and before any tile is built: both size against it. */
gm = gridMetrics();
document.documentElement.style.setProperty('--cols', String(gm.cols));
- const dock = items.filter(i => i.type === 'app' && i.dock && !i.hidden).slice(0, 4);
+ const dock = items.filter(i => i.type === 'app' && i.dock && !i.hidden).slice(0, DOCK_MAX);
document.body.classList.toggle('no-dock', !dock.length);
const pages = paginate();
totalPages = pages.length;
diff --git a/public/js/limits.js b/public/js/limits.js
new file mode 100644
index 0000000..e4a37fd
--- /dev/null
+++ b/public/js/limits.js
@@ -0,0 +1,4 @@
+/* The server requires this file directly. Keep it free of the DOM, of window
+ and of imports. */
+
+export const DOCK_MAX = 4;
diff --git a/public/js/listbox.js b/public/js/listbox.js
index f325e1d..3dfe163 100644
--- a/public/js/listbox.js
+++ b/public/js/listbox.js
@@ -6,10 +6,10 @@
Do not use popover. The top layer makes iOS collapse its browser toolbar,
which exposes a strip of page canvas and misplaces the menu. */
-import { nextActiveIndex } from '/js/admin-logic.js?v=69e57d35';
+import { nextActiveIndex } from '/js/admin-logic.js?v=e3673bd7';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
-import { qa } from '/js/utils.js?v=70cbc405';
+import { qa } from '/js/utils.js?v=5d2b6f16';
const CHEV =
'';
diff --git a/public/js/spotlight.js b/public/js/spotlight.js
index d4c4d65..3a843bb 100644
--- a/public/js/spotlight.js
+++ b/public/js/spotlight.js
@@ -1,10 +1,12 @@
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
-import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=70cbc405';
+import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=5d2b6f16';
import { t } from '/js/i18n.js?v=1f1ea9c1';
/* Attached to the window so a re-open can undo the previous one. */
const _w = /** @type {any} */ (window);
+const ACTIVATES_ON_SPACE = 'a[href], button, input, select, textarea, summary, [role="button"], [contenteditable]';
+
/* `isMob` is a function, not a flag: the window can cross the breakpoint while
this module is loaded. */
export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDesktop, openFolderMobile, folderGlyph }) {
@@ -16,7 +18,8 @@ export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDeskto
const live = el('sres-live');
let si = 0,
cur = [],
- returnFocus = null;
+ returnFocus = null,
+ openGen = 0;
inp.setAttribute('role', 'combobox');
inp.setAttribute('aria-autocomplete', 'list');
@@ -174,13 +177,18 @@ export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDeskto
ov.classList.toggle('kb', bottom > 120);
}
+ /* Focus here, not in a later frame: keys typed in between are lost. Keep `on`
+ before showModal. WebKit refuses focus while the dialog is display:none. */
function open(ch) {
if (ov.open) return;
+ const gen = ++openGen;
returnFocus = document.activeElement;
- ov.showModal();
ov.classList.add('on');
+ ov.showModal();
inp.value = ch || '';
render(inp.value);
+ inp.focus();
+ if (inp.setSelectionRange) inp.setSelectionRange(inp.value.length, inp.value.length);
if (MOB() && window.visualViewport) {
window.visualViewport.addEventListener('resize', _applyKbLayout);
window.visualViewport.addEventListener('scroll', _applyKbLayout);
@@ -189,18 +197,11 @@ export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDeskto
window.visualViewport.removeEventListener('scroll', _applyKbLayout);
};
}
- if (MOB()) {
- inp.focus();
- if (inp.setSelectionRange) inp.setSelectionRange(inp.value.length, inp.value.length);
- }
requestAnimationFrame(() =>
requestAnimationFrame(() => {
+ if (gen !== openGen) return;
ov.classList.add('vis');
_applyKbLayout();
- if (!MOB()) {
- inp.focus();
- if (inp.setSelectionRange) inp.setSelectionRange(inp.value.length, inp.value.length);
- }
}),
);
}
@@ -230,6 +231,7 @@ export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDeskto
function close() {
if (!ov.open) return;
+ openGen++;
ov.classList.remove('vis');
inp.setAttribute('aria-expanded', 'false');
inp.setAttribute('aria-activedescendant', '');
@@ -291,6 +293,7 @@ export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDeskto
return;
}
if (e.key === 'Enter') {
+ if (e.isComposing || e.keyCode === 229) return;
const s = qa('.sr', res)[si];
if (s) {
s.click();
@@ -304,7 +307,9 @@ export function initSpotlight({ getItems, isMob, CB, iconChain, openFolderDeskto
e => {
if (ov.classList.contains('on')) return;
if (e.key === 'Escape' || e.key.length !== 1 || e.metaKey || e.ctrlKey || e.altKey) return;
+ if (e.key === ' ' && e.target instanceof Element && e.target.closest(ACTIVATES_ON_SPACE)) return;
e.stopImmediatePropagation();
+ e.preventDefault();
open(e.key);
},
true,
diff --git a/public/js/ui.js b/public/js/ui.js
index a85705b..e388cb3 100644
--- a/public/js/ui.js
+++ b/public/js/ui.js
@@ -13,7 +13,7 @@ import {
q,
qa,
setUserText,
-} from '/js/utils.js?v=70cbc405';
+} from '/js/utils.js?v=5d2b6f16';
import { t, currentLang } from '/js/i18n.js?v=1f1ea9c1';
import { toneForColor } from '/js/label-contrast.js?v=c1ac6fb8';
import { mobileMetrics, gridColumnWidth, gridCellCount } from '/js/mobile-metrics.js?v=ab5fe77e';
diff --git a/public/js/utils.js b/public/js/utils.js
index 88fb097..848f370 100644
--- a/public/js/utils.js
+++ b/public/js/utils.js
@@ -111,12 +111,9 @@ export const tgt = e => /** @type {HTMLInputElement} */ (e.target);
/* breg is passed in to avoid a circular import. */
export function mkWrap(item, sz, r, isz, cls, breg) {
const w = mk('div');
- if (cls) w.className = cls;
+ w.className = cls ? `plate ${cls}` : 'plate';
const wrapBg = item.system === 'settings' ? '#027eae' : clr(item.color);
- w.style.cssText = `width:${sz}px;height:${sz}px;border-radius:${r}px;background:${wrapBg};position:relative;flex-shrink:0;overflow:visible;display:flex;align-items:center;justify-content:center;box-shadow:inset 1px 1px 0 rgba(255,255,255,.18),inset -1px -1px 0 rgba(0,0,0,.14);`;
- const g = mk('div');
- g.style.cssText = `position:absolute;inset:0;border-radius:${r}px;pointer-events:none;z-index:2;background:linear-gradient(135deg,rgba(255,255,255,.10) 0%,transparent 60%);`;
- w.appendChild(g);
+ w.style.cssText = `--pw:${sz}px;--tc:${wrapBg};width:${sz}px;height:${sz}px;border-radius:${r}px;background-color:${wrapBg};position:relative;flex-shrink:0;overflow:visible;display:flex;align-items:center;justify-content:center;`;
const rawIcon = item.iconUrl || '';
if (item.system === 'settings') {
const si = Math.min(sz, Math.round(isz * 1.22));
diff --git a/public/js/widget-config-form.js b/public/js/widget-config-form.js
index 9b2b7d8..ffe2573 100644
--- a/public/js/widget-config-form.js
+++ b/public/js/widget-config-form.js
@@ -3,9 +3,9 @@
import { t } from '/js/i18n.js?v=1f1ea9c1';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
-import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=fd784739';
-import { createListbox } from '/js/listbox.js?v=bedd1104';
-import { renderColorControl } from '/js/admin-color-control.js?v=837961fa';
+import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=69f5c7f9';
+import { createListbox } from '/js/listbox.js?v=2b633751';
+import { renderColorControl } from '/js/admin-color-control.js?v=d0c89032';
import {
seedCarried,
applyOptionSet,
@@ -13,9 +13,9 @@ import {
requiredFieldMissing,
groupBounds,
visibleFieldFlags,
-} from '/js/admin-logic.js?v=69e57d35';
+} from '/js/admin-logic.js?v=e3673bd7';
import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';
-import { qi } from '/js/utils.js?v=70cbc405';
+import { qi } from '/js/utils.js?v=5d2b6f16';
const PE =
'';
diff --git a/src/content/docs/docs/index.md b/src/content/docs/docs/index.md
index 4adc261..4d883c7 100644
--- a/src/content/docs/docs/index.md
+++ b/src/content/docs/docs/index.md
@@ -5,7 +5,7 @@ description: Stackyard is a calm, self-hosted homelab dashboard for your apps, w
Stackyard is a self-hosted dashboard for your homelab. Apps, folders, a few widgets and status badges, in one container.
-
+
- **Calm.** A health badge shows only when something is wrong.
- **Badges from any API.** Pick a value from a service's API response. No code. See [Badges](/docs/badges/).
diff --git a/src/pages/index.astro b/src/pages/index.astro
index d1ed66b..fa5d256 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -10,7 +10,7 @@ const description =
const site = Astro.site?.href.replace(/\/$/, '') ?? '';
const canonical = `${site}/`;
-const ogImage = `${site}/img/og-7da9dc1a.jpg`;
+const ogImage = `${site}/img/og-64722d2b.jpg`;
const publisher = {
'@type': 'Organization',