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
7 changes: 2 additions & 5 deletions public/signal/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,8 @@
}

function syncLogos(theme) {
document.querySelectorAll("[data-logo]").forEach(function (img) {
var darkSrc = img.getAttribute("data-logo-dark");
var lightSrc = img.getAttribute("data-logo-light");
if (!darkSrc || !lightSrc) return;
img.src = theme === "dark" ? darkSrc : lightSrc;
document.querySelectorAll("picture source[data-logo-theme]").forEach(function (source) {
source.media = source.getAttribute("data-logo-theme") === theme ? "all" : "not all";
});
}

Expand Down
13 changes: 4 additions & 9 deletions src/layouts/SignalLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,10 @@ const primaryNav = [
<header class="nav" id="nav">
<div class="nav__inner">
<a class="nav__brand" href="/" aria-label="OpenTubeX home">
<img
data-logo
data-logo-dark={markDarkSrc}
data-logo-light={markLightSrc}
src={markLightSrc}
alt=""
width="32"
height="32"
/>
<picture>
<source data-logo-theme="dark" media="(prefers-color-scheme: dark)" srcset={markDarkSrc} type="image/svg+xml" />
<img src={markLightSrc} alt="" width="32" height="32" />
</picture>
<span>OpenTubeX</span>
</a>
<nav class="nav__links" id="nav-links" aria-label="Primary">
Expand Down
14 changes: 4 additions & 10 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,10 @@ const optimizedScreenshots = await Promise.all(screenshots.map(async (shot) => (

<div class="hero__content">
<div class="hero__copy">
<img
class="hero__logo"
data-logo
data-logo-dark={logoDarkSrc}
data-logo-light={logoLightSrc}
src={logoLightSrc}
alt="OpenTubeX"
width="420"
height="137"
/>
<picture>
<source data-logo-theme="dark" media="(prefers-color-scheme: dark)" srcset={logoDarkSrc} type="image/svg+xml" />
<img class="hero__logo" src={logoLightSrc} alt="OpenTubeX" width="420" height="137" />
</picture>
<h1 class="hero__headline" id="hero-heading">Watch YouTube.<br /><span>Make it yours.</span></h1>
<p class="hero__lede">
A highly customizable, privacy-focused YouTube client that aims to provide the best UX.
Expand Down
13 changes: 8 additions & 5 deletions src/scripts/jump-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ export {};

document.querySelectorAll<HTMLSelectElement>('[data-jump-select]').forEach((select) => {
select.addEventListener('change', () => {
if (select.value.startsWith('/') && !select.value.startsWith('//')) {
window.location.assign(select.value);
const href = select.value;
if (href.startsWith('/') && !href.startsWith('//')) {
const links = select.closest('nav')?.querySelectorAll<HTMLAnchorElement>('a[href]');
const link = Array.from(links ?? []).find((link) => link.getAttribute('href') === href);
if (link?.origin === window.location.origin) link.click();
return;
}
if (!select.value.startsWith('#')) return;
const target = document.getElementById(decodeURIComponent(select.value.slice(1)));
if (!href.startsWith('#')) return;
const target = document.getElementById(decodeURIComponent(href.slice(1)));
if (!target) return;

// Native fragment navigation honors each target's scroll-margin-top.
window.location.hash = select.value;
window.location.hash = href;
target.tabIndex = -1;
target.focus({ preventScroll: true });
});
Expand Down
66 changes: 66 additions & 0 deletions tests/jump-select.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect, test } from 'bun:test';
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';

const script = new Bun.Transpiler({ loader: 'ts', target: 'browser' }).transformSync(
readFileSync('src/scripts/jump-select.ts', 'utf8').replace('export {};', ''),
);

function picker(href: string, linkHref?: string) {
let change = () => {};
let clicks = 0;
let focused = false;
const location = { origin: 'https://opentubex.org', hash: '' };
const target = {
tabIndex: 0,
focus(options: FocusOptions) {
focused = options.preventScroll === true;
},
};
const links = linkHref === undefined ? [] : [{
origin: new URL(linkHref, location.origin).origin,
getAttribute: () => linkHref,
click: () => { clicks++; },
}];
const select = {
value: href,
closest: () => ({ querySelectorAll: () => links }),
addEventListener: (_event: string, listener: () => void) => { change = listener; },
};
runInNewContext(script, {
document: {
querySelectorAll: () => [select],
getElementById: (id: string) => id === 'section name' ? target : null,
},
window: { location },
});
change();
return { clicks, focused, location, target };
}

test('the picker follows the matching same-origin index link', () => {
expect(picker('/docs/installing/', '/docs/installing/').clicks).toBe(1);
});

test('the picker ignores destinations absent from its navigation links', () => {
expect(picker('/unlisted/', '/docs/installing/').clicks).toBe(0);
});

test('the picker rejects external and non-path destinations', () => {
for (const href of ['//example.org/', '/\\example.org/', 'https://example.org/', 'data:text/plain,hello', '']) {
expect(picker(href, href).clicks).toBe(0);
}
});

test('fragment navigation decodes the target and focuses it without another scroll', () => {
const result = picker('#section%20name');
expect(result.location.hash).toBe('#section%20name');
expect(result.target.tabIndex).toBe(-1);
expect(result.focused).toBe(true);
});

test('missing fragment targets leave navigation unchanged', () => {
const result = picker('#missing');
expect(result.location.hash).toBe('');
expect(result.focused).toBe(false);
});
35 changes: 35 additions & 0 deletions tests/theme-logos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from 'bun:test';
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';

test('theme changes switch logo media without copying URLs from DOM attributes', () => {
const sources = ['dark', 'dark'].map((theme) => ({
media: '(prefers-color-scheme: dark)',
srcset: `/assets/${theme}.svg`,
getAttribute: () => theme,
}));
const storage = new Map<string, string>();
const window: {
matchMedia: () => { matches: boolean; addEventListener: () => void };
otxTheme?: { apply: (preference: string) => void };
} = { matchMedia: () => ({ matches: true, addEventListener() {} }) };
runInNewContext(readFileSync('public/signal/theme.js', 'utf8'), {
window,
localStorage: {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
},
document: {
documentElement: { dataset: {} },
getElementById: () => null,
querySelectorAll: (selector: string) => selector === 'picture source[data-logo-theme]' ? sources : [],
addEventListener() {},
},
});
for (const [preference, media] of [['light', 'not all'], ['dark', 'all'], ['auto', 'all']]) {
window.otxTheme!.apply(preference);
expect(sources.map((source) => source.media)).toEqual([media, media]);
expect(sources.map((source) => source.srcset)).toEqual(['/assets/dark.svg', '/assets/dark.svg']);
}
});
Loading