Apply the Nocturne fidelity pass to the web UI
This commit is contained in:
+128
-27
@@ -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();
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
<title>veskslop debug</title>
|
||||
<link rel="icon" href="data:," />
|
||||
<link rel="stylesheet" href="nocturne.css" />
|
||||
<link rel="stylesheet" href="tokens-status.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="media.css" />
|
||||
<link rel="stylesheet" href="style-overrides.css" />
|
||||
<link rel="stylesheet" href="settings-popups.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="vk-app">
|
||||
@@ -622,6 +625,7 @@
|
||||
<div id="toasts" class="vk-toasts"></div>
|
||||
|
||||
<script src="icons.js"></script>
|
||||
<script src="polish.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="media.js"></script>
|
||||
|
||||
+134
-73
@@ -6,7 +6,7 @@
|
||||
right: 0;
|
||||
top: var(--media-top, 50px);
|
||||
bottom: 0;
|
||||
/* Below the settings slide-over (80); media modals stay above it. */
|
||||
/* Chrome 40 · media 70 · settings 1000 · settings dialogs 1050 · toasts 1100 */
|
||||
z-index: 70;
|
||||
display: flex;
|
||||
background: var(--color-bg);
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
.vk-media-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vk-media-toolbar {
|
||||
@@ -71,7 +71,7 @@
|
||||
gap: 6px;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
color: var(--color-neutral-300);
|
||||
@@ -99,7 +99,7 @@
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
cursor: pointer;
|
||||
@@ -124,10 +124,10 @@
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 6px;
|
||||
font: 600 10px/1 ui-monospace, Menlo, monospace;
|
||||
font: 500 10px/1 ui-monospace, Menlo, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
|
||||
color: var(--color-neutral-100);
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@
|
||||
right: 6px;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
|
||||
color: var(--color-neutral-100);
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
max-height: 46vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
}
|
||||
@@ -212,7 +212,7 @@
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
}
|
||||
@@ -258,14 +258,14 @@
|
||||
.vk-media-viewer-media video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.vk-media-viewer-media iframe {
|
||||
width: min(880px, 100%);
|
||||
aspect-ratio: 16 / 9;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.vk-media-viewer.vertical .vk-media-viewer-media {
|
||||
@@ -297,7 +297,7 @@
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-accent-400, #7aa2ff);
|
||||
color: var(--color-accent-300);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
@@ -310,9 +310,9 @@
|
||||
}
|
||||
|
||||
.vk-media-card-kind {
|
||||
font: 600 10px/1 ui-monospace, Menlo, monospace;
|
||||
font: 500 10px/1 ui-monospace, Menlo, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-accent-400, var(--color-neutral-300));
|
||||
color: var(--color-accent-300);
|
||||
}
|
||||
|
||||
.vk-media-card-name {
|
||||
@@ -354,7 +354,7 @@
|
||||
border-radius: 50%;
|
||||
background: var(--color-neutral-800);
|
||||
color: var(--color-neutral-300);
|
||||
font: 600 9px/1 ui-monospace, Menlo, monospace;
|
||||
font: 500 9px/1 ui-monospace, Menlo, monospace;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
width: 90px;
|
||||
padding: 3px 6px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-900);
|
||||
color: var(--color-neutral-200);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
@@ -384,23 +384,23 @@
|
||||
}
|
||||
|
||||
.vk-seg-image {
|
||||
background: #4f8cff;
|
||||
background: var(--color-data-1);
|
||||
}
|
||||
|
||||
.vk-seg-gif {
|
||||
background: #8a6cff;
|
||||
background: var(--color-data-2);
|
||||
}
|
||||
|
||||
.vk-seg-video {
|
||||
background: #ff7a45;
|
||||
background: var(--color-data-3);
|
||||
}
|
||||
|
||||
.vk-seg-audio {
|
||||
background: #2fbf71;
|
||||
background: var(--color-data-4);
|
||||
}
|
||||
|
||||
.vk-seg-other {
|
||||
background: #6b7280;
|
||||
background: var(--color-data-5);
|
||||
}
|
||||
|
||||
.vk-media-modal {
|
||||
@@ -424,7 +424,7 @@
|
||||
gap: 10px;
|
||||
width: min(520px, 92vw);
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
}
|
||||
@@ -465,7 +465,8 @@
|
||||
.vk-media-tree-head {
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
background: var(--color-neutral-900);
|
||||
color: var(--color-neutral-200);
|
||||
font: 500 12px/1.2 inherit;
|
||||
@@ -510,7 +511,7 @@
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-400, #4f8cff);
|
||||
background: var(--color-accent-400);
|
||||
animation: vk-media-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -531,21 +532,22 @@
|
||||
}
|
||||
|
||||
.vk-media-error {
|
||||
color: #ff8080;
|
||||
color: var(--color-danger-300);
|
||||
}
|
||||
|
||||
/* Tinted, not flooded — and 10px so the count is actually readable. */
|
||||
.vk-media-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 7px;
|
||||
background: #e5484d;
|
||||
color: #fff;
|
||||
font: 600 9px/1 ui-monospace, Menlo, monospace;
|
||||
border-radius: 8px;
|
||||
background: var(--color-danger-800);
|
||||
color: var(--color-danger-200);
|
||||
font: 500 10px/1 ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
/* Bot tab: backfill jobs, stats, and settings. */
|
||||
@@ -565,11 +567,11 @@
|
||||
|
||||
.vk-bot-limited {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
font: 500 11px/1.4 ui-monospace, Menlo, monospace;
|
||||
color: #ffb0b0;
|
||||
background: color-mix(in srgb, #ff5555 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #ff5555 35%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
font: 400 11px/1.4 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-danger-200);
|
||||
background: var(--color-danger-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-danger-700);
|
||||
}
|
||||
|
||||
.vk-bot-limited[hidden] {
|
||||
@@ -577,8 +579,8 @@
|
||||
}
|
||||
|
||||
.vk-bot-section {
|
||||
border: 1px solid var(--color-divider);
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px 10px;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
@@ -592,7 +594,7 @@
|
||||
}
|
||||
|
||||
.vk-bot-title {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -625,8 +627,8 @@
|
||||
}
|
||||
|
||||
.vk-bot-job {
|
||||
border: 1px solid var(--color-neutral-800);
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -634,7 +636,7 @@
|
||||
}
|
||||
|
||||
.vk-bot-job-running {
|
||||
border-color: color-mix(in srgb, var(--color-accent, #4f8cff) 45%, transparent);
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent-700);
|
||||
}
|
||||
|
||||
.vk-bot-job-head {
|
||||
@@ -646,7 +648,7 @@
|
||||
|
||||
.vk-bot-job-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -656,27 +658,28 @@
|
||||
}
|
||||
|
||||
.vk-bot-state {
|
||||
font: 600 10px/1.4 ui-monospace, Menlo, monospace;
|
||||
font: 500 10px/1.4 ui-monospace, Menlo, monospace;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-800);
|
||||
color: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.vk-bot-state-running {
|
||||
background: color-mix(in srgb, var(--color-accent, #4f8cff) 25%, transparent);
|
||||
color: var(--color-accent-300, #9fc1ff);
|
||||
background: var(--color-accent-900);
|
||||
color: var(--color-accent-200);
|
||||
}
|
||||
|
||||
.vk-bot-state-paused {
|
||||
background: color-mix(in srgb, #ffaa33 20%, transparent);
|
||||
color: #ffc880;
|
||||
background: var(--color-warn-900);
|
||||
color: var(--color-warn-200);
|
||||
}
|
||||
|
||||
.vk-bot-state-done {
|
||||
background: color-mix(in srgb, #44cc88 20%, transparent);
|
||||
color: #8fe0b8;
|
||||
background: var(--color-ok-900);
|
||||
color: var(--color-ok-200);
|
||||
}
|
||||
|
||||
.vk-bot-job-member {
|
||||
@@ -687,14 +690,14 @@
|
||||
|
||||
.vk-bot-progress {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-800);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vk-bot-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-accent-400, #4f8cff);
|
||||
background: var(--color-accent-400);
|
||||
}
|
||||
|
||||
.vk-bot-job-actions {
|
||||
@@ -710,7 +713,7 @@
|
||||
|
||||
.vk-bot-group-title {
|
||||
cursor: pointer;
|
||||
font: 600 11px/1.6 ui-monospace, Menlo, monospace;
|
||||
font: 500 11px/1.6 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
@@ -739,11 +742,11 @@
|
||||
}
|
||||
|
||||
.vk-media-limited {
|
||||
color: #ff8080;
|
||||
color: var(--color-danger-300);
|
||||
}
|
||||
|
||||
.vk-media-done-note {
|
||||
color: color-mix(in srgb, var(--color-neutral-400) 55%, transparent);
|
||||
color: var(--color-neutral-500);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -754,7 +757,7 @@
|
||||
|
||||
.vk-bot-eta {
|
||||
cursor: help;
|
||||
text-decoration: underline dotted color-mix(in srgb, var(--color-neutral-400) 60%, transparent);
|
||||
text-decoration: underline dotted var(--color-neutral-600);
|
||||
}
|
||||
|
||||
/* Stream buffer: scrollable room while the next page loads. */
|
||||
@@ -778,7 +781,7 @@
|
||||
/* Advanced drawer: the "Do not include" group. */
|
||||
.vk-media-exclude-title {
|
||||
cursor: pointer;
|
||||
font: 600 11px/1.6 ui-monospace, Menlo, monospace;
|
||||
font: 500 11px/1.6 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
@@ -800,8 +803,8 @@
|
||||
|
||||
/* The YouTube title catch-up lane, inside the Bot tab backfill section. */
|
||||
.vk-bot-lane {
|
||||
border: 1px dashed var(--color-neutral-700);
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
@@ -814,15 +817,15 @@
|
||||
}
|
||||
|
||||
.vk-bot-progress-cached {
|
||||
background: var(--color-accent-400, #4f8cff);
|
||||
background: var(--color-accent-400);
|
||||
}
|
||||
|
||||
.vk-bot-progress-pending {
|
||||
background: var(--color-neutral-700);
|
||||
background: var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.vk-bot-progress-failed {
|
||||
background: #c04040;
|
||||
background: var(--color-danger-700);
|
||||
}
|
||||
|
||||
.vk-bot-history {
|
||||
@@ -830,20 +833,20 @@
|
||||
}
|
||||
|
||||
.vk-bot-history-title {
|
||||
font: 600 11px/1.6 ui-monospace, Menlo, monospace;
|
||||
font: 500 11px/1.6 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-300);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.vk-bot-history-entry {
|
||||
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-500);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.vk-bot-hint {
|
||||
margin-top: 8px;
|
||||
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-500);
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* Infinite/page toggle and pager. */
|
||||
@@ -856,7 +859,7 @@
|
||||
.vk-media-toggle {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--color-divider);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -870,8 +873,14 @@
|
||||
}
|
||||
|
||||
.vk-toggle-half.vk-toggle-on {
|
||||
background: var(--color-accent-400, #4f8cff);
|
||||
color: #fff;
|
||||
background: var(--color-accent-900);
|
||||
color: var(--color-accent-200);
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent-700);
|
||||
}
|
||||
|
||||
.vk-toggle-half:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.vk-media-page-nav {
|
||||
@@ -893,7 +902,59 @@
|
||||
}
|
||||
|
||||
.vk-media-viewer-title {
|
||||
font: 600 13px/1.5 ui-monospace, Menlo, monospace;
|
||||
font: 500 13px/1.5 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* ── Focus: these controls are bare <button>s with background and border
|
||||
reset, so they need the accent ring pinned back on. ───────────────────── */
|
||||
|
||||
.vk-media-link:focus-visible,
|
||||
.vk-media-tree-head:focus-visible,
|
||||
.vk-media-play:focus-visible,
|
||||
.vk-media-media:focus-visible,
|
||||
.vk-bot-group-title:focus-visible,
|
||||
.vk-media-exclude-title:focus-visible,
|
||||
.vk-media-advanced summary:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ── Storage bar legend: hue is never the only channel. Render one of these
|
||||
per segment beside the bar so the categories survive a colour-blind
|
||||
reader, a projector, and a screenshot. ────────────────────────────────── */
|
||||
|
||||
.vk-media-usage-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.vk-media-usage-key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font: 400 10px/1.4 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.vk-media-usage-key i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: none;
|
||||
border-radius: 2px;
|
||||
background: var(--color-data-5);
|
||||
}
|
||||
|
||||
.vk-media-usage-key.image i { background: var(--color-data-1); }
|
||||
.vk-media-usage-key.gif i { background: var(--color-data-2); }
|
||||
.vk-media-usage-key.video i { background: var(--color-data-3); }
|
||||
.vk-media-usage-key.audio i { background: var(--color-data-4); }
|
||||
|
||||
/* Over the warn threshold the bar itself should say so. */
|
||||
.vk-media-usage.over {
|
||||
box-shadow: inset 0 0 0 1px var(--color-warn-700);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Veskslop UI polish — progressive enhancement, no markup edits.
|
||||
|
||||
Three things CSS cannot reach:
|
||||
· close buttons that are the letter "X"
|
||||
· the play control that is a ▶ character at 44px
|
||||
· help text trapped in a native title tooltip
|
||||
|
||||
Each fix is idempotent and runs again on DOM changes, so it survives the
|
||||
Media and Bot tabs rebuilding their trees. Load AFTER icons.js.
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
(function () {
|
||||
const VK = window.VK;
|
||||
if (!VK || !VK.icon) return;
|
||||
|
||||
const GLYPH_CLOSE = /^[xX✕✖×]$/;
|
||||
const GLYPH_PLAY = /^[▶►▸]$/;
|
||||
|
||||
function polish(root) {
|
||||
if (!root || root.nodeType !== 1) return;
|
||||
|
||||
/* Text-glyph close buttons → the x icon, with a real label. */
|
||||
root.querySelectorAll("button, .vk-chip").forEach((el) => {
|
||||
const text = (el.textContent || "").trim();
|
||||
if (GLYPH_CLOSE.test(text)) {
|
||||
el.innerHTML = VK.icon("x", 13);
|
||||
el.classList.add("vk-icon-btn");
|
||||
if (!el.getAttribute("aria-label")) el.setAttribute("aria-label", "Close");
|
||||
} else if (GLYPH_PLAY.test(text)) {
|
||||
el.innerHTML = VK.icon("play", 22);
|
||||
if (!el.getAttribute("aria-label")) el.setAttribute("aria-label", "Play");
|
||||
}
|
||||
});
|
||||
|
||||
/* .vk-media-play is a ▶ character styled at 44px. */
|
||||
root.querySelectorAll(".vk-media-play").forEach((el) => {
|
||||
if (el.querySelector("svg")) return;
|
||||
el.innerHTML = VK.icon("play", 22);
|
||||
if (!el.getAttribute("aria-label")) el.setAttribute("aria-label", "Play");
|
||||
});
|
||||
|
||||
/* Help marks: move title → data-tip so the styled tooltip is the only
|
||||
one that fires, and keep the text reachable for screen readers. */
|
||||
root.querySelectorAll(".vk-info").forEach((el) => {
|
||||
const tip = el.getAttribute("title");
|
||||
if (tip) {
|
||||
el.dataset.tip = tip;
|
||||
el.removeAttribute("title");
|
||||
el.setAttribute("aria-label", tip);
|
||||
}
|
||||
if (!el.textContent.trim()) el.textContent = "i";
|
||||
if (!el.hasAttribute("tabindex")) el.tabIndex = 0;
|
||||
el.setAttribute("role", "note");
|
||||
});
|
||||
|
||||
if (VK.hydrateIcons) VK.hydrateIcons(root);
|
||||
}
|
||||
|
||||
function run() {
|
||||
polish(document.body);
|
||||
new MutationObserver((records) => {
|
||||
for (const r of records) {
|
||||
r.addedNodes.forEach((n) => polish(n));
|
||||
}
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,476 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Veskslop — settings, menus, dialogs and toasts.
|
||||
|
||||
The shell reads as Nocturne; these surfaces did not. They were built from
|
||||
flat 1px rules, bare native controls and text glyphs, so opening Settings
|
||||
or a scrape dialog felt like leaving the app. Nothing here changes
|
||||
behaviour or markup — it is presentation only.
|
||||
|
||||
Load LAST: nocturne.css · tokens-status.css · style.css · media.css ·
|
||||
style-overrides.css · settings-popups.css
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── 1. The scrim ─────────────────────────────────────────────────────────
|
||||
A flat 55% wash left the page behind perfectly legible, so the eye kept
|
||||
reading it. Deepen it, blur what is behind, and fade it in. */
|
||||
|
||||
.vk-overlay {
|
||||
background: color-mix(in srgb, var(--color-bg) 72%, transparent);
|
||||
backdrop-filter: blur(3px) saturate(0.9);
|
||||
animation: vk-scrim-in 140ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes vk-scrim-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 2. Settings panel ────────────────────────────────────────────────────
|
||||
A slide-over with a square left edge butting into a blurred page reads as
|
||||
a torn sheet. Round the leading corners, give it the system's hairline,
|
||||
and let it arrive from the edge it is attached to. */
|
||||
|
||||
.vk-settings-panel {
|
||||
width: min(720px, 100%);
|
||||
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg), inset 1px 0 0 var(--color-neutral-800);
|
||||
animation: vk-panel-in 200ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes vk-panel-in {
|
||||
from {
|
||||
transform: translateX(18px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Head: sticky, so the title survives a long scroll, and on the system's
|
||||
fading rule rather than a flat one. */
|
||||
.vk-settings-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: 13px 16px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: none;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-neutral-800) 40px,
|
||||
var(--color-neutral-800) calc(100% - 40px),
|
||||
transparent
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
background-size: 100% 1px;
|
||||
}
|
||||
|
||||
.vk-settings-title {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.vk-settings-body {
|
||||
gap: 26px;
|
||||
padding: 18px 16px 28px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-neutral-800) transparent;
|
||||
}
|
||||
|
||||
.vk-settings-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.vk-settings-body::-webkit-scrollbar-thumb {
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: var(--color-neutral-800) padding-box;
|
||||
}
|
||||
|
||||
.vk-settings-body::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-neutral-700) padding-box;
|
||||
}
|
||||
|
||||
/* ── 3. Section heads ─────────────────────────────────────────────────────
|
||||
Label, breathing room, then the actions pinned right on the same
|
||||
baseline — previously the buttons crowded the label. */
|
||||
|
||||
.vk-section-head {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 2px 9px;
|
||||
}
|
||||
|
||||
.vk-settings-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── 4. Cards and rows ────────────────────────────────────────────────────
|
||||
The card was a flat fill; rows ran into each other. Give the card the
|
||||
system edge and separate rows with fading rules, inset from both ends. */
|
||||
|
||||
.vk-settings-card {
|
||||
padding: 4px 12px;
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.vk-status-row {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 9px 0;
|
||||
box-shadow: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-neutral-800) 16px,
|
||||
var(--color-neutral-800) calc(100% - 16px),
|
||||
transparent
|
||||
)
|
||||
no-repeat bottom / 100% 1px;
|
||||
}
|
||||
|
||||
.vk-status-row:last-child,
|
||||
.vk-settings-card > .vk-status-row:last-of-type {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.vk-status-row .vk-status-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.vk-status-row .vk-status-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── 5. The info affordance ───────────────────────────────────────────────
|
||||
.vk-info was an unstyled letter "i" sitting in the text, and its help
|
||||
text only existed as a native browser tooltip — half a second of delay,
|
||||
no styling, and invisible on touch. Make it a real mark with a themed
|
||||
tooltip. polish.js moves title → data-tip so the native one stops
|
||||
double-firing; the title fallback below keeps it working without JS. */
|
||||
|
||||
.vk-info {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-700);
|
||||
font: 500 9px/1 var(--font-heading);
|
||||
color: var(--color-neutral-400);
|
||||
cursor: help;
|
||||
position: relative;
|
||||
transition: color 120ms, box-shadow 120ms;
|
||||
}
|
||||
|
||||
.vk-info:hover,
|
||||
.vk-info:focus-visible {
|
||||
color: var(--color-accent-300);
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent-700);
|
||||
}
|
||||
|
||||
.vk-info::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
z-index: 30;
|
||||
width: max-content;
|
||||
max-width: 280px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-neutral-900);
|
||||
box-shadow: var(--shadow-md), inset 0 0 0 1px var(--color-neutral-800);
|
||||
font: 400 11.5px/1.5 var(--font-body);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
color: var(--color-neutral-200);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms;
|
||||
}
|
||||
|
||||
.vk-info:not([data-tip])::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.vk-info:hover::after,
|
||||
.vk-info:focus-visible::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── 6. Collapsible groups ────────────────────────────────────────────────
|
||||
The summary was a bare line of uppercase text; nothing said it was a
|
||||
control. Give it a row of its own, a hover tint and a caret that turns. */
|
||||
|
||||
.vk-settings-summary {
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-neutral-900);
|
||||
transition: background 120ms, color 120ms;
|
||||
}
|
||||
|
||||
.vk-settings-summary:hover {
|
||||
background: var(--color-neutral-800);
|
||||
color: var(--color-accent-200);
|
||||
}
|
||||
|
||||
.vk-settings-summary .vk-caret {
|
||||
display: inline-flex;
|
||||
color: var(--color-neutral-400);
|
||||
transition: transform 160ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
.vk-settings-group[open] > .vk-settings-summary {
|
||||
color: var(--color-accent-200);
|
||||
}
|
||||
|
||||
.vk-settings-group-body {
|
||||
gap: 24px;
|
||||
padding: 16px 0 4px 10px;
|
||||
box-shadow: inset 1px 0 0 var(--color-neutral-900);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* ── 7. Native controls ───────────────────────────────────────────────────
|
||||
Selects kept the platform arrow and a 1px border; numbers kept their
|
||||
spinners; ranges kept the platform track. All three read as unstyled. */
|
||||
|
||||
.vk-select {
|
||||
appearance: none;
|
||||
padding: 5px 26px 5px 9px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
background-color: var(--color-bg);
|
||||
background-image: none;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 120ms;
|
||||
}
|
||||
|
||||
/* The caret is a mask, not a coloured data-URI, so it takes its colour from
|
||||
the token rather than from a hex baked into the URL. */
|
||||
.vk-select-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.vk-select-wrap::after,
|
||||
.vk-status-value:has(> .vk-select)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
top: 50%;
|
||||
translate: 0 -50%;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
pointer-events: none;
|
||||
background: var(--color-neutral-400);
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='none' stroke='%23000' stroke-width='26' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M208 96l-80 80-80-80'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
|
||||
.vk-status-value:has(> .vk-select) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vk-select:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-700);
|
||||
}
|
||||
|
||||
.vk-select:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vk-media-input,
|
||||
input[type="number"].input {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.vk-media-input::-webkit-outer-spin-button,
|
||||
.vk-media-input::-webkit-inner-spin-button {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
appearance: none;
|
||||
height: 18px;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-neutral-800);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-neutral-800);
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: -4.5px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-200);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-200);
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
}
|
||||
|
||||
input[type="range"]:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ── 8. Menus ─────────────────────────────────────────────────────────────
|
||||
The guild picker floated with no edge, and the active item was a tint
|
||||
with nothing to anchor it. */
|
||||
|
||||
.vk-menu {
|
||||
padding: 5px;
|
||||
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--color-neutral-800);
|
||||
animation: vk-menu-in 120ms ease-out;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-neutral-800) transparent;
|
||||
}
|
||||
|
||||
@keyframes vk-menu-in {
|
||||
from {
|
||||
transform: translateY(-4px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vk-menu-item {
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 100ms, color 100ms;
|
||||
}
|
||||
|
||||
.vk-menu-item.active {
|
||||
box-shadow: inset 2px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
/* ── 9. Dialogs ───────────────────────────────────────────────────────────
|
||||
The voice-profile drawer was square-topped and flat-bottomed against the
|
||||
scrim; the media modals had their own unrelated corner radii. */
|
||||
|
||||
.vk-profile-panel {
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.vk-profile-panel .vk-settings-head {
|
||||
cursor: grab;
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
}
|
||||
|
||||
.vk-profile-panel .vk-settings-head:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* A grab handle, so the drag target is discoverable. */
|
||||
.vk-profile-panel .vk-settings-head::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
width: 34px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.vk-profile-panel .vk-settings-head {
|
||||
position: relative;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.vk-media-modal-box {
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.vk-media-head strong {
|
||||
font: 500 14px/1.2 var(--font-heading);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ── 10. Toasts ───────────────────────────────────────────────────────────
|
||||
Icon slot, roles carried by tokens, and an entrance that comes from the
|
||||
corner they live in. */
|
||||
|
||||
.vk-toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
line-height: 1.5;
|
||||
box-shadow: var(--shadow-md), inset 0 0 0 1px var(--color-neutral-800);
|
||||
animation: vk-toast-in 160ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes vk-toast-in {
|
||||
from {
|
||||
transform: translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vk-toast .vk-icon {
|
||||
flex: none;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.vk-toast.error .vk-icon {
|
||||
color: var(--color-danger-400);
|
||||
}
|
||||
|
||||
.vk-toast.warn .vk-icon {
|
||||
color: var(--color-warn-400);
|
||||
}
|
||||
|
||||
.vk-toast.ok .vk-icon {
|
||||
color: var(--color-ok-400);
|
||||
}
|
||||
|
||||
/* ── 11. Motion ───────────────────────────────────────────────────────────*/
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vk-overlay,
|
||||
.vk-settings-panel,
|
||||
.vk-menu,
|
||||
.vk-toast {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.vk-settings-summary .vk-caret {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Veskslop style overrides — Nocturne fidelity pass.
|
||||
|
||||
Small corrections to style.css. Load LAST, after style.css and media.css.
|
||||
Fold into style.css when convenient; kept separate here so nothing you
|
||||
edited gets clobbered.
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── 1. Weight ────────────────────────────────────────────────────────────
|
||||
Nocturne: headings never go past 500; hierarchy is size and space.
|
||||
"Speaking" was font-weight 700, which also reflows the row as it toggles.
|
||||
Carry it in color instead — the level meter beside it is the loud signal. */
|
||||
|
||||
.vk-person-name.speaking,
|
||||
.vk-voice-member .speaking,
|
||||
.vk-right-name .speaking,
|
||||
.vk-right-name .vk-name.speaking {
|
||||
font-weight: 500;
|
||||
color: var(--color-accent-200);
|
||||
}
|
||||
|
||||
/* ── 2. Focus ─────────────────────────────────────────────────────────────
|
||||
nocturne.css gives :focus-visible a 2px accent ring, but every control
|
||||
built from a bare <button> that resets background and border loses the
|
||||
ring's contrast against its own surface. Pin it back on explicitly. */
|
||||
|
||||
.vk-rail-row:focus-visible,
|
||||
.vk-rail-voice-head:focus-visible,
|
||||
.vk-menu-item:focus-visible,
|
||||
.vk-guild-mark:focus-visible,
|
||||
.vk-scope-btn:focus-visible,
|
||||
.vk-console-tab:focus-visible,
|
||||
.vk-console-button:focus-visible,
|
||||
.vk-chip:focus-visible,
|
||||
.vk-star:focus-visible,
|
||||
.vk-tab:focus-visible,
|
||||
.vk-settings-summary:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Inset the ring where the control sits flush in a scroller, so it is not
|
||||
clipped by the overflow edge. */
|
||||
.vk-rail-row:focus-visible,
|
||||
.vk-menu-item:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* ── 3. Presence ──────────────────────────────────────────────────────────
|
||||
idle and dnd both landed on neutral-200, so they were indistinguishable.
|
||||
idle drops a step; dnd keeps the bright dot and adds the bar that makes
|
||||
it readable without color. */
|
||||
|
||||
.vk-presence.idle {
|
||||
background: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.vk-presence.dnd {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--color-neutral-200);
|
||||
}
|
||||
|
||||
.vk-presence.dnd::after {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 1.5px;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
/* ── 4. Rules ─────────────────────────────────────────────────────────────
|
||||
Nocturne rules fade to transparent over 48px a side. The channel header
|
||||
does this; the settings panel's dozens of flat rules do not, which is
|
||||
what makes that panel read as a different product. */
|
||||
|
||||
.vk-settings-head,
|
||||
.vk-status-row {
|
||||
box-shadow: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-neutral-900) 24px,
|
||||
var(--color-neutral-900) calc(100% - 24px),
|
||||
transparent
|
||||
)
|
||||
no-repeat bottom / 100% 1px;
|
||||
}
|
||||
|
||||
.vk-settings-head {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-neutral-800) 24px,
|
||||
var(--color-neutral-800) calc(100% - 24px),
|
||||
transparent
|
||||
)
|
||||
no-repeat bottom / 100% 1px;
|
||||
}
|
||||
|
||||
.vk-status-row:last-child {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* ── 5. Toasts ────────────────────────────────────────────────────────────
|
||||
The error toast was a neutral-200 stripe — the same mark the ok toast
|
||||
used, one hue apart. Now they are genuinely different roles. */
|
||||
|
||||
.vk-toast.error {
|
||||
box-shadow: var(--shadow-md), inset 3px 0 0 var(--color-danger-400);
|
||||
}
|
||||
|
||||
.vk-toast.warn {
|
||||
box-shadow: var(--shadow-md), inset 3px 0 0 var(--color-warn-400);
|
||||
}
|
||||
|
||||
.vk-toast.ok {
|
||||
box-shadow: var(--shadow-md), inset 3px 0 0 var(--color-ok-400);
|
||||
}
|
||||
|
||||
/* ── 6. Offline ───────────────────────────────────────────────────────────
|
||||
opacity .5 + saturate .6 pushes every label under 3:1 — the one state
|
||||
where you most need to read what is on screen. Dim the chrome, not the
|
||||
type, and say why. */
|
||||
|
||||
.vk-offline .vk-body,
|
||||
.vk-offline .vk-console {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vk-offline .vk-body::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: color-mix(in srgb, var(--color-bg) 45%, transparent);
|
||||
}
|
||||
|
||||
.vk-offline .vk-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vk-offline .vk-composer,
|
||||
.vk-offline .vk-rail,
|
||||
.vk-offline .vk-right {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
/* ── 7. The bridge dot ────────────────────────────────────────────────────
|
||||
.error was neutral-200 — a white dot reads as "on". */
|
||||
|
||||
.vk-dot.error {
|
||||
background: var(--color-danger-400);
|
||||
}
|
||||
|
||||
.vk-dot.warn {
|
||||
background: var(--color-warn-400);
|
||||
}
|
||||
|
||||
/* ── 8. QR ────────────────────────────────────────────────────────────────
|
||||
Pure white is correct here (scanner contrast), but give it the system's
|
||||
edge so it is not a bare rectangle floating on the panel. */
|
||||
|
||||
.vk-qr {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Nocturne status + data tokens — additive layer for Veskslop.
|
||||
|
||||
Why this file exists: Nocturne ships one accent and a neutral ramp. It has
|
||||
no error / warning / success role and no categorical data scale, so every
|
||||
feature that needed one invented its own hex (#e5484d, #ffaa33, #44cc88,
|
||||
#4f8cff, #ff8080 …). That is how a system drifts.
|
||||
|
||||
These roles are generated the same way Nocturne's own ramps are: OKLCH,
|
||||
on the SAME shared lightness scale, so step 400 of danger has the same
|
||||
visual weight as step 400 of accent or neutral. Chroma stays low — the
|
||||
accent is still the loudest thing on screen.
|
||||
|
||||
Lightness scale, read off Nocturne's neutral ramp:
|
||||
200 .924 · 300 .859 · 400 .763 · 700 .443 · 800 .339 · 900 .240
|
||||
|
||||
Load AFTER nocturne.css and BEFORE style.css.
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
:root {
|
||||
/* danger — hue 25. Failed jobs, rate limits, destructive confirms. */
|
||||
--color-danger-200: oklch(0.924 0.045 25);
|
||||
--color-danger-300: oklch(0.859 0.075 25);
|
||||
--color-danger-400: oklch(0.763 0.105 25);
|
||||
--color-danger-700: oklch(0.443 0.085 25);
|
||||
--color-danger-800: oklch(0.339 0.065 25);
|
||||
--color-danger-900: oklch(0.24 0.045 25);
|
||||
|
||||
/* warn — hue 75. Paused, degraded, over-threshold, mentions. */
|
||||
--color-warn-200: oklch(0.924 0.045 75);
|
||||
--color-warn-300: oklch(0.859 0.075 75);
|
||||
--color-warn-400: oklch(0.763 0.105 75);
|
||||
--color-warn-700: oklch(0.443 0.085 75);
|
||||
--color-warn-800: oklch(0.339 0.065 75);
|
||||
--color-warn-900: oklch(0.24 0.045 75);
|
||||
|
||||
/* ok — hue 155. Completed, healthy, connected. Used sparingly: the
|
||||
accent already carries "live", so ok is for terminal success only. */
|
||||
--color-ok-200: oklch(0.924 0.05 155);
|
||||
--color-ok-300: oklch(0.859 0.08 155);
|
||||
--color-ok-400: oklch(0.763 0.11 155);
|
||||
--color-ok-700: oklch(0.443 0.09 155);
|
||||
--color-ok-800: oklch(0.339 0.07 155);
|
||||
--color-ok-900: oklch(0.24 0.05 155);
|
||||
|
||||
/* Categorical data scale — for the media storage bar and nothing else.
|
||||
One lightness, five hues stepping away from the accent, all at low
|
||||
chroma so the bar reads as a muted family rather than five alarms.
|
||||
Always pair with a text legend: hue is never the only channel. */
|
||||
--color-data-1: oklch(0.62 0.075 289); /* image — the accent's own hue */
|
||||
--color-data-2: oklch(0.62 0.075 250); /* gif */
|
||||
--color-data-3: oklch(0.62 0.075 205); /* video */
|
||||
--color-data-4: oklch(0.62 0.075 155); /* audio */
|
||||
--color-data-5: oklch(0.62 0.02 289); /* other — near-neutral */
|
||||
|
||||
/* Mention highlight: was a loose #d29922 declared mid-stylesheet. */
|
||||
--color-mention: var(--color-warn-400);
|
||||
}
|
||||
|
||||
/* ── Status helpers ────────────────────────────────────────────────────────
|
||||
Tinted, never flooded. Full-opacity ink on the tint so small text clears
|
||||
contrast; the fill is a deep ramp step, not an alpha wash. */
|
||||
|
||||
.vk-status-tint-danger {
|
||||
background: var(--color-danger-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-danger-700);
|
||||
color: var(--color-danger-200);
|
||||
}
|
||||
|
||||
.vk-status-tint-warn {
|
||||
background: var(--color-warn-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-warn-700);
|
||||
color: var(--color-warn-200);
|
||||
}
|
||||
|
||||
.vk-status-tint-ok {
|
||||
background: var(--color-ok-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-ok-700);
|
||||
color: var(--color-ok-200);
|
||||
}
|
||||
|
||||
.vk-status-tint-neutral {
|
||||
background: var(--color-neutral-900);
|
||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||
color: var(--color-neutral-300);
|
||||
}
|
||||
Reference in New Issue
Block a user