Merge pull request 'fix(ui): measure real nav height (--fc-nav-h) — Explore fits on 1080p, no breadcrumb tuck (#1481)' (#227) 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 6s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m50s

This commit was merged in pull request #227.
This commit is contained in:
2026-07-13 15:32:25 -04:00
8 changed files with 39 additions and 11 deletions
+21 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<header class="fc-topnav" :class="{ 'fc-topnav--chrome': hasStickyChrome }"> <header ref="navEl" 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" />
@@ -64,14 +64,32 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router' 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'
const system = useSystemStore() const system = useSystemStore()
onMounted(() => system.refreshHealth())
// Publish the nav's REAL height as --fc-nav-h so full-height workspaces
// (Explore/Subscriptions) and sticky sub-headers pin to it exactly instead of a
// hardcoded 64px that Vuetify 4's MD3 sizing broke — the Explore breadcrumb was
// tucking under a taller nav (#1481). ResizeObserver keeps it live as the nav
// reflows (per-view teleported actions, mobile breakpoint, chip state changes).
const navEl = ref(null)
let navRO = null
onMounted(() => {
system.refreshHealth()
if (navEl.value && 'ResizeObserver' in window) {
navRO = new ResizeObserver(() => {
const h = navEl.value?.offsetHeight
if (h) document.documentElement.style.setProperty('--fc-nav-h', `${h}px`)
})
navRO.observe(navEl.value)
}
})
onBeforeUnmount(() => { navRO?.disconnect() })
// Views that pin a sticky sub-header (filter bar / tabs) directly under the nav // 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 // declare `meta.stickyChrome`. On those, the nav doesn't fade to transparent at
@@ -306,7 +306,7 @@ function pushFilter(mutate) {
frosted block pinned directly under the 64px TopNav and continuous with it. */ frosted block pinned directly under the 64px TopNav and continuous with it. */
.fc-filterbar-wrap { .fc-filterbar-wrap {
position: sticky; position: sticky;
top: 64px; top: var(--fc-nav-h, 64px); /* pins at the nav's real measured bottom (#1481) */
z-index: 5; z-index: 5;
/* Attach to the TopNav: cancel the v-container's top padding (pt-2 = 8px) /* Attach to the TopNav: cancel the v-container's top padding (pt-2 = 8px)
so the bar sits flush at 64px even at scroll 0 — without this it detaches so the bar sits flush at 64px even at scroll 0 — without this it detaches
+7
View File
@@ -78,6 +78,13 @@
:root { :root {
--fc-chrome-rgb: 20, 23, 26; /* obsidian #14171A — matches the TopNav */ --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 */ --fc-chrome-seam: 0.46; /* alpha where the nav hands off to the sub-header */
/* Actual TopNav height, measured live (ResizeObserver in TopNav.vue) and used
by full-height workspaces (Explore/Subscriptions: calc(100vh - var)) and by
every sticky sub-header pinned beneath the nav (top: var). This was a
hardcoded 64px in ~6 places; Vuetify 4's MD3 sizing made the real nav a
different height, so the Explore workspace overflowed and its breadcrumb
tucked under the nav (#1481). This fallback is only used pre-measure. */
--fc-nav-h: 64px;
} }
/* Applied to a sticky sub-header so it continues the nav's fade instead of /* 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 restarting it. Percentage stops so the fade always spans the element's height
+1 -1
View File
@@ -154,7 +154,7 @@ function clearFilter(key) {
<style scoped> <style scoped>
.fc-browse__head { .fc-browse__head {
position: sticky; position: sticky;
top: 64px; /* directly under AppShell's 64px sticky TopNav */ top: var(--fc-nav-h, 64px); /* pins at the nav's real measured bottom (#1481) */
z-index: 4; z-index: 4;
/* Background is the shared .fc-chrome-continues fade — it continues the nav's /* Background is the shared .fc-chrome-continues fade — it continues the nav's
gradient instead of a solid surface band (operator 2026-07-13). */ gradient instead of a solid surface band (operator 2026-07-13). */
+5 -2
View File
@@ -286,10 +286,13 @@ onUnmounted(() => {
<style scoped> <style scoped>
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); } .fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
/* Full-height workspace under the sticky top nav. */ /* Full-height workspace under the sticky top nav. --fc-nav-h is the nav's REAL
measured height (set by TopNav) — a hardcoded 64px here overflowed the
viewport under Vuetify 4's taller nav and tucked the breadcrumb under it
(#1481). Panes scroll internally, so an exact fit keeps everything on screen. */
.fc-ex { .fc-ex {
display: flex; flex-direction: column; display: flex; flex-direction: column;
height: calc(100vh - 64px); height: calc(100vh - var(--fc-nav-h, 64px));
min-height: 0; min-height: 0;
} }
+1 -1
View File
@@ -299,7 +299,7 @@ onMounted(() => {
band (operator 2026-07-13). */ band (operator 2026-07-13). */
.fc-series__head { .fc-series__head {
position: sticky; position: sticky;
top: 64px; top: var(--fc-nav-h, 64px); /* pins at the nav's real measured bottom (#1481) */
z-index: 4; z-index: 4;
padding-bottom: 12px; padding-bottom: 12px;
} }
+1 -1
View File
@@ -11,7 +11,7 @@
continues the nav's gradient across the strip (operator 2026-07-13). --> continues the nav's gradient across the strip (operator 2026-07-13). -->
<v-tabs <v-tabs
v-model="tab" color="accent" class="mb-4 fc-chrome-continues" v-model="tab" color="accent" class="mb-4 fc-chrome-continues"
style="position: sticky; top: 64px; z-index: 4;" style="position: sticky; top: var(--fc-nav-h, 64px); z-index: 4;"
> >
<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>
+2 -2
View File
@@ -55,8 +55,8 @@ const { tab } = useTabQuery(VALID_TABS, 'subscriptions')
/* Fixed-height hub: the tabs (and each tab's sticky control bar) stay /* Fixed-height hub: the tabs (and each tab's sticky control bar) stay
put while ONLY the tab content scrolls — previously the whole view put while ONLY the tab content scrolls — previously the whole view
scrolled instead of just the subscription list (operator-flagged scrolled instead of just the subscription list (operator-flagged
2026-05-28). 64px = the TopNav height (AppShell .fc-content pad-top). */ 2026-05-28). --fc-nav-h = the TopNav's real measured height (#1481). */
height: calc(100vh - 64px); height: calc(100vh - var(--fc-nav-h, 64px));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;