Apply the Nocturne fidelity pass to the web UI

This commit is contained in:
Fisto
2026-09-21 19:31:29 +02:00
parent 267c31337c
commit a4d61781e9
10 changed files with 1336 additions and 100 deletions
+128 -27
View File
@@ -1,41 +1,126 @@
"use strict";
/* ─────────────────────────────────────────────────────────────────────────
Veskslop icons — Phosphor geometry.
Replaces the hand-drawn 16-unit set. Three things change:
1. Grid. Phosphor draws on 256 units; the old set drew on 16, so curves
were quantised to half-pixels and the mic, gear and camera all had
visibly different optical weights from each other.
2. Terminals. Round caps and joins throughout, as Phosphor regular.
3. Stroke. Held at a constant ~1.35 device pixels whatever the render
size, instead of a fixed 1.3 unit stroke that thickened as icons grew.
A 44px icon and a 13px icon now read as the same weight of line.
The API is unchanged — VK.icon(name, size) and VK.iconEl(name, size) —
and every previous name still resolves, so nothing needs editing to
adopt this. New names are listed at the bottom.
If you would rather ship the real Phosphor set, swap this file for
@phosphor-icons/web and keep VK.icon as a thin wrapper; the names below
are Phosphor's own, so the mapping is one to one.
───────────────────────────────────────────────────────────────────────── */
window.VK = window.VK ?? {};
const ICON_PATHS = {
hash: '<path d="M6 2L4.5 14M11.5 2L10 14M2.5 5.5h12M2 10.5h12"/>',
volume: '<path d="M8.5 2.5 4.5 6H2v4h2.5l4 3.5z"/><path d="M11 5.5a3.5 3.5 0 0 1 0 5"/>',
mic: '<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/>',
/* ── Channels and navigation ── */
hash: '<path d="M96 40 72 216M184 40l-24 176M52 96h152M44 160h152"/>',
users: '<circle cx="104" cy="96" r="44"/><path d="M24 200a88 88 0 0 1 160 0"/><path d="M160 54a44 44 0 0 1 0 84"/><path d="M232 200a88 88 0 0 0-40-38"/>',
user: '<circle cx="128" cy="92" r="48"/><path d="M36 204a96 96 0 0 1 184 0"/>',
chat: '<path d="M52 212l12-36A88 88 0 1 1 128 216a87 87 0 0 1-40-10z"/><path d="M96 120h64M96 152h40"/>',
activity: '<path d="M24 128h40l24-64 40 128 24-64h80"/>',
robot: '<rect x="28" y="72" width="200" height="136" rx="24"/><path d="M128 28v44"/><circle cx="128" cy="24" r="10"/><path d="M92 136v12M164 136v12"/><path d="M104 176h48"/>',
/* ── Voice ── */
mic: '<rect x="96" y="24" width="64" height="112" rx="32"/><path d="M196 124a68 68 0 0 1-136 0"/><path d="M128 192v40M92 232h72"/>',
micSlash:
'<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/><path d="M2.5 13.5 13.5 2.5"/>',
speaker: '<path d="M3 10V8a5 5 0 0 1 10 0v2"/><rect x="1.5" y="9.5" width="3" height="4.5" rx="1.5"/><rect x="11.5" y="9.5" width="3" height="4.5" rx="1.5"/>',
speakerSlash:
'<path d="M3 10V8a5 5 0 0 1 10 0v2"/><rect x="1.5" y="9.5" width="3" height="4.5" rx="1.5"/><rect x="11.5" y="9.5" width="3" height="4.5" rx="1.5"/><path d="M2 14 14 2"/>',
monitor: '<rect x="1.5" y="2.5" width="13" height="9" rx="1.5"/><path d="M5.5 14h5M8 11.5v2.5"/>',
camera: '<rect x="1.5" y="4.5" width="9" height="7" rx="1.5"/><path d="M10.5 8l4-2.2v4.4z"/>',
caretDown: '<path d="M3.5 6l4.5 4.5L12.5 6"/>',
caretUp: '<path d="M3.5 10L8 5.5l4.5 4.5"/>',
x: '<path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/>',
lock: '<rect x="3.5" y="7" width="9" height="7" rx="1.5"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"/>',
search: '<circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14 14"/>',
gear: '<circle cx="8" cy="8" r="2.2"/><path d="M8 1.8v2M8 12.2v2M1.8 8h2M12.2 8h2M3.6 3.6l1.4 1.4M11 11l1.4 1.4M12.4 3.6 11 5M5 11l-1.4 1.4"/>',
wave: '<path d="M2 6.5V4.5h6v2M5 4.5V12"/><path d="M10.5 5.5a3.2 3.2 0 0 1 0 5"/><path d="M12.8 3.5a6 6 0 0 1 0 9"/>',
warning: '<path d="M8 2 1.5 13.5h13z"/><path d="M8 6.5v3.5M8 12v.5"/>',
refresh: '<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9"/><path d="M13.5 2.5V6H10"/>',
play: '<path d="M5 3.5 12.5 8 5 12.5z"/>',
microphone: '<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/>',
users: '<circle cx="6" cy="5.5" r="2.5"/><path d="M1.5 13.5c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"/><path d="M11 3.5a2.5 2.5 0 0 1 0 5"/>',
activity: '<path d="M1.5 8h3l2-4.5 2.5 9L11 8h3.5"/>'
'<rect x="96" y="24" width="64" height="112" rx="32"/><path d="M196 124a68 68 0 0 1-136 0"/><path d="M128 192v40M92 232h72"/><path d="M44 36l168 184"/>',
volume: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 96a44 44 0 0 1 0 64"/>',
speaker: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 96a44 44 0 0 1 0 64"/><path d="M212 68a80 80 0 0 1 0 120"/>',
speakerSlash: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 100l44 56M232 100l-44 56"/>',
headphones: '<path d="M44 176v-48a84 84 0 0 1 168 0v48"/><rect x="20" y="152" width="48" height="68" rx="20"/><rect x="188" y="152" width="48" height="68" rx="20"/>',
headphonesSlash:
'<path d="M44 176v-48a84 84 0 0 1 168 0v48"/><rect x="20" y="152" width="48" height="68" rx="20"/><rect x="188" y="152" width="48" height="68" rx="20"/><path d="M44 36l168 184"/>',
waveform: '<path d="M32 108v40M72 76v104M112 40v176M152 76v104M192 100v56M232 116v24"/>',
wave: '<path d="M32 92V64h96v28M80 64v128M52 192h56"/><path d="M168 96a44 44 0 0 1 0 64"/><path d="M200 70a80 80 0 0 1 0 116"/>',
/* ── Media ── */
monitor: '<rect x="24" y="48" width="208" height="136" rx="20"/><path d="M96 216h64M128 184v32"/>',
screenshare: '<rect x="24" y="48" width="208" height="136" rx="20"/><path d="M96 216h64"/><path d="M128 76v72M100 104l28-28 28 28"/>',
camera: '<rect x="24" y="64" width="144" height="128" rx="20"/><path d="M168 112l64-36v104l-64-36z"/>',
image: '<rect x="32" y="48" width="192" height="160" rx="20"/><circle cx="96" cy="100" r="16"/><path d="M32 172l56-48 48 40 32-24 56 48"/>',
film: '<rect x="32" y="48" width="192" height="160" rx="20"/><path d="M88 48v160M168 48v160M32 128h192M32 88h56M32 168h56M168 88h56M168 168h56"/>',
audio: '<circle cx="76" cy="188" r="32"/><circle cx="196" cy="164" r="28"/><path d="M108 188V72l116-28v120"/>',
folder: '<path d="M216 208H40a12 12 0 0 1-12-12V60a12 12 0 0 1 12-12h56l28 36h92a12 12 0 0 1 12 12v100a12 12 0 0 1-12 12z"/>',
play: '<path d="M76 40l148 88-148 88z"/>',
pause: '<rect x="76" y="48" width="40" height="160" rx="10"/><rect x="140" y="48" width="40" height="160" rx="10"/>',
stop: '<rect x="56" y="56" width="144" height="144" rx="20"/>',
/* ── Controls ── */
caretDown: '<path d="M208 96l-80 80-80-80"/>',
caretUp: '<path d="M48 160l80-80 80 80"/>',
caretRight: '<path d="M96 48l80 80-80 80"/>',
caretLeft: '<path d="M160 208 80 128l80-80"/>',
x: '<path d="M200 56 56 200M200 200 56 56"/>',
check: '<path d="M216 72 104 184l-56-56"/>',
plus: '<path d="M40 128h176M128 40v176"/>',
minus: '<path d="M40 128h176"/>',
dots: '<circle cx="128" cy="128" r="14"/><circle cx="60" cy="128" r="14"/><circle cx="196" cy="128" r="14"/>',
search: '<circle cx="116" cy="116" r="76"/><path d="M170 170l54 54"/>',
funnel: '<path d="M32 56h192l-76 88v72l-40-24v-48z"/>',
gear: '<circle cx="128" cy="128" r="40"/><path d="M128 20l26 22 34-4 13 32 32 13-4 34 22 27-22 27 4 34-32 13-13 32-34-4-26 22-26-22-34 4-13-32-32-13 4-34-22-27 22-27-4-34 32-13 13-32 34 4z"/>',
sliders: '<path d="M40 88h72M156 88h60M40 168h28M112 168h104"/><circle cx="134" cy="88" r="22"/><circle cx="90" cy="168" r="22"/>',
refresh: '<path d="M176 92h44V48"/><path d="M220 92A100 100 0 0 0 44 78"/><path d="M80 164H36v44"/><path d="M36 164a100 100 0 0 0 176 14"/>',
download: '<path d="M128 32v128M80 112l48 48 48-48M40 208h176"/>',
copy: '<rect x="80" y="80" width="144" height="144" rx="16"/><path d="M176 80V48a16 16 0 0 0-16-16H48a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h32"/>',
pencil: '<path d="M92 216H48a8 8 0 0 1-8-8v-44L164 40l52 52z"/><path d="M136 68l52 52"/>',
trash: '<path d="M36 64h184M96 64V40h64v24M60 64v148a16 16 0 0 0 16 16h104a16 16 0 0 0 16-16V64M108 108v72M148 108v72"/>',
pin: '<path d="M100 28h56l-8 60 40 44H68l40-44z"/><path d="M128 132v96"/>',
popout: '<path d="M144 40h72v72"/><path d="M128 128 216 40"/><path d="M188 152v56a12 12 0 0 1-12 12H48a12 12 0 0 1-12-12V80a12 12 0 0 1 12-12h56"/>',
lock: '<rect x="40" y="88" width="176" height="128" rx="20"/><path d="M88 88V56a40 40 0 0 1 80 0v32"/>',
star: '<path d="M128 24l30 62 68 10-49 48 12 68-61-32-61 32 12-68-49-48 68-10z"/>',
/* ── Status ── */
info: '<circle cx="128" cy="128" r="96"/><path d="M120 124h12v56M104 180h48"/><circle cx="126" cy="86" r="10"/>',
warning: '<path d="M110 40 22 190a20 20 0 0 0 18 30h176a20 20 0 0 0 18-30L146 40a20 20 0 0 0-36 0z"/><path d="M128 104v40"/><circle cx="128" cy="180" r="10"/>',
error: '<circle cx="128" cy="128" r="96"/><path d="M160 96l-64 64M96 96l64 64"/>',
ok: '<circle cx="128" cy="128" r="96"/><path d="M172 100l-56 56-32-32"/>',
clock: '<circle cx="128" cy="128" r="96"/><path d="M128 72v56h48"/>'
};
const FILLED = new Set(["star"]);
/* Aliases — every name the app used before this file landed. */
ICON_PATHS.microphone = ICON_PATHS.mic;
ICON_PATHS.deafen = ICON_PATHS.headphonesSlash;
ICON_PATHS.listen = ICON_PATHS.headphones;
ICON_PATHS.video = ICON_PATHS.camera;
ICON_PATHS.close = ICON_PATHS.x;
ICON_PATHS.settings = ICON_PATHS.gear;
ICON_PATHS.tts = ICON_PATHS.wave;
ICON_PATHS.magnify = ICON_PATHS.search;
ICON_PATHS.star = '<path d="M8 1.6l1.9 4 4.4.6-3.2 3 .8 4.3L8 11.5 4.1 13.5l.8-4.3-3.2-3 4.4-.6z"/>';
const FILLED = new Set(["star", "play", "dots"]);
/* Solid marks keep a hairline of their own so they hold shape at 10-11px. */
const HAIRLINE = new Set(["dots"]);
window.VK.icon = function icon(name, size = 13) {
const path = ICON_PATHS[name] ?? "";
const fill = FILLED.has(name) ? "currentColor" : "none";
return `<svg width="${size}" height="${size}" viewBox="0 0 16 16" fill="${fill}" stroke="currentColor" stroke-width="1.3" aria-hidden="true">${path}</svg>`;
const path = ICON_PATHS[name];
if (!path) {
if (name) console.warn("VK.icon: unknown icon", name);
return "";
}
const filled = FILLED.has(name);
/* Constant optical weight: ~1.35 device px of stroke at any render size. */
const sw = Math.min(30, Math.max(12, +((1.35 / size) * 256).toFixed(1)));
return (
`<svg width="${size}" height="${size}" viewBox="0 0 256 256" ` +
`fill="${filled ? "currentColor" : "none"}" ` +
`stroke="${filled && !HAIRLINE.has(name) ? "none" : "currentColor"}" ` +
`stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round" ` +
`aria-hidden="true" focusable="false">${path}</svg>`
);
};
window.VK.iconEl = function iconEl(name, size = 13) {
@@ -44,3 +129,19 @@ window.VK.iconEl = function iconEl(name, size = 13) {
span.innerHTML = window.VK.icon(name, size);
return span;
};
/* Fills any empty [data-icon] span. Safe to call repeatedly — it skips
anything already rendered, so it will not fight whatever app.js does. */
window.VK.hydrateIcons = function hydrateIcons(root = document) {
root.querySelectorAll("[data-icon]").forEach((el) => {
if (el.firstElementChild) return;
const size = Number(el.dataset.iconSize) || 13;
el.innerHTML = window.VK.icon(el.dataset.icon, size);
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => window.VK.hydrateIcons());
} else {
window.VK.hydrateIcons();
}