feat(ui): Youreka favicon, logout confirmation, nav active states, brand alignment

- favicon is the Youreka logo (64px PNG generated from the source mark),
  on the tools pages and server error pages
- signing out asks first: a Carbon cds-modal (Cancel / danger Sign out)
  shared by the header and the mobile side nav; Cancel closes, Sign out
  confirms and logs out
- the current tool is now visibly selected: Carbon marks active nav via
  a[aria-current=page] INSIDE the shadow, but aria-current sits on the
  host, so the rule never matched — the header item now gets a blue
  underline and the side-nav link a blue left border (via ::part(link)
  for colour, plus the host for the marker)
- logo and wordmark are middle-aligned inside a flex brand wrapper
- tests: favicon/modal/signout-trigger markup and the active-tool marking
  on both navs (45 total)
This commit is contained in:
2026-10-01 04:08:09 +01:00
parent 17c407883d
commit 2e4a51d018
7 changed files with 83 additions and 8 deletions
+2 -1
View File
@@ -7,12 +7,13 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@carbon/styles@1.69.0/css/styles.min.css">
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' rx='4' fill='%230f62fe'/><path d='M12 5a6 6 0 0 0-6 6c0 2.6.8 4.1 1.5 5.2' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M15.5 19a7.5 7.5 0 0 0 3-9.7A6 6 0 0 0 7.1 7.9' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M9.5 18.5a5.5 5.5 0 0 1-.8-7.6 3.8 3.8 0 0 1 7.2 1.5 4.3 4.3 0 0 1-3.4 4.6' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/></svg>">
<link rel="icon" type="image/png" href="/assets/youreka-favicon.png">
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-croppa@1.3.8/dist/vue-croppa.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-croppa@1.3.8/dist/vue-croppa.min.css">
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/ui-shell.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/modal.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/button.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/notification.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/tag.min.js"></script>
+2 -1
View File
@@ -7,9 +7,10 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@carbon/styles@1.69.0/css/styles.min.css">
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' rx='4' fill='%230f62fe'/><path d='M12 5a6 6 0 0 0-6 6c0 2.6.8 4.1 1.5 5.2' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M15.5 19a7.5 7.5 0 0 0 3-9.7A6 6 0 0 0 7.1 7.9' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M9.5 18.5a5.5 5.5 0 0 1-.8-7.6 3.8 3.8 0 0 1 7.2 1.5 4.3 4.3 0 0 1-3.4 4.6' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/></svg>">
<link rel="icon" type="image/png" href="/assets/youreka-favicon.png">
<script src="https://cdn.socket.io/4.4.0/socket.io.min.js" integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj" crossorigin="anonymous"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/ui-shell.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/modal.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/button.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/notification.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/progress-bar.min.js"></script>
+15 -1
View File
@@ -29,9 +29,23 @@ cds-tile, cds-clickable-tile { display: block; }
/* ── header + side nav (Carbon ui-shell) ────────────────────────────────── */
cds-header:not(:defined), cds-side-nav:not(:defined) { visibility: hidden; }
.brand-logo { display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--cds-border-subtle, #e0e0e0); border-radius: 4px; padding: 2px; margin-right: 0.5rem; }
.brand-inner { display: inline-flex; align-items: center; gap: 0.5rem; }
.brand-logo { display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--cds-border-subtle, #e0e0e0); border-radius: 4px; padding: 2px; }
.brand-logo img { display: block; }
.brand-suffix { color: var(--cds-text-secondary, #525252); font-weight: 400; }
/* the current tool is marked on the hosts (aria-current sits outside the shadow,
so Carbon's own [aria-current=page] rule never matches) */
cds-header-nav-item[aria-current="page"] {
border-bottom: 2px solid var(--cds-border-interactive, #0f62fe);
font-weight: 600;
}
cds-header-nav-item[aria-current="page"]::part(link) { color: var(--cds-text-primary, #161616); }
cds-side-nav-link[aria-current="page"] {
border-left: 3px solid var(--cds-border-interactive, #0f62fe);
font-weight: 600;
}
cds-side-nav-link[aria-current="page"]::part(link) { color: var(--cds-text-primary, #161616); }
.header-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; padding-right: 1rem; }
.header-agent { margin-left: 1rem; }
/* mobile: account actions live in the side nav, not the header */
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+2 -1
View File
@@ -7,8 +7,9 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@carbon/styles@1.69.0/css/styles.min.css">
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' rx='4' fill='%230f62fe'/><path d='M12 5a6 6 0 0 0-6 6c0 2.6.8 4.1 1.5 5.2' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M15.5 19a7.5 7.5 0 0 0 3-9.7A6 6 0 0 0 7.1 7.9' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/><path d='M9.5 18.5a5.5 5.5 0 0 1-.8-7.6 3.8 3.8 0 0 1 7.2 1.5 4.3 4.3 0 0 1-3.4 4.6' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round'/></svg>">
<link rel="icon" type="image/png" href="/assets/youreka-favicon.png">
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/ui-shell.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/modal.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/tile.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/tag.min.js"></script>
<style>
+40 -3
View File
@@ -73,6 +73,10 @@ function errorPage(res, status, title, message, actions = {}) {
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)} · YOUreka Tools</title>
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" type="image/png" href="/assets/youreka-favicon.png">
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/modal.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/ui-shell.min.js"></script>
<script type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v2.50.0/tag.min.js"></script>
</head>
<body class="app cds--g100">
${appHeader(null, null)}
@@ -120,13 +124,13 @@ const appHeader = (session, active) => {
const name = session ? (session.firstName || session.phone || '') : '';
const right = session
? `<span class="user-chip"><span class="avatar">${esc(name.trim().slice(0, 2).toUpperCase())}</span>${esc(name)}</span>
<a class="linklike" href="/auth/logout">Sign out</a>`
<a class="linklike" href="/auth/logout" data-signout>Sign out</a>`
: '<a class="linklike" href="/auth/login">Sign in</a>';
return `
<cds-header aria-label="YOUreka Tools">
<cds-header-menu-button button-label-active="Close menu" button-label-inactive="Open menu" id="header-menu-button"></cds-header-menu-button>
<cds-header-name href="/">
<span class="brand-logo"><img src="/assets/youreka-logo.png" alt="" width="20" height="20"></span>YOUreka<span class="brand-suffix"> Tools</span>
<span class="brand-inner"><span class="brand-logo"><img src="/assets/youreka-logo.png" alt="" width="20" height="20"></span><span>YOUreka<span class="brand-suffix"> Tools</span></span></span>
</cds-header-name>
<cds-header-nav>
${toolLink('/cropper', 'Image Cropper', 'cropper')}
@@ -144,9 +148,42 @@ const appHeader = (session, active) => {
${sideLink('/cropper', 'Image Cropper', 'cropper')}
${sideLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
<cds-side-nav-divider></cds-side-nav-divider>
<cds-side-nav-link href="/auth/logout">Sign out</cds-side-nav-link>
<cds-side-nav-link href="/auth/logout" data-signout>Sign out</cds-side-nav-link>
</cds-side-nav-items>
</cds-side-nav>
<cds-modal id="signout-modal" size="xs" aria-label="Sign out of YOUreka Tools">
<cds-modal-header>
<cds-modal-close-button></cds-modal-close-button>
<cds-modal-label>Account</cds-modal-label>
<cds-modal-heading>Sign out of YOUreka Tools?</cds-modal-heading>
</cds-modal-header>
<cds-modal-body>You will need to authenticate again to use the tools.</cds-modal-body>
<cds-modal-footer>
<cds-modal-footer-button kind="secondary" data-modal-close>Cancel</cds-modal-footer-button>
<cds-modal-footer-button kind="danger" id="signout-confirm">Sign out</cds-modal-footer-button>
</cds-modal-footer>
</cds-modal>
<script>
// Confirm before signing out (Carbon modal)
(function () {
function wire() {
var modal = document.getElementById('signout-modal');
var confirm = document.getElementById('signout-confirm');
if (!modal || !confirm) return;
document.addEventListener('click', function (event) {
var trigger = event.target && event.target.closest ? event.target.closest('[data-signout]') : null;
if (!trigger) return;
event.preventDefault();
modal.open = true;
});
confirm.addEventListener('click', function () {
window.location.href = '/auth/logout';
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
else wire();
})();
</script>
`;
};
+22 -1
View File
@@ -312,6 +312,22 @@ test('injected session values cannot break out of the script tag', async () => {
}
});
test('the active tool is marked on both navigation surfaces', async () => {
const { base, stop } = await bootApp();
try {
const cookies = await login(base);
const cropper = await (await fetch(`${base}/cropper`, { headers: { cookie: cookies } })).text();
assert.match(cropper, /cds-header-nav-item href="\/cropper" aria-current="page"/);
assert.match(cropper, /cds-side-nav-link href="\/cropper" aria-current="page"/);
assert.ok(
!/cds-header-nav-item href="\/fingerprint" aria-current/.test(cropper),
'the other tool is not marked current'
);
} finally {
await stop();
}
});
test('all pages render on the g100 dark theme', async () => {
const { base, stop } = await bootApp();
try {
@@ -342,11 +358,16 @@ test('landing header is server-rendered for the session', async () => {
assert.match(anon, /cds-side-nav-link href="\/auth\/logout"/);
assert.match(anon, /youreka-logo\.png/);
assert.match(anon, /header-menu-button/);
// favicon is the Youreka logo; sign-out is confirmed via a Carbon modal
assert.match(anon, /rel="icon"[^>]*youreka-favicon\.png/);
assert.match(anon, /data-signout/);
assert.match(anon, /cds-modal id="signout-modal"/);
assert.match(anon, /id="signout-confirm"/);
const cookies = await login(base);
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();
assert.match(authed, /Ada/);
assert.match(authed, /href="\/auth\/logout">Sign out</);
assert.match(authed, /href="\/auth\/logout"[^>]*>Sign out</);
} finally {
await stop();
}