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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- App titles now take their color from the background behind them, per icon over
a wallpaper.

- An app can now be dropped between two folders, on the top or bottom edge of a
folder row.

Expand Down
6 changes: 6 additions & 0 deletions tsconfig.frontend.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@
"/js/jitter.js": [
"./ui/js/jitter.js"
],
"/js/label-contrast.js": [
"./ui/js/label-contrast.js"
],
"/js/label-contrast.js?v=*": [
"./ui/js/label-contrast.js"
],
"/js/layout.js": [
"./ui/js/layout.js"
],
Expand Down
14 changes: 14 additions & 0 deletions ui/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
--icon-sheen: linear-gradient(135deg,rgba(255,255,255,.10) 0%,transparent 60%);
--label-color: rgba(255,255,255,.90);
--label-shadow: 0 1px 4px rgba(0,0,0,.70);
/* The tone the labels take on a background measured as light. The shadow
is a hairline: the colour carries the contrast, not the shadow. */
--label-color-dark: rgba(0,0,0,.92);
--label-shadow-dark: 0 0 2px rgba(255,255,255,.55);
--bounce-spring: cubic-bezier(.34,1.56,.64,1);
--page-ease: cubic-bezier(.77,0,.175,1);
/* The page behind the wallpaper, and the boot veil over the unstyled
Expand Down Expand Up @@ -194,6 +198,13 @@
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* Labels measured against a light background. The tone is set on the
element by the dashboard, never by the theme: the background is the
user's, not the system's. */
.ilabel[data-tone="dark"], .dyn-mob-label[data-tone="dark"], .dyn-fold-label[data-tone="dark"] {
color:var(--label-color-dark); text-shadow:var(--label-shadow-dark);
}

/* ── Badge — reference size, scaled by --sc (desktop overrides below) ── */
.badge {
position:absolute;
Expand Down Expand Up @@ -720,6 +731,9 @@ a:focus-visible, button:focus-visible, [role="button"]:focus-visible,
--label-shadow: 0 1px 4px rgba(0,0,0,.95);
}
.ilabel, .dyn-mob-label, .dyn-fold-label, .dyn-fold-inner-label { color:var(--label-primary); }
.ilabel[data-tone="dark"], .dyn-mob-label[data-tone="dark"], .dyn-fold-label[data-tone="dark"] {
color:#000000; text-shadow:0 0 2px rgba(255,255,255,.95);
}
.ctrl { color:var(--label-primary); background:rgba(255,255,255,.22); }
.srn { color:var(--label-primary); }
.srh, .sr-section { color:rgba(235,235,245,.85); }
Expand Down
4 changes: 2 additions & 2 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<title>Stackyard</title>
<link rel="stylesheet" href="/css/tokens.css?v=2f8e06aa">
<link rel="stylesheet" href="/css/dashboard.css?v=352a7817">
<link rel="stylesheet" href="/css/dashboard.css?v=7e018053">
</head>
<body>
<h1 class="visually-hidden">Stackyard</h1>
Expand Down Expand Up @@ -53,5 +53,5 @@ <h1 class="visually-hidden">Stackyard</h1>
</div>
</div>
</div>
<script type="module" src="/js/dashboard.js?v=8d614ca6"></script>
<script type="module" src="/js/dashboard.js?v=460d40f4"></script>
</body></html>
65 changes: 62 additions & 3 deletions ui/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ import { trapFocus } from '/js/dialog.js?v=05935547';
import { jitter } from '/js/jitter.js?v=4edf48f2';
import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=28416a75';
import { startWakeLock } from '/js/wake-lock.js?v=6b9591cf';
import {
applyLabelTones,
loadSamplingImage,
parseCssColor,
relativeLuminance,
sampleImage,
toneForLuminances,
} from '/js/label-contrast.js?v=322fb66e';

/* Recomputed, never stored: the window can cross the breakpoint after load. */
let MOB = isMobileLayout();
Expand Down Expand Up @@ -437,6 +445,43 @@ function syncMobPages() {
}
}

/** The background as the label tones see it: a sampled wallpaper grid, or one
tone for a solid colour. Null on both means the labels keep their default. */
let bgTone = { grid: null, tone: null };

/** Re-tone the labels. Cheap: it re-reads the sampled grid, never the image. */
function retone() {
requestAnimationFrame(() => applyLabelTones(bgTone));
}

function toneForColor(color) {
const rgb = parseCssColor(color);
if (!rgb) return null;
return toneForLuminances([relativeLuminance(rgb[0], rgb[1], rgb[2])]);
}

/** The readable copy of the current wallpaper, held so a resize re-samples
without fetching again. */
let _bgSample = null;

async function sampleWallpaper(url, brightness) {
const img = await loadSamplingImage(url);
_bgSample = img ? { img, brightness } : null;
bgTone = { grid: img ? sampleImage(img, window.innerWidth, window.innerHeight, brightness) : null, tone: null };
retone();
}

/* The wallpaper is sized to cover the viewport, so a resize moves which part of
it every label sits on. */
function resampleBg() {
if (!_bgSample) return retone();
bgTone = {
grid: sampleImage(_bgSample.img, window.innerWidth, window.innerHeight, _bgSample.brightness),
tone: null,
};
retone();
}

async function applyBg() {
const root = document.documentElement;
try {
Expand All @@ -446,10 +491,15 @@ async function applyBg() {
root.style.setProperty('--bg-image', 'none');
root.style.setProperty('--bg-color', safeColor);
root.style.setProperty('--bg-brightness', '1');
bgTone = { grid: null, tone: toneForColor(safeColor) };
retone();
} else if (bg.type === 'url' && bg.url) {
root.style.setProperty('--bg-image', `url('${sanitizeCssUrl(bg.url)}')`);
const url = sanitizeCssUrl(bg.url);
const brightness = Number(bg.brightness ?? 0.62);
root.style.setProperty('--bg-image', `url('${url}')`);
root.style.setProperty('--bg-color', '#0d1117');
root.style.setProperty('--bg-brightness', String(bg.brightness ?? 0.62));
root.style.setProperty('--bg-brightness', String(brightness));
sampleWallpaper(url, brightness);
} else if (bg.type === 'unsplash') {
let url = readWallpaperCache(storeGet(WALLPAPER_STORE), bg, Date.now());
if (!url) {
Expand All @@ -460,11 +510,13 @@ async function applyBg() {
}
if (url) {
const shown = url;
const brightness = Number(bg.brightness ?? 0.62);
const img = new Image();
img.onload = () => {
root.style.setProperty('--bg-image', `url('${sanitizeCssUrl(shown)}')`);
root.style.setProperty('--bg-color', '#0d1117');
root.style.setProperty('--bg-brightness', String(bg.brightness ?? 0.62));
root.style.setProperty('--bg-brightness', String(brightness));
sampleWallpaper(sanitizeCssUrl(shown), brightness);
};
img.src = shown;
}
Expand Down Expand Up @@ -710,12 +762,14 @@ async function boot() {
buildMobile();
syncMobPages();
goTo(restorePage(storeGet(PAGE_STORE), totalPages), null, false);
retone();
}),
);
} else {
document.body.classList.remove('is-mob');
buildDesktop();
goTo(restorePage(storeGet(PAGE_STORE), totalPages), null, false);
retone();
}
};
buildLayout();
Expand Down Expand Up @@ -807,6 +861,7 @@ async function boot() {
clearTimeout(_rt);
_rt = setTimeout(() => {
if (MOB) buildLayout();
resampleBg();
}, 150);
},
{ passive: true },
Expand All @@ -817,7 +872,10 @@ async function boot() {
pixel: a rebuild tears down and remounts every widget iframe. */
let _dz,
_slots = desktopSlots();
let _rs;
window.addEventListener('resize', () => {
clearTimeout(_rs);
_rs = setTimeout(resampleBg, 200);
if (MOB) return;
clearTimeout(_dz);
_dz = setTimeout(() => {
Expand All @@ -828,6 +886,7 @@ async function boot() {
_slots = slots;
buildDesktop();
goTo(Math.min(pg, totalPages - 1), null, false);
retone();
}, 200);
});

Expand Down
Loading