simplify root layout for route group structure
This commit is contained in:
parent
f79e3f2a10
commit
4b96633f69
3 changed files with 3 additions and 304 deletions
|
|
@ -16,7 +16,6 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
account,
|
account,
|
||||||
currentUser,
|
currentUser,
|
||||||
// Pass auth data for client-side store initialization
|
|
||||||
auth: locals.auth
|
auth: locals.auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,89 +3,14 @@
|
||||||
import 'modern-normalize/modern-normalize.css'
|
import 'modern-normalize/modern-normalize.css'
|
||||||
import '$src/app.scss'
|
import '$src/app.scss'
|
||||||
|
|
||||||
import Navigation from '$lib/components/Navigation.svelte'
|
import { browser } from '$app/environment'
|
||||||
import Sidebar from '$lib/components/ui/Sidebar.svelte'
|
|
||||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
|
||||||
import { Tooltip } from 'bits-ui'
|
|
||||||
import { beforeNavigate, afterNavigate } from '$app/navigation'
|
|
||||||
import { browser, dev } from '$app/environment'
|
|
||||||
import { QueryClientProvider } from '@tanstack/svelte-query'
|
import { QueryClientProvider } from '@tanstack/svelte-query'
|
||||||
import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
|
|
||||||
import type { LayoutData } from './$types'
|
import type { LayoutData } from './$types'
|
||||||
|
|
||||||
// Get `data` and `children` from the router via $props()
|
|
||||||
// QueryClient is now created in +layout.ts for SSR support
|
|
||||||
const { data, children } = $props<{
|
const { data, children } = $props<{
|
||||||
data: LayoutData & { [key: string]: any } // Allow any data to pass through from child pages
|
data: LayoutData & { [key: string]: any }
|
||||||
children: () => any
|
children: () => any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Reference to the scrolling container
|
|
||||||
let mainContent: HTMLElement | undefined;
|
|
||||||
|
|
||||||
// Store scroll positions for each visited route
|
|
||||||
const scrollPositions = new Map<string, number>();
|
|
||||||
|
|
||||||
// Save scroll position before navigating away and close sidebar
|
|
||||||
beforeNavigate(({ from }) => {
|
|
||||||
// Close sidebar when navigating
|
|
||||||
sidebar.close()
|
|
||||||
|
|
||||||
// Save scroll position for the current route
|
|
||||||
if (from && mainContent) {
|
|
||||||
const key = from.url.pathname + from.url.search;
|
|
||||||
scrollPositions.set(key, mainContent.scrollTop);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle scroll restoration or reset after navigation
|
|
||||||
afterNavigate(({ from, to, type }) => {
|
|
||||||
if (!mainContent || !to) return;
|
|
||||||
|
|
||||||
// Use requestAnimationFrame to ensure DOM has updated
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!mainContent) return;
|
|
||||||
const key = to.url.pathname + to.url.search;
|
|
||||||
|
|
||||||
// Only restore scroll for browser back/forward navigation
|
|
||||||
if (type === 'popstate' && scrollPositions.has(key)) {
|
|
||||||
// User clicked back/forward button - restore their position
|
|
||||||
mainContent.scrollTop = scrollPositions.get(key) || 0;
|
|
||||||
} else {
|
|
||||||
// Any other navigation type (link, goto, enter, etc.) - go to top
|
|
||||||
mainContent.scrollTop = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Optional: Export snapshot for session persistence
|
|
||||||
export const snapshot = {
|
|
||||||
capture: () => {
|
|
||||||
if (!mainContent) return { scroll: 0, positions: [] };
|
|
||||||
return {
|
|
||||||
scroll: mainContent.scrollTop,
|
|
||||||
positions: Array.from(scrollPositions.entries())
|
|
||||||
};
|
|
||||||
},
|
|
||||||
restore: (data: any) => {
|
|
||||||
if (!data || !mainContent) return;
|
|
||||||
|
|
||||||
// Restore saved positions map
|
|
||||||
if (data.positions) {
|
|
||||||
scrollPositions.clear();
|
|
||||||
data.positions.forEach(([key, value]: [string, number]) => {
|
|
||||||
scrollPositions.set(key, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore current scroll position after DOM is ready
|
|
||||||
if (browser) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (mainContent) mainContent.scrollTop = data.scroll || 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -93,221 +18,5 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<QueryClientProvider client={data.queryClient}>
|
<QueryClientProvider client={data.queryClient}>
|
||||||
{#if dev}
|
{@render children?.()}
|
||||||
<SvelteQueryDevtools />
|
|
||||||
{/if}
|
|
||||||
<Tooltip.Provider>
|
|
||||||
<div class="app-container" class:sidebar-open={sidebar.isOpen}>
|
|
||||||
<div class="main-pane">
|
|
||||||
<div class="nav-blur-background"></div>
|
|
||||||
<div class="main-navigation">
|
|
||||||
<Navigation
|
|
||||||
isAuthenticated={data?.isAuthenticated}
|
|
||||||
account={data?.account}
|
|
||||||
currentUser={data?.currentUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<main class="main-content" bind:this={mainContent}>
|
|
||||||
{@render children?.()}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Sidebar
|
|
||||||
open={sidebar.isOpen}
|
|
||||||
title={sidebar.title}
|
|
||||||
onclose={() => sidebar.close()}
|
|
||||||
scrollable={sidebar.scrollable}
|
|
||||||
onsave={sidebar.onsave}
|
|
||||||
saveLabel={sidebar.saveLabel}
|
|
||||||
element={sidebar.element}
|
|
||||||
onback={sidebar.onback}
|
|
||||||
>
|
|
||||||
{#if sidebar.component}
|
|
||||||
<svelte:component this={sidebar.component} {...sidebar.componentProps} />
|
|
||||||
{:else if sidebar.content}
|
|
||||||
{@render sidebar.content()}
|
|
||||||
{/if}
|
|
||||||
</Sidebar>
|
|
||||||
</div>
|
|
||||||
</Tooltip.Provider>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use '$src/themes/effects' as *;
|
|
||||||
@use '$src/themes/layout' as *;
|
|
||||||
@use '$src/themes/spacing' as *;
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--sidebar-width: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
// Main pane with content
|
|
||||||
.main-pane {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
transition: margin-right $duration-slide ease-in-out;
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
// Blur background that shifts with main pane
|
|
||||||
.nav-blur-background {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 81px; // Matches $nav-height
|
|
||||||
z-index: 1; // Lower z-index so scrollbar appears above
|
|
||||||
pointer-events: none;
|
|
||||||
transition: right $duration-slide ease-in-out;
|
|
||||||
|
|
||||||
// Color gradient for the background
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
color-mix(in srgb, var(--background) 85%, transparent) 0%,
|
|
||||||
color-mix(in srgb, var(--background) 60%, transparent) 50%,
|
|
||||||
color-mix(in srgb, var(--background) 20%, transparent) 85%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
// Single blur value applied to entire element
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
-webkit-backdrop-filter: blur(6px);
|
|
||||||
|
|
||||||
// Mask gradient to fade out the blur effect progressively
|
|
||||||
mask-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
black 0%,
|
|
||||||
black 40%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
-webkit-mask-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
black 0%,
|
|
||||||
black 40%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation wrapper - fixed but shifts with main-pane
|
|
||||||
.main-navigation {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 10; // Above blur but below scrollbar
|
|
||||||
transition: right $duration-slide ease-in-out;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main content area with independent scroll
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
position: relative;
|
|
||||||
padding-top: 81px; // Space for fixed navigation (matches $nav-height)
|
|
||||||
padding-bottom: 20vh; // Extra space at bottom for comfortable scrolling
|
|
||||||
z-index: 2; // Ensure scrollbar is above blur background
|
|
||||||
|
|
||||||
// Note: scroll-behavior removed to prevent unwanted animations
|
|
||||||
// Scroll is controlled programmatically in the script
|
|
||||||
|
|
||||||
// Use overlay scrollbars that auto-hide on macOS
|
|
||||||
overflow-y: overlay;
|
|
||||||
|
|
||||||
// Thin, minimal scrollbar styling
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Firefox scrollbar styling
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When sidebar is open, adjust main pane and navigation
|
|
||||||
&.sidebar-open {
|
|
||||||
.main-pane {
|
|
||||||
margin-right: var(--sidebar-width, 420px);
|
|
||||||
|
|
||||||
// Blur background and navigation shift with the main pane
|
|
||||||
.nav-blur-background,
|
|
||||||
.main-navigation {
|
|
||||||
right: var(--sidebar-width, 420px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile: don't adjust margin, use overlay
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin-right: 0;
|
|
||||||
|
|
||||||
.nav-blur-background,
|
|
||||||
.main-navigation {
|
|
||||||
right: 0; // Don't shift on mobile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile adjustments
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.app-container {
|
|
||||||
.main-pane {
|
|
||||||
.main-content {
|
|
||||||
// Improve mobile scrolling performance
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlay backdrop when sidebar is open on mobile
|
|
||||||
&.sidebar-open::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 99;
|
|
||||||
animation: fadeIn $duration-quick ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade in animation for mobile backdrop
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
* Creates a QueryClient instance for SSR support with TanStack Query v6.
|
* Creates a QueryClient instance for SSR support with TanStack Query v6.
|
||||||
* The QueryClient is created here so it can be used for prefetching in
|
* The QueryClient is created here so it can be used for prefetching in
|
||||||
* child page load functions.
|
* child page load functions.
|
||||||
*
|
|
||||||
* @module routes/+layout
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { LayoutLoad } from './$types'
|
import type { LayoutLoad } from './$types'
|
||||||
|
|
@ -15,7 +13,6 @@ import { authStore } from '$lib/stores/auth.store'
|
||||||
|
|
||||||
export const load: LayoutLoad = async ({ data }) => {
|
export const load: LayoutLoad = async ({ data }) => {
|
||||||
// Initialize auth store from server data BEFORE creating QueryClient
|
// Initialize auth store from server data BEFORE creating QueryClient
|
||||||
// This ensures auth is ready when mutations initialize
|
|
||||||
if (browser && data.auth) {
|
if (browser && data.auth) {
|
||||||
authStore.initFromServer(
|
authStore.initFromServer(
|
||||||
data.auth.accessToken,
|
data.auth.accessToken,
|
||||||
|
|
@ -27,21 +24,15 @@ export const load: LayoutLoad = async ({ data }) => {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
// Disable queries on server - they will be prefetched explicitly
|
|
||||||
enabled: browser,
|
enabled: browser,
|
||||||
// Cache data for 5 minutes before considering it stale
|
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
// Keep unused data in cache for 30 minutes
|
|
||||||
gcTime: 1000 * 60 * 30,
|
gcTime: 1000 * 60 * 30,
|
||||||
// Retry failed requests twice
|
|
||||||
retry: 2,
|
retry: 2,
|
||||||
// Don't refetch on window focus by default
|
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pass through server data (account, currentUser, etc.) along with queryClient
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
queryClient
|
queryClient
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue