201 lines
7.0 KiB
TypeScript
201 lines
7.0 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
|
import "./globals.css";
|
|
import { Inter, Source_Serif_4, Newsreader, Fraunces, JetBrains_Mono } from "next/font/google";
|
|
import { cn } from "@/lib/utils";
|
|
import "@/modules"; // registers all module manifests
|
|
import { auth } from "@/lib/auth";
|
|
import { db } from "@/lib/db";
|
|
import { asc, eq } from "drizzle-orm";
|
|
import { dashboards, users } from "@/modules/_core/schema";
|
|
import type { DashboardMeta } from "@/app/d/actions";
|
|
import { getQuickAdds } from "@/modules/_core";
|
|
import { QuickAddProvider } from "@/components/quick-add-provider";
|
|
import { QuickAddSheet } from "@/components/quick-add-sheet";
|
|
import { QuickAddCreateHost } from "@/components/quick-add/create-host";
|
|
import { CommandPalette } from "@/components/command-palette";
|
|
import { PwaRegister } from "@/components/pwa-register";
|
|
import { InstallPrompt } from "@/components/install-prompt";
|
|
import { AppShell } from "@/components/app-shell";
|
|
import { AppToaster } from "@/components/app-toaster";
|
|
import { AssistantBubble } from "@/modules/agent/components/assistant-bubble";
|
|
import { DEFAULT_ASSISTANT_NAME } from "@/lib/assistant-config";
|
|
import { isLlmConfigured } from "@/lib/llm";
|
|
import { DEFAULT_THEME, navStyleToDataNav } from "@/modules/_core/themes";
|
|
import type { Palette, ThemeMode, FontPair, Density, NavStyle } from "@/modules/_core/themes";
|
|
|
|
const inter = Inter({ subsets: ["latin"], variable: "--sans-inter", display: "swap" });
|
|
const sourceSerif = Source_Serif_4({
|
|
subsets: ["latin"],
|
|
variable: "--serif-source",
|
|
display: "swap",
|
|
});
|
|
const newsreader = Newsreader({
|
|
subsets: ["latin"],
|
|
variable: "--serif-newsreader",
|
|
display: "swap",
|
|
});
|
|
const fraunces = Fraunces({
|
|
subsets: ["latin"],
|
|
variable: "--serif-fraunces",
|
|
display: "swap",
|
|
});
|
|
const jetbrains = JetBrains_Mono({
|
|
subsets: ["latin"],
|
|
variable: "--mono-jb",
|
|
display: "swap",
|
|
});
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: "#1F1B16",
|
|
viewportFit: "cover",
|
|
};
|
|
|
|
export const metadata: Metadata = {
|
|
title: "famapp",
|
|
description: "Family coordination app",
|
|
manifest: "/manifest.webmanifest",
|
|
appleWebApp: {
|
|
capable: true,
|
|
statusBarStyle: "default",
|
|
title: "famapp",
|
|
},
|
|
icons: {
|
|
icon: [
|
|
{ url: "/favicon.svg", type: "image/svg+xml" },
|
|
{ url: "/icon-32.png", sizes: "32x32", type: "image/png" },
|
|
{ url: "/icon-16.png", sizes: "16x16", type: "image/png" },
|
|
],
|
|
apple: "/icon-180.png",
|
|
},
|
|
};
|
|
|
|
// Pre-paint: read user's theme prefs from localStorage and apply data-* + .dark.
|
|
// Falls back to clay/serif-sans/regular/sidebar/system if nothing is stored.
|
|
const prePaintScript = `(function(){
|
|
try {
|
|
var palette = localStorage.getItem('themePalette') || localStorage.getItem('theme') || 'clay';
|
|
var mode = localStorage.getItem('themeMode') || 'system';
|
|
var fontPair = localStorage.getItem('themeFontPair') || 'serif-sans';
|
|
var density = localStorage.getItem('themeDensity') || 'regular';
|
|
var navStyle = localStorage.getItem('themeNavStyle') || 'rail-desktop';
|
|
var dataNav = navStyle === 'compact-rail' ? 'rail'
|
|
: navStyle === 'top-nav' ? 'top'
|
|
: 'sidebar';
|
|
if (window.matchMedia && window.matchMedia('(max-width: 759px)').matches) {
|
|
dataNav = navStyle === 'fab-only' ? 'fab' : 'bottom';
|
|
}
|
|
var dark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
var html = document.documentElement;
|
|
html.setAttribute('data-theme', palette);
|
|
html.setAttribute('data-font-pair', fontPair);
|
|
html.setAttribute('data-density', density);
|
|
html.setAttribute('data-nav', dataNav);
|
|
if (dark) html.classList.add('dark'); else html.classList.remove('dark');
|
|
} catch(e) {}
|
|
})();`;
|
|
|
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
let palette: Palette = DEFAULT_THEME.palette;
|
|
let mode: ThemeMode = DEFAULT_THEME.mode;
|
|
let fontPair: FontPair = DEFAULT_THEME.fontPair;
|
|
let density: Density = DEFAULT_THEME.density;
|
|
let navStyle: NavStyle = DEFAULT_THEME.navStyle;
|
|
let userDashboards: DashboardMeta[] = [];
|
|
let signedIn = false;
|
|
let assistantEnabled = false;
|
|
let assistantName = DEFAULT_ASSISTANT_NAME;
|
|
let assistantModel: string | null = null;
|
|
|
|
const session = await auth();
|
|
if (session?.user?.id) {
|
|
signedIn = true;
|
|
const [row] = await db
|
|
.select({
|
|
themePalette: users.themePalette,
|
|
themeMode: users.themeMode,
|
|
themeFontPair: users.themeFontPair,
|
|
themeDensity: users.themeDensity,
|
|
themeNavStyle: users.themeNavStyle,
|
|
assistantEnabled: users.assistantEnabled,
|
|
assistantName: users.assistantName,
|
|
assistantModel: users.assistantModel,
|
|
})
|
|
.from(users)
|
|
.where(eq(users.id, session.user.id))
|
|
.limit(1);
|
|
if (row) {
|
|
palette = row.themePalette as Palette;
|
|
mode = row.themeMode as ThemeMode;
|
|
fontPair = row.themeFontPair as FontPair;
|
|
density = row.themeDensity as Density;
|
|
navStyle = row.themeNavStyle as NavStyle;
|
|
assistantEnabled = row.assistantEnabled;
|
|
assistantName = row.assistantName?.trim() || DEFAULT_ASSISTANT_NAME;
|
|
assistantModel = row.assistantModel?.trim() || null;
|
|
}
|
|
userDashboards = await db
|
|
.select({
|
|
id: dashboards.id,
|
|
name: dashboards.name,
|
|
slug: dashboards.slug,
|
|
isDefault: dashboards.isDefault,
|
|
position: dashboards.position,
|
|
})
|
|
.from(dashboards)
|
|
.where(eq(dashboards.userId, session.user.id))
|
|
.orderBy(asc(dashboards.position), asc(dashboards.createdAt));
|
|
}
|
|
|
|
// Server-side initial dark guess: only for `dark` mode (system mode is corrected
|
|
// before paint by the inline script). Avoids a flash on signed-in users.
|
|
const isDark = mode === "dark";
|
|
const initialDataNav = navStyleToDataNav(navStyle);
|
|
|
|
const quickAdds = getQuickAdds();
|
|
|
|
const fontVars = cn(
|
|
inter.variable,
|
|
sourceSerif.variable,
|
|
newsreader.variable,
|
|
fraunces.variable,
|
|
jetbrains.variable,
|
|
);
|
|
|
|
return (
|
|
<html
|
|
lang="en"
|
|
data-theme={palette}
|
|
data-font-pair={fontPair}
|
|
data-density={density}
|
|
data-nav={initialDataNav}
|
|
className={cn(fontVars, isDark ? "dark" : "")}
|
|
suppressHydrationWarning
|
|
>
|
|
<head>
|
|
<script dangerouslySetInnerHTML={{ __html: prePaintScript }} />
|
|
</head>
|
|
<body>
|
|
<QuickAddProvider actions={quickAdds}>
|
|
<AppShell signedIn={signedIn} navStyle={navStyle} dashboards={userDashboards}>
|
|
{children}
|
|
</AppShell>
|
|
<QuickAddSheet />
|
|
<QuickAddCreateHost />
|
|
<CommandPalette />
|
|
<InstallPrompt />
|
|
<PwaRegister />
|
|
{signedIn && assistantEnabled && session?.user?.id ? (
|
|
<AssistantBubble
|
|
configured={isLlmConfigured()}
|
|
userId={session.user.id}
|
|
assistantName={assistantName}
|
|
assistantModel={assistantModel}
|
|
/>
|
|
) : null}
|
|
<AppToaster position="bottom-right" />
|
|
</QuickAddProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|