refactor: components read the obsidian surface from its token, not a typed rgba (3108)
Thirteen rgba(20, 23, 26, ...) across six components (the task counted eleven). They now use --v-theme-background, which Vuetify maps to obsidian; ArtistHeader's banner fade uses --fc-chrome-rgb, since it is nav-style chrome. A spec fails if the literal reappears outside a comment. Values are unchanged, so nothing should render differently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -103,9 +103,9 @@ const stats = computed(() => {
|
|||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(20, 23, 26, 0.92) 0%,
|
rgba(var(--fc-chrome-rgb), 0.92) 0%,
|
||||||
rgba(20, 23, 26, 0.65) 60%,
|
rgba(var(--fc-chrome-rgb), 0.65) 60%,
|
||||||
rgba(20, 23, 26, 0) 100%
|
rgba(var(--fc-chrome-rgb), 0) 100%
|
||||||
);
|
);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
-webkit-backdrop-filter: blur(2px);
|
-webkit-backdrop-filter: blur(2px);
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ function pushFilter(mutate) {
|
|||||||
opaque than the bar/nav so the controls stay legible. */
|
opaque than the bar/nav so the controls stay legible. */
|
||||||
.fc-filterbar-wrap :deep(.v-field),
|
.fc-filterbar-wrap :deep(.v-field),
|
||||||
.fc-filterbar-wrap :deep(.v-btn-group) {
|
.fc-filterbar-wrap :deep(.v-btn-group) {
|
||||||
background-color: rgba(20, 23, 26, 0.72);
|
background-color: rgba(var(--v-theme-background), 0.72);
|
||||||
}
|
}
|
||||||
/* Media toggle (All / Images / Videos) as ONE cohesive segmented control.
|
/* Media toggle (All / Images / Videos) as ONE cohesive segmented control.
|
||||||
FC's global VBtn { rounded: 'pill' } default made Vuetify 4 pill-round each
|
FC's global VBtn { rounded: 'pill' } default made Vuetify 4 pill-round each
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ function nextFrame() {
|
|||||||
--fc-side-w: 320px;
|
--fc-side-w: 320px;
|
||||||
/* Obsidian haze (#14171A = 20,23,26) — same palette as TopNav,
|
/* Obsidian haze (#14171A = 20,23,26) — same palette as TopNav,
|
||||||
mid-opacity + blur so the page behind shows through faintly. */
|
mid-opacity + blur so the page behind shows through faintly. */
|
||||||
background: rgba(20, 23, 26, 0.65);
|
background: rgba(var(--v-theme-background), 0.65);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -221,7 +221,7 @@ function nextFrame() {
|
|||||||
}
|
}
|
||||||
.fc-viewer__close, .fc-viewer__nav {
|
.fc-viewer__close, .fc-viewer__nav {
|
||||||
position: absolute; top: 50%;
|
position: absolute; top: 50%;
|
||||||
background: rgba(20, 23, 26, 0.7);
|
background: rgba(var(--v-theme-background), 0.7);
|
||||||
color: rgb(var(--v-theme-parchment, 232 228 216));
|
color: rgb(var(--v-theme-parchment, 232 228 216));
|
||||||
border: 1px solid rgb(var(--v-theme-surface-light));
|
border: 1px solid rgb(var(--v-theme-surface-light));
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -359,7 +359,7 @@ function nextFrame() {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
/* Opaque obsidian so the scrolling panel never bleeds through the
|
/* Opaque obsidian so the scrolling panel never bleeds through the
|
||||||
haze behind the pinned image. */
|
haze behind the pinned image. */
|
||||||
background: rgb(20, 23, 26);
|
background: rgb(var(--v-theme-background));
|
||||||
}
|
}
|
||||||
.fc-viewer__side {
|
.fc-viewer__side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* Center viewer. */
|
/* Center viewer. */
|
||||||
.fc-ex__viewer {
|
.fc-ex__viewer {
|
||||||
background: rgb(20, 23, 26);
|
background: rgb(var(--v-theme-background));
|
||||||
display: flex; flex-direction: column; min-width: 0; min-height: 0;
|
display: flex; flex-direction: column; min-width: 0; min-height: 0;
|
||||||
}
|
}
|
||||||
.fc-ex__canvas { flex: 1 1 auto; display: flex; min-height: 0; min-width: 0; }
|
.fc-ex__canvas { flex: 1 1 auto; display: flex; min-height: 0; min-width: 0; }
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ onUnmounted(() => {
|
|||||||
.fc-reader__thumb img { width: 100%; height: auto; display: block; }
|
.fc-reader__thumb img { width: 100%; height: auto; display: block; }
|
||||||
.fc-reader__thumbnum {
|
.fc-reader__thumbnum {
|
||||||
position: absolute; bottom: 4px; right: 4px;
|
position: absolute; bottom: 4px; right: 4px;
|
||||||
background: rgba(20, 23, 26, 0.8); color: rgb(var(--v-theme-on-surface));
|
background: rgba(var(--v-theme-background), 0.8); color: rgb(var(--v-theme-on-surface));
|
||||||
padding: 1px 5px; border-radius: 4px; font-size: 0.7rem;
|
padding: 1px 5px; border-radius: 4px; font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
.fc-reader__content {
|
.fc-reader__content {
|
||||||
@@ -302,7 +302,7 @@ onUnmounted(() => {
|
|||||||
.fc-reader__indicator {
|
.fc-reader__indicator {
|
||||||
position: fixed; bottom: 1.5rem; right: 1.5rem;
|
position: fixed; bottom: 1.5rem; right: 1.5rem;
|
||||||
padding: 4px 10px; border-radius: 999px;
|
padding: 4px 10px; border-radius: 999px;
|
||||||
background: rgba(20, 23, 26, 0.6);
|
background: rgba(var(--v-theme-background), 0.6);
|
||||||
border: 1px solid rgb(var(--v-theme-surface-light));
|
border: 1px solid rgb(var(--v-theme-surface-light));
|
||||||
font-size: 0.8rem; color: rgb(var(--v-theme-on-surface-variant));
|
font-size: 0.8rem; color: rgb(var(--v-theme-on-surface-variant));
|
||||||
z-index: 100; pointer-events: none;
|
z-index: 100; pointer-events: none;
|
||||||
|
|||||||
@@ -338,18 +338,18 @@ onMounted(() => {
|
|||||||
position: absolute; top: 6px; left: 6px;
|
position: absolute; top: 6px; left: 6px;
|
||||||
display: inline-flex; align-items: center; gap: 2px;
|
display: inline-flex; align-items: center; gap: 2px;
|
||||||
padding: 1px 6px; border-radius: 999px; font-size: 11px;
|
padding: 1px 6px; border-radius: 999px; font-size: 11px;
|
||||||
background: rgba(20, 23, 26, 0.75);
|
background: rgba(var(--v-theme-background), 0.75);
|
||||||
color: rgb(var(--v-theme-warning, var(--v-theme-accent)));
|
color: rgb(var(--v-theme-warning, var(--v-theme-accent)));
|
||||||
}
|
}
|
||||||
/* Kebab sits top-right of the cover, opposite the gap badge. Tinted backing so
|
/* Kebab sits top-right of the cover, opposite the gap badge. Tinted backing so
|
||||||
it stays legible over any cover image. */
|
it stays legible over any cover image. */
|
||||||
.fc-sbcard__kebab {
|
.fc-sbcard__kebab {
|
||||||
position: absolute; top: 4px; right: 4px;
|
position: absolute; top: 4px; right: 4px;
|
||||||
background: rgba(20, 23, 26, 0.6) !important;
|
background: rgba(var(--v-theme-background), 0.6) !important;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
}
|
}
|
||||||
.fc-sbcard__kebab:hover { background: rgba(20, 23, 26, 0.85) !important; }
|
.fc-sbcard__kebab:hover { background: rgba(var(--v-theme-background), 0.85) !important; }
|
||||||
.fc-sbcard__body { padding: 8px 10px; }
|
.fc-sbcard__body { padding: 8px 10px; }
|
||||||
.fc-sbcard__name {
|
.fc-sbcard__name {
|
||||||
font-family: 'Fraunces', Georgia, serif; font-size: 15px; font-weight: 600;
|
font-family: 'Fraunces', Georgia, serif; font-size: 15px; font-weight: 600;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// The obsidian surface written out as a number instead of read from its token.
|
||||||
|
//
|
||||||
|
// fabled-tokens.js is the palette's single copy ("do not hand-edit hexes;
|
||||||
|
// re-mirror here"). Thirteen hand-typed `rgba(20, 23, 26, …)` across six
|
||||||
|
// components meant a changed obsidian would miss most of the UI (#3108).
|
||||||
|
// Components read `--v-theme-background` (Vuetify maps background → obsidian)
|
||||||
|
// or, for nav-style chrome, `--fc-chrome-rgb`. Comments may still name it.
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||||
|
import { join, relative } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const SRC = fileURLToPath(new URL('../src', import.meta.url))
|
||||||
|
const LITERAL = /rgba?\(\s*20\s*,\s*23\s*,\s*26\b/
|
||||||
|
|
||||||
|
function vueFiles (dir) {
|
||||||
|
return readdirSync(dir).flatMap((name) => {
|
||||||
|
const path = join(dir, name)
|
||||||
|
if (statSync(path).isDirectory()) return vueFiles(path)
|
||||||
|
return name.endsWith('.vue') ? [path] : []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripComments (text) {
|
||||||
|
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/<!--[\s\S]*?-->/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('palette literals', () => {
|
||||||
|
it('no component hand-types the obsidian surface', () => {
|
||||||
|
const offenders = vueFiles(SRC).flatMap((path) =>
|
||||||
|
stripComments(readFileSync(path, 'utf8'))
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => LITERAL.test(line))
|
||||||
|
.map((line) => `${relative(SRC, path)}: ${line.trim()}`)
|
||||||
|
)
|
||||||
|
expect(offenders).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user