CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 31s
CI and images / integration (push) Successful in 2m17s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m39s
CI and images / smoke-web (push) Successful in 55s
CI and images / promote (push) Skipped
The router had no scrollBehavior, so Settings entered from a scrolled feed opened scrolled too, its heading and tab strip under the nav. Back/forward restores the saved position, and a new query on the same view keeps its place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
134 lines
7.5 KiB
JavaScript
134 lines
7.5 KiB
JavaScript
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
|
import SettingsView from './views/SettingsView.vue'
|
|
import GalleryView from './views/GalleryView.vue'
|
|
import ShowcaseView from './views/ShowcaseView.vue'
|
|
import ExploreView from './views/ExploreView.vue'
|
|
import BrowseView from './views/BrowseView.vue'
|
|
import ArtistView from './views/ArtistView.vue'
|
|
import SeriesView from './views/SeriesView.vue'
|
|
import SeriesManageView from './views/SeriesManageView.vue'
|
|
import SeriesReaderView from './views/SeriesReaderView.vue'
|
|
import SubscriptionsView from './views/SubscriptionsView.vue'
|
|
import PostsView from './views/PostsView.vue'
|
|
|
|
// The application's front door. `/` redirects here. Changing the front door
|
|
// is a one-line edit (e.g. '/gallery' or '/tags').
|
|
//
|
|
// Moved from '/showcase' to '/latest' (milestone #387 B1). Showcase answers
|
|
// "show me something" — a random TABLESAMPLE, lean-back, and it can never tell
|
|
// you anything is wrong. The feed answers "what arrived?", which is a question
|
|
// the operator has every day, and it is the only view where a failing source
|
|
// shows up on its own as a creator who has gone quiet. The per-artist "new
|
|
// since last visit" badges (#597) were a workaround for this view not being
|
|
// the door. Showcase is demoted to a nav entry, not removed.
|
|
export const FRONT_DOOR = '/latest'
|
|
|
|
const routes = [
|
|
// Root is a redirect only — no meta.title so it stays out of the nav.
|
|
{ path: '/', redirect: FRONT_DOOR },
|
|
|
|
// The front door (#387 B1): the post feed as its own surface. Reuses
|
|
// PostsView unchanged — it is already a self-contained feed (own container,
|
|
// own store, infinite scroll, deep-link anchoring), and Browse only ever
|
|
// wrapped it in a tab strip. Mounting it directly IS the difference the
|
|
// promotion was after: a door you arrive at, not a hub you navigate out of.
|
|
//
|
|
// No stickyChrome: unlike Browse/Gallery/Settings this view has no sticky
|
|
// sub-header for the nav to butt against, so the nav keeps its normal fade.
|
|
// `props` turns on the ingestion-status ribbon (#387 B3). Only here: inside
|
|
// Browse's Posts tab the same view renders without it.
|
|
{ path: '/latest', name: 'latest', component: PostsView, props: { statusRibbon: true },
|
|
meta: { title: 'Latest', navOrder: 5 } },
|
|
|
|
// FC-2: image backbone
|
|
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase', navOrder: 10 } },
|
|
{ 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
|
|
// (left) while tagging the focused image (center viewer + modal-parity tag
|
|
// rail). Optional anchor param — the bare /explore nav entry SEEDS a random
|
|
// image so the tab kick-starts a rabbit hole. navOrder pins it after Gallery.
|
|
{ path: '/explore/:imageId?', name: 'explore', component: ExploreView, meta: { title: 'Explore', navOrder: 25 } },
|
|
// 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
|
|
// standalone paths redirect into the matching tab (below).
|
|
{ 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).
|
|
{ path: '/artist/:slug', name: 'artist', component: ArtistView },
|
|
// Series browse — a nav entry (meta.title).
|
|
{ path: '/series', name: 'series', component: SeriesView, meta: { title: 'Series', navOrder: 40, stickyChrome: true } },
|
|
// Series management — no meta.title (reached from a series card/tag).
|
|
{ path: '/series/:tagId', name: 'series-manage', component: SeriesManageView },
|
|
// Series reader — immersive (no top nav, no meta.title).
|
|
{ path: '/series/:tagId/read', name: 'series-read', component: SeriesReaderView, meta: { immersive: true } },
|
|
|
|
// FC-3: subscription backbone — purely management (sources/downloads),
|
|
// distinct from the Browse hub.
|
|
{ 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).
|
|
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings', stickyChrome: true } },
|
|
// System health is a Settings TAB, not a route of its own (operator
|
|
// 2026-09-02). It first shipped as /system reachable only from the health
|
|
// dot, which is a target you have to already suspect something to go
|
|
// looking for. Settings is where someone goes to ask the instance about
|
|
// itself. The path stays as a redirect so the dot's old link, and any
|
|
// bookmark from that build, still land somewhere real.
|
|
{ path: '/system', name: 'system', redirect: () => ({ name: 'settings', query: { tab: 'system' } }) },
|
|
|
|
// 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
|
|
// route NAMES stay so existing { name: 'posts' | 'artists' | 'tags' } links
|
|
// and path pushes keep resolving.
|
|
{ path: '/posts', name: 'posts', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'posts' } }) },
|
|
{ path: '/artists', name: 'artists', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'artists' } }) },
|
|
{ path: '/tags', name: 'tags', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'tags' } }) },
|
|
|
|
// Bookmark/back-button safety net for the routes that got folded into
|
|
// /subscriptions (no meta.title — stay out of TopNav).
|
|
{ path: '/credentials', redirect: '/subscriptions?tab=settings' },
|
|
{ path: '/downloads', redirect: '/subscriptions?tab=downloads' }
|
|
]
|
|
|
|
// Browser uses HTML5 history; non-browser (Vitest/SSR) falls back to memory
|
|
// history so the module is importable without `window`.
|
|
const history =
|
|
typeof window !== 'undefined' ? createWebHistory() : createMemoryHistory()
|
|
|
|
// Where the window lands after a navigation. Without this the router keeps the
|
|
// window's scroll offset, so a view entered from halfway down another one opened
|
|
// halfway down itself, its heading and tab strip under the nav (operator-flagged
|
|
// 2026-09-24, Settings entered from a scrolled feed).
|
|
// - back/forward returns to where that entry was left;
|
|
// - the SAME view with a new query (a gallery filter, a Browse or
|
|
// Subscriptions tab) keeps its place — that is not a new page;
|
|
// - a new view opens at its top.
|
|
export function scrollBehavior(to, from, savedPosition) {
|
|
if (savedPosition) return savedPosition
|
|
if (from.matched.length && to.path === from.path) return false
|
|
if (to.hash) return { el: to.hash }
|
|
return { top: 0 }
|
|
}
|
|
|
|
const router = createRouter({
|
|
history,
|
|
routes,
|
|
scrollBehavior
|
|
})
|
|
|
|
const DEFAULT_TITLE = 'FabledCurator'
|
|
|
|
// Keep the tab title in sync with the route on EVERY navigation. List routes set
|
|
// it from meta.title; detail routes (artist, series) have no meta.title, so they
|
|
// reset to the default here and then overwrite it with their own dynamic title
|
|
// (e.g. ArtistView sets "<artist> — FabledCurator"). Without this reset the
|
|
// previous view's dynamic title stuck around — an artist name showing on the
|
|
// Showcase tab, operator-flagged 2026-06-07.
|
|
router.afterEach((to) => {
|
|
if (typeof document === 'undefined') return
|
|
document.title = to.meta?.title
|
|
? `${to.meta.title} — ${DEFAULT_TITLE}`
|
|
: DEFAULT_TITLE
|
|
})
|
|
|
|
export default router
|