Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions frontend/src/lib/layout/Footer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import { t } from '../stores/i18n.svelte.js';
</script>

<footer class="border-t mt-auto" style="background-color: var(--ds-surface); border-color: var(--ds-border);">
<footer
data-testid="app-footer"
class="border-t mt-auto flex-shrink-0"
style="background-color: var(--ds-surface); border-color: var(--ds-border);"
>
<div class="w-full px-6 py-6">
<div class="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
<!-- Logo and branding -->
Expand All @@ -19,24 +23,29 @@
<!-- Links -->
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-3 text-sm">
<button
data-testid="footer-about"
onclick={() => navigate('/about')}
class="text-[var(--ds-text-link)] hover:text-[var(--ds-text-link-hovered)] transition-colors cursor-pointer bg-transparent border-none"
>
{t('footer.aboutWindshift')}
</button>
<button
data-testid="footer-api-docs"
onclick={() => navigate('/api-docs')}
class="text-[var(--ds-text-link)] hover:text-[var(--ds-text-link-hovered)] transition-colors cursor-pointer bg-transparent border-none"
>
{t('footer.apiReference')}
</button>
<button
data-testid="footer-licenses"
onclick={() => navigate('/licenses')}
class="text-[var(--ds-text-link)] hover:text-[var(--ds-text-link-hovered)] transition-colors cursor-pointer bg-transparent border-none"
>
{t('footer.licenses')}
</button>
<a href="https://github.com/Windshiftapp/core/issues" target="_blank" rel="noopener noreferrer" class="text-[var(--ds-text-link)] hover:text-[var(--ds-text-link-hovered)] transition-colors">
<a
data-testid="footer-report-problem"
href="https://github.com/Windshiftapp/core/issues" target="_blank" rel="noopener noreferrer" class="text-[var(--ds-text-link)] hover:text-[var(--ds-text-link-hovered)] transition-colors">
{t('footer.reportProblem')}
</a>
</div>
Expand Down
28 changes: 13 additions & 15 deletions frontend/src/lib/pages/MainApp.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import Spinner from '../components/Spinner.svelte';
import EmailVerificationBanner from '../features/notifications/EmailVerificationBanner.svelte';
import CollectionNavigation from '../features/collections/CollectionNavigation.svelte';
import Footer from '../layout/Footer.svelte';
import MainSidebar from '../layout/MainSidebar.svelte';
import {
GLOBAL_COLLECTION_VIEWS,
Expand Down Expand Up @@ -34,6 +33,7 @@
import { useMainAppLifecycle } from './useMainAppLifecycle.js';
import {
CREATE_MODAL_WORKSPACE_VIEWS,
isMainAppFooterView,
MAIN_APP_COMPONENT_LOADERS,
MAIN_APP_TEST_VIEWS,
prefetchIdleMainAppComponents,
Expand Down Expand Up @@ -272,6 +272,12 @@
const showCollectionNav = $derived(
!$uiStore.reviewFullscreen && GLOBAL_COLLECTION_VIEWS.has($currentRoute.view)
);
// The scroll column only needs to be a flex column so the page footer can sit
// at the bottom of short pages; leave other routes on the original block
// layout to avoid disturbing full-height app surfaces.
const showFooterLayout = $derived(
!$uiStore.reviewFullscreen && isMainAppFooterView(effectiveView)
);

let previousRoutePath = $state(null);
$effect(() => {
Expand Down Expand Up @@ -422,7 +428,12 @@
? `width: ${terminalState.splitPercent}%; flex-shrink: 0;`
: 'flex: 1;'}
>
<main data-testid="main-content-scroll" class="flex-1 min-h-0 overflow-y-auto overscroll-contain">
<main
data-testid="main-content-scroll"
class="flex-1 min-h-0 overflow-y-auto overscroll-contain"
class:flex={showFooterLayout}
class:flex-col={showFooterLayout}
>
<MainRouteContent view={effectiveView} route={$currentRoute} {lazyComponents} />
</main>
</div>
Expand Down Expand Up @@ -451,14 +462,6 @@
</div>
</div>

<footer
class="authenticated-footer flex-shrink-0 transition-[margin] duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none"
style={!$uiStore.reviewFullscreen
? `margin-left: ${$uiStore.navExpanded ? '200px' : '64px'}`
: ''}
>
<Footer />
</footer>
</div>

<MainAppOverlays
Expand Down Expand Up @@ -526,11 +529,6 @@
border: 0;
background: color-mix(in srgb, var(--ds-blanket, #091e42) 54%, transparent);
}

.authenticated-footer {
margin-left: 4rem !important;
transform: none !important;
}
}

:global(.themed-nav) {
Expand Down
38 changes: 36 additions & 2 deletions frontend/src/lib/pages/MainRouteContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,35 @@
import Button from '../components/Button.svelte';
import DelayedMount from '../components/DelayedMount.svelte';
import Spinner from '../components/Spinner.svelte';
import Footer from '../layout/Footer.svelte';
import PermissionGuard from '../layout/PermissionGuard.svelte';
import { authStore } from '../stores/auth.svelte.js';
import { workspacesStore } from '../stores/workspaces.svelte.js';
import { t } from '../stores/i18n.svelte.js';
import { moduleSettings } from '../stores/moduleSettings.js';
import { uiStore } from '../stores/ui.svelte.js';
import UnauthorizedAccess from './UnauthorizedAccess.svelte';
import {
getMainAppLazyState,
getMainAppRouteProps,
isMainAppFooterView,
resolveMainAppRoute,
} from './mainAppRoutes.js';

let { view, route, lazyComponents } = $props();

const routeEntry = $derived(resolveMainAppRoute(view));
const wrapper = $derived(routeEntry.config?.wrapper || null);
const routeState = $derived(getMainAppLazyState(lazyComponents, view));
// Only render the footer on opted-in routes and only once the route has
// rendered successfully, so it never appears under a spinner or error state.
const showFooter = $derived(
isMainAppFooterView(view) &&
Boolean(routeState.component) &&
!routeState.loading &&
!routeState.error &&
!$uiStore.reviewFullscreen
);
const routeProps = $derived.by(() =>
getMainAppRouteProps(view, route, {
currentUser: authStore.currentUser,
Expand Down Expand Up @@ -53,6 +66,12 @@
</div>
{/snippet}

{#snippet routeFooter()}
{#if showFooter}
<Footer />
{/if}
{/snippet}

{#snippet lazyLoadedComponent(lazyView, props)}
{@const state = getMainAppLazyState(lazyComponents, lazyView)}

Expand Down Expand Up @@ -93,23 +112,38 @@
</div>
{:else if routeEntry.config}
{#if wrapper === 'surface-full'}
<div class="h-full min-h-0 overflow-y-auto" style="background-color: var(--ds-surface);">
<div
class="h-full min-h-0 overflow-y-auto"
class:flex={showFooter}
class:flex-col={showFooter}
style="background-color: var(--ds-surface);"
>
{@render lazyLoadedComponent(view, routeProps)}
{@render routeFooter()}
</div>
{:else if wrapper === 'surface-padded'}
<div class="p-6" style="background-color: var(--ds-surface);">
{@render lazyLoadedComponent(view, routeProps)}
</div>
{@render routeFooter()}
{:else if wrapper === 'surface-admin'}
<div class="px-16 py-12 flex-1 overflow-y-auto" style="background-color: var(--ds-surface);">
<div
class="flex-1 overflow-y-auto px-16 py-12"
class:flex={showFooter}
class:flex-col={showFooter}
style="background-color: var(--ds-surface);"
>
{@render lazyLoadedComponent(view, routeProps)}
{@render routeFooter()}
</div>
{:else if wrapper === 'surface'}
<div style="background-color: var(--ds-surface);">
{@render lazyLoadedComponent(view, routeProps)}
</div>
{@render routeFooter()}
{:else}
{@render lazyLoadedComponent(view, routeProps)}
{@render routeFooter()}
{/if}
{:else}
{@render lazyLoadedComponent('workspaces', { showAdminHeader: false })}
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/lib/pages/mainAppRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,42 @@ export const MAIN_APP_TEST_VIEWS = new Set([
'test-set-detail',
]);

// Views whose content grows with the document and should end with the app
// footer. Viewport-filling surfaces (boards, canvases, editors, Svelte Flow
// designers, internally-scrolled lists) are intentionally absent.
export const MAIN_APP_FOOTER_VIEWS = new Set([
'homepage',
'workspaces',
'workspace-settings',
'workspace-list',
'collection-list',
'collections-edit',
'teams-list',
'team-detail',
'profile',
'security',
'notifications',
'approvals-inbox',
'search',
'about',
'api-docs',
'licenses',
'404',
'organizations',
'workspace-detail',
'workspace-overview',
'personal-workspace',
'workspace-look-and-feel',
'workspace-board-config',
'collection-board-config',
'workspace-nav-config',
'collection-nav-config',
'asset-settings',
'configuration-set-detail',
'logbook',
'logbook-document',
]);

export const WORKSPACE_SETTINGS_TABS = {
'workspace-settings-categories': 'categories',
'workspace-settings-members': 'members',
Expand Down Expand Up @@ -537,6 +573,12 @@ export function resolveMainAppRoute(view) {
return { key: null, config: null };
}

/** True when the view ends with the app footer. Resolves through matchViews so
* settings tabs and similar aliases map to their base route. */
export function isMainAppFooterView(view) {
return MAIN_APP_FOOTER_VIEWS.has(resolveMainAppRoute(view).key);
}

export function resolveEffectiveMainAppView(currentRoute, personalWorkspaceId) {
if (currentRoute.view !== 'item-detail') return currentRoute.view;

Expand Down
Loading