feat(shared): clickable logo links every tool's header to project home
The .app-header__logo SVG was decorative on every tool. Web's strongest convention is "click logo → go home" — so users tapping it expecting that fallback got nothing. Now the logo is wrapped in an anchor whose href reflects the URL the page was loaded from: file:// → no wrap (no server home to point at) / → wrap, href=/ (deployment root) /index.html / /<tool>.html → wrap, href=/ (root, no project) /<project>/... → wrap, href=/<project> (project landing) The wrap happens client-side at DOMContentLoaded via shared/logo.js, loaded by every tool's build.sh after toast/nav. Idempotent — a template-supplied anchor or a second mount call is a no-op. The companion shared/logo.css adds a subtle hover/focus affordance (opacity 0.82, focus ring) so the logo reads as clickable without otherwise altering its visual weight. Tools opt out by setting window.zddc.logo.disabled = true before DOMContentLoaded (e.g. for deployments that pin the logo to an external destination). Five Playwright tests (tests/logo.spec.js) lock the contract: no-wrap on file://, href=/ at root, href=/<project> in project subtree, aria-label matches target, idempotent re-mount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
837cf47924
commit
7fd96c7c78
13 changed files with 314 additions and 1 deletions
|
|
@ -22,6 +22,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/base.css" \
|
"css/base.css" \
|
||||||
"css/layout.css" \
|
"css/layout.css" \
|
||||||
"css/components.css" \
|
"css/components.css" \
|
||||||
|
|
@ -42,6 +43,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/preview-lib.js" \
|
"../shared/preview-lib.js" \
|
||||||
"js/init.js" \
|
"js/init.js" \
|
||||||
"js/parser.js" \
|
"js/parser.js" \
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/base.css" \
|
"css/base.css" \
|
||||||
"css/tree.css" \
|
"css/tree.css" \
|
||||||
> "$css_temp"
|
> "$css_temp"
|
||||||
|
|
@ -38,6 +39,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/help.js" \
|
"../shared/help.js" \
|
||||||
"../shared/preview-lib.js" \
|
"../shared/preview-lib.js" \
|
||||||
"js/init.js" \
|
"js/init.js" \
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/base.css" \
|
"css/base.css" \
|
||||||
"css/layout.css" \
|
"css/layout.css" \
|
||||||
"css/spreadsheet.css" \
|
"css/spreadsheet.css" \
|
||||||
|
|
@ -41,6 +42,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/preview-lib.js" \
|
"../shared/preview-lib.js" \
|
||||||
"js/app.js" \
|
"js/app.js" \
|
||||||
"js/utils.js" \
|
"js/utils.js" \
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/form.css" \
|
"css/form.css" \
|
||||||
> "$css_temp"
|
> "$css_temp"
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/help.js" \
|
"../shared/help.js" \
|
||||||
"js/app.js" \
|
"js/app.js" \
|
||||||
"js/context.js" \
|
"js/context.js" \
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/landing.css" \
|
"css/landing.css" \
|
||||||
> "$css_temp"
|
> "$css_temp"
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/help.js" \
|
"../shared/help.js" \
|
||||||
"js/landing.js" \
|
"js/landing.js" \
|
||||||
> "$js_raw"
|
> "$js_raw"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/base.css" \
|
"css/base.css" \
|
||||||
"css/editor.css" \
|
"css/editor.css" \
|
||||||
"css/toc.css" \
|
"css/toc.css" \
|
||||||
|
|
@ -45,6 +46,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/preview-lib.js" \
|
"../shared/preview-lib.js" \
|
||||||
"js/app.js" \
|
"js/app.js" \
|
||||||
"js/utils.js" \
|
"js/utils.js" \
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ export default defineConfig({
|
||||||
name: 'nav',
|
name: 'nav',
|
||||||
testMatch: 'nav.spec.js',
|
testMatch: 'nav.spec.js',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'logo',
|
||||||
|
testMatch: 'logo.spec.js',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'zddc',
|
name: 'zddc',
|
||||||
testMatch: 'zddc.spec.js',
|
testMatch: 'zddc.spec.js',
|
||||||
|
|
|
||||||
21
shared/logo.css
Normal file
21
shared/logo.css
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* shared/logo.css — paired with shared/logo.js. The wrapping anchor
|
||||||
|
inherits the logo's box and adds a subtle hover/focus affordance
|
||||||
|
so it reads as clickable without altering the logo's visual weight. */
|
||||||
|
|
||||||
|
.app-header__logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: opacity 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__logo-link:hover .app-header__logo,
|
||||||
|
.app-header__logo-link:focus-visible .app-header__logo {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__logo-link:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
82
shared/logo.js
Normal file
82
shared/logo.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
// shared/logo.js — turn the inert <svg class="app-header__logo"> on
|
||||||
|
// every tool's header into a clickable link. The destination is the
|
||||||
|
// nearest "home" the user can sensibly back out to:
|
||||||
|
//
|
||||||
|
// file:// → no wrap (no server home)
|
||||||
|
// http(s)://host/ → wrap, href = /
|
||||||
|
// http(s)://host/<tool>.html (deployment root)→ wrap, href = /
|
||||||
|
// http(s)://host/<project>/... → wrap, href = /<project>
|
||||||
|
//
|
||||||
|
// When inside a project, the logo takes the user to the project
|
||||||
|
// landing (synthetic page with the four lifecycle-stage cards + MDL
|
||||||
|
// instructions). When at the deployment root, the logo points at /
|
||||||
|
// (the project picker). Offline, the logo stays decorative — there's
|
||||||
|
// no real "home" to go to.
|
||||||
|
//
|
||||||
|
// Mounts as a sibling-replacement on DOMContentLoaded: wraps the
|
||||||
|
// existing logo SVG in an <a>, preserving classes and attributes.
|
||||||
|
// Idempotent: re-mounting on an already-wrapped logo is a no-op.
|
||||||
|
//
|
||||||
|
// Tools that want to override (e.g. a deployment that pins logo to
|
||||||
|
// an external URL) can set window.zddc.logo.disabled = true before
|
||||||
|
// DOMContentLoaded and inject their own anchor.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (!window.zddc) window.zddc = {};
|
||||||
|
if (window.zddc.logo) return;
|
||||||
|
|
||||||
|
function projectSegment(pathname) {
|
||||||
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
|
if (parts.length === 0) return null;
|
||||||
|
var first = parts[0];
|
||||||
|
// Tool HTMLs at the deployment root (index.html, archive.html
|
||||||
|
// with ?projects=...) don't carry a project segment.
|
||||||
|
if (first.indexOf('.') !== -1) return null;
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetHref() {
|
||||||
|
if (typeof location === 'undefined') return null;
|
||||||
|
if (location.protocol !== 'http:' && location.protocol !== 'https:') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (window.zddc.logo && window.zddc.logo.disabled) return null;
|
||||||
|
var seg = projectSegment(location.pathname);
|
||||||
|
return seg ? '/' + encodeURIComponent(seg) : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount() {
|
||||||
|
var logo = document.querySelector('.app-header__logo');
|
||||||
|
if (!logo) return;
|
||||||
|
// Already wrapped (template-supplied anchor, or a previous mount).
|
||||||
|
if (logo.parentElement && logo.parentElement.tagName === 'A' &&
|
||||||
|
logo.parentElement.classList.contains('app-header__logo-link')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var href = targetHref();
|
||||||
|
if (!href) return;
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = href;
|
||||||
|
a.className = 'app-header__logo-link';
|
||||||
|
var label = href === '/' ? 'ZDDC home' : 'Project home';
|
||||||
|
a.title = label;
|
||||||
|
a.setAttribute('aria-label', label);
|
||||||
|
logo.parentNode.insertBefore(a, logo);
|
||||||
|
a.appendChild(logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.zddc.logo = {
|
||||||
|
mount: mount,
|
||||||
|
// Test seam.
|
||||||
|
_projectSegment: projectSegment,
|
||||||
|
_targetHref: targetHref,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||||
|
} else {
|
||||||
|
mount();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -21,6 +21,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/table.css" \
|
"css/table.css" \
|
||||||
"../form/css/form.css" \
|
"../form/css/form.css" \
|
||||||
> "$css_temp"
|
> "$css_temp"
|
||||||
|
|
@ -37,6 +38,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/help.js" \
|
"../shared/help.js" \
|
||||||
"js/mode.js" \
|
"js/mode.js" \
|
||||||
"js/app.js" \
|
"js/app.js" \
|
||||||
|
|
|
||||||
85
tests/logo.spec.js
Normal file
85
tests/logo.spec.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Tests for shared/logo.js — turns the .app-header__logo SVG into a
|
||||||
|
// link to the project landing (or deployment root). Mounts at
|
||||||
|
// DOMContentLoaded across every tool's bundle.
|
||||||
|
//
|
||||||
|
// Strategy: serve any tool's compiled HTML at multiple URL paths via a
|
||||||
|
// tiny in-process HTTP server, so we can verify the wrapping anchor's
|
||||||
|
// href reflects the URL the page was loaded from.
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const HTML_PATH = path.resolve('classifier/dist/classifier.html');
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let baseUrl;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
const html = fs.readFileSync(HTML_PATH, 'utf8');
|
||||||
|
server = http.createServer((req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
res.end(html);
|
||||||
|
});
|
||||||
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||||
|
baseUrl = `http://127.0.0.1:${server.address().port}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
if (server) await new Promise(r => server.close(r));
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('shared/logo.js', () => {
|
||||||
|
|
||||||
|
test('does NOT wrap the logo on file://', async ({ page }) => {
|
||||||
|
const filePath = path.resolve('classifier/dist/classifier.html');
|
||||||
|
await page.goto(`file://${filePath}`, { waitUntil: 'load' });
|
||||||
|
const wrapped = await page.evaluate(() => {
|
||||||
|
const logo = document.querySelector('.app-header__logo');
|
||||||
|
return logo && logo.parentElement && logo.parentElement.tagName === 'A';
|
||||||
|
});
|
||||||
|
expect(wrapped).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wraps with href=/ at the deployment root', async ({ page }) => {
|
||||||
|
await page.goto(`${baseUrl}/`, { waitUntil: 'load' });
|
||||||
|
const got = await page.evaluate(() => {
|
||||||
|
const a = document.querySelector('.app-header__logo-link');
|
||||||
|
return a && a.getAttribute('href');
|
||||||
|
});
|
||||||
|
expect(got).toBe('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wraps with href=/<project> when inside a project subtree', async ({ page }) => {
|
||||||
|
await page.goto(`${baseUrl}/Project-1/working/casey/notes.md`, { waitUntil: 'load' });
|
||||||
|
const got = await page.evaluate(() => {
|
||||||
|
const a = document.querySelector('.app-header__logo-link');
|
||||||
|
return a && a.getAttribute('href');
|
||||||
|
});
|
||||||
|
expect(got).toBe('/Project-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the wrapper carries an aria-label matching its target', async ({ page }) => {
|
||||||
|
await page.goto(`${baseUrl}/Project-1/staging/`, { waitUntil: 'load' });
|
||||||
|
const probe = await page.evaluate(() => {
|
||||||
|
const a = document.querySelector('.app-header__logo-link');
|
||||||
|
return a && {
|
||||||
|
href: a.getAttribute('href'),
|
||||||
|
label: a.getAttribute('aria-label'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(probe.href).toBe('/Project-1');
|
||||||
|
expect(probe.label).toBe('Project home');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mount is idempotent — already-wrapped logo is left alone', async ({ page }) => {
|
||||||
|
await page.goto(`${baseUrl}/Project-1/`, { waitUntil: 'load' });
|
||||||
|
const wrapperCount = await page.evaluate(() => {
|
||||||
|
// Run mount a second time — should be a no-op.
|
||||||
|
window.zddc.logo.mount();
|
||||||
|
return document.querySelectorAll('.app-header__logo-link').length;
|
||||||
|
});
|
||||||
|
expect(wrapperCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -25,6 +25,7 @@ concat_files \
|
||||||
"../shared/base.css" \
|
"../shared/base.css" \
|
||||||
"../shared/toast.css" \
|
"../shared/toast.css" \
|
||||||
"../shared/nav.css" \
|
"../shared/nav.css" \
|
||||||
|
"../shared/logo.css" \
|
||||||
"css/base.css" \
|
"css/base.css" \
|
||||||
"css/layout.css" \
|
"css/layout.css" \
|
||||||
"css/forms.css" \
|
"css/forms.css" \
|
||||||
|
|
@ -51,6 +52,7 @@ concat_files \
|
||||||
"../shared/theme.js" \
|
"../shared/theme.js" \
|
||||||
"../shared/toast.js" \
|
"../shared/toast.js" \
|
||||||
"../shared/nav.js" \
|
"../shared/nav.js" \
|
||||||
|
"../shared/logo.js" \
|
||||||
"../shared/preview-lib.js" \
|
"../shared/preview-lib.js" \
|
||||||
"js/app.js" \
|
"js/app.js" \
|
||||||
"js/reactive.js" \
|
"js/reactive.js" \
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,28 @@ body.help-open .app-header {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* shared/logo.css — paired with shared/logo.js. The wrapping anchor
|
||||||
|
inherits the logo's box and adds a subtle hover/focus affordance
|
||||||
|
so it reads as clickable without altering the logo's visual weight. */
|
||||||
|
|
||||||
|
.app-header__logo-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: opacity 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__logo-link:hover .app-header__logo,
|
||||||
|
.app-header__logo-link:focus-visible .app-header__logo {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__logo-link:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* tables/ — directory-of-YAML table view. Reuses tokens from shared/base.css. */
|
/* tables/ — directory-of-YAML table view. Reuses tokens from shared/base.css. */
|
||||||
|
|
||||||
.table-main {
|
.table-main {
|
||||||
|
|
@ -1070,7 +1092,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · snail-candle-citrus</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-10 12:33:10 · 837cf47-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
@ -2294,6 +2316,89 @@ body.help-open .app-header {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// shared/logo.js — turn the inert <svg class="app-header__logo"> on
|
||||||
|
// every tool's header into a clickable link. The destination is the
|
||||||
|
// nearest "home" the user can sensibly back out to:
|
||||||
|
//
|
||||||
|
// file:// → no wrap (no server home)
|
||||||
|
// http(s)://host/ → wrap, href = /
|
||||||
|
// http(s)://host/<tool>.html (deployment root)→ wrap, href = /
|
||||||
|
// http(s)://host/<project>/... → wrap, href = /<project>
|
||||||
|
//
|
||||||
|
// When inside a project, the logo takes the user to the project
|
||||||
|
// landing (synthetic page with the four lifecycle-stage cards + MDL
|
||||||
|
// instructions). When at the deployment root, the logo points at /
|
||||||
|
// (the project picker). Offline, the logo stays decorative — there's
|
||||||
|
// no real "home" to go to.
|
||||||
|
//
|
||||||
|
// Mounts as a sibling-replacement on DOMContentLoaded: wraps the
|
||||||
|
// existing logo SVG in an <a>, preserving classes and attributes.
|
||||||
|
// Idempotent: re-mounting on an already-wrapped logo is a no-op.
|
||||||
|
//
|
||||||
|
// Tools that want to override (e.g. a deployment that pins logo to
|
||||||
|
// an external URL) can set window.zddc.logo.disabled = true before
|
||||||
|
// DOMContentLoaded and inject their own anchor.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (!window.zddc) window.zddc = {};
|
||||||
|
if (window.zddc.logo) return;
|
||||||
|
|
||||||
|
function projectSegment(pathname) {
|
||||||
|
var parts = pathname.split('/').filter(Boolean);
|
||||||
|
if (parts.length === 0) return null;
|
||||||
|
var first = parts[0];
|
||||||
|
// Tool HTMLs at the deployment root (index.html, archive.html
|
||||||
|
// with ?projects=...) don't carry a project segment.
|
||||||
|
if (first.indexOf('.') !== -1) return null;
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetHref() {
|
||||||
|
if (typeof location === 'undefined') return null;
|
||||||
|
if (location.protocol !== 'http:' && location.protocol !== 'https:') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (window.zddc.logo && window.zddc.logo.disabled) return null;
|
||||||
|
var seg = projectSegment(location.pathname);
|
||||||
|
return seg ? '/' + encodeURIComponent(seg) : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount() {
|
||||||
|
var logo = document.querySelector('.app-header__logo');
|
||||||
|
if (!logo) return;
|
||||||
|
// Already wrapped (template-supplied anchor, or a previous mount).
|
||||||
|
if (logo.parentElement && logo.parentElement.tagName === 'A' &&
|
||||||
|
logo.parentElement.classList.contains('app-header__logo-link')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var href = targetHref();
|
||||||
|
if (!href) return;
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = href;
|
||||||
|
a.className = 'app-header__logo-link';
|
||||||
|
var label = href === '/' ? 'ZDDC home' : 'Project home';
|
||||||
|
a.title = label;
|
||||||
|
a.setAttribute('aria-label', label);
|
||||||
|
logo.parentNode.insertBefore(a, logo);
|
||||||
|
a.appendChild(logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.zddc.logo = {
|
||||||
|
mount: mount,
|
||||||
|
// Test seam.
|
||||||
|
_projectSegment: projectSegment,
|
||||||
|
_targetHref: targetHref,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', mount, { once: true });
|
||||||
|
} else {
|
||||||
|
mount();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ZDDC shared help panel — open/close logic.
|
* ZDDC shared help panel — open/close logic.
|
||||||
* Works with all four tools regardless of their module pattern.
|
* Works with all four tools regardless of their module pattern.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue