Merge pull request 'fix(ui): Vuetify-4 review polish — active-tab indicator + one continuous chrome gradient' (#226) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m44s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m44s
This commit was merged in pull request #226.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="fc-topnav">
|
<header class="fc-topnav" :class="{ 'fc-topnav--chrome': hasStickyChrome }">
|
||||||
<div class="fc-nav-left">
|
<div class="fc-nav-left">
|
||||||
<RouterLink :to="FRONT_DOOR" class="fc-brand" aria-label="FabledCurator home">
|
<RouterLink :to="FRONT_DOOR" class="fc-brand" aria-label="FabledCurator home">
|
||||||
<img src="/favicon.svg" alt="" class="fc-brand__glyph" width="22" height="22" />
|
<img src="/favicon.svg" alt="" class="fc-brand__glyph" width="22" height="22" />
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import router, { FRONT_DOOR } from '../router.js'
|
import router, { FRONT_DOOR } from '../router.js'
|
||||||
import { useSystemStore } from '../stores/system.js'
|
import { useSystemStore } from '../stores/system.js'
|
||||||
import PipelineStatusChip from './PipelineStatusChip.vue'
|
import PipelineStatusChip from './PipelineStatusChip.vue'
|
||||||
@@ -72,6 +73,13 @@ import PipelineStatusChip from './PipelineStatusChip.vue'
|
|||||||
const system = useSystemStore()
|
const system = useSystemStore()
|
||||||
onMounted(() => system.refreshHealth())
|
onMounted(() => system.refreshHealth())
|
||||||
|
|
||||||
|
// Views that pin a sticky sub-header (filter bar / tabs) directly under the nav
|
||||||
|
// declare `meta.stickyChrome`. On those, the nav doesn't fade to transparent at
|
||||||
|
// its bottom — it hands off at the shared seam alpha so the sub-header can
|
||||||
|
// continue the SAME fade (see .fc-chrome-continues in app.css). One gradient.
|
||||||
|
const route = useRoute()
|
||||||
|
const hasStickyChrome = computed(() => !!route.meta?.stickyChrome)
|
||||||
|
|
||||||
// Every route with a meta.title is a nav entry. Order by meta.navOrder —
|
// Every route with a meta.title is a nav entry. Order by meta.navOrder —
|
||||||
// router.getRoutes() does NOT guarantee declaration order, so explicit numbers
|
// router.getRoutes() does NOT guarantee declaration order, so explicit numbers
|
||||||
// pin the sequence (e.g. Explore after Gallery). Routes without one fall to the
|
// pin the sequence (e.g. Explore after Gallery). Routes without one fall to the
|
||||||
@@ -129,6 +137,20 @@ const health = computed(() => {
|
|||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
-webkit-backdrop-filter: blur(2px);
|
-webkit-backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
/* On a view with a sticky sub-header pinned beneath (meta.stickyChrome), the nav
|
||||||
|
stops fading at the shared seam alpha instead of going fully transparent — the
|
||||||
|
sub-header (.fc-chrome-continues) picks the fade up from there, so the two read
|
||||||
|
as one continuous gradient. Compound selector out-specifies .fc-topnav so it
|
||||||
|
wins regardless of Vite's production CSS ordering. --fc-chrome-* come from the
|
||||||
|
global :root in app.css (custom props inherit into scoped styles). */
|
||||||
|
.fc-topnav.fc-topnav--chrome {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(var(--fc-chrome-rgb), 0.92) 0%,
|
||||||
|
rgba(var(--fc-chrome-rgb), 0.72) 55%,
|
||||||
|
rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.fc-brand {
|
.fc-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fc-filterbar-wrap">
|
<div class="fc-filterbar-wrap fc-chrome-continues">
|
||||||
<div class="fc-filterbar">
|
<div class="fc-filterbar">
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
@@ -313,20 +313,10 @@ function pushFilter(mutate) {
|
|||||||
and a gap shows through when scrolled to the top. */
|
and a gap shows through when scrolled to the top. */
|
||||||
margin-top: -8px;
|
margin-top: -8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
/* EXACT same gradiated obsidian (#14171A = 20,23,26) frost as the TopNav so
|
/* The frost itself (obsidian fade + blur) is the shared .fc-chrome-continues
|
||||||
the two read as one continuous piece of chrome — images scroll visibly
|
primitive: it CONTINUES the TopNav's fade from the seam alpha to transparent
|
||||||
under both. The nav's gradient fades to transparent at ITS bottom; this
|
rather than re-darkening, so the nav + bar read as one gradient (operator
|
||||||
bar re-darkens at its top, so a faint seam (the page/image showing through
|
2026-07-13). This block only owns the sticky positioning now. */
|
||||||
the nav's transparent edge) separates them when scrolled to the very top,
|
|
||||||
while under-scroll they frost as one. */
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(20, 23, 26, 0.92) 0%,
|
|
||||||
rgba(20, 23, 26, 0.65) 60%,
|
|
||||||
rgba(20, 23, 26, 0) 100%
|
|
||||||
);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
-webkit-backdrop-filter: blur(2px);
|
|
||||||
}
|
}
|
||||||
.fc-filterbar {
|
.fc-filterbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const routes = [
|
|||||||
|
|
||||||
// FC-2: image backbone
|
// FC-2: image backbone
|
||||||
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase', navOrder: 10 } },
|
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase', navOrder: 10 } },
|
||||||
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery', navOrder: 20 } },
|
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery', navOrder: 20, stickyChrome: true } },
|
||||||
// Explore: a 3-pane tagging workspace — walk an image's visual neighbours
|
// Explore: a 3-pane tagging workspace — walk an image's visual neighbours
|
||||||
// (left) while tagging the focused image (center viewer + modal-parity tag
|
// (left) while tagging the focused image (center viewer + modal-parity tag
|
||||||
// rail). Optional anchor param — the bare /explore nav entry SEEDS a random
|
// rail). Optional anchor param — the bare /explore nav entry SEEDS a random
|
||||||
@@ -29,11 +29,11 @@ const routes = [
|
|||||||
// Browse hub (operator-asked 2026-06-09): Posts / Artists / Tags as tabs —
|
// Browse hub (operator-asked 2026-06-09): Posts / Artists / Tags as tabs —
|
||||||
// the three "browse the library by an axis" surfaces. One nav entry; the old
|
// the three "browse the library by an axis" surfaces. One nav entry; the old
|
||||||
// standalone paths redirect into the matching tab (below).
|
// standalone paths redirect into the matching tab (below).
|
||||||
{ path: '/browse', name: 'browse', component: BrowseView, meta: { title: 'Browse', navOrder: 30 } },
|
{ path: '/browse', name: 'browse', component: BrowseView, meta: { title: 'Browse', navOrder: 30, stickyChrome: true } },
|
||||||
// Artist detail — no meta.title (reached by clicking an artist, not nav).
|
// Artist detail — no meta.title (reached by clicking an artist, not nav).
|
||||||
{ path: '/artist/:slug', name: 'artist', component: ArtistView },
|
{ path: '/artist/:slug', name: 'artist', component: ArtistView },
|
||||||
// Series browse — a nav entry (meta.title).
|
// Series browse — a nav entry (meta.title).
|
||||||
{ path: '/series', name: 'series', component: SeriesView, meta: { title: 'Series', navOrder: 40 } },
|
{ path: '/series', name: 'series', component: SeriesView, meta: { title: 'Series', navOrder: 40, stickyChrome: true } },
|
||||||
// Series management — no meta.title (reached from a series card/tag).
|
// Series management — no meta.title (reached from a series card/tag).
|
||||||
{ path: '/series/:tagId', name: 'series-manage', component: SeriesManageView },
|
{ path: '/series/:tagId', name: 'series-manage', component: SeriesManageView },
|
||||||
// Series reader — immersive (no top nav, no meta.title).
|
// Series reader — immersive (no top nav, no meta.title).
|
||||||
@@ -41,10 +41,10 @@ const routes = [
|
|||||||
|
|
||||||
// FC-3: subscription backbone — purely management (sources/downloads),
|
// FC-3: subscription backbone — purely management (sources/downloads),
|
||||||
// distinct from the Browse hub.
|
// distinct from the Browse hub.
|
||||||
{ path: '/subscriptions', name: 'subscriptions', component: SubscriptionsView, meta: { title: 'Subscriptions', navOrder: 50 } },
|
{ path: '/subscriptions', name: 'subscriptions', component: SubscriptionsView, meta: { title: 'Subscriptions', navOrder: 50, stickyChrome: true } },
|
||||||
|
|
||||||
// Settings — config, pinned to the right of the nav (TopNav special-cases it).
|
// Settings — config, pinned to the right of the nav (TopNav special-cases it).
|
||||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } },
|
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings', stickyChrome: true } },
|
||||||
|
|
||||||
// The old standalone paths now redirect into the Browse hub, preserving any
|
// The old standalone paths now redirect into the Browse hub, preserving any
|
||||||
// deep-link query (e.g. /posts?post_id=N → /browse?tab=posts&post_id=N). The
|
// deep-link query (e.g. /posts?post_id=N → /browse?tab=posts&post_id=N). The
|
||||||
|
|||||||
@@ -49,3 +49,47 @@
|
|||||||
ul, ol, figure, details, summary { padding: 0; margin: 0; }
|
ul, ol, figure, details, summary { padding: 0; margin: 0; }
|
||||||
h1, h2, h3, h4, h5, h6, p { margin: 0; }
|
h1, h2, h3, h4, h5, h6, p { margin: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active-tab indicator (operator-flagged 2026-07-13 in the Vuetify-4 review): v4's
|
||||||
|
MD3 v-tab "slider" underline renders wider than the tab and floats below it. The
|
||||||
|
active tab's TEXT is already accent-coloured (color="accent"), so drop the slider
|
||||||
|
and mark the active tab with a subtle accent fill + rounded top — a clean,
|
||||||
|
unambiguous highlight app-wide (Subscriptions / Browse / Settings / Series). */
|
||||||
|
.v-tab__slider { display: none !important; }
|
||||||
|
.v-tab[aria-selected="true"],
|
||||||
|
.v-tab.v-tab--selected {
|
||||||
|
background: rgb(var(--v-theme-accent) / 0.12);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Continuous chrome fade (operator-asked 2026-07-13: "group the sub-nav as
|
||||||
|
part of the nav and use a single gradient in them"). ------------------------
|
||||||
|
The TopNav and any sticky sub-header pinned directly beneath it (Gallery's
|
||||||
|
filter bar, the Browse/Series/Settings/Subscriptions tabs bars) used to each
|
||||||
|
paint their OWN dark-to-transparent gradient (or a solid band), so the fade
|
||||||
|
read as happening TWICE — dark, fade out, then dark again. Instead the two
|
||||||
|
share ONE obsidian fade: the nav paints the TOP half (opaque → the seam
|
||||||
|
alpha) and the sub-header paints the CONTINUATION (seam alpha → transparent)
|
||||||
|
over its own height. Both reference --fc-chrome-seam, so the alphas meet
|
||||||
|
exactly at the 64px boundary — no re-darkening, no doubling, one gradient.
|
||||||
|
|
||||||
|
--fc-chrome-seam is the single knob: raise it for a heavier sub-header (more
|
||||||
|
legible tabs/controls over scrolling content), lower it for a lighter fade. */
|
||||||
|
:root {
|
||||||
|
--fc-chrome-rgb: 20, 23, 26; /* obsidian #14171A — matches the TopNav */
|
||||||
|
--fc-chrome-seam: 0.46; /* alpha where the nav hands off to the sub-header */
|
||||||
|
}
|
||||||
|
/* Applied to a sticky sub-header so it continues the nav's fade instead of
|
||||||
|
restarting it. Percentage stops so the fade always spans the element's height
|
||||||
|
(survives the filter bar's expanding refine panel). The blur keeps tabs and
|
||||||
|
controls legible as the fill thins toward transparent — the solid-surface
|
||||||
|
bars it replaces had none, so it must live here. */
|
||||||
|
.fc-chrome-continues {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 0%,
|
||||||
|
rgba(var(--fc-chrome-rgb), 0) 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
-webkit-backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
switcher and the search stay reachable no matter how far you scroll.
|
switcher and the search stay reachable no matter how far you scroll.
|
||||||
Background uses the theme surface token so content scrolls cleanly
|
Background uses the theme surface token so content scrolls cleanly
|
||||||
under it (matches SettingsView's sticky tabs). -->
|
under it (matches SettingsView's sticky tabs). -->
|
||||||
<div class="fc-browse__head">
|
<div class="fc-browse__head fc-chrome-continues">
|
||||||
<v-container fluid class="py-0">
|
<v-container fluid class="py-0">
|
||||||
<!-- Tabs and search share one row: the axis switcher on the left, the
|
<!-- Tabs and search share one row: the axis switcher on the left, the
|
||||||
search field + active-scope chips on the right (operator-asked
|
search field + active-scope chips on the right (operator-asked
|
||||||
@@ -156,7 +156,8 @@ function clearFilter(key) {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 64px; /* directly under AppShell's 64px sticky TopNav */
|
top: 64px; /* directly under AppShell's 64px sticky TopNav */
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
background: rgb(var(--v-theme-surface));
|
/* Background is the shared .fc-chrome-continues fade — it continues the nav's
|
||||||
|
gradient instead of a solid surface band (operator 2026-07-13). */
|
||||||
}
|
}
|
||||||
.fc-browse__bar {
|
.fc-browse__bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
the 64px TopNav (operator-asked 2026-06-12), so the axis switcher and
|
the 64px TopNav (operator-asked 2026-06-12), so the axis switcher and
|
||||||
search/sort stay reachable on a long grid. The controls can't sit
|
search/sort stay reachable on a long grid. The controls can't sit
|
||||||
inside v-window (it clips sticky children), so they're hoisted here. -->
|
inside v-window (it clips sticky children), so they're hoisted here. -->
|
||||||
<div class="fc-series__head">
|
<div class="fc-series__head fc-chrome-continues">
|
||||||
<v-tabs v-model="tab" density="compact">
|
<v-tabs v-model="tab" density="compact">
|
||||||
<v-tab value="browse">Browse</v-tab>
|
<v-tab value="browse">Browse</v-tab>
|
||||||
<v-tab value="suggestions">
|
<v-tab value="suggestions">
|
||||||
@@ -294,12 +294,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Sticky header (tabs + active-tab controls) pinned under the 64px TopNav, so
|
/* Sticky header (tabs + active-tab controls) pinned under the 64px TopNav, so
|
||||||
content scrolls cleanly beneath it. Surface bg matches SettingsView. */
|
content scrolls cleanly beneath it. Background is the shared
|
||||||
|
.fc-chrome-continues fade — continues the nav's gradient rather than a solid
|
||||||
|
band (operator 2026-07-13). */
|
||||||
.fc-series__head {
|
.fc-series__head {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 64px;
|
top: 64px;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
background: rgb(var(--v-theme-surface));
|
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
.fc-series-browse__controls {
|
.fc-series-browse__controls {
|
||||||
|
|||||||
@@ -7,13 +7,11 @@
|
|||||||
<!-- Sticky tabs: operator-flagged 2026-05-25 — long Import / Maintenance
|
<!-- Sticky tabs: operator-flagged 2026-05-25 — long Import / Maintenance
|
||||||
panels pushed the tab strip out of the viewport, forcing a scroll-
|
panels pushed the tab strip out of the viewport, forcing a scroll-
|
||||||
to-top just to change tab. AppShell's TopNav is 64px sticky, so the
|
to-top just to change tab. AppShell's TopNav is 64px sticky, so the
|
||||||
tab strip lives directly under it. Background uses the theme surface
|
tab strip lives directly under it. The .fc-chrome-continues fade
|
||||||
token so it visually merges with the page rather than the
|
continues the nav's gradient across the strip (operator 2026-07-13). -->
|
||||||
translucent v-tabs default. -->
|
|
||||||
<v-tabs
|
<v-tabs
|
||||||
v-model="tab" color="accent" class="mb-4"
|
v-model="tab" color="accent" class="mb-4 fc-chrome-continues"
|
||||||
style="position: sticky; top: 64px; z-index: 4;
|
style="position: sticky; top: 64px; z-index: 4;"
|
||||||
background: rgb(var(--v-theme-surface));"
|
|
||||||
>
|
>
|
||||||
<v-tab value="overview">Overview</v-tab>
|
<v-tab value="overview">Overview</v-tab>
|
||||||
<v-tab value="activity">Activity</v-tab>
|
<v-tab value="activity">Activity</v-tab>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
align-tabs="start"
|
align-tabs="start"
|
||||||
color="accent"
|
color="accent"
|
||||||
density="compact"
|
density="compact"
|
||||||
class="fc-subs-tabs"
|
class="fc-subs-tabs fc-chrome-continues"
|
||||||
>
|
>
|
||||||
<v-tab value="subscriptions">
|
<v-tab value="subscriptions">
|
||||||
<v-icon start>mdi-account-multiple-check</v-icon>
|
<v-icon start>mdi-account-multiple-check</v-icon>
|
||||||
@@ -63,7 +63,10 @@ const { tab } = useTabQuery(VALID_TABS, 'subscriptions')
|
|||||||
}
|
}
|
||||||
.fc-subs-tabs {
|
.fc-subs-tabs {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-bottom: 1px solid rgb(var(--v-theme-on-surface-variant) / 0.18);
|
/* Cancel the shell's pt-2 so the tabs sit flush under the 64px nav, letting
|
||||||
|
the .fc-chrome-continues fade read as one gradient with it (operator
|
||||||
|
2026-07-13). The fade replaces the old border-bottom separator. */
|
||||||
|
margin-top: -8px;
|
||||||
}
|
}
|
||||||
.fc-subs-window {
|
.fc-subs-window {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user