Apply the Nocturne fidelity pass to the web UI
This commit is contained in:
@@ -40,6 +40,14 @@ mobile draft at 390px.
|
|||||||
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
||||||
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
||||||
|
- [x] C74 Nocturne fidelity pass from the third party. Integrated the applied tree from
|
||||||
|
`Debug WebUI mockups(1).zip`: `tokens-status.css` (danger/warn/ok roles and the data
|
||||||
|
scale), `style-overrides.css`, `settings-popups.css`, `polish.js`, the Phosphor
|
||||||
|
`icons.js`, and the token cleanup in `media.css`. `index.html` loads them in their order
|
||||||
|
(the two override sheets last, `polish.js` after `icons.js`). The writeup lives in
|
||||||
|
[docs/ui-changes.md](docs/ui-changes.md). Their tree matched ours file-for-file apart
|
||||||
|
from those files, so nothing regressed. UI smoke test passes; the TTS panel renders the
|
||||||
|
voice-processing block correctly under the new sheets.
|
||||||
- [x] C73 Echo on the vox voice. `aecho`'s `in_gain`/`out_gain` scale the dry signal
|
- [x] C73 Echo on the vox voice. `aecho`'s `in_gain`/`out_gain` scale the dry signal
|
||||||
as well as the repeats, so a low echo level quieted the whole line (the classic path's
|
as well as the repeats, so a low echo level quieted the whole line (the classic path's
|
||||||
loudnorm hid it; the streaming path has no measurement, so vox + stream word mode just
|
loudnorm hid it; the streaming path has no measurement, so vox + stream word mode just
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ This file lists the project documents.
|
|||||||
| [auth.md](auth.md) | QR login and token persistence |
|
| [auth.md](auth.md) | QR login and token persistence |
|
||||||
| [testing.md](testing.md) | Tests and the live test setup |
|
| [testing.md](testing.md) | Tests and the live test setup |
|
||||||
| [decisions.md](decisions.md) | Design decisions and their reasons |
|
| [decisions.md](decisions.md) | Design decisions and their reasons |
|
||||||
|
| [ui-changes.md](ui-changes.md) | The Nocturne fidelity pass over the web UI (third-party writeup) |
|
||||||
|
|
||||||
## Documents outside `docs/`
|
## Documents outside `docs/`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
# Veskslop UI — Nocturne fidelity pass
|
||||||
|
|
||||||
|
The shell (header, rail, call strip, composer, TTS box, console, right column)
|
||||||
|
is faithful to the design. The drift is all in the layer added since: the Media
|
||||||
|
archive and Bot tabs, plus a few spots in `style.css`.
|
||||||
|
|
||||||
|
**Part two** (icons, settings, menus, dialogs, toasts) starts further down.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" href="nocturne.css" />
|
||||||
|
<link rel="stylesheet" href="tokens-status.css" /> <!-- new, additive -->
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<link rel="stylesheet" href="media.css" /> <!-- replaces yours -->
|
||||||
|
<link rel="stylesheet" href="style-overrides.css" /> <!-- new, load last -->
|
||||||
|
<link rel="stylesheet" href="settings-popups.css" /> <!-- new, load last -->
|
||||||
|
|
||||||
|
<script src="icons.js"></script> <!-- replaces yours -->
|
||||||
|
<script src="polish.js"></script> <!-- new, after icons.js -->
|
||||||
|
```
|
||||||
|
|
||||||
|
`tokens-status.css`, `style-overrides.css`, `settings-popups.css` and `polish.js`
|
||||||
|
are new files. `media.css` and `icons.js` are your files with the changes
|
||||||
|
applied — diff them, don't assume.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The root cause
|
||||||
|
|
||||||
|
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: `#e5484d`, `#ffaa33`, `#44cc88`, `#ff5555`, `#ff8080`,
|
||||||
|
`#c04040`, `#4f8cff`, `#8a6cff`, `#ff7a45`, `#2fbf71`, `#6b7280`, `#d29922`.
|
||||||
|
Eighteen hexes, four different reds, none on the system's lightness scale.
|
||||||
|
|
||||||
|
`tokens-status.css` fills that hole properly — danger, warn, ok and a five-step
|
||||||
|
data scale, generated in OKLCH on Nocturne's **own** shared lightness scale, so
|
||||||
|
step 400 of danger carries the same visual weight as step 400 of accent. Chroma
|
||||||
|
stays low; the accent is still the loudest thing on screen.
|
||||||
|
|
||||||
|
Everything else in this patch is downstream of having those tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
|
||||||
|
### 1. Colour
|
||||||
|
|
||||||
|
- 18 hard-coded hexes replaced with tokens. Zero remain.
|
||||||
|
- **Wrong-hue fallbacks removed.** `var(--color-accent-400, #4f8cff)` appeared
|
||||||
|
six times. `#4f8cff` is blue; Nocturne's accent is blurple `#9184d9`. Any time
|
||||||
|
that fallback fired — a load-order slip, a scoping mistake — the Media and Bot
|
||||||
|
tabs turned a different colour than the rest of the app. Silent, and hard to
|
||||||
|
spot because it only happens when something else is already broken.
|
||||||
|
- The storage bar's five saturated hues become the low-chroma `--color-data-*`
|
||||||
|
family. Nocturne's rule is that contrast comes from the tonal ramps, not from
|
||||||
|
saturation; five alarm colours in a 10px bar broke it harder than anything
|
||||||
|
else on screen.
|
||||||
|
- `--color-mention: #d29922` was declared in a stray second `:root` block 400
|
||||||
|
lines into `style.css`. It now lives with the other tokens as `--color-warn-400`.
|
||||||
|
|
||||||
|
### 2. Floods
|
||||||
|
|
||||||
|
Nocturne outlines; it does not fill.
|
||||||
|
|
||||||
|
- `.vk-toggle-half.vk-toggle-on` was a solid `accent-400` panel with `#fff`
|
||||||
|
text — the one thing the guide names as a don't. Now tinted and outlined, the
|
||||||
|
same treatment `.vk-scope-btn.active` already uses.
|
||||||
|
- `.vk-media-badge` was `#e5484d` with white text at 9px. Now a `danger-800`
|
||||||
|
tint with `danger-200` ink at 10px.
|
||||||
|
- `.vk-bot-state-*` pills moved from alpha washes over saturated hues to deep
|
||||||
|
ramp fills with full-opacity ink — an alpha wash over an unknown backdrop is
|
||||||
|
how small text loses contrast without anyone noticing.
|
||||||
|
|
||||||
|
### 3. Weight
|
||||||
|
|
||||||
|
Nocturne heads cap at 500; hierarchy is size and space. Removed 11 instances of
|
||||||
|
`font: 600` and `font-weight: 600` in `media.css`, plus `font-weight: 700` on
|
||||||
|
the speaking state in `style.css`. That last one also **reflowed the row every
|
||||||
|
time someone started or stopped talking** — the names jittered. Speaking now
|
||||||
|
reads in `accent-200`, with the level meter beside it doing the loud work.
|
||||||
|
|
||||||
|
### 4. Edges and radii
|
||||||
|
|
||||||
|
`border: 1px solid` → the system's `inset 0 0 0 1px` edge, so elevation stays
|
||||||
|
"hairline plus ambient darkness" rather than a drawn box. Literal `3px / 5px /
|
||||||
|
6px / 7px` radii → `--radius-sm` / `--radius-md`. Six different corner radii
|
||||||
|
were in play across the Bot tab.
|
||||||
|
|
||||||
|
### 5. Focus
|
||||||
|
|
||||||
|
`nocturne.css` sets a 2px accent `:focus-visible` ring, but every control built
|
||||||
|
from a bare `<button>` that resets `background` and `border` loses it against
|
||||||
|
its own surface. Explicitly restored on 18 controls: rail rows, guild marks,
|
||||||
|
scope buttons, console tabs, chips, stars, mobile tabs, settings summaries,
|
||||||
|
media links, tree heads, group titles. **This is the only keyboard path through
|
||||||
|
the app** — worth checking by tabbing the whole thing once.
|
||||||
|
|
||||||
|
### 6. Contrast
|
||||||
|
|
||||||
|
- `.vk-media-done-note` was `color-mix(neutral-400 55%, transparent)` at 10px —
|
||||||
|
roughly 2.4:1. Now `neutral-500`.
|
||||||
|
- `.vk-bot-history-entry` and friends: 10px mono at `neutral-500` sits just
|
||||||
|
under 4.5:1 on surface. Lifted one step to `neutral-400`.
|
||||||
|
- **The offline state was the worst of it.** `opacity: .5` plus
|
||||||
|
`saturate(.6)` on the entire body and console pushed every label under 3:1 —
|
||||||
|
in the one state where you most need to read what is on screen and work out
|
||||||
|
what died. Now the chrome dims behind a scrim and the type stays legible.
|
||||||
|
Input is still blocked.
|
||||||
|
|
||||||
|
### 7. Status marks that were saying the wrong thing
|
||||||
|
|
||||||
|
- `.vk-dot.error` was `neutral-200` — a bright white dot, which reads as
|
||||||
|
healthy. Now `danger-400`.
|
||||||
|
- `.vk-toast.error` and `.vk-toast.ok` differed only by one hue step of the same
|
||||||
|
near-white. Now genuinely different roles, plus a `warn` variant.
|
||||||
|
- `.vk-presence.idle` and dnd both landed on `neutral-200`, so they were
|
||||||
|
indistinguishable. idle drops a step; dnd keeps the bright dot and gains the
|
||||||
|
bar that makes it readable without colour.
|
||||||
|
|
||||||
|
### 8. Rules
|
||||||
|
|
||||||
|
Nocturne's rules fade to transparent over 48px a side. The channel header does
|
||||||
|
this; the settings panel's dozens of flat `inset 0 -1px 0` rules did not, which
|
||||||
|
is most of why that panel reads as a different product bolted on. Now fading.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Needs a small markup change
|
||||||
|
|
||||||
|
The storage bar is colour-only. `media.css` now carries
|
||||||
|
`.vk-media-usage-legend` / `.vk-media-usage-key` — render one key per segment
|
||||||
|
beside the bar:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="vk-media-usage-legend">
|
||||||
|
<span class="vk-media-usage-key image"><i></i>images 4.1 GiB</span>
|
||||||
|
<span class="vk-media-usage-key gif"><i></i>gifs 0.8</span>
|
||||||
|
<span class="vk-media-usage-key video"><i></i>video 11.2</span>
|
||||||
|
<span class="vk-media-usage-key audio"><i></i>audio 0.3</span>
|
||||||
|
<span class="vk-media-usage-key"><i></i>other 0.1</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `.over` to `.vk-media-usage` past the warn threshold.
|
||||||
|
|
||||||
|
Also: `.vk-presence.dnd` now exists as a state. If `app.js` only emits
|
||||||
|
`online` / `idle` / `offline`, dnd is still rendering as plain online.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Not changed — worth a conversation
|
||||||
|
|
||||||
|
**The Media and Bot tabs are a second application inside the shell.** They
|
||||||
|
don't use the rail, the right column or the console; they take over the whole
|
||||||
|
body with their own toolbar, their own grid, their own modals at their own
|
||||||
|
z-index tier. The patch makes them *look* like the rest of the app. It doesn't
|
||||||
|
make them *behave* like it. Worth deciding whether Media is a scope alongside
|
||||||
|
Guild/DM — which is what the segmented control currently implies — or a separate
|
||||||
|
surface that should stop pretending to be one.
|
||||||
|
|
||||||
|
**Settings is carrying nine sections in one slide-over**, several of which
|
||||||
|
(Flextalk voice profile, Bot audio, Speech) are deep enough to be their own
|
||||||
|
screens. The `<details>` grouping is holding for now, but the voice profile
|
||||||
|
panel already had to escape into its own draggable dialog, which is the usual
|
||||||
|
sign.
|
||||||
|
|
||||||
|
**`.vk-media-play` is a text glyph** (font-size 44px) where the rest of the app
|
||||||
|
uses Phosphor. Small, but it is the only place an icon doesn't match.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part two — icons, settings, menus, dialogs
|
||||||
|
|
||||||
|
## 9. Icons
|
||||||
|
|
||||||
|
`icons.js` is replaced. The old set was drawn on a 16-unit grid at a fixed 1.3
|
||||||
|
stroke; the new one is Phosphor geometry on Phosphor's 256 grid.
|
||||||
|
|
||||||
|
- **Optical weight is now constant.** Stroke is computed per render size
|
||||||
|
(`1.35 / size * 256`), so a 44px icon and an 11px icon carry the same line.
|
||||||
|
Before, stroke scaled with the icon and large ones looked heavy.
|
||||||
|
- **Round caps and joins throughout** — Phosphor regular's terminal, which the
|
||||||
|
old mic, camera and gear each handled differently.
|
||||||
|
- **The gear, mic, camera, refresh and warning were redrawn.** The gear was a
|
||||||
|
circle with eight straight spokes; the mic was a rounded rect over an arc.
|
||||||
|
They read as icons now rather than as diagrams.
|
||||||
|
- **19 icons added**: headphones / headphonesSlash (the listening and deafened
|
||||||
|
states, which had no mark of their own), screenshare, waveform, image, film,
|
||||||
|
audio, folder, pause, stop, check, plus, minus, dots, funnel, sliders,
|
||||||
|
download, copy, pencil, trash, pin, popout, info, error, ok, clock.
|
||||||
|
- **Every old name still resolves** — aliases cover `microphone`, `deafen`,
|
||||||
|
`video`, `close`, `settings`, `tts`, `magnify` — so nothing needs editing.
|
||||||
|
- Unknown names now warn in the console instead of rendering an empty `<svg>`.
|
||||||
|
That surfaced one already: `star` was used before it was defined, and worked
|
||||||
|
only because a later line patched it in.
|
||||||
|
- `VK.hydrateIcons(root)` fills any empty `[data-icon]`; it skips anything
|
||||||
|
already rendered, so it won't fight `app.js`.
|
||||||
|
|
||||||
|
If you would rather ship the real Phosphor package, swap the file for
|
||||||
|
`@phosphor-icons/web` and keep `VK.icon` as a wrapper — the names are Phosphor's
|
||||||
|
own, so the mapping is one to one.
|
||||||
|
|
||||||
|
## 10. `polish.js` — what CSS can't reach
|
||||||
|
|
||||||
|
Three text glyphs are doing an icon's job. This is a small idempotent pass with
|
||||||
|
a MutationObserver, so it also catches the Media and Bot tabs rebuilding:
|
||||||
|
|
||||||
|
- `<button id="scrape-x">X</button>` and its siblings → the `x` icon, plus
|
||||||
|
`aria-label="Close"`. There were several, in the scrape and clean modals.
|
||||||
|
- `.vk-media-play`'s ▶ character at 44px → the `play` icon.
|
||||||
|
- `.vk-info`'s `title` → `data-tip`, so the styled tooltip below is the only one
|
||||||
|
that fires, plus `aria-label` and `tabindex` so the help is reachable at all.
|
||||||
|
|
||||||
|
No markup edits. If you'd rather fold these into the source, the script says
|
||||||
|
exactly what to change.
|
||||||
|
|
||||||
|
## 11. Settings, menus, dialogs, toasts
|
||||||
|
|
||||||
|
`settings-popups.css`. Presentation only, no behaviour or markup changes.
|
||||||
|
|
||||||
|
- **Scrim.** 55% flat wash → 72% of the page ground with a 3px blur. The page
|
||||||
|
behind was legible enough that the eye kept reading it.
|
||||||
|
- **Panel.** Square left edge → `--radius-lg` on the leading corners, the
|
||||||
|
system's hairline, and a 200ms slide from the edge it is attached to.
|
||||||
|
- **Head is sticky** and sits on a fading rule. Settings scrolls a long way;
|
||||||
|
the title used to leave.
|
||||||
|
- **Section rows.** The card was a flat fill with rows running together. Now the
|
||||||
|
system edge, rows separated by rules that fade at both ends, values on
|
||||||
|
tabular numerals so they align down the column.
|
||||||
|
- **`.vk-info` was an unstyled letter `i`** with its help text trapped in a
|
||||||
|
native tooltip — half a second of delay, no styling, invisible on touch. It is
|
||||||
|
now a 14px ringed mark with a themed tooltip. Eleven help strings were sitting
|
||||||
|
in `title` attributes where most people would never see them.
|
||||||
|
- **Collapsible groups.** The summary was a bare uppercase line with nothing
|
||||||
|
saying it was a control. It is a row now — hover tint, turning caret, and the
|
||||||
|
body indented against a hairline so nesting is visible.
|
||||||
|
- **Native controls.** Selects kept the platform arrow and a 1px border, numbers
|
||||||
|
kept their spinners, ranges kept the platform track. All three now use the
|
||||||
|
system's edge, accent thumb and focus ring. The select caret is a mask taking
|
||||||
|
`--color-neutral-400`, not a hex baked into a data URI.
|
||||||
|
- **Menus** get the hairline, a 120ms entrance and an accent bar on the active
|
||||||
|
item instead of a tint with nothing anchoring it.
|
||||||
|
- **Dialogs.** The voice-profile drawer was square-topped against the scrim and
|
||||||
|
the media modals had unrelated radii. Both on `--radius-lg` and the system
|
||||||
|
edge now; the profile drawer gains a grab handle, since its head was already
|
||||||
|
the drag target but nothing said so.
|
||||||
|
- **Toasts** get an icon slot coloured by role from `tokens-status.css`.
|
||||||
|
- All of it respects `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
## Worth doing next
|
||||||
|
|
||||||
|
`.vk-status-value:has(> .vk-select)` carries the select caret. If a select ever
|
||||||
|
ships outside a status row, wrap it in `.vk-select-wrap` — the rule is there.
|
||||||
+128
-27
@@ -1,41 +1,126 @@
|
|||||||
"use strict";
|
"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 ?? {};
|
window.VK = window.VK ?? {};
|
||||||
|
|
||||||
const ICON_PATHS = {
|
const ICON_PATHS = {
|
||||||
hash: '<path d="M6 2L4.5 14M11.5 2L10 14M2.5 5.5h12M2 10.5h12"/>',
|
/* ── Channels and navigation ── */
|
||||||
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"/>',
|
hash: '<path d="M96 40 72 216M184 40l-24 176M52 96h152M44 160h152"/>',
|
||||||
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"/>',
|
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:
|
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"/>',
|
'<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"/>',
|
||||||
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"/>',
|
volume: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 96a44 44 0 0 1 0 64"/>',
|
||||||
speakerSlash:
|
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"/>',
|
||||||
'<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"/>',
|
speakerSlash: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 100l44 56M232 100l-44 56"/>',
|
||||||
monitor: '<rect x="1.5" y="2.5" width="13" height="9" rx="1.5"/><path d="M5.5 14h5M8 11.5v2.5"/>',
|
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"/>',
|
||||||
camera: '<rect x="1.5" y="4.5" width="9" height="7" rx="1.5"/><path d="M10.5 8l4-2.2v4.4z"/>',
|
headphonesSlash:
|
||||||
caretDown: '<path d="M3.5 6l4.5 4.5L12.5 6"/>',
|
'<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"/>',
|
||||||
caretUp: '<path d="M3.5 10L8 5.5l4.5 4.5"/>',
|
waveform: '<path d="M32 108v40M72 76v104M112 40v176M152 76v104M192 100v56M232 116v24"/>',
|
||||||
x: '<path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/>',
|
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"/>',
|
||||||
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"/>',
|
/* ── Media ── */
|
||||||
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"/>',
|
monitor: '<rect x="24" y="48" width="208" height="136" rx="20"/><path d="M96 216h64M128 184v32"/>',
|
||||||
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"/>',
|
screenshare: '<rect x="24" y="48" width="208" height="136" rx="20"/><path d="M96 216h64"/><path d="M128 76v72M100 104l28-28 28 28"/>',
|
||||||
warning: '<path d="M8 2 1.5 13.5h13z"/><path d="M8 6.5v3.5M8 12v.5"/>',
|
camera: '<rect x="24" y="64" width="144" height="128" rx="20"/><path d="M168 112l64-36v104l-64-36z"/>',
|
||||||
refresh: '<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9"/><path d="M13.5 2.5V6H10"/>',
|
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"/>',
|
||||||
play: '<path d="M5 3.5 12.5 8 5 12.5z"/>',
|
film: '<rect x="32" y="48" width="192" height="160" rx="20"/><path d="M88 48v160M168 48v160M32 128h192M32 88h56M32 168h56M168 88h56M168 168h56"/>',
|
||||||
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"/>',
|
audio: '<circle cx="76" cy="188" r="32"/><circle cx="196" cy="164" r="28"/><path d="M108 188V72l116-28v120"/>',
|
||||||
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"/>',
|
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"/>',
|
||||||
activity: '<path d="M1.5 8h3l2-4.5 2.5 9L11 8h3.5"/>'
|
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) {
|
window.VK.icon = function icon(name, size = 13) {
|
||||||
const path = ICON_PATHS[name] ?? "";
|
const path = ICON_PATHS[name];
|
||||||
const fill = FILLED.has(name) ? "currentColor" : "none";
|
if (!path) {
|
||||||
return `<svg width="${size}" height="${size}" viewBox="0 0 16 16" fill="${fill}" stroke="currentColor" stroke-width="1.3" aria-hidden="true">${path}</svg>`;
|
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) {
|
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);
|
span.innerHTML = window.VK.icon(name, size);
|
||||||
return span;
|
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>
|
<title>veskslop debug</title>
|
||||||
<link rel="icon" href="data:," />
|
<link rel="icon" href="data:," />
|
||||||
<link rel="stylesheet" href="nocturne.css" />
|
<link rel="stylesheet" href="nocturne.css" />
|
||||||
|
<link rel="stylesheet" href="tokens-status.css" />
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
<link rel="stylesheet" href="media.css" />
|
<link rel="stylesheet" href="media.css" />
|
||||||
|
<link rel="stylesheet" href="style-overrides.css" />
|
||||||
|
<link rel="stylesheet" href="settings-popups.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="vk-app">
|
<div class="vk-app">
|
||||||
@@ -622,6 +625,7 @@
|
|||||||
<div id="toasts" class="vk-toasts"></div>
|
<div id="toasts" class="vk-toasts"></div>
|
||||||
|
|
||||||
<script src="icons.js"></script>
|
<script src="icons.js"></script>
|
||||||
|
<script src="polish.js"></script>
|
||||||
<script src="api.js"></script>
|
<script src="api.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
<script src="media.js"></script>
|
<script src="media.js"></script>
|
||||||
|
|||||||
+134
-73
@@ -6,7 +6,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
top: var(--media-top, 50px);
|
top: var(--media-top, 50px);
|
||||||
bottom: 0;
|
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;
|
z-index: 70;
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
.vk-media-title {
|
.vk-media-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-toolbar {
|
.vk-media-toolbar {
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
color: var(--color-neutral-300);
|
color: var(--color-neutral-300);
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -124,10 +124,10 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
left: 6px;
|
left: 6px;
|
||||||
font: 600 10px/1 ui-monospace, Menlo, monospace;
|
font: 500 10px/1 ui-monospace, Menlo, monospace;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
color: rgba(255, 255, 255, 0.75);
|
color: var(--color-neutral-100);
|
||||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +137,8 @@
|
|||||||
right: 6px;
|
right: 6px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: rgba(255, 255, 255, 0.72);
|
color: var(--color-neutral-100);
|
||||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
max-height: 46vh;
|
max-height: 46vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
}
|
}
|
||||||
@@ -258,14 +258,14 @@
|
|||||||
.vk-media-viewer-media video {
|
.vk-media-viewer-media video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-viewer-media iframe {
|
.vk-media-viewer-media iframe {
|
||||||
width: min(880px, 100%);
|
width: min(880px, 100%);
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-viewer.vertical .vk-media-viewer-media {
|
.vk-media-viewer.vertical .vk-media-viewer-media {
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--color-accent-400, #7aa2ff);
|
color: var(--color-accent-300);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -310,9 +310,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-card-kind {
|
.vk-media-card-kind {
|
||||||
font: 600 10px/1 ui-monospace, Menlo, monospace;
|
font: 500 10px/1 ui-monospace, Menlo, monospace;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
color: var(--color-accent-400, var(--color-neutral-300));
|
color: var(--color-accent-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-card-name {
|
.vk-media-card-name {
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-neutral-800);
|
background: var(--color-neutral-800);
|
||||||
color: var(--color-neutral-300);
|
color: var(--color-neutral-300);
|
||||||
font: 600 9px/1 ui-monospace, Menlo, monospace;
|
font: 500 9px/1 ui-monospace, Menlo, monospace;
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@
|
|||||||
width: 90px;
|
width: 90px;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
color: var(--color-neutral-200);
|
color: var(--color-neutral-200);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
@@ -384,23 +384,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-seg-image {
|
.vk-seg-image {
|
||||||
background: #4f8cff;
|
background: var(--color-data-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-seg-gif {
|
.vk-seg-gif {
|
||||||
background: #8a6cff;
|
background: var(--color-data-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-seg-video {
|
.vk-seg-video {
|
||||||
background: #ff7a45;
|
background: var(--color-data-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-seg-audio {
|
.vk-seg-audio {
|
||||||
background: #2fbf71;
|
background: var(--color-data-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-seg-other {
|
.vk-seg-other {
|
||||||
background: #6b7280;
|
background: var(--color-data-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-modal {
|
.vk-media-modal {
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: min(520px, 92vw);
|
width: min(520px, 92vw);
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border-radius: var(--radius-lg, 12px);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
}
|
}
|
||||||
@@ -465,7 +465,8 @@
|
|||||||
.vk-media-tree-head {
|
.vk-media-tree-head {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-sm, 6px);
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
background: var(--color-neutral-900);
|
background: var(--color-neutral-900);
|
||||||
color: var(--color-neutral-200);
|
color: var(--color-neutral-200);
|
||||||
font: 500 12px/1.2 inherit;
|
font: 500 12px/1.2 inherit;
|
||||||
@@ -510,7 +511,7 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-accent-400, #4f8cff);
|
background: var(--color-accent-400);
|
||||||
animation: vk-media-pulse 1.2s ease-in-out infinite;
|
animation: vk-media-pulse 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,21 +532,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-error {
|
.vk-media-error {
|
||||||
color: #ff8080;
|
color: var(--color-danger-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tinted, not flooded — and 10px so the count is actually readable. */
|
||||||
.vk-media-badge {
|
.vk-media-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 14px;
|
min-width: 16px;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
background: #e5484d;
|
background: var(--color-danger-800);
|
||||||
color: #fff;
|
color: var(--color-danger-200);
|
||||||
font: 600 9px/1 ui-monospace, Menlo, monospace;
|
font: 500 10px/1 ui-monospace, Menlo, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bot tab: backfill jobs, stats, and settings. */
|
/* Bot tab: backfill jobs, stats, and settings. */
|
||||||
@@ -565,11 +567,11 @@
|
|||||||
|
|
||||||
.vk-bot-limited {
|
.vk-bot-limited {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-sm);
|
||||||
font: 500 11px/1.4 ui-monospace, Menlo, monospace;
|
font: 400 11px/1.4 ui-monospace, Menlo, monospace;
|
||||||
color: #ffb0b0;
|
color: var(--color-danger-200);
|
||||||
background: color-mix(in srgb, #ff5555 14%, transparent);
|
background: var(--color-danger-900);
|
||||||
border: 1px solid color-mix(in srgb, #ff5555 35%, transparent);
|
box-shadow: inset 0 0 0 1px var(--color-danger-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-limited[hidden] {
|
.vk-bot-limited[hidden] {
|
||||||
@@ -577,8 +579,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-section {
|
.vk-bot-section {
|
||||||
border: 1px solid var(--color-divider);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-md);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
@@ -592,7 +594,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-title {
|
.vk-bot-title {
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,8 +627,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-job {
|
.vk-bot-job {
|
||||||
border: 1px solid var(--color-neutral-800);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
border-radius: 5px;
|
border-radius: var(--radius-sm);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -634,7 +636,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-job-running {
|
.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 {
|
.vk-bot-job-head {
|
||||||
@@ -646,7 +648,7 @@
|
|||||||
|
|
||||||
.vk-bot-job-title {
|
.vk-bot-job-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,27 +658,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-state {
|
.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;
|
text-transform: uppercase;
|
||||||
padding: 0 5px;
|
padding: 1px 6px;
|
||||||
border-radius: 3px;
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-neutral-800);
|
background: var(--color-neutral-800);
|
||||||
color: var(--color-neutral-300);
|
color: var(--color-neutral-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-state-running {
|
.vk-bot-state-running {
|
||||||
background: color-mix(in srgb, var(--color-accent, #4f8cff) 25%, transparent);
|
background: var(--color-accent-900);
|
||||||
color: var(--color-accent-300, #9fc1ff);
|
color: var(--color-accent-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-state-paused {
|
.vk-bot-state-paused {
|
||||||
background: color-mix(in srgb, #ffaa33 20%, transparent);
|
background: var(--color-warn-900);
|
||||||
color: #ffc880;
|
color: var(--color-warn-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-state-done {
|
.vk-bot-state-done {
|
||||||
background: color-mix(in srgb, #44cc88 20%, transparent);
|
background: var(--color-ok-900);
|
||||||
color: #8fe0b8;
|
color: var(--color-ok-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-job-member {
|
.vk-bot-job-member {
|
||||||
@@ -687,14 +690,14 @@
|
|||||||
|
|
||||||
.vk-bot-progress {
|
.vk-bot-progress {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 2px;
|
border-radius: var(--radius-sm);
|
||||||
background: var(--color-neutral-800);
|
background: var(--color-neutral-800);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-progress-fill {
|
.vk-bot-progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--color-accent-400, #4f8cff);
|
background: var(--color-accent-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-job-actions {
|
.vk-bot-job-actions {
|
||||||
@@ -710,7 +713,7 @@
|
|||||||
|
|
||||||
.vk-bot-group-title {
|
.vk-bot-group-title {
|
||||||
cursor: pointer;
|
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);
|
color: var(--color-neutral-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,11 +742,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-limited {
|
.vk-media-limited {
|
||||||
color: #ff8080;
|
color: var(--color-danger-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-done-note {
|
.vk-media-done-note {
|
||||||
color: color-mix(in srgb, var(--color-neutral-400) 55%, transparent);
|
color: var(--color-neutral-500);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,7 +757,7 @@
|
|||||||
|
|
||||||
.vk-bot-eta {
|
.vk-bot-eta {
|
||||||
cursor: help;
|
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. */
|
/* Stream buffer: scrollable room while the next page loads. */
|
||||||
@@ -778,7 +781,7 @@
|
|||||||
/* Advanced drawer: the "Do not include" group. */
|
/* Advanced drawer: the "Do not include" group. */
|
||||||
.vk-media-exclude-title {
|
.vk-media-exclude-title {
|
||||||
cursor: pointer;
|
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);
|
color: var(--color-neutral-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,8 +803,8 @@
|
|||||||
|
|
||||||
/* The YouTube title catch-up lane, inside the Bot tab backfill section. */
|
/* The YouTube title catch-up lane, inside the Bot tab backfill section. */
|
||||||
.vk-bot-lane {
|
.vk-bot-lane {
|
||||||
border: 1px dashed var(--color-neutral-700);
|
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
|
||||||
border-radius: 5px;
|
border-radius: var(--radius-sm);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -814,15 +817,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-progress-cached {
|
.vk-bot-progress-cached {
|
||||||
background: var(--color-accent-400, #4f8cff);
|
background: var(--color-accent-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-progress-pending {
|
.vk-bot-progress-pending {
|
||||||
background: var(--color-neutral-700);
|
background: var(--color-neutral-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-progress-failed {
|
.vk-bot-progress-failed {
|
||||||
background: #c04040;
|
background: var(--color-danger-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-history {
|
.vk-bot-history {
|
||||||
@@ -830,20 +833,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-history-title {
|
.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);
|
color: var(--color-neutral-300);
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-history-entry {
|
.vk-bot-history-entry {
|
||||||
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
|
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
|
||||||
color: var(--color-neutral-500);
|
color: var(--color-neutral-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-bot-hint {
|
.vk-bot-hint {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
|
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
|
||||||
color: var(--color-neutral-500);
|
color: var(--color-neutral-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Infinite/page toggle and pager. */
|
/* Infinite/page toggle and pager. */
|
||||||
@@ -856,7 +859,7 @@
|
|||||||
.vk-media-toggle {
|
.vk-media-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border: 1px solid var(--color-divider);
|
border: 1px solid var(--color-divider);
|
||||||
border-radius: 5px;
|
border-radius: var(--radius-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,8 +873,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-toggle-half.vk-toggle-on {
|
.vk-toggle-half.vk-toggle-on {
|
||||||
background: var(--color-accent-400, #4f8cff);
|
background: var(--color-accent-900);
|
||||||
color: #fff;
|
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 {
|
.vk-media-page-nav {
|
||||||
@@ -893,7 +902,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vk-media-viewer-title {
|
.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);
|
color: var(--color-text);
|
||||||
margin-bottom: 6px;
|
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