Links: arrows for moving around, ↗ for outside pages
Links to another page (All peers, Log, Add the first one) are ink with an arrow that nudges on hover, the back link gets ←, and links that open an outside page get ↗ and "opens in a new tab" for screen readers, on the setup page too. Peer names look the same everywhere, and links in text get a pale underline. Buttons and the sidebar stay as they are.
This commit is contained in:
@@ -33,8 +33,20 @@
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; }
|
||||
body { background: var(--ground); color: var(--ink); font-family: var(--sans); font-size: 14px; line-height: 1.45; }
|
||||
a { color: var(--link); }
|
||||
a:hover { color: var(--link-hover); }
|
||||
a { color: var(--link); text-decoration-color: rgba(28, 92, 171, .35); text-underline-offset: 3px; }
|
||||
a:hover { color: var(--link-hover); text-decoration-color: currentColor; }
|
||||
/* Links to another page (go, back) are ink with an arrow that nudges on
|
||||
hover; outside links (ext) keep the link colour and get ↗. */
|
||||
a.go, a.back { color: var(--ink); font-size: 13px; font-weight: 500; text-decoration: none; white-space: nowrap; }
|
||||
a.go:hover, a.back:hover { color: var(--link); }
|
||||
a.go .ar { margin-left: 4px; }
|
||||
a.back .ar { margin-right: 4px; }
|
||||
a.ext .ar { margin-left: 2px; font-size: .8em; }
|
||||
.ar { display: inline-block; transition: transform .15s; }
|
||||
a.go:hover .ar { transform: translateX(3px); }
|
||||
a.back:hover .ar { transform: translateX(-3px); }
|
||||
a.ext:hover .ar { transform: translate(2px, -2px); }
|
||||
@media (prefers-reduced-motion: reduce) { .ar { transition: none; } }
|
||||
:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
||||
[hidden] { display: none !important; }
|
||||
.mono { font-family: var(--mono); font-size: 13px; }
|
||||
@@ -86,7 +98,7 @@ a.brand, a.brand:hover { color: #fff; text-decoration: none; }
|
||||
.titleline { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
|
||||
h1 { margin: 0; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; }
|
||||
.sub { margin: 4px 0 0; color: var(--ink-2); }
|
||||
.back { font-size: 13px; margin-bottom: -8px; }
|
||||
.back { margin-bottom: -8px; align-self: flex-start; }
|
||||
|
||||
/* cards */
|
||||
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 20px; min-width: 0; }
|
||||
@@ -151,7 +163,7 @@ tr:last-child td { border-bottom: 0; }
|
||||
.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
td .note { font-size: 12px; color: var(--ink-3); }
|
||||
a.pname { color: var(--ink); font-weight: 600; text-decoration: none; }
|
||||
a.pname:hover, a.pname:focus-visible { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
|
||||
a.pname:hover { color: var(--link); }
|
||||
.empty { padding: 24px 12px; margin: 0; text-align: center; color: var(--ink-3); }
|
||||
|
||||
/* forms */
|
||||
@@ -334,8 +346,8 @@ dialog::backdrop { background: rgba(22, 23, 26, .55); }
|
||||
.setupbox { width: 100%; max-width: 400px; display: flex; flex-direction: column; gap: 24px; margin: auto 0; }
|
||||
.setupbox h1 { font-size: 22px; }
|
||||
.setupbox p { margin: 0; color: #c9c9c3; }
|
||||
.setupbox a { color: #9cc3f5; }
|
||||
.setupbox a:hover { color: #fff; }
|
||||
.setupbox a { color: #9cc3f5; text-decoration-color: rgba(156, 195, 245, .4); }
|
||||
.setupbox a:hover { color: #fff; text-decoration-color: currentColor; }
|
||||
.setupbox .center, .setupbox.center { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.setupbox.center { gap: 20px; }
|
||||
.setupbox .ghost { opacity: .45; }
|
||||
|
||||
@@ -169,6 +169,14 @@
|
||||
|
||||
const badge = (st) => h('span', { class: 'badge' }, h('span', { class: st.dot }), st.label);
|
||||
|
||||
// go links to another page of the app; back returns to one. Their arrows
|
||||
// nudge on hover. ext opens an outside page in a new tab, marked with ↗.
|
||||
const arrow = (c) => h('span', { class: 'ar', 'aria-hidden': 'true' }, c);
|
||||
const go = (href, text) => h('a', { class: 'go', href }, text, arrow('→'));
|
||||
const back = (href, text) => h('a', { class: 'back', href }, arrow('←'), text);
|
||||
const ext = (href, text) => h('a', { class: 'ext', href, target: '_blank', rel: 'noopener' }, text, arrow('↗'), h('span', { class: 'sr' }, ' (opens in a new tab)'));
|
||||
const peerLink = (p) => h('a', { class: 'pname', href: '#/peers/' + p.id }, p.name);
|
||||
|
||||
// svg builds an SVG element; attrs are set as attributes.
|
||||
function svg(tag, attrs, ...kids) {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||||
@@ -1086,17 +1094,17 @@
|
||||
|
||||
h('div', { class: 'cols' },
|
||||
h('section', { class: 'card flush' },
|
||||
h('div', { class: 'cardhead' }, h('h2', null, 'Peers'), h('a', { href: '#/peers' }, 'All peers')),
|
||||
h('div', { class: 'cardhead' }, h('h2', null, 'Peers'), go('#/peers', 'All peers')),
|
||||
top.length ? h('div', { class: 'tbl' }, h('table', { class: 'narrow' },
|
||||
h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Status'), h('th', { class: 'num' }, 'Download, 24 h'), h('th', { class: 'num' }, 'Upload, 24 h'))),
|
||||
h('tbody', null, top.map((p) => h('tr', null,
|
||||
h('td', null, h('a', { href: '#/peers/' + p.id }, p.name)),
|
||||
h('td', null, peerLink(p)),
|
||||
h('td', null, badge(peerState(p))),
|
||||
h('td', { class: 'num' }, fmtBytes(p.stats.down24h)),
|
||||
h('td', { class: 'num' }, fmtBytes(p.stats.up24h)))))))
|
||||
: h('p', { class: 'empty' }, 'No peers yet. ', h('a', { href: '#/peers/new' }, 'Add the first one'))),
|
||||
: h('p', { class: 'empty' }, 'No peers yet. ', go('#/peers/new', 'Add the first one'))),
|
||||
logs ? h('section', { class: 'card' },
|
||||
h('div', { class: 'cardhead' }, h('h2', null, 'Recent activity'), h('a', { href: '#/log' }, 'Log')),
|
||||
h('div', { class: 'cardhead' }, h('h2', null, 'Recent activity'), go('#/log', 'Log')),
|
||||
logs.lines.length
|
||||
? h('div', null, logs.lines.map((l) => h('div', { class: 'ev' }, h('time', { datetime: l.time }, fmtWhen(l.time)), h('span', null, describeAudit(l)))))
|
||||
: h('p', { class: 'empty' }, 'No changes yet.')) : null));
|
||||
@@ -1294,7 +1302,7 @@
|
||||
h('tbody', null, online.map(({ p, r, hist }) => {
|
||||
const idle = r[0] + r[1] < IDLE_BPS;
|
||||
return h('tr', { class: idle ? 'idle' : null },
|
||||
h('td', null, h('a', { href: '#/peers/' + p.id }, p.name), idle ? h('span', { class: 'tag plain' }, 'idle') : null),
|
||||
h('td', null, peerLink(p), idle ? h('span', { class: 'tag plain' }, 'idle') : null),
|
||||
h('td', null, rateSpark(hist)),
|
||||
h('td', { class: 'num' }, rate(r[0], idle)),
|
||||
h('td', { class: 'num' }, rate(r[1], idle)),
|
||||
@@ -1424,7 +1432,7 @@
|
||||
return hit && keep;
|
||||
});
|
||||
tbody.replaceChildren(...sorted(rows).map((p) => h('tr', null,
|
||||
h('td', null, h('a', { class: 'pname', href: '#/peers/' + p.id }, p.name), p.note ? h('div', { class: 'note' }, p.note) : null),
|
||||
h('td', null, peerLink(p), p.note ? h('div', { class: 'note' }, p.note) : null),
|
||||
h('td', { class: 'mono' }, p.ipv4),
|
||||
h('td', null, badge(peerState(p))),
|
||||
h('td', { class: 'mono muted' }, p.stats.endpoint || '–',
|
||||
@@ -1579,7 +1587,7 @@
|
||||
drawPreview();
|
||||
|
||||
fill(wrap,
|
||||
h('a', { class: 'back', href: '#/peers' }, '← Peers'),
|
||||
back('#/peers', 'Peers'),
|
||||
h('div', null, h('h1', null, 'Add peer'), h('p', { class: 'sub' }, 'Assigns the next free address and adds the peer to ' + srv.interface + ' without a restart.')),
|
||||
h('div', { class: 'split' }, form,
|
||||
h('aside', { class: 'card aside', 'aria-labelledby': 'pv' },
|
||||
@@ -1736,7 +1744,7 @@
|
||||
};
|
||||
|
||||
fill(wrap,
|
||||
h('a', { class: 'back', href: '#/peers' }, '← Peers'),
|
||||
back('#/peers', 'Peers'),
|
||||
h('div', { class: 'head' },
|
||||
h('div', null,
|
||||
h('div', { class: 'titleline' }, h('h1', null, p.name), badge(st.key === 'online' ? { ...st, label: 'Online · handshake ' + ago(p.stats.lastHandshake) } : st)),
|
||||
@@ -1787,7 +1795,7 @@
|
||||
: h('p', { class: 'empty' }, 'No connections recorded yet.'),
|
||||
sessions.length > SHORT ? h('div', { style: { margin: '8px 12px 0' } }, sessMore) : null,
|
||||
h('p', { class: 'hint', style: { margin: '4px 12px 12px' } }, 'Country and network: ',
|
||||
h('a', { href: 'https://db-ip.com', target: '_blank', rel: 'noopener' }, 'IP Geolocation by DB-IP'),
|
||||
ext('https://db-ip.com', 'IP Geolocation by DB-IP'),
|
||||
'. Kept as long as the daily traffic history.')),
|
||||
|
||||
h('form', { class: 'card', onSubmit: save },
|
||||
@@ -2117,7 +2125,7 @@
|
||||
notes ? h('div', { class: 'upnotes' },
|
||||
h('div', { class: 'hd' }, h('strong', null, 'What\'s new in ' + rel.version),
|
||||
h('span', { class: 'muted' }, 'Released ' + fmtDate(rel.published) + ' · from ' + srcName()),
|
||||
h('a', { href: rel.url, target: '_blank', rel: 'noopener' }, 'Full notes on ' + srcName())),
|
||||
ext(rel.url, 'Full notes on ' + srcName())),
|
||||
/security/i.test(notes.summary) ? h('p', { class: 'notice' }, 'Includes security fixes.') : null,
|
||||
notes.summary ? h('p', null, mdInline(notes.summary)) : null,
|
||||
notes.items.length ? h('ul', null, notes.items.map((t) => h('li', null, mdInline(t)))) : null) : null,
|
||||
@@ -2125,7 +2133,7 @@
|
||||
h('div', { class: 'hd' }, h('strong', null, 'Update this server'), h('span', { class: 'muted' }, 'Run on the server. VPN connections stay up.')),
|
||||
h('pre', { class: 'code' }, cmds),
|
||||
h('div', null, h('button', { type: 'button', class: 'btn small', onClick: () => copy(cmds) }, 'Copy commands'))) : null,
|
||||
st.available && !st.file ? h('p', null, 'No release file is built for this platform. ', h('a', { href: rel.url, target: '_blank', rel: 'noopener' }, 'See the release')) : null,
|
||||
st.available && !st.file ? h('p', null, 'No release file is built for this platform. ', ext(rel.url, 'See the release')) : null,
|
||||
h('fieldset', { class: 'section' }, h('legend', { class: 'legend' }, 'Release source'),
|
||||
h('div', { class: 'grid' }, SOURCES.map(([k, name, where]) => h('label', { class: 'opt' },
|
||||
h('input', { type: 'radio', name: 'upsrc', value: k, checked: st.source === k, onChange: () => save({ source: k }) }),
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
}
|
||||
|
||||
// Same drawing as favicon.svg.
|
||||
// ext opens an outside page in a new tab, marked with ↗ as in the app.
|
||||
const ext = (href, text) => h('a', { class: 'ext', href, target: '_blank', rel: 'noopener' }, text,
|
||||
h('span', { class: 'ar', 'aria-hidden': 'true' }, '↗'), h('span', { class: 'sr' }, ' (opens in a new tab)'));
|
||||
|
||||
function logo(size, plain) {
|
||||
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
for (const [k, v] of Object.entries({ width: size, height: size, viewBox: '0 0 64 64', 'aria-hidden': 'true' })) s.setAttribute(k, v);
|
||||
@@ -108,9 +112,9 @@
|
||||
h('div', { class: 'notice' }, 'Save it now. This page can\'t be opened again: the private key exists only here and isn\'t stored anywhere.'),
|
||||
h('ol', { class: 'steps' },
|
||||
step(1, 'Install WireGuard',
|
||||
h('p', null, h('a', { href: 'https://apps.apple.com/app/wireguard/id1441195209', rel: 'noopener' }, 'App Store'), ' · ',
|
||||
h('a', { href: 'https://play.google.com/store/apps/details?id=com.wireguard.android', rel: 'noopener' }, 'Google Play'), ' · ',
|
||||
h('a', { href: 'https://www.wireguard.com/install/', rel: 'noopener' }, 'Other systems'))),
|
||||
h('p', null, ext('https://apps.apple.com/app/wireguard/id1441195209', 'App Store'), ' · ',
|
||||
ext('https://play.google.com/store/apps/details?id=com.wireguard.android', 'Google Play'), ' · ',
|
||||
ext('https://www.wireguard.com/install/', 'Other systems'))),
|
||||
step(2, 'Add the profile',
|
||||
h('button', { type: 'button', class: 'btn primary', onClick: download }, 'Download ' + file),
|
||||
h('p', null, 'Open the downloaded file with WireGuard, or in WireGuard tap + and choose “Create from file”.')),
|
||||
|
||||
Reference in New Issue
Block a user