Release v0.1.23
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api';
|
||||
import AuthGate from '$lib/components/AuthGate.svelte';
|
||||
import WorkspaceBootCard from '$lib/components/app-shell/WorkspaceBootCard.svelte';
|
||||
import WorkspaceQuickAccess from '$lib/components/app-shell/WorkspaceQuickAccess.svelte';
|
||||
import WorkspaceSearchPalette from '$lib/components/app-shell/WorkspaceSearchPalette.svelte';
|
||||
import WorkspaceSignedOutCard from '$lib/components/app-shell/WorkspaceSignedOutCard.svelte';
|
||||
import WorkspaceTabletNav from '$lib/components/app-shell/WorkspaceTabletNav.svelte';
|
||||
import { PALETTE_RESULT_LIMIT, buildSessionKey, filterSearchItems } from '$lib/components/app-shell/utils';
|
||||
import ClientPrimaryRail from '$lib/components/navigation/ClientPrimaryRail.svelte';
|
||||
import ClientTopbar from '$lib/components/navigation/ClientTopbar.svelte';
|
||||
import WhatsNewDialog from '$lib/components/WhatsNewDialog.svelte';
|
||||
import { currentChangelog } from '$lib/changelog';
|
||||
import { hasSeenVersion, markVersionSeen } from '$lib/whats-new';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { clientSession, hasModuleAccess, sessionHydrated } from '$lib/session';
|
||||
import { featureFlags } from '$lib/features';
|
||||
import {
|
||||
canCreateMixSession as sessionCanCreateMixSession,
|
||||
canCreateMixWorksheet as sessionCanCreateMixWorksheet,
|
||||
canOpenDashboard as sessionCanOpenDashboard,
|
||||
canOpenEditor as sessionCanOpenEditor,
|
||||
canOpenMixCalculator as sessionCanOpenMixCalculator,
|
||||
canOpenMixMaster as sessionCanOpenMixMaster,
|
||||
canOpenCustomerOrdering as sessionCanOpenCustomerOrdering,
|
||||
canManageOrdering as sessionCanManageOrdering,
|
||||
canOpenProductCosting as sessionCanOpenProductCosting,
|
||||
canOpenReporting as sessionCanOpenReporting,
|
||||
canOpenSettings as sessionCanOpenSettings,
|
||||
canOpenThroughput as sessionCanOpenThroughput,
|
||||
canUseWorkspaceSearch as sessionCanUseWorkspaceSearch,
|
||||
getWorkspaceRole,
|
||||
getWorkspaceHomeHref as sessionWorkspaceHomeHref,
|
||||
isWorkspaceRouteAllowed
|
||||
} from '$lib/workspace-access';
|
||||
import {
|
||||
baseSearchItems,
|
||||
buildClientNavEntries,
|
||||
clientBreadcrumbs,
|
||||
dashboardItem,
|
||||
editorItem,
|
||||
ingredientsEditorItem,
|
||||
footerLinks,
|
||||
mixCalculatorItem,
|
||||
orderingItem,
|
||||
orderingManageChildren,
|
||||
orderingManageGroup,
|
||||
pageTitle,
|
||||
productCostingItem,
|
||||
reportingItem,
|
||||
throughputItem,
|
||||
type FooterLink,
|
||||
type NavEntry,
|
||||
type SearchItem,
|
||||
type NavItem,
|
||||
workingDocumentItems
|
||||
} from '$lib/navigation/client-navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import packageInfo from '../../../package.json';
|
||||
|
||||
let { children } = $props();
|
||||
const isRootRoute = $derived(page.url.pathname === '/');
|
||||
|
||||
let paletteOpen = $state(false);
|
||||
let paletteQuery = $state('');
|
||||
let quickMenuOpen = $state(false);
|
||||
let userMenuOpen = $state(false);
|
||||
let navOpen = $state(false);
|
||||
let showBottomNav = $state(false);
|
||||
let whatsNewOpen = $state(false);
|
||||
// The user identity we've already run the "what's new" check for this mount,
|
||||
// so the dialog is evaluated once per login rather than on every navigation.
|
||||
let whatsNewCheckedFor = $state<string | null>(null);
|
||||
let isRestoringSession = $state(false);
|
||||
let restoredSessionKey = $state<string | null>(null);
|
||||
let seededSearchItems = $state<SearchItem[]>([]);
|
||||
let seededSearchKey = $state<string | null>(null);
|
||||
let bootDelayDone = $state(false);
|
||||
const appVersion = `v${packageInfo.version}`;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const canOpenDashboard = $derived(sessionCanOpenDashboard($clientSession));
|
||||
const canOpenMixMaster = $derived(sessionCanOpenMixMaster($clientSession));
|
||||
const canCreateMixWorksheet = $derived(sessionCanCreateMixWorksheet($clientSession));
|
||||
const canOpenMixCalculator = $derived(sessionCanOpenMixCalculator($clientSession));
|
||||
const canCreateMixSession = $derived(sessionCanCreateMixSession($clientSession));
|
||||
const canOpenEditor = $derived(sessionCanOpenEditor($clientSession));
|
||||
const canOpenSettings = $derived(sessionCanOpenSettings($clientSession));
|
||||
const canUseWorkspaceSearch = $derived(sessionCanUseWorkspaceSearch($clientSession));
|
||||
const workspaceHomeHref = $derived(sessionWorkspaceHomeHref($clientSession));
|
||||
const currentRouteAllowed = $derived(isWorkspaceRouteAllowed($clientSession, page.url.pathname));
|
||||
const routeGuardPending = $derived(!!$clientSession && (isRestoringSession || !currentRouteAllowed));
|
||||
const shellPathname = $derived(routeGuardPending ? workspaceHomeHref : page.url.pathname);
|
||||
const shellTitle = $derived(routeGuardPending ? 'Loading Workspace' : pageTitle(page.url.pathname));
|
||||
const shellBreadcrumbs = $derived(
|
||||
routeGuardPending ? clientBreadcrumbs(workspaceHomeHref, $clientSession) : clientBreadcrumbs(page.url.pathname, $clientSession)
|
||||
);
|
||||
const visibleDashboardItem = $derived(canOpenDashboard ? dashboardItem : null);
|
||||
const visibleWorkingDocumentItems = $derived(
|
||||
!$clientSession
|
||||
? workingDocumentItems
|
||||
: workingDocumentItems.filter((item) => {
|
||||
if (item.href === '/mixes') return canOpenMixMaster;
|
||||
return !item.moduleKey || hasModuleAccess($clientSession, item.moduleKey);
|
||||
})
|
||||
);
|
||||
const visibleMixCalculatorItem = $derived(canOpenMixCalculator ? mixCalculatorItem : null);
|
||||
const visibleProductCostingItem = $derived(sessionCanOpenProductCosting($clientSession) ? productCostingItem : null);
|
||||
const canOpenThroughput = $derived(sessionCanOpenThroughput($clientSession));
|
||||
const visibleThroughputItem = $derived(canOpenThroughput ? throughputItem : null);
|
||||
// Ordering serves two audiences: internal staff get the management console
|
||||
// (/ordering/manage), customers get the catalogue (/ordering).
|
||||
const canManageOrdering = $derived(sessionCanManageOrdering($clientSession));
|
||||
const canOpenCustomerOrdering = $derived(sessionCanOpenCustomerOrdering($clientSession));
|
||||
// Internal staff get the collapsible "Order Management" family (queue +
|
||||
// products/customers/pricing/settings/integrations); customers get the single
|
||||
// catalogue link. Either way it becomes one NavEntry the rail can render.
|
||||
const visibleOrderingEntry = $derived<NavEntry | null>(
|
||||
canManageOrdering
|
||||
? { kind: 'group', group: orderingManageGroup }
|
||||
: canOpenCustomerOrdering
|
||||
? { kind: 'item', item: orderingItem }
|
||||
: null
|
||||
);
|
||||
const visibleReportingItem = $derived(sessionCanOpenReporting($clientSession) ? reportingItem : null);
|
||||
const visibleEditorItem = $derived(canOpenEditor ? editorItem : null);
|
||||
const visibleIngredientsEditorItem = $derived(canOpenEditor ? ingredientsEditorItem : null);
|
||||
// Grouped desktop rail: Dashboard, a collapsible "Costing" family, then the
|
||||
// standalone operations/insights modules. Built from the same access-filtered
|
||||
// items, so a role only ever sees the families it may open.
|
||||
const navEntries = $derived(
|
||||
buildClientNavEntries({
|
||||
dashboard: visibleDashboardItem,
|
||||
costing: [
|
||||
...(visibleMixCalculatorItem ? [visibleMixCalculatorItem] : []),
|
||||
...(visibleProductCostingItem ? [visibleProductCostingItem] : []),
|
||||
...(visibleEditorItem ? [visibleEditorItem] : []),
|
||||
...(visibleIngredientsEditorItem ? [visibleIngredientsEditorItem] : []),
|
||||
...visibleWorkingDocumentItems
|
||||
],
|
||||
throughput: visibleThroughputItem,
|
||||
ordering: visibleOrderingEntry,
|
||||
reporting: visibleReportingItem
|
||||
})
|
||||
);
|
||||
const isOperationsUser = $derived($clientSession?.role_name === 'Operations');
|
||||
const workspaceRole = $derived(getWorkspaceRole($clientSession));
|
||||
const visibleFooterLinks = $derived([
|
||||
...(!isOperationsUser ? footerLinks : [])
|
||||
] as FooterLink[]);
|
||||
const primaryBottomNavigation = $derived(
|
||||
[
|
||||
...(visibleDashboardItem ? [visibleDashboardItem] : []),
|
||||
...(visibleMixCalculatorItem ? [visibleMixCalculatorItem] : []),
|
||||
...(visibleProductCostingItem ? [visibleProductCostingItem] : []),
|
||||
...visibleWorkingDocumentItems.slice(0, 2)
|
||||
]
|
||||
);
|
||||
const workingDocumentsActive = $derived(
|
||||
visibleWorkingDocumentItems.some((item) => matchesRoute(item.href, page.url.pathname))
|
||||
);
|
||||
const visibleBaseSearchItems = $derived(
|
||||
baseSearchItems.filter((item) => {
|
||||
if (item.href === '/') return canOpenDashboard;
|
||||
if (item.href === '/mixes') return canOpenMixMaster;
|
||||
if (item.href === '/mixes/new') return canCreateMixWorksheet;
|
||||
if (item.href === '/mix-calculator') return canOpenMixCalculator;
|
||||
if (item.href === '/product-costing') return sessionCanOpenProductCosting($clientSession);
|
||||
if (item.href === '/editor') return canOpenEditor;
|
||||
if (item.href === '/ingredients') return canOpenEditor;
|
||||
if (item.href === '/reporting') return sessionCanOpenReporting($clientSession);
|
||||
if (item.href === '/settings') return canOpenSettings;
|
||||
return true;
|
||||
})
|
||||
);
|
||||
const searchItems = $derived([...visibleBaseSearchItems, ...seededSearchItems]);
|
||||
const showWorkspaceBoot = $derived(!isRootRoute && (!$sessionHydrated || !bootDelayDone));
|
||||
|
||||
function openPalette(query = '') {
|
||||
paletteQuery = query;
|
||||
paletteOpen = true;
|
||||
quickMenuOpen = false;
|
||||
userMenuOpen = false;
|
||||
navOpen = false;
|
||||
}
|
||||
|
||||
function syncViewport() {
|
||||
showBottomNav = window.innerWidth <= 1180;
|
||||
|
||||
if (!showBottomNav) {
|
||||
navOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runSearchItem(item: SearchItem) {
|
||||
paletteOpen = false;
|
||||
paletteQuery = '';
|
||||
await goto(item.href);
|
||||
}
|
||||
|
||||
async function openSettings() {
|
||||
quickMenuOpen = false;
|
||||
userMenuOpen = false;
|
||||
navOpen = false;
|
||||
await goto('/settings');
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
try {
|
||||
if ($clientSession?.role === 'internal') {
|
||||
await api.internalLogout();
|
||||
} else {
|
||||
await api.clientLogout();
|
||||
}
|
||||
} catch {
|
||||
// Clearing the local session remains the safe fallback.
|
||||
} finally {
|
||||
clientSession.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const paletteState = $derived(filterSearchItems(searchItems, paletteQuery, PALETTE_RESULT_LIMIT));
|
||||
|
||||
$effect(() => {
|
||||
page.url.pathname;
|
||||
quickMenuOpen = false;
|
||||
userMenuOpen = false;
|
||||
paletteOpen = false;
|
||||
paletteQuery = '';
|
||||
navOpen = false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const hydrated = $sessionHydrated;
|
||||
const sessionKey = buildSessionKey($clientSession);
|
||||
|
||||
if (!hydrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionKey) {
|
||||
isRestoringSession = false;
|
||||
restoredSessionKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (restoredSessionKey === sessionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoredSessionKey = sessionKey;
|
||||
isRestoringSession = true;
|
||||
|
||||
// Internal Hunter Stock Feeds users are refreshed against /api/access/me;
|
||||
// legacy client-portal users keep using /api/auth/client/session.
|
||||
const refresh = $clientSession?.role === 'internal' ? api.internalSession() : api.clientSession();
|
||||
|
||||
refresh
|
||||
.then((session) => {
|
||||
restoredSessionKey = `${session.role}:${session.email}:${session.user_id ?? ''}`;
|
||||
clientSession.set(session);
|
||||
return invalidateAll();
|
||||
})
|
||||
.catch(() => {
|
||||
restoredSessionKey = null;
|
||||
clientSession.clear();
|
||||
})
|
||||
.finally(() => {
|
||||
isRestoringSession = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Search palette items are seeded lazily — three list endpoints worth of
|
||||
// data only when the user actually opens the palette, not on every login or
|
||||
// navigation. Subsequent opens hit the api.ts cache.
|
||||
$effect(() => {
|
||||
const hydrated = $sessionHydrated;
|
||||
const session = $clientSession;
|
||||
const sessionKey = buildSessionKey(session);
|
||||
const shouldSeed = paletteOpen;
|
||||
|
||||
if (!hydrated || !session || !sessionKey) {
|
||||
seededSearchItems = [];
|
||||
seededSearchKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldSeed || seededSearchKey === sessionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
seededSearchKey = sessionKey;
|
||||
|
||||
Promise.all([
|
||||
sessionCanOpenMixMaster(session) ? api.mixes() : Promise.resolve([]),
|
||||
featureFlags.mixCalculatorSessionHistory && sessionCanOpenMixCalculator(session)
|
||||
? api.mixCalculatorSessions()
|
||||
: Promise.resolve([])
|
||||
])
|
||||
.then(([mixes, sessions]) => {
|
||||
if (seededSearchKey !== sessionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
seededSearchItems = [
|
||||
...mixes.map((mix) => ({
|
||||
href: `/mixes/${mix.id}`,
|
||||
label: mix.name,
|
||||
description: `Mix · ${mix.client_name} · ${mix.total_mix_kg}kg`,
|
||||
keywords: `mix ${mix.name} ${mix.client_name} ${mix.notes ?? ''} ${mix.ingredients.map((ingredient) => ingredient.raw_material_name).join(' ')}`
|
||||
})),
|
||||
...sessions.map((savedSession) => ({
|
||||
href: `/mix-calculator/${savedSession.id}`,
|
||||
label: `${savedSession.session_number} · ${savedSession.product_name}`,
|
||||
description: `Mix Session · ${savedSession.prepared_by_name} · ${savedSession.mix_date}`,
|
||||
keywords: `mix calculator session ${savedSession.session_number} ${savedSession.product_name} ${savedSession.mix_name} ${savedSession.client_name} ${savedSession.prepared_by_name} ${savedSession.notes ?? ''}`
|
||||
}))
|
||||
];
|
||||
})
|
||||
.catch(() => {
|
||||
if (seededSearchKey === sessionKey) {
|
||||
seededSearchItems = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($sessionHydrated && !$clientSession && !isRootRoute) {
|
||||
goto('/', { replaceState: true });
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!$sessionHydrated || !$clientSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentRouteAllowed || page.url.pathname === workspaceHomeHref) {
|
||||
return;
|
||||
}
|
||||
|
||||
goto(workspaceHomeHref, { replaceState: true });
|
||||
});
|
||||
|
||||
// Surface the release notes once per version per user, right after login.
|
||||
// hasSeenVersion keeps this to a single appearance: once dismissed (which
|
||||
// records the version), it won't return until the next version ships.
|
||||
$effect(() => {
|
||||
if (!$sessionHydrated || !$clientSession || !currentChangelog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = buildSessionKey($clientSession);
|
||||
if (!userKey) {
|
||||
return;
|
||||
}
|
||||
if (whatsNewCheckedFor === userKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
whatsNewCheckedFor = userKey;
|
||||
if (!hasSeenVersion(userKey, currentChangelog.version)) {
|
||||
whatsNewOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
function dismissWhatsNew() {
|
||||
const userKey = buildSessionKey($clientSession);
|
||||
if (userKey && currentChangelog) {
|
||||
markVersionSeen(userKey, currentChangelog.version);
|
||||
}
|
||||
whatsNewOpen = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const bootTimer = window.setTimeout(() => {
|
||||
bootDelayDone = true;
|
||||
}, 1500);
|
||||
|
||||
syncViewport();
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
const isTypingField =
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target instanceof HTMLSelectElement ||
|
||||
target?.isContentEditable;
|
||||
|
||||
if (canUseWorkspaceSearch && ((event.key === 'k' && (event.metaKey || event.ctrlKey)) || (!isTypingField && event.key === '/'))) {
|
||||
event.preventDefault();
|
||||
openPalette();
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
paletteOpen = false;
|
||||
quickMenuOpen = false;
|
||||
userMenuOpen = false;
|
||||
navOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
window.addEventListener('resize', syncViewport);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(bootTimer);
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
window.removeEventListener('resize', syncViewport);
|
||||
};
|
||||
});
|
||||
|
||||
const userInitials = $derived(
|
||||
($clientSession?.name ?? '')
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((w: string) => w[0])
|
||||
.join('')
|
||||
.toUpperCase() || '?'
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{shellTitle} | Hunter Premium Produce</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if !$clientSession}
|
||||
<div class="signed-out-shell">
|
||||
{#if isRootRoute}
|
||||
{@render children()}
|
||||
{:else if showWorkspaceBoot}
|
||||
<WorkspaceBootCard />
|
||||
{:else}
|
||||
<WorkspaceSignedOutCard />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="app-shell">
|
||||
{#if !showBottomNav}
|
||||
<ClientPrimaryRail
|
||||
currentPath={shellPathname}
|
||||
entries={navEntries}
|
||||
brandHref={workspaceHomeHref}
|
||||
footerItems={visibleFooterLinks}
|
||||
{appVersion}
|
||||
{currentYear}
|
||||
{canOpenSettings}
|
||||
onOpenSettings={openSettings}
|
||||
onSignOut={signOut}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class:bottom-nav-layout={showBottomNav} class="main-shell">
|
||||
<ClientTopbar
|
||||
breadcrumbs={shellBreadcrumbs}
|
||||
title={shellTitle}
|
||||
sessionHydrated={$sessionHydrated}
|
||||
session={$clientSession}
|
||||
{userInitials}
|
||||
{userMenuOpen}
|
||||
{canUseWorkspaceSearch}
|
||||
{canOpenSettings}
|
||||
onOpenPalette={() => canUseWorkspaceSearch && openPalette()}
|
||||
onToggleUserMenu={() => {
|
||||
userMenuOpen = !userMenuOpen;
|
||||
quickMenuOpen = false;
|
||||
}}
|
||||
onOpenSettings={openSettings}
|
||||
onSignOut={signOut}
|
||||
onShowWhatsNew={() => (whatsNewOpen = true)}
|
||||
/>
|
||||
|
||||
<main class="content">
|
||||
<AuthGate
|
||||
blocked={routeGuardPending}
|
||||
label={isRestoringSession ? 'Checking Session' : 'Applying Access Rules'}
|
||||
title={isRestoringSession ? 'Restoring your client workspace.' : 'Routing you to an authorised page.'}
|
||||
detail={
|
||||
isRestoringSession
|
||||
? 'Refreshing the saved session before rendering workspace content.'
|
||||
: `The ${workspaceRole} role cannot open this route, so the workspace is redirecting before any page content mounts.`
|
||||
}
|
||||
>
|
||||
{@render children()}
|
||||
</AuthGate>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<WorkspaceQuickAccess
|
||||
bind:quickMenuOpen
|
||||
{canOpenMixMaster}
|
||||
{canCreateMixWorksheet}
|
||||
{canOpenMixCalculator}
|
||||
{canCreateMixSession}
|
||||
{canUseWorkspaceSearch}
|
||||
onOpenPalette={() => openPalette('')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<WorkspaceTabletNav
|
||||
bind:navOpen
|
||||
{showBottomNav}
|
||||
{primaryBottomNavigation}
|
||||
{visibleDashboardItem}
|
||||
{visibleMixCalculatorItem}
|
||||
{visibleProductCostingItem}
|
||||
{visibleThroughputItem}
|
||||
{visibleEditorItem}
|
||||
{visibleReportingItem}
|
||||
{visibleWorkingDocumentItems}
|
||||
{visibleFooterLinks}
|
||||
{orderingManageGroup}
|
||||
{orderingManageChildren}
|
||||
{orderingItem}
|
||||
{canManageOrdering}
|
||||
{canOpenCustomerOrdering}
|
||||
{canCreateMixWorksheet}
|
||||
{canCreateMixSession}
|
||||
{canOpenSettings}
|
||||
{canUseWorkspaceSearch}
|
||||
pagePath={page.url.pathname}
|
||||
onOpenPalette={() => openPalette('')}
|
||||
onOpenSettings={openSettings}
|
||||
onSignOut={signOut}
|
||||
/>
|
||||
|
||||
{/if}
|
||||
|
||||
{#if $clientSession && whatsNewOpen && currentChangelog}
|
||||
<WhatsNewDialog entry={currentChangelog} onClose={dismissWhatsNew} />
|
||||
{/if}
|
||||
|
||||
{#if $clientSession && paletteOpen}
|
||||
<WorkspaceSearchPalette
|
||||
bind:query={paletteQuery}
|
||||
filteredSearchItems={paletteState.filteredItems}
|
||||
hiddenResultCount={paletteState.hiddenResultCount}
|
||||
onClose={() => (paletteOpen = false)}
|
||||
onRunSearchItem={runSearchItem}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 252px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-app);
|
||||
}
|
||||
|
||||
.signed-out-shell {
|
||||
min-height: 100vh;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.main-shell {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-app);
|
||||
}
|
||||
|
||||
.content {
|
||||
--content-padding: 1.34rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: var(--content-padding);
|
||||
overflow: auto;
|
||||
background: var(--color-bg-app);
|
||||
}
|
||||
|
||||
.main-shell.bottom-nav-layout .content {
|
||||
padding-bottom: 7.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content {
|
||||
--content-padding: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1181px) {
|
||||
.bottom-nav-layout .content {
|
||||
padding-bottom: 1.34rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.content {
|
||||
--content-padding: 0.92rem;
|
||||
padding: 0.92rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user