Refine sidebar collapsed and expanded spacing
This commit is contained in:
@@ -1,14 +1,28 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Activity, BarChart3, BookOpenText, ChartCandlestick, ChevronLeft, ChevronRight, Eye, Landmark, LineChart, LogOut, Menu, NotebookTabs, Search } from 'lucide-react';
|
import {
|
||||||
import Link from 'next/link';
|
Activity,
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
BarChart3,
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
BookOpenText,
|
||||||
import { authClient } from '@/lib/auth-client';
|
ChartCandlestick,
|
||||||
import { TaskDetailModal } from '@/components/notifications/task-detail-modal';
|
ChevronLeft,
|
||||||
import { TaskNotificationsTrigger } from '@/components/notifications/task-notifications-trigger';
|
ChevronRight,
|
||||||
|
Eye,
|
||||||
|
Landmark,
|
||||||
|
LineChart,
|
||||||
|
LogOut,
|
||||||
|
Menu,
|
||||||
|
NotebookTabs,
|
||||||
|
Search,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { TaskDetailModal } from "@/components/notifications/task-detail-modal";
|
||||||
|
import { TaskNotificationsTrigger } from "@/components/notifications/task-notifications-trigger";
|
||||||
import {
|
import {
|
||||||
companyAnalysisQueryOptions,
|
companyAnalysisQueryOptions,
|
||||||
companyFinancialStatementsQueryOptions,
|
companyFinancialStatementsQueryOptions,
|
||||||
@@ -17,13 +31,13 @@ import {
|
|||||||
latestPortfolioInsightQueryOptions,
|
latestPortfolioInsightQueryOptions,
|
||||||
portfolioSummaryQueryOptions,
|
portfolioSummaryQueryOptions,
|
||||||
recentTasksQueryOptions,
|
recentTasksQueryOptions,
|
||||||
watchlistQueryOptions
|
watchlistQueryOptions,
|
||||||
} from '@/lib/query/options';
|
} from "@/lib/query/options";
|
||||||
import { buildGraphingHref } from '@/lib/graphing/catalog';
|
import { buildGraphingHref } from "@/lib/graphing/catalog";
|
||||||
import type { ActiveContext, NavGroup, NavItem } from '@/lib/types';
|
import type { ActiveContext, NavGroup, NavItem } from "@/lib/types";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTaskNotificationsCenter } from '@/hooks/use-task-notifications-center';
|
import { useTaskNotificationsCenter } from "@/hooks/use-task-notifications-center";
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type AppShellProps = {
|
type AppShellProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -40,104 +54,104 @@ type NavConfigItem = NavItem & {
|
|||||||
|
|
||||||
const NAV_ITEMS: NavConfigItem[] = [
|
const NAV_ITEMS: NavConfigItem[] = [
|
||||||
{
|
{
|
||||||
id: 'home',
|
id: "home",
|
||||||
href: '/',
|
href: "/",
|
||||||
label: 'Home',
|
label: "Home",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
group: 'overview',
|
group: "overview",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
mobilePrimary: true
|
mobilePrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'analysis',
|
id: "analysis",
|
||||||
href: '/analysis',
|
href: "/analysis",
|
||||||
label: 'Analysis',
|
label: "Analysis",
|
||||||
icon: LineChart,
|
icon: LineChart,
|
||||||
group: 'research',
|
group: "research",
|
||||||
matchMode: 'prefix',
|
matchMode: "prefix",
|
||||||
preserveTicker: true,
|
preserveTicker: true,
|
||||||
mobilePrimary: true
|
mobilePrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'research',
|
id: "research",
|
||||||
href: '/research',
|
href: "/research",
|
||||||
label: 'Research',
|
label: "Research",
|
||||||
icon: NotebookTabs,
|
icon: NotebookTabs,
|
||||||
group: 'research',
|
group: "research",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
preserveTicker: true,
|
preserveTicker: true,
|
||||||
mobilePrimary: true
|
mobilePrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'graphing',
|
id: "graphing",
|
||||||
href: '/graphing',
|
href: "/graphing",
|
||||||
label: 'Graphing',
|
label: "Graphing",
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
group: 'research',
|
group: "research",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
preserveTicker: true,
|
preserveTicker: true,
|
||||||
mobilePrimary: false
|
mobilePrimary: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'financials',
|
id: "financials",
|
||||||
href: '/financials',
|
href: "/financials",
|
||||||
label: 'Financials',
|
label: "Financials",
|
||||||
icon: Landmark,
|
icon: Landmark,
|
||||||
group: 'research',
|
group: "research",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
preserveTicker: true,
|
preserveTicker: true,
|
||||||
mobilePrimary: false
|
mobilePrimary: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'filings',
|
id: "filings",
|
||||||
href: '/filings',
|
href: "/filings",
|
||||||
label: 'Filings',
|
label: "Filings",
|
||||||
icon: BookOpenText,
|
icon: BookOpenText,
|
||||||
group: 'research',
|
group: "research",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
preserveTicker: true,
|
preserveTicker: true,
|
||||||
mobilePrimary: true
|
mobilePrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'search',
|
id: "search",
|
||||||
href: '/search',
|
href: "/search",
|
||||||
label: 'Search',
|
label: "Search",
|
||||||
icon: Search,
|
icon: Search,
|
||||||
group: 'research',
|
group: "research",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
preserveTicker: true,
|
preserveTicker: true,
|
||||||
mobilePrimary: false
|
mobilePrimary: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'portfolio',
|
id: "portfolio",
|
||||||
href: '/portfolio',
|
href: "/portfolio",
|
||||||
label: 'Portfolio',
|
label: "Portfolio",
|
||||||
icon: ChartCandlestick,
|
icon: ChartCandlestick,
|
||||||
group: 'portfolio',
|
group: "portfolio",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
mobilePrimary: true
|
mobilePrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'watchlist',
|
id: "watchlist",
|
||||||
href: '/watchlist',
|
href: "/watchlist",
|
||||||
label: 'Coverage',
|
label: "Coverage",
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
group: 'portfolio',
|
group: "portfolio",
|
||||||
matchMode: 'exact',
|
matchMode: "exact",
|
||||||
mobilePrimary: true
|
mobilePrimary: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const GROUP_LABELS: Record<NavGroup, string> = {
|
const GROUP_LABELS: Record<NavGroup, string> = {
|
||||||
overview: 'Overview',
|
overview: "Overview",
|
||||||
research: 'Research',
|
research: "Research",
|
||||||
portfolio: 'Portfolio'
|
portfolio: "Portfolio",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SIDEBAR_PREFERENCE_KEY = 'fiscal-shell-sidebar-collapsed';
|
const SIDEBAR_PREFERENCE_KEY = "fiscal-shell-sidebar-collapsed";
|
||||||
|
|
||||||
function normalizeTicker(value: string | null | undefined) {
|
function normalizeTicker(value: string | null | undefined) {
|
||||||
const normalized = value?.trim().toUpperCase() ?? '';
|
const normalized = value?.trim().toUpperCase() ?? "";
|
||||||
return normalized.length > 0 ? normalized : null;
|
return normalized.length > 0 ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +160,12 @@ function toTickerHref(baseHref: string, activeTicker: string | null) {
|
|||||||
return baseHref;
|
return baseHref;
|
||||||
}
|
}
|
||||||
|
|
||||||
const separator = baseHref.includes('?') ? '&' : '?';
|
const separator = baseHref.includes("?") ? "&" : "?";
|
||||||
return `${baseHref}${separator}ticker=${encodeURIComponent(activeTicker)}`;
|
return `${baseHref}${separator}ticker=${encodeURIComponent(activeTicker)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveNavHref(item: NavItem, context: ActiveContext) {
|
function resolveNavHref(item: NavItem, context: ActiveContext) {
|
||||||
if (item.href === '/graphing') {
|
if (item.href === "/graphing") {
|
||||||
return buildGraphingHref(context.activeTicker);
|
return buildGraphingHref(context.activeTicker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,87 +177,85 @@ function resolveNavHref(item: NavItem, context: ActiveContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isItemActive(item: NavItem, pathname: string) {
|
function isItemActive(item: NavItem, pathname: string) {
|
||||||
if (item.matchMode === 'prefix') {
|
if (item.matchMode === "prefix") {
|
||||||
return pathname === item.href || pathname.startsWith(`${item.href}/`);
|
return pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathname === item.href;
|
return pathname === item.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDefaultBreadcrumbs(pathname: string, activeTicker: string | null) {
|
function buildDefaultBreadcrumbs(
|
||||||
const analysisHref = toTickerHref('/analysis', activeTicker);
|
pathname: string,
|
||||||
const researchHref = toTickerHref('/research', activeTicker);
|
activeTicker: string | null,
|
||||||
|
) {
|
||||||
|
const analysisHref = toTickerHref("/analysis", activeTicker);
|
||||||
|
const researchHref = toTickerHref("/research", activeTicker);
|
||||||
const graphingHref = buildGraphingHref(activeTicker);
|
const graphingHref = buildGraphingHref(activeTicker);
|
||||||
const financialsHref = toTickerHref('/financials', activeTicker);
|
const financialsHref = toTickerHref("/financials", activeTicker);
|
||||||
const filingsHref = toTickerHref('/filings', activeTicker);
|
const filingsHref = toTickerHref("/filings", activeTicker);
|
||||||
|
|
||||||
if (pathname === '/') {
|
if (pathname === "/") {
|
||||||
return [{ label: 'Home' }];
|
return [{ label: "Home" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/analysis/reports/')) {
|
if (pathname.startsWith("/analysis/reports/")) {
|
||||||
return [
|
return [
|
||||||
{ label: 'Analysis', href: analysisHref },
|
{ label: "Analysis", href: analysisHref },
|
||||||
{ label: 'Reports', href: analysisHref },
|
{ label: "Reports", href: analysisHref },
|
||||||
{ label: activeTicker ?? 'Summary' }
|
{ label: activeTicker ?? "Summary" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/analysis')) {
|
if (pathname.startsWith("/analysis")) {
|
||||||
return [{ label: 'Analysis' }];
|
return [{ label: "Analysis" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/research')) {
|
if (pathname.startsWith("/research")) {
|
||||||
return [
|
return [
|
||||||
{ label: 'Analysis', href: analysisHref },
|
{ label: "Analysis", href: analysisHref },
|
||||||
{ label: 'Research', href: researchHref }
|
{ label: "Research", href: researchHref },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/financials')) {
|
if (pathname.startsWith("/financials")) {
|
||||||
|
return [{ label: "Analysis", href: analysisHref }, { label: "Financials" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith("/graphing")) {
|
||||||
return [
|
return [
|
||||||
{ label: 'Analysis', href: analysisHref },
|
{ label: "Analysis", href: analysisHref },
|
||||||
{ label: 'Financials' }
|
{ label: "Graphing", href: graphingHref },
|
||||||
|
{ label: activeTicker ?? "Compare Set" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/graphing')) {
|
if (pathname.startsWith("/filings")) {
|
||||||
return [
|
return [{ label: "Analysis", href: analysisHref }, { label: "Filings" }];
|
||||||
{ label: 'Analysis', href: analysisHref },
|
|
||||||
{ label: 'Graphing', href: graphingHref },
|
|
||||||
{ label: activeTicker ?? 'Compare Set' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/filings')) {
|
if (pathname.startsWith("/search")) {
|
||||||
return [
|
return [{ label: "Analysis", href: analysisHref }, { label: "Search" }];
|
||||||
{ label: 'Analysis', href: analysisHref },
|
|
||||||
{ label: 'Filings' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/search')) {
|
if (pathname.startsWith("/portfolio")) {
|
||||||
return [
|
return [{ label: "Portfolio" }];
|
||||||
{ label: 'Analysis', href: analysisHref },
|
|
||||||
{ label: 'Search' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/portfolio')) {
|
if (pathname.startsWith("/watchlist")) {
|
||||||
return [{ label: 'Portfolio' }];
|
return [{ label: "Portfolio", href: "/portfolio" }, { label: "Coverage" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/watchlist')) {
|
return [{ label: "Home", href: "/" }, { label: pathname }];
|
||||||
return [
|
|
||||||
{ label: 'Portfolio', href: '/portfolio' },
|
|
||||||
{ label: 'Coverage' }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{ label: 'Home', href: '/' }, { label: pathname }];
|
export function AppShell({
|
||||||
}
|
title,
|
||||||
|
subtitle,
|
||||||
export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs, children }: AppShellProps) {
|
actions,
|
||||||
|
activeTicker,
|
||||||
|
breadcrumbs,
|
||||||
|
children,
|
||||||
|
}: AppShellProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -252,35 +264,47 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
const [hasLoadedSidebarPreference, setHasLoadedSidebarPreference] = useState(false);
|
const [hasLoadedSidebarPreference, setHasLoadedSidebarPreference] =
|
||||||
|
useState(false);
|
||||||
const notifications = useTaskNotificationsCenter();
|
const notifications = useTaskNotificationsCenter();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
const sessionUser = (session?.user ?? null) as { name?: string | null; email?: string | null; role?: unknown } | null;
|
const sessionUser = (session?.user ?? null) as {
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
role?: unknown;
|
||||||
|
} | null;
|
||||||
|
|
||||||
const role = typeof sessionUser?.role === 'string'
|
const role =
|
||||||
|
typeof sessionUser?.role === "string"
|
||||||
? sessionUser.role
|
? sessionUser.role
|
||||||
: Array.isArray(sessionUser?.role)
|
: Array.isArray(sessionUser?.role)
|
||||||
? sessionUser.role.filter((entry): entry is string => typeof entry === 'string').join(', ')
|
? sessionUser.role
|
||||||
|
.filter((entry): entry is string => typeof entry === "string")
|
||||||
|
.join(", ")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const displayName = sessionUser?.name || sessionUser?.email || 'Authenticated user';
|
const displayName =
|
||||||
|
sessionUser?.name || sessionUser?.email || "Authenticated user";
|
||||||
|
|
||||||
const derivedTickerFromPath = useMemo(() => {
|
const derivedTickerFromPath = useMemo(() => {
|
||||||
if (!pathname.startsWith('/analysis/reports/')) {
|
if (!pathname.startsWith("/analysis/reports/")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = pathname.split('/').filter(Boolean);
|
const segments = pathname.split("/").filter(Boolean);
|
||||||
const tickerSegment = segments[2];
|
const tickerSegment = segments[2];
|
||||||
return tickerSegment ? normalizeTicker(decodeURIComponent(tickerSegment)) : null;
|
return tickerSegment
|
||||||
|
? normalizeTicker(decodeURIComponent(tickerSegment))
|
||||||
|
: null;
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const context: ActiveContext = useMemo(() => {
|
const context: ActiveContext = useMemo(() => {
|
||||||
const queryTicker = normalizeTicker(searchParams.get('ticker'));
|
const queryTicker = normalizeTicker(searchParams.get("ticker"));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pathname,
|
pathname,
|
||||||
activeTicker: normalizeTicker(activeTicker) ?? queryTicker ?? derivedTickerFromPath
|
activeTicker:
|
||||||
|
normalizeTicker(activeTicker) ?? queryTicker ?? derivedTickerFromPath,
|
||||||
};
|
};
|
||||||
}, [activeTicker, derivedTickerFromPath, pathname, searchParams]);
|
}, [activeTicker, derivedTickerFromPath, pathname, searchParams]);
|
||||||
|
|
||||||
@@ -290,16 +314,16 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
href,
|
href,
|
||||||
active: isItemActive(item, pathname)
|
active: isItemActive(item, pathname),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [context, pathname]);
|
}, [context, pathname]);
|
||||||
|
|
||||||
const groupedNav = useMemo(() => {
|
const groupedNav = useMemo(() => {
|
||||||
const groups: Array<{ group: NavGroup; items: typeof navEntries }> = [
|
const groups: Array<{ group: NavGroup; items: typeof navEntries }> = [
|
||||||
{ group: 'overview', items: [] },
|
{ group: "overview", items: [] },
|
||||||
{ group: 'research', items: [] },
|
{ group: "research", items: [] },
|
||||||
{ group: 'portfolio', items: [] }
|
{ group: "portfolio", items: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const entry of navEntries) {
|
for (const entry of navEntries) {
|
||||||
@@ -331,62 +355,72 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
const prefetchForHref = (href: string) => {
|
const prefetchForHref = (href: string) => {
|
||||||
router.prefetch(href);
|
router.prefetch(href);
|
||||||
|
|
||||||
if (href.startsWith('/analysis')) {
|
if (href.startsWith("/analysis")) {
|
||||||
if (context.activeTicker) {
|
if (context.activeTicker) {
|
||||||
void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker));
|
void queryClient.prefetchQuery(
|
||||||
|
companyAnalysisQueryOptions(context.activeTicker),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith('/financials')) {
|
if (href.startsWith("/financials")) {
|
||||||
if (context.activeTicker) {
|
if (context.activeTicker) {
|
||||||
void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker));
|
void queryClient.prefetchQuery(
|
||||||
|
companyAnalysisQueryOptions(context.activeTicker),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith('/graphing')) {
|
if (href.startsWith("/graphing")) {
|
||||||
if (context.activeTicker) {
|
if (context.activeTicker) {
|
||||||
void queryClient.prefetchQuery(companyFinancialStatementsQueryOptions({
|
void queryClient.prefetchQuery(
|
||||||
|
companyFinancialStatementsQueryOptions({
|
||||||
ticker: context.activeTicker,
|
ticker: context.activeTicker,
|
||||||
surfaceKind: 'income_statement',
|
surfaceKind: "income_statement",
|
||||||
cadence: 'annual',
|
cadence: "annual",
|
||||||
includeDimensions: false,
|
includeDimensions: false,
|
||||||
includeFacts: false,
|
includeFacts: false,
|
||||||
limit: 16
|
limit: 16,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith('/filings')) {
|
if (href.startsWith("/filings")) {
|
||||||
void queryClient.prefetchQuery(filingsQueryOptions({
|
void queryClient.prefetchQuery(
|
||||||
|
filingsQueryOptions({
|
||||||
ticker: context.activeTicker ?? undefined,
|
ticker: context.activeTicker ?? undefined,
|
||||||
limit: 120
|
limit: 120,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith('/search')) {
|
if (href.startsWith("/search")) {
|
||||||
if (context.activeTicker) {
|
if (context.activeTicker) {
|
||||||
void queryClient.prefetchQuery(companyAnalysisQueryOptions(context.activeTicker));
|
void queryClient.prefetchQuery(
|
||||||
|
companyAnalysisQueryOptions(context.activeTicker),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith('/portfolio')) {
|
if (href.startsWith("/portfolio")) {
|
||||||
void queryClient.prefetchQuery(holdingsQueryOptions());
|
void queryClient.prefetchQuery(holdingsQueryOptions());
|
||||||
void queryClient.prefetchQuery(portfolioSummaryQueryOptions());
|
void queryClient.prefetchQuery(portfolioSummaryQueryOptions());
|
||||||
void queryClient.prefetchQuery(latestPortfolioInsightQueryOptions());
|
void queryClient.prefetchQuery(latestPortfolioInsightQueryOptions());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith('/watchlist')) {
|
if (href.startsWith("/watchlist")) {
|
||||||
void queryClient.prefetchQuery(watchlistQueryOptions());
|
void queryClient.prefetchQuery(watchlistQueryOptions());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href === '/') {
|
if (href === "/") {
|
||||||
void queryClient.prefetchQuery(portfolioSummaryQueryOptions());
|
void queryClient.prefetchQuery(portfolioSummaryQueryOptions());
|
||||||
void queryClient.prefetchQuery(filingsQueryOptions({ limit: 200 }));
|
void queryClient.prefetchQuery(filingsQueryOptions({ limit: 200 }));
|
||||||
void queryClient.prefetchQuery(watchlistQueryOptions());
|
void queryClient.prefetchQuery(watchlistQueryOptions());
|
||||||
@@ -396,8 +430,10 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedPreference = window.localStorage.getItem(SIDEBAR_PREFERENCE_KEY);
|
const storedPreference = window.localStorage.getItem(
|
||||||
if (storedPreference === 'true') {
|
SIDEBAR_PREFERENCE_KEY,
|
||||||
|
);
|
||||||
|
if (storedPreference === "true") {
|
||||||
setIsSidebarCollapsed(true);
|
setIsSidebarCollapsed(true);
|
||||||
}
|
}
|
||||||
setHasLoadedSidebarPreference(true);
|
setHasLoadedSidebarPreference(true);
|
||||||
@@ -408,7 +444,10 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.localStorage.setItem(SIDEBAR_PREFERENCE_KEY, String(isSidebarCollapsed));
|
window.localStorage.setItem(
|
||||||
|
SIDEBAR_PREFERENCE_KEY,
|
||||||
|
String(isSidebarCollapsed),
|
||||||
|
);
|
||||||
}, [hasLoadedSidebarPreference, isSidebarCollapsed]);
|
}, [hasLoadedSidebarPreference, isSidebarCollapsed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -418,7 +457,13 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const runPrefetch = () => {
|
const runPrefetch = () => {
|
||||||
const prioritized = navEntries.filter((entry) => entry.id === 'analysis' || entry.id === 'graphing' || entry.id === 'filings' || entry.id === 'portfolio');
|
const prioritized = navEntries.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.id === "analysis" ||
|
||||||
|
entry.id === "graphing" ||
|
||||||
|
entry.id === "filings" ||
|
||||||
|
entry.id === "portfolio",
|
||||||
|
);
|
||||||
for (const entry of prioritized) {
|
for (const entry of prioritized) {
|
||||||
prefetchForHref(entry.href);
|
prefetchForHref(entry.href);
|
||||||
}
|
}
|
||||||
@@ -456,7 +501,7 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
try {
|
try {
|
||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
router.replace('/auth/signin');
|
router.replace("/auth/signin");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
}
|
}
|
||||||
@@ -467,18 +512,32 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
<div className="ambient-grid" aria-hidden="true" />
|
<div className="ambient-grid" aria-hidden="true" />
|
||||||
<div className="noise-layer" aria-hidden="true" />
|
<div className="noise-layer" aria-hidden="true" />
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto flex min-h-screen w-full max-w-[1440px] gap-6 px-4 pb-10 pt-4 sm:px-5 sm:pb-12 sm:pt-6 md:px-8 lg:gap-8">
|
<div
|
||||||
<aside
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'hidden shrink-0 flex-col gap-6 border-r border-[color:var(--line-weak)] transition-[width,padding] duration-200 lg:flex',
|
"relative z-10 mx-auto flex min-h-screen w-full max-w-[1440px] gap-6 px-4 pb-10 pt-4 sm:px-5 sm:pb-12 sm:pt-6 md:px-8 lg:gap-8",
|
||||||
isSidebarCollapsed ? 'w-20 pr-3' : 'w-72 pr-6'
|
isSidebarCollapsed ? "lg:pl-3" : "lg:pl-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"hidden shrink-0 flex-col gap-4 border-r border-[color:var(--line-weak)] transition-[width,padding] duration-200 lg:flex",
|
||||||
|
isSidebarCollapsed ? "w-16 pr-1" : "w-72 pr-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-start gap-3",
|
||||||
|
isSidebarCollapsed ? "justify-center" : "justify-between",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn('flex items-start gap-3', isSidebarCollapsed ? 'justify-center' : 'justify-between')}>
|
|
||||||
{!isSidebarCollapsed ? (
|
{!isSidebarCollapsed ? (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="terminal-caption text-xs uppercase tracking-[0.25em] text-[color:var(--terminal-muted)]">Fiscal Clone</p>
|
<p className="terminal-caption text-xs uppercase tracking-[0.25em] text-[color:var(--terminal-muted)]">
|
||||||
<h1 className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)]">Neon Desk</h1>
|
Fiscal Clone
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)]">
|
||||||
|
Neon Desk
|
||||||
|
</h1>
|
||||||
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">
|
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">
|
||||||
Financial intelligence cockpit with durable AI workflows.
|
Financial intelligence cockpit with durable AI workflows.
|
||||||
</p>
|
</p>
|
||||||
@@ -489,22 +548,38 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setIsSidebarCollapsed((prev) => !prev)}
|
onClick={() => setIsSidebarCollapsed((prev) => !prev)}
|
||||||
className={cn('shrink-0', isSidebarCollapsed ? 'min-h-11 w-11 px-0' : 'min-h-11 w-11 px-0')}
|
className={cn(
|
||||||
aria-label={isSidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
"shrink-0",
|
||||||
|
isSidebarCollapsed
|
||||||
|
? "min-h-10 w-10 px-0"
|
||||||
|
: "min-h-11 w-11 px-0",
|
||||||
|
)}
|
||||||
|
aria-label={
|
||||||
|
isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"
|
||||||
|
}
|
||||||
aria-pressed={isSidebarCollapsed}
|
aria-pressed={isSidebarCollapsed}
|
||||||
title={isSidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
title={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
>
|
>
|
||||||
{isSidebarCollapsed ? <ChevronRight className="size-4" /> : <ChevronLeft className="size-4" />}
|
{isSidebarCollapsed ? (
|
||||||
|
<ChevronRight className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className="size-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="space-y-4" aria-label="Primary">
|
<nav className="space-y-3" aria-label="Primary">
|
||||||
{groupedNav.map(({ group, items }) => (
|
{groupedNav.map(({ group, items }) => (
|
||||||
<div key={group} className="space-y-2">
|
<div key={group} className="space-y-1.5">
|
||||||
{isSidebarCollapsed ? (
|
{isSidebarCollapsed ? (
|
||||||
<div className="mx-auto h-px w-8 bg-[color:var(--line-weak)]" aria-hidden="true" />
|
<div
|
||||||
|
className="mx-auto h-px w-8 bg-[color:var(--line-weak)]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="terminal-caption px-2 text-[11px] uppercase tracking-[0.18em] text-[color:var(--terminal-muted)]">{GROUP_LABELS[group]}</p>
|
<p className="terminal-caption px-2 text-[11px] uppercase tracking-[0.18em] text-[color:var(--terminal-muted)]">
|
||||||
|
{GROUP_LABELS[group]}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
@@ -513,17 +588,19 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.id}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
aria-current={item.active ? 'page' : undefined}
|
aria-current={item.active ? "page" : undefined}
|
||||||
aria-label={isSidebarCollapsed ? item.label : undefined}
|
aria-label={isSidebarCollapsed ? item.label : undefined}
|
||||||
onMouseEnter={() => prefetchForHref(item.href)}
|
onMouseEnter={() => prefetchForHref(item.href)}
|
||||||
onFocus={() => prefetchForHref(item.href)}
|
onFocus={() => prefetchForHref(item.href)}
|
||||||
title={item.label}
|
title={item.label}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center rounded-xl border border-transparent text-sm transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent',
|
"flex items-center rounded-xl border border-transparent text-sm transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent",
|
||||||
isSidebarCollapsed ? 'justify-center px-0 py-3' : 'gap-3 px-3 py-2',
|
isSidebarCollapsed
|
||||||
|
? "justify-center px-0 py-2.5"
|
||||||
|
: "gap-3 px-2.5 py-1.5",
|
||||||
item.active
|
item.active
|
||||||
? 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)] shadow-[inset_2px_0_0_var(--accent)]'
|
? "border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)] shadow-[inset_2px_0_0_var(--accent)]"
|
||||||
: 'text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]'
|
: "text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
@@ -535,38 +612,37 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div
|
{!isSidebarCollapsed ? (
|
||||||
className={cn(
|
<>
|
||||||
'mt-auto rounded-[1rem] border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)]',
|
|
||||||
isSidebarCollapsed ? 'p-2' : 'p-3'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isSidebarCollapsed ? (
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full min-h-12 px-0"
|
className="w-full"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => void signOut()}
|
onClick={() => void signOut()}
|
||||||
disabled={isSigningOut}
|
disabled={isSigningOut}
|
||||||
aria-label={isSigningOut ? 'Signing out' : 'Sign out'}
|
|
||||||
title={displayName}
|
|
||||||
>
|
>
|
||||||
<LogOut className="size-4" />
|
<LogOut className="size-4" />
|
||||||
|
{isSigningOut ? "Signing out..." : "Sign out"}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<>
|
<div className="rounded-[1rem] border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-2.5">
|
||||||
<p className="text-xs uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">Runtime</p>
|
<p className="text-xs uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">
|
||||||
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">{displayName}</p>
|
Runtime
|
||||||
{role ? <p className="mt-1 text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Role: {role}</p> : null}
|
</p>
|
||||||
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
|
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">
|
||||||
AI and market data are driven by environment configuration and live API tasks.
|
{displayName}
|
||||||
|
</p>
|
||||||
|
{role ? (
|
||||||
|
<p className="mt-1 text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">
|
||||||
|
Role: {role}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
|
||||||
|
AI and market data are driven by environment configuration and
|
||||||
|
live API tasks.
|
||||||
</p>
|
</p>
|
||||||
<Button className="mt-3 w-full" variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
|
|
||||||
<LogOut className="size-4" />
|
|
||||||
{isSigningOut ? 'Signing out...' : 'Sign out'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1 pb-24 lg:pb-0">
|
<div className="min-w-0 flex-1 pb-24 lg:pb-0">
|
||||||
@@ -590,17 +666,28 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between">
|
||||||
<div className="min-w-0 pr-6 sm:pr-0">
|
<div className="min-w-0 pr-6 sm:pr-0">
|
||||||
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">Live System</p>
|
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">
|
||||||
<h2 className="mt-2 text-xl font-semibold text-[color:var(--terminal-bright)] sm:text-2xl md:text-3xl">{title}</h2>
|
Live System
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-2 text-xl font-semibold text-[color:var(--terminal-bright)] sm:text-2xl md:text-3xl">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
{subtitle ? (
|
{subtitle ? (
|
||||||
<p className="mt-1 text-sm text-[color:var(--terminal-muted)]">{subtitle}</p>
|
<p className="mt-1 text-sm text-[color:var(--terminal-muted)]">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-end">
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-end">
|
||||||
{actions}
|
{actions}
|
||||||
<Button variant="ghost" className="max-sm:hidden sm:inline-flex lg:hidden" onClick={() => void signOut()} disabled={isSigningOut}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="max-sm:hidden sm:inline-flex lg:hidden"
|
||||||
|
onClick={() => void signOut()}
|
||||||
|
disabled={isSigningOut}
|
||||||
|
>
|
||||||
<LogOut className="size-4" />
|
<LogOut className="size-4" />
|
||||||
{isSigningOut ? 'Signing out...' : 'Sign out'}
|
{isSigningOut ? "Signing out..." : "Sign out"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -615,18 +702,28 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
const isLast = index === breadcrumbItems.length - 1;
|
const isLast = index === breadcrumbItems.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`${item.label}-${index}`} className="flex items-center gap-2">
|
<li
|
||||||
|
key={`${item.label}-${index}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
{item.href && !isLast ? (
|
{item.href && !isLast ? (
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onMouseEnter={() => prefetchForHref(item.href as string)}
|
onMouseEnter={() =>
|
||||||
|
prefetchForHref(item.href as string)
|
||||||
|
}
|
||||||
onFocus={() => prefetchForHref(item.href as string)}
|
onFocus={() => prefetchForHref(item.href as string)}
|
||||||
className="rounded px-1 py-0.5 text-[color:var(--accent)] transition hover:text-[color:var(--accent-strong)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]"
|
className="rounded px-1 py-0.5 text-[color:var(--accent)] transition hover:text-[color:var(--accent-strong)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className={cn(isLast ? 'text-[color:var(--terminal-bright)]' : '')} aria-current={isLast ? 'page' : undefined}>
|
<span
|
||||||
|
className={cn(
|
||||||
|
isLast ? "text-[color:var(--terminal-bright)]" : "",
|
||||||
|
)}
|
||||||
|
aria-current={isLast ? "page" : undefined}
|
||||||
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -645,7 +742,10 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
className="fixed inset-x-0 bottom-0 z-40 border-t border-[color:var(--line-weak)] bg-[color:rgba(24,27,32,0.96)] px-2 py-2 backdrop-blur lg:hidden"
|
className="fixed inset-x-0 bottom-0 z-40 border-t border-[color:var(--line-weak)] bg-[color:rgba(24,27,32,0.96)] px-2 py-2 backdrop-blur lg:hidden"
|
||||||
aria-label="Mobile primary"
|
aria-label="Mobile primary"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex w-full max-w-[1300px] items-center justify-between gap-1 px-1" style={{ paddingBottom: 'calc(env(safe-area-inset-bottom) * 0.5)' }}>
|
<div
|
||||||
|
className="mx-auto flex w-full max-w-[1300px] items-center justify-between gap-1 px-1"
|
||||||
|
style={{ paddingBottom: "calc(env(safe-area-inset-bottom) * 0.5)" }}
|
||||||
|
>
|
||||||
{mobilePrimaryEntries.map((item) => {
|
{mobilePrimaryEntries.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|
||||||
@@ -653,14 +753,14 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.id}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
aria-current={item.active ? 'page' : undefined}
|
aria-current={item.active ? "page" : undefined}
|
||||||
onMouseEnter={() => prefetchForHref(item.href)}
|
onMouseEnter={() => prefetchForHref(item.href)}
|
||||||
onFocus={() => prefetchForHref(item.href)}
|
onFocus={() => prefetchForHref(item.href)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-w-0 flex-1 flex-col items-center gap-1 rounded-lg px-2 py-1.5 text-[11px] transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]',
|
"flex min-w-0 flex-1 flex-col items-center gap-1 rounded-lg px-2 py-1.5 text-[11px] transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]",
|
||||||
item.active
|
item.active
|
||||||
? 'bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]'
|
? "bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]"
|
||||||
: 'text-[color:var(--terminal-muted)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]'
|
: "text-[color:var(--terminal-muted)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
@@ -683,15 +783,20 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{isMoreOpen ? (
|
{isMoreOpen ? (
|
||||||
<div className="fixed inset-0 z-50 bg-[color:rgba(0,0,0,0.55)] lg:hidden" onClick={() => setIsMoreOpen(false)}>
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-[color:rgba(0,0,0,0.55)] lg:hidden"
|
||||||
|
onClick={() => setIsMoreOpen(false)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
className="absolute inset-x-3 rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-3 shadow-[0_20px_60px_rgba(0,0,0,0.45)]"
|
className="absolute inset-x-3 rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-3 shadow-[0_20px_60px_rgba(0,0,0,0.45)]"
|
||||||
style={{ bottom: 'calc(4.7rem + env(safe-area-inset-bottom))' }}
|
style={{ bottom: "calc(4.7rem + env(safe-area-inset-bottom))" }}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<p className="terminal-caption mb-2 px-1 text-[11px] uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">More destinations</p>
|
<p className="terminal-caption mb-2 px-1 text-[11px] uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">
|
||||||
|
More destinations
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{mobileMoreEntries.map((item) => {
|
{mobileMoreEntries.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
@@ -699,15 +804,15 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.id}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
aria-current={item.active ? 'page' : undefined}
|
aria-current={item.active ? "page" : undefined}
|
||||||
onMouseEnter={() => prefetchForHref(item.href)}
|
onMouseEnter={() => prefetchForHref(item.href)}
|
||||||
onFocus={() => prefetchForHref(item.href)}
|
onFocus={() => prefetchForHref(item.href)}
|
||||||
onClick={() => setIsMoreOpen(false)}
|
onClick={() => setIsMoreOpen(false)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-lg border px-3 py-2 text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]',
|
"flex items-center gap-2 rounded-lg border px-3 py-2 text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]",
|
||||||
item.active
|
item.active
|
||||||
? 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]'
|
? "border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]"
|
||||||
: 'border-transparent text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]'
|
: "border-transparent text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
@@ -724,7 +829,7 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
|
|||||||
className="col-span-2 flex items-center justify-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-sm text-[color:var(--terminal-muted)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--terminal-bright)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]"
|
className="col-span-2 flex items-center justify-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-sm text-[color:var(--terminal-muted)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--terminal-bright)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--line-strong)]"
|
||||||
>
|
>
|
||||||
<LogOut className="size-4" />
|
<LogOut className="size-4" />
|
||||||
{isSigningOut ? 'Signing out...' : 'Sign out'}
|
{isSigningOut ? "Signing out..." : "Sign out"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user