feat: architectural grid editor fullPage + slash commands + doc info panel + AI title

This commit is contained in:
Antigravity
2026-05-07 22:29:02 +00:00
parent 0d8252aec0
commit e458b63115
126 changed files with 7652 additions and 1110 deletions

View File

@@ -159,6 +159,7 @@ export function AgentsPageClient({
role: formData.get('role') as string,
sourceUrls: formData.get('sourceUrls') ? JSON.parse(formData.get('sourceUrls') as string) : undefined,
sourceNotebookId: (formData.get('sourceNotebookId') as string) || undefined,
sourceNoteIds: formData.get('sourceNoteIds') ? JSON.parse(formData.get('sourceNoteIds') as string) : undefined,
targetNotebookId: (formData.get('targetNotebookId') as string) || undefined,
frequency: formData.get('frequency') as string,
tools: formData.get('tools') ? JSON.parse(formData.get('tools') as string) : undefined,
@@ -168,6 +169,8 @@ export function AgentsPageClient({
scheduledTime: (formData.get('scheduledTime') as string) || undefined,
scheduledDay: formData.get('scheduledDay') ? Number(formData.get('scheduledDay')) : undefined,
timezone: (formData.get('timezone') as string) || undefined,
slideTheme: (formData.get('slideTheme') as string) || undefined,
slideStyle: (formData.get('slideStyle') as string) || undefined,
}
if (editingAgent) {
await updateAgent(editingAgent.id, data)
@@ -196,70 +199,30 @@ export function AgentsPageClient({
const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents])
return (
/* Full-bleed layout: -m-4 cancels the p-4 of the parent <main> */
<div className="flex -m-4 h-[calc(100vh-4rem)] overflow-hidden">
/* Full-bleed layout */
<div className="flex flex-col h-full overflow-hidden">
{/* ── LEFT SIDEBAR ── */}
<aside className="w-60 flex-shrink-0 flex flex-col bg-muted/30 border-r border-border/40 h-full font-display">
{/* Brand */}
<div className="flex items-center gap-3 px-5 py-5 border-b border-border/40">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4 text-primary-foreground" />
</div>
<span className="font-bold text-base tracking-tight">{t('agents.title')}</span>
</div>
{/* Nav */}
<nav className="flex-1 p-3 space-y-0.5">
<button
onClick={() => setActiveTab('dashboard')}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium rounded-lg transition-all ${
activeTab === 'dashboard'
? 'bg-primary/10 text-primary border-r-2 border-primary'
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
}`}
>
<Bot className="w-4 h-4" />
{/* ── Top header bar — architectural grid style ── */}
<header className="flex items-center justify-between px-12 py-10 border-b border-border/40 flex-shrink-0">
<div>
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight">
{t('agents.myAgents')}
</button>
</nav>
{/* Footer: Help */}
<div className="p-3 border-t border-border/40">
<button
onClick={() => setShowHelp(true)}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-background/70 hover:text-foreground rounded-lg transition-all"
>
<HelpCircle className="w-4 h-4" />
{t('agents.help.btnLabel')}
</button>
</h1>
<p className="text-[11px] text-muted-foreground uppercase tracking-[0.2em] font-bold mt-2">
{t('agents.subtitle')}
</p>
</div>
</aside>
<button
onClick={handleCreate}
className="flex items-center gap-2 px-6 py-3 text-[13px] font-medium uppercase tracking-[0.12em] border border-foreground text-foreground hover:bg-foreground hover:text-background transition-all"
>
<Plus className="w-4 h-4" />
{t('agents.newAgent')}
</button>
</header>
{/* ── MAIN CONTENT ── */}
<div className="flex-1 flex flex-col min-w-0 bg-background overflow-hidden">
{/* Top header bar */}
<header className="flex items-center justify-between px-8 py-4 border-b border-border/40 bg-background flex-shrink-0 font-display">
<div>
<h1 className="text-xl font-bold tracking-tight">
{t('agents.myAgents')}
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
{t('agents.subtitle')}
</p>
</div>
<button
onClick={handleCreate}
className="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg shadow-sm hover:shadow-md hover:shadow-primary/20 transition-all"
>
<Plus className="w-4 h-4" />
{t('agents.newAgent')}
</button>
</header>
{/* Scrollable content area */}
<main className="flex-1 overflow-y-auto p-8">
{/* ── Scrollable content area ── */}
<main className="flex-1 overflow-y-auto px-12 py-10">
{/* Dashboard tab - agents + templates */}
{activeTab === 'dashboard' && (
@@ -329,7 +292,6 @@ export function AgentsPageClient({
</>
)}
</main>
</div>
{/* Sliding panels */}
{showForm && (

View File

@@ -1,5 +1,4 @@
import { Suspense } from "react";
import { HeaderWrapper } from "@/components/header-wrapper";
import { Sidebar } from "@/components/sidebar";
import { ProvidersWrapper } from "@/components/providers-wrapper";
import { auth } from "@/auth";
@@ -7,28 +6,23 @@ import { headers } from "next/headers";
import { detectUserLanguage, parseAcceptLanguage } from "@/lib/i18n/detect-user-language";
import { loadTranslations } from "@/lib/i18n/load-translations";
import { getAISettings } from "@/app/actions/ai-settings";
import { AIChat } from "@/components/ai-chat";
import { AIChatLayoutBridge } from "@/components/ai-chat-layout-bridge";
export default async function MainLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// Read browser language hint from Accept-Language header
const headersList = await headers();
const browserLang = parseAcceptLanguage(headersList.get("accept-language"));
// Run auth + language detection + translation loading in parallel
const [session, initialLanguage] = await Promise.all([
auth(),
detectUserLanguage(browserLang),
]);
// Load initial translations server-side to prevent hydration mismatch
const initialTranslations = await loadTranslations(initialLanguage);
// Load AI settings to conditionally render AI features
const aiSettings = session?.user?.id
? await getAISettings(session.user.id)
: null;
@@ -36,25 +30,17 @@ export default async function MainLayout({
return (
<ProvidersWrapper initialLanguage={initialLanguage} initialTranslations={initialTranslations}>
<div className="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-white overflow-hidden h-screen flex flex-col">
{/* Top Navigation - Style Keep */}
<HeaderWrapper user={session?.user} />
{/* No top-bar header — sidebar-only navigation (architectural-grid design) */}
<div className="flex h-screen overflow-hidden bg-[#E5E2D9]">
<Suspense fallback={<div className="hidden w-80 shrink-0 md:block" />}>
<Sidebar user={session?.user} />
</Suspense>
{/* Main Layout */}
<div className="flex flex-1 overflow-hidden relative">
{/* Sidebar Navigation - Style Keep */}
<Suspense fallback={<div className="w-64 flex-none hidden md:flex" />}>
<Sidebar className="w-64 flex-none flex-col bg-white dark:bg-[#1e2128] border-e border-slate-200 dark:border-slate-800 overflow-y-auto hidden md:flex" user={session?.user} />
</Suspense>
<main className="memento-paper-texture flex min-h-0 flex-1 flex-col overflow-y-auto scroll-smooth">
{children}
</main>
{/* Main Content Area */}
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-background-light dark:bg-background-dark p-4 scroll-smooth">
{children}
</main>
{/* AI Chat Drawer — only shown if user has Assistant IA enabled */}
{showAIAssistant && <AIChat />}
</div>
{showAIAssistant && <AIChatLayoutBridge />}
</div>
</ProvidersWrapper>
);

View File

@@ -10,10 +10,12 @@ export default async function HomePage() {
const notesViewMode =
settings?.notesViewMode === 'masonry'
? 'masonry' as const
: settings?.notesViewMode === 'tabs' || settings?.notesViewMode === 'list'
? 'tabs' as const
: 'masonry' as const
? ('masonry' as const)
: settings?.notesViewMode === 'tabs'
? ('tabs' as const)
: settings?.notesViewMode === 'list'
? ('list' as const)
: ('masonry' as const)
return (
<HomeClient

View File

@@ -1,132 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { SettingsSection, SettingSelect } from '@/components/settings'
import { updateAISettings as updateAI } from '@/app/actions/ai-settings'
import { updateUserSettings as updateUser } from '@/app/actions/user-settings'
import { useLanguage } from '@/lib/i18n'
interface AppearanceSettingsFormProps {
initialTheme: string
initialFontSize: string
initialCardSizeMode?: string
}
export function AppearanceSettingsForm({ initialTheme, initialFontSize, initialCardSizeMode = 'variable' }: AppearanceSettingsFormProps) {
const router = useRouter()
const [theme, setTheme] = useState(initialTheme)
const [fontSize, setFontSize] = useState(initialFontSize)
const [cardSizeMode, setCardSizeMode] = useState(initialCardSizeMode)
const { t } = useLanguage()
const handleThemeChange = async (value: string) => {
setTheme(value)
localStorage.setItem('theme-preference', value)
// Instant visual update
const root = document.documentElement
root.removeAttribute('data-theme')
root.classList.remove('dark')
if (value === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.add('dark')
} else if (value === 'dark') {
root.classList.add('dark')
} else if (value === 'light') {
root.setAttribute('data-theme', 'light')
} else {
root.setAttribute('data-theme', value)
if (['midnight', 'blue', 'sepia'].includes(value)) root.classList.add('dark')
}
// Save to DB (no need for router.refresh - localStorage handles immediate visuals)
await updateUser({ theme: value as 'light' | 'dark' | 'auto' | 'sepia' | 'midnight' | 'blue' })
}
const handleFontSizeChange = async (value: string) => {
setFontSize(value)
// Instant visual update
const fontSizeMap: Record<string, string> = {
'small': '14px', 'medium': '16px', 'large': '18px', 'extra-large': '20px'
}
const root = document.documentElement
root.style.setProperty('--user-font-size', fontSizeMap[value] || '16px')
await updateAI({ fontSize: value as any })
}
const handleCardSizeModeChange = async (value: string) => {
setCardSizeMode(value)
localStorage.setItem('card-size-mode', value)
await updateUser({ cardSizeMode: value as 'variable' | 'uniform' })
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('appearance.title')}</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('appearance.description')}
</p>
</div>
<SettingsSection
title={t('settings.theme')}
icon={<span className="text-2xl">🎨</span>}
description={t('settings.themeLight') + ' / ' + t('settings.themeDark')}
>
<SettingSelect
label={t('settings.theme')}
description={t('settings.selectLanguage')}
value={theme}
options={[
{ value: 'slate', label: t('settings.themeLight') },
{ value: 'dark', label: t('settings.themeDark') },
{ value: 'sepia', label: 'Sepia' },
{ value: 'midnight', label: 'Midnight' },
{ value: 'blue', label: 'Blue' },
{ value: 'auto', label: t('settings.themeSystem') },
]}
onChange={handleThemeChange}
/>
</SettingsSection>
<SettingsSection
title={t('profile.fontSize')}
icon={<span className="text-2xl">📝</span>}
description={t('profile.fontSizeDescription')}
>
<SettingSelect
label={t('profile.fontSize')}
description={t('profile.selectFontSize')}
value={fontSize}
options={[
{ value: 'small', label: t('profile.fontSizeSmall') },
{ value: 'medium', label: t('profile.fontSizeMedium') },
{ value: 'large', label: t('profile.fontSizeLarge') },
]}
onChange={handleFontSizeChange}
/>
</SettingsSection>
<SettingsSection
title={t('settings.cardSizeMode')}
icon={<span className="text-2xl">📐</span>}
description={t('settings.cardSizeModeDescription')}
>
<SettingSelect
label={t('settings.cardSizeMode')}
description={t('settings.selectCardSizeMode')}
value={cardSizeMode}
options={[
{ value: 'variable', label: t('settings.cardSizeVariable') },
{ value: 'uniform', label: t('settings.cardSizeUniform') },
]}
onChange={handleCardSizeModeChange}
/>
</SettingsSection>
</div>
)
}

View File

@@ -6,11 +6,12 @@ import { updateUserSettings } from '@/app/actions/user-settings'
import { useLanguage } from '@/lib/i18n'
import { toast } from 'sonner'
import { Palette, Type, LayoutGrid, Maximize2 } from 'lucide-react'
import { applyDocumentTheme, normalizeThemeId, type ThemeId } from '@/lib/apply-document-theme'
interface AppearanceSettingsClientProps {
initialFontSize: string
initialTheme: string
initialNotesViewMode: 'masonry' | 'tabs'
initialNotesViewMode: 'masonry' | 'tabs' | 'list'
initialCardSizeMode?: 'variable' | 'uniform'
initialFontFamily?: string
}
@@ -23,27 +24,18 @@ export function AppearanceSettingsClient({
initialFontFamily = 'inter',
}: AppearanceSettingsClientProps) {
const { t } = useLanguage()
const [theme, setTheme] = useState(initialTheme || 'light')
const [theme, setTheme] = useState<ThemeId>(normalizeThemeId(initialTheme || 'light'))
const [fontSize, setFontSize] = useState(initialFontSize || 'medium')
const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs'>(initialNotesViewMode)
const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs' | 'list'>(initialNotesViewMode)
const [cardSizeMode, setCardSizeMode] = useState<'variable' | 'uniform'>(initialCardSizeMode)
const [fontFamily, setFontFamily] = useState(initialFontFamily)
const handleThemeChange = async (value: string) => {
setTheme(value)
localStorage.setItem('theme-preference', value)
const root = document.documentElement
root.removeAttribute('data-theme')
root.classList.remove('dark')
if (value === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.add('dark')
} else if (value === 'dark') {
root.classList.add('dark')
} else {
root.setAttribute('data-theme', value)
if (['midnight'].includes(value)) root.classList.add('dark')
}
await updateUserSettings({ theme: value as any })
const next = normalizeThemeId(value)
setTheme(next)
localStorage.setItem('theme-preference', next)
applyDocumentTheme(next)
await updateUserSettings({ theme: next })
toast.success(t('settings.settingsSaved') || 'Saved')
}
@@ -56,7 +48,7 @@ export function AppearanceSettingsClient({
}
const handleNotesViewChange = async (value: string) => {
const mode = value === 'tabs' ? 'tabs' : 'masonry'
const mode = value === 'tabs' ? 'tabs' : value === 'list' ? 'list' : 'masonry'
setNotesViewMode(mode)
await updateAISettings({ notesViewMode: mode })
toast.success(t('settings.settingsSaved') || 'Saved')
@@ -87,12 +79,14 @@ export function AppearanceSettingsClient({
value,
options,
onChange,
optionGroups,
}: {
icon: React.ElementType
title: string
description: string
value: string
options: { value: string; label: string }[]
options?: { value: string; label: string }[]
optionGroups?: { label: string; options: { value: string; label: string }[] }[]
onChange: (v: string) => void
}) => (
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
@@ -111,17 +105,56 @@ export function AppearanceSettingsClient({
onChange={(e) => onChange(e.target.value)}
className="w-full h-11 px-4 bg-muted border border-border rounded-lg text-foreground text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none appearance-none cursor-pointer transition-colors"
>
{options.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
{optionGroups
? optionGroups.map((g) => (
<optgroup key={g.label} label={g.label}>
{g.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</optgroup>
))
: options?.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
)
const themeOptionGroups = [
{
label: t('settings.themeBaseGroup'),
options: [
{ value: 'light', label: t('settings.themeLight') },
{ value: 'dark', label: t('settings.themeDark') },
{ value: 'auto', label: t('settings.themeSystem') },
],
},
{
label: t('settings.themePalettesGroup'),
options: [
{ value: 'sepia', label: t('settings.themeSepia') },
{ value: 'midnight', label: t('settings.themeMidnight') },
{ value: 'rose', label: t('settings.themeRose') },
{ value: 'green', label: t('settings.themeGreen') },
{ value: 'lavender', label: t('settings.themeLavender') },
{ value: 'sand', label: t('settings.themeSand') },
{ value: 'ocean', label: t('settings.themeOcean') },
{ value: 'sunset', label: t('settings.themeSunset') },
{ value: 'blue', label: t('settings.themeBlue') },
],
},
]
return (
<div className="space-y-8">
<div>
@@ -135,19 +168,7 @@ export function AppearanceSettingsClient({
title={t('settings.theme')}
description={t('appearance.selectTheme')}
value={theme}
options={[
{ value: 'light', label: t('settings.themeLight') },
{ value: 'dark', label: t('settings.themeDark') },
{ value: 'sepia', label: 'Sepia' },
{ value: 'midnight', label: 'Midnight' },
{ value: 'rose', label: 'Rose' },
{ value: 'green', label: 'Green' },
{ value: 'lavender', label: 'Lavender' },
{ value: 'sand', label: 'Sand' },
{ value: 'ocean', label: 'Ocean' },
{ value: 'sunset', label: 'Sunset' },
{ value: 'auto', label: t('settings.themeSystem') },
]}
optionGroups={themeOptionGroups}
onChange={handleThemeChange}
/>
@@ -168,7 +189,7 @@ export function AppearanceSettingsClient({
<SelectCard
icon={Type}
title={t('appearance.fontFamilyLabel') || 'Police'}
description={t('appearance.fontFamilyDescription') || 'Choisissez la police de l\'application'}
description={t('appearance.fontFamilyDescription') || "Choisissez la police de l'application"}
value={fontFamily}
options={[
{ value: 'inter', label: 'Inter' },
@@ -184,6 +205,7 @@ export function AppearanceSettingsClient({
value={notesViewMode}
options={[
{ value: 'masonry', label: t('appearance.notesViewMasonry') },
{ value: 'list', label: t('appearance.notesViewList') },
{ value: 'tabs', label: t('appearance.notesViewTabs') },
]}
onChange={handleNotesViewChange}

View File

@@ -19,7 +19,13 @@ export default async function AppearanceSettingsPage() {
<AppearanceSettingsClient
initialFontSize={aiSettings.fontSize}
initialTheme={userSettings.theme}
initialNotesViewMode={aiSettings.notesViewMode === 'masonry' ? 'masonry' : 'tabs'}
initialNotesViewMode={
aiSettings.notesViewMode === 'masonry'
? 'masonry'
: aiSettings.notesViewMode === 'list'
? 'list'
: 'tabs'
}
initialCardSizeMode={userSettings.cardSizeMode}
initialFontFamily={aiSettings.fontFamily || 'inter'}
/>

View File

@@ -59,13 +59,11 @@ function pickUserAISettingsForDb(input: UserAISettingsData): Partial<Record<User
out[key] = v
}
}
if (out.notesViewMode === 'list') {
out.notesViewMode = 'tabs'
}
if (
out.notesViewMode != null &&
out.notesViewMode !== 'masonry' &&
out.notesViewMode !== 'tabs'
out.notesViewMode !== 'tabs' &&
out.notesViewMode !== 'list'
) {
delete out.notesViewMode
}
@@ -167,9 +165,11 @@ const getCachedAISettings = unstable_cache(
const viewMode =
raw === 'masonry'
? ('masonry' as const)
: raw === 'list' || raw === 'tabs'
: raw === 'tabs'
? ('tabs' as const)
: ('masonry' as const)
: raw === 'list'
? ('list' as const)
: ('masonry' as const)
return {
titleSuggestions: settings.titleSuggestions,

View File

@@ -5,6 +5,7 @@ import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import bcrypt from 'bcryptjs'
import { z } from 'zod'
import { normalizeThemeId } from '@/lib/apply-document-theme'
const ProfileSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
@@ -88,10 +89,12 @@ export async function updateTheme(theme: string) {
const session = await auth()
if (!session?.user?.id) return { error: 'Unauthorized' }
const normalized = normalizeThemeId(theme)
try {
await prisma.user.update({
where: { id: session.user.id },
data: { theme },
data: { theme: normalized },
})
revalidatePath('/')
revalidatePath('/settings/profile')

View File

@@ -3,9 +3,11 @@
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath, updateTag } from 'next/cache'
import { unstable_cache } from 'next/cache'
import { normalizeThemeId, type ThemeId } from '@/lib/apply-document-theme'
export type UserSettingsData = {
theme?: 'light' | 'dark' | 'auto' | 'sepia' | 'midnight' | 'blue'
theme?: ThemeId
cardSizeMode?: 'variable' | 'uniform'
}
@@ -13,7 +15,6 @@ export type UserSettingsData = {
* Update user settings (theme, etc.)
*/
export async function updateUserSettings(settings: UserSettingsData) {
const session = await auth()
if (!session?.user?.id) {
@@ -22,11 +23,14 @@ export async function updateUserSettings(settings: UserSettingsData) {
}
try {
const result = await prisma.user.update({
where: { id: session.user.id },
data: settings
})
const data: { theme?: string; cardSizeMode?: 'variable' | 'uniform' } = {}
if (settings.theme !== undefined) data.theme = normalizeThemeId(settings.theme)
if (settings.cardSizeMode !== undefined) data.cardSizeMode = settings.cardSizeMode
await prisma.user.update({
where: { id: session.user.id },
data,
})
revalidatePath('/', 'layout')
updateTag('user-settings')
@@ -38,28 +42,23 @@ export async function updateUserSettings(settings: UserSettingsData) {
}
}
/**
* Get user settings for current user (Cached)
*/
import { unstable_cache } from 'next/cache'
// Internal cached function
const getCachedUserSettings = unstable_cache(
async (userId: string) => {
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { theme: true, cardSizeMode: true }
select: { theme: true, cardSizeMode: true },
})
return {
theme: (user?.theme || 'light') as 'light' | 'dark' | 'auto' | 'sepia' | 'midnight' | 'blue',
cardSizeMode: (user?.cardSizeMode || 'variable') as 'variable' | 'uniform'
theme: normalizeThemeId(user?.theme || 'light'),
cardSizeMode: (user?.cardSizeMode || 'variable') as 'variable' | 'uniform',
}
} catch (error) {
console.error('Error getting user settings:', error)
return {
theme: 'light' as const
theme: 'light' as const satisfies ThemeId,
cardSizeMode: 'variable' as const,
}
}
},
@@ -77,8 +76,8 @@ export async function getUserSettings(userId?: string) {
if (!id) {
return {
theme: 'light' as const,
cardSizeMode: 'variable' as const
theme: 'light' as const satisfies ThemeId,
cardSizeMode: 'variable' as const,
}
}

View File

@@ -1,7 +1,26 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { prisma } from '@/lib/prisma'
import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'
export async function GET(req: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const id = req.nextUrl.searchParams.get('id')
if (!id) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 })
}
const canvas = await prisma.canvas.findFirst({
where: { id, userId: session.user.id },
select: { id: true, name: true, data: true, updatedAt: true },
})
if (!canvas) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json({ canvas })
}
export async function POST(req: Request) {
try {

View File

@@ -17,6 +17,8 @@ export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const includeArchived = searchParams.get('archived') === 'true'
const search = searchParams.get('search')
const notebookId = searchParams.get('notebookId')
const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined
let where: any = {
userId: session.user.id,
@@ -27,6 +29,10 @@ export async function GET(request: NextRequest) {
where.isArchived = false
}
if (notebookId) {
where.notebookId = notebookId
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
@@ -40,7 +46,8 @@ export async function GET(request: NextRequest) {
{ isPinned: 'desc' },
{ order: 'asc' },
{ updatedAt: 'desc' }
]
],
...(limit ? { take: limit } : {}),
})
return NextResponse.json({

View File

@@ -12,22 +12,32 @@
--breakpoint-large-desktop: 1440px;
--breakpoint-ultra-wide: 1920px;
/* Custom colors matching Keep design */
--color-primary: #64748b;
--color-background-light: #f7f7f8;
--color-background-dark: #1a1d23;
/* Memento — Architectural Grid (réf. architectural-grid1) */
--color-memento-desk: #E5E2D9;
--color-memento-paper: #F2F0E9;
--color-memento-ink: #1C1C1C;
--color-primary: #1C1C1C;
--color-memento-accent: #1C1C1C;
--color-memento-paper-elevated: #faf9f5;
--color-background-light: var(--color-memento-paper);
--color-background-dark: #202020;
/* Stitch Design Tokens */
--font-sans: var(--font-inter);
--font-heading: var(--font-manrope);
--shadow-level-1: 0px 4px 20px rgba(15, 23, 42, 0.05);
--shadow-level-2: 0px 10px 30px rgba(15, 23, 42, 0.08);
--font-heading: var(--font-memento-serif), ui-serif, Georgia, "Times New Roman", serif;
--shadow-level-1: 0 2px 4px rgba(0,0,0,0.04), 0 4px 8px rgba(0,0,0,0.06);
--shadow-level-2: 0 2px 8px rgba(0,0,0,0.06), 0 8px 16px rgba(0,0,0,0.08);
--shadow-level-3: 0 4px 8px rgba(0,0,0,0.04), 0 16px 32px rgba(0,0,0,0.12);
--shadow-acrylic: 0 8px 32px rgba(0,0,0,0.08);
--shadow-card-rest: 0 1px 2px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04);
--shadow-card-hover: 0 2px 8px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.06);
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Custom scrollbar for better aesthetics */
/* Custom scrollbar for better aesthetics - Architectural Minimalist */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 3px;
height: 3px;
}
::-webkit-scrollbar-track {
@@ -35,16 +45,211 @@
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
background: rgba(28, 28, 28, 0.05);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
background: rgba(28, 28, 28, 0.15);
}
.dark ::-webkit-scrollbar-thumb {
background: #475569;
background: rgba(255, 255, 255, 0.08);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.18);
}
/* Win11 Acrylic / Mica blur utilities */
@utility acrylic {
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
}
@utility acrylic-heavy {
backdrop-filter: blur(40px) saturate(200%);
-webkit-backdrop-filter: blur(40px) saturate(200%);
}
@utility acrylic-light {
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(24px) saturate(1.35);
-webkit-backdrop-filter: blur(24px) saturate(1.35);
}
@utility font-memento-serif {
font-family: var(--font-memento-serif), ui-serif, Georgia, "Times New Roman", serif;
}
@utility acrylic-dark {
background: rgba(32, 32, 32, 0.75);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
}
@utility win11-shadow {
box-shadow: var(--shadow-card-rest);
transition: box-shadow var(--transition-normal);
}
@utility win11-shadow-hover {
box-shadow: var(--shadow-card-hover);
}
/* Architectural Grid — texture & navigation (réf. architectural-grid1) */
.memento-paper-texture {
background-color: var(--background);
background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png");
background-size: auto;
}
html.dark .memento-paper-texture {
background-image: none;
}
.memento-sidebar-depth {
box-shadow: 1px 0 10px rgba(0, 0, 0, 0.05);
}
html.dark .memento-sidebar-depth {
box-shadow: 4px 0 24px -8px rgba(0, 0, 0, 0.4);
}
html:not(.dark) .memento-active-nav {
background: linear-gradient(to right, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.custom-scrollbar::-webkit-scrollbar {
width: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(28, 28, 28, 0.08);
border-radius: 10px;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
background: rgba(28, 28, 28, 0.2);
}
.ai-glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.sidebar-shadow {
box-shadow: 1px 0 10px rgba(0, 0, 0, 0.05);
}
/* ──────────────────────────────────────────────
ARCHITECTURAL GRID — Design Tokens Aliases
Compatible with shadcn token system
────────────────────────────────────────────── */
/* CSS aliases: ink = foreground, paper = background */
:root {
--ink: var(--foreground);
--paper: var(--background);
--muted-ink: var(--muted-foreground);
--ai-accent: #75B2D6;
}
/* Sidebar toggle view button (Notebooks / Agents) */
.sidebar-view-toggle {
display: flex;
background: rgba(255, 255, 255, 0.5);
padding: 2px;
border-radius: 999px;
border: 1px solid var(--border);
}
.sidebar-view-toggle-btn {
padding: 6px;
border-radius: 999px;
transition: all 150ms;
color: var(--muted-foreground);
}
.sidebar-view-toggle-btn:hover {
color: var(--foreground);
}
.sidebar-view-toggle-btn.active {
background: var(--foreground);
color: var(--background);
}
/* Dark mode toggle */
html.dark .sidebar-view-toggle {
background: rgba(255, 255, 255, 0.08);
}
html.dark .sidebar-view-toggle-btn.active {
background: var(--primary);
color: var(--primary-foreground);
}
/* Inbox section separator */
.sidebar-inbox-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-radius: 12px;
transition: all 200ms;
width: 100%;
text-align: left;
}
.sidebar-inbox-item:hover {
background: rgba(255, 255, 255, 0.4);
}
.sidebar-inbox-item.active {
background: linear-gradient(to right, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.5);
}
html.dark .sidebar-inbox-item:hover {
background: rgba(255, 255, 255, 0.06);
}
html.dark .sidebar-inbox-item.active {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.12);
}
/* Animated tab indicator for AI panel */
.ai-tab-indicator {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--foreground);
}
/* Note date in editorial view */
.note-date-badge {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 700;
color: var(--muted-foreground);
opacity: 0.7;
}
/* AI send button accent */
.ai-send-btn {
background: var(--ai-accent);
color: white;
border-radius: 8px;
padding: 8px;
transition: transform 100ms, opacity 100ms;
}
.ai-send-btn:hover {
opacity: 0.9;
transform: scale(1.05);
}
.ai-send-btn:active {
transform: scale(0.95);
}
/* Dark mode active nav */
html.dark .memento-active-nav {
background: rgba(255, 255, 255, 0.09);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Custom Prose overrides for compact notes */
@@ -105,133 +310,139 @@
:root {
--radius: 0.5rem;
--background: #f8fafc; /* Sub-surface off-white */
--foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */
--card: oklch(1 0 0); /* Blanc pur */
--card-foreground: oklch(0.2 0.02 230);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 230);
--primary: #0284c7; /* Sky Blue */
--primary-foreground: #ffffff; /* Blanc */
--secondary: #e2e8f0; /* Gris-bleu très pâle */
--secondary-foreground: #1e293b;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--accent-foreground: #0284c7;
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: #cbd5e1; /* Gris-bleu visible */
--input: #cbd5e1; /* Bordure visible pour inputs/checkbox */
--ring: #0284c7;
--memento-desk: #E5E2D9;
--background: #F2F0E9;
--foreground: #1C1C1C;
--card: #faf9f5;
--card-foreground: #1C1C1C;
--popover: #faf9f5;
--popover-foreground: #1C1C1C;
--primary: #1C1C1C;
--primary-foreground: #F2F0E9;
--secondary: #e8e6df;
--secondary-foreground: #1C1C1C;
--muted: #e3e1d8;
--muted-foreground: rgba(28, 28, 28, 0.58);
--accent: #ebe9e2;
--accent-foreground: #1C1C1C;
--destructive: #c42b1c;
--border: rgba(28, 28, 28, 0.1);
--input: rgba(28, 28, 28, 0.12);
--ring: rgba(28, 28, 28, 0.35);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.97 0.004 230);
--sidebar-foreground: oklch(0.2 0.02 230);
--sidebar-primary: oklch(0.45 0.08 230);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.94 0.005 230);
--sidebar-accent-foreground: oklch(0.2 0.02 230);
--sidebar-border: oklch(0.9 0.008 230);
--sidebar-ring: oklch(0.7 0.005 230);
--sidebar: color-mix(in oklab, #ffffff 65%, #F2F0E9);
--sidebar-foreground: #1C1C1C;
--sidebar-primary: #1C1C1C;
--sidebar-primary-foreground: #F2F0E9;
--sidebar-accent: rgba(255, 255, 255, 0.45);
--sidebar-accent-foreground: #1C1C1C;
--sidebar-border: rgba(28, 28, 28, 0.1);
--sidebar-ring: rgba(28, 28, 28, 0.35);
}
html:not(.dark) {
--memento-sidebar-glow: color-mix(in oklab, #ffffff 72%, #F2F0E9 28%);
}
html.dark {
--memento-sidebar-glow: color-mix(in oklab, var(--primary) 22%, transparent);
}
[data-theme='light'] {
--background: oklch(0.985 0.003 230); /* Blanc grisâtre */
--foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */
--card: oklch(1 0 0); /* Blanc pur */
--card-foreground: oklch(0.2 0.02 230);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 230);
--primary: oklch(0.45 0.08 230); /* Gris-bleu doux */
--primary-foreground: oklch(0.99 0 0); /* Blanc */
--secondary: oklch(0.94 0.005 230); /* Gris-bleu très pâle */
--secondary-foreground: oklch(0.2 0.02 230);
--muted: oklch(0.92 0.005 230);
--muted-foreground: oklch(0.6 0.01 230);
--accent: oklch(0.92 0.005 230);
--accent-foreground: oklch(0.2 0.02 230);
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: oklch(0.85 0.008 230); /* Gris-bleu visible */
--input: oklch(0.85 0.008 230); /* Bordure visible */
--ring: oklch(0.7 0.005 230);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 230);
--sidebar: oklch(0.97 0.004 230);
--sidebar-foreground: oklch(0.2 0.02 230);
--sidebar-primary: oklch(0.45 0.08 230);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.94 0.005 230);
--sidebar-accent-foreground: oklch(0.2 0.02 230);
--sidebar-border: oklch(0.85 0.008 230);
--sidebar-ring: oklch(0.7 0.005 230);
--memento-desk: #E5E2D9;
--background: #F2F0E9;
--foreground: #1C1C1C;
--card: #faf9f5;
--card-foreground: #1C1C1C;
--popover: #faf9f5;
--popover-foreground: #1C1C1C;
--primary: #1C1C1C;
--primary-foreground: #F2F0E9;
--secondary: #e8e6df;
--secondary-foreground: #1C1C1C;
--muted: #e3e1d8;
--muted-foreground: rgba(28, 28, 28, 0.58);
--accent: #ebe9e2;
--accent-foreground: #1C1C1C;
--destructive: #c42b1c;
--border: rgba(28, 28, 28, 0.1);
--input: rgba(28, 28, 28, 0.12);
--ring: rgba(28, 28, 28, 0.35);
--sidebar: color-mix(in oklab, #ffffff 65%, #F2F0E9);
--sidebar-foreground: #1C1C1C;
--sidebar-primary: #1C1C1C;
--sidebar-primary-foreground: #F2F0E9;
--sidebar-accent: rgba(255, 255, 255, 0.45);
--sidebar-accent-foreground: #1C1C1C;
--sidebar-border: rgba(28, 28, 28, 0.1);
--sidebar-ring: rgba(28, 28, 28, 0.35);
}
[data-theme='light'].dark {
--background: oklch(0.14 0.005 230); /* Noir grisâtre */
--foreground: oklch(0.97 0.003 230); /* Blanc grisâtre */
--card: oklch(0.18 0.006 230); /* Gris-bleu foncé */
--card-foreground: oklch(0.97 0.003 230);
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
--primary: oklch(0.55 0.08 230); /* Gris-bleu plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
--secondary: oklch(0.24 0.006 230);
--secondary-foreground: oklch(0.97 0.003 230);
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.26 0.008 230);
--accent-foreground: oklch(0.97 0.003 230);
--background: oklch(0.16 0.006 75);
--foreground: oklch(0.96 0.004 85);
--card: oklch(0.2 0.008 75);
--card-foreground: oklch(0.96 0.004 85);
--popover: oklch(0.2 0.008 75);
--popover-foreground: oklch(0.96 0.004 85);
--primary: oklch(0.78 0.03 75);
--primary-foreground: oklch(0.18 0.02 60);
--secondary: oklch(0.26 0.008 75);
--secondary-foreground: oklch(0.96 0.004 85);
--muted: oklch(0.24 0.008 75);
--muted-foreground: oklch(0.62 0.012 75);
--accent: oklch(0.28 0.01 75);
--accent-foreground: oklch(0.96 0.004 85);
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.33 0.01 230);
--input: oklch(0.33 0.01 230);
--ring: oklch(0.6 0.01 230);
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
--sidebar: oklch(0.12 0.005 230);
--sidebar-foreground: oklch(0.97 0.003 230);
--sidebar-primary: oklch(0.55 0.08 230);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.24 0.006 230);
--sidebar-accent-foreground: oklch(0.97 0.003 230);
--sidebar-border: oklch(0.28 0.01 230);
--sidebar-ring: oklch(0.6 0.01 230);
--border: oklch(0.33 0.012 75);
--input: oklch(0.33 0.012 75);
--ring: oklch(0.58 0.02 75);
--sidebar: oklch(0.14 0.006 75);
--sidebar-foreground: oklch(0.96 0.004 85);
--sidebar-primary: oklch(0.78 0.03 75);
--sidebar-primary-foreground: oklch(0.18 0.02 60);
--sidebar-accent: oklch(0.26 0.01 75);
--sidebar-accent-foreground: oklch(0.96 0.004 85);
--sidebar-border: oklch(0.3 0.012 75);
--sidebar-ring: oklch(0.58 0.02 75);
}
.dark {
--background: oklch(0.14 0.005 230); /* Noir grisâtre */
--foreground: oklch(0.97 0.003 230); /* Blanc grisâtre */
--card: oklch(0.18 0.006 230); /* Gris-bleu foncé */
--card-foreground: oklch(0.97 0.003 230);
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
--primary: oklch(0.55 0.08 230); /* Gris-bleu plus clair */
--primary-foreground: oklch(0.1 0 0); /* Noir */
--secondary: oklch(0.24 0.006 230);
--secondary-foreground: oklch(0.97 0.003 230);
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.26 0.008 230);
--accent-foreground: oklch(0.97 0.003 230);
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.33 0.01 230);
--input: oklch(0.33 0.01 230);
--ring: oklch(0.6 0.01 230);
--background: #202020;
--foreground: #ffffff;
--card: #2d2d2d;
--card-foreground: #ffffff;
--popover: #2d2d2d;
--popover-foreground: #ffffff;
--primary: #d6d3d1;
--primary-foreground: #1c1917;
--secondary: #2d2d2d;
--secondary-foreground: #ffffff;
--muted: #2d2d2d;
--muted-foreground: #9e9e9e;
--accent: #383838;
--accent-foreground: #ffffff;
--destructive: #ff6b6b;
--border: #3d3d3d;
--input: #3d3d3d;
--ring: #a8a29e;
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.12 0.005 230);
--sidebar-foreground: oklch(0.97 0.003 230);
--sidebar-primary: oklch(0.55 0.08 230);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.24 0.006 230);
--sidebar-accent-foreground: oklch(0.97 0.003 230);
--sidebar-border: oklch(0.28 0.01 230);
--sidebar-ring: oklch(0.6 0.01 230);
--sidebar: rgba(32, 32, 32, 0.75);
--sidebar-foreground: #ffffff;
--sidebar-primary: #d6d3d1;
--sidebar-primary-foreground: #1c1917;
--sidebar-accent: #383838;
--sidebar-accent-foreground: #ffffff;
--sidebar-border: #3d3d3d;
--sidebar-ring: #a8a29e;
}
[data-theme='midnight'] {
@@ -781,8 +992,12 @@ html.font-system * {
}
body {
@apply bg-background text-foreground;
/* Body inherits from html, can be adjusted per language */
@apply text-foreground;
background-color: var(--memento-desk);
}
html.dark body {
background-color: var(--background);
}
/* Latin languages use default (inherits from html) */
@@ -1073,7 +1288,7 @@ html.font-system * {
background: var(--popover);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);
box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.06);
padding: 2px;
display: flex;
align-items: center;
@@ -1081,7 +1296,7 @@ html.font-system * {
z-index: 100;
}
.dark .notion-bubble-menu {
box-shadow: 0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06);
box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.4);
}
.notion-bubble-btn {
@@ -1095,7 +1310,10 @@ html.font-system * {
background: transparent;
color: var(--foreground);
cursor: pointer;
transition: all 0.12s ease;
transition: all 0.15s ease;
}
.notion-bubble-btn:hover {
background: var(--accent);
}
.notion-bubble-btn:hover {
background: var(--accent);
@@ -1114,7 +1332,7 @@ html.font-system * {
background: var(--popover);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.08);
padding: 4px;
min-width: 150px;
z-index: 110;
@@ -1134,7 +1352,7 @@ html.font-system * {
cursor: pointer;
font-size: 0.8125rem;
color: var(--foreground);
transition: background 0.1s ease;
transition: background 0.15s ease;
}
.notion-ai-subitem:hover {
background: var(--accent);
@@ -1171,8 +1389,8 @@ html.font-system * {
.notion-ai-result-modal {
background: var(--popover);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 16px 48px rgba(0,0,0,0.12);
padding: 20px;
width: min(520px, 90vw);
max-height: 80vh;
@@ -1226,8 +1444,8 @@ html.font-system * {
.notion-image-modal {
background: var(--popover);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 16px 48px rgba(0,0,0,0.15);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 16px 48px rgba(0,0,0,0.08);
padding: 20px;
width: 400px;
max-width: 90vw;
@@ -1239,15 +1457,16 @@ html.font-system * {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 8px;
border-radius: 4px;
background: var(--background);
color: var(--foreground);
font-size: 0.8125rem;
outline: none;
transition: border-color 0.15s ease;
transition: border-color var(--transition-fast);
}
.notion-modal-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 1px var(--primary);
}
.notion-modal-input::placeholder {
color: var(--muted-foreground);
@@ -1255,13 +1474,13 @@ html.font-system * {
}
.notion-modal-btn {
padding: 6px 14px;
border-radius: 6px;
border-radius: 4px;
font-size: 0.8125rem;
border: 1px solid var(--border);
background: transparent;
color: var(--foreground);
cursor: pointer;
transition: background 0.1s ease;
transition: background var(--transition-fast);
}
.notion-modal-btn:hover {
background: var(--accent);
@@ -1284,22 +1503,22 @@ html.font-system * {
z-index: 9999;
background: var(--popover);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.08);
padding: 6px;
min-width: 320px;
max-height: 420px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
animation: slash-enter 0.12s ease;
animation: slash-enter 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes slash-enter {
from { opacity: 0; transform: translateY(-4px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.dark .notion-slash-menu {
box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3);
box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.5);
}
/* Header hint */
@@ -1331,14 +1550,14 @@ html.font-system * {
align-items: center;
gap: 4px;
padding: 3px 9px;
border-radius: 20px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
border: 1px solid var(--border);
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
transition: all 0.12s ease;
transition: all 0.15s ease;
white-space: nowrap;
}
.notion-slash-tab:hover {
@@ -1415,11 +1634,11 @@ html.font-system * {
padding: 6px 8px;
border: none;
background: transparent;
border-radius: 7px;
border-radius: 6px;
cursor: pointer;
text-align: left;
color: var(--foreground);
transition: background 0.1s ease, transform 0.08s ease;
transition: background 0.15s ease, transform 0.08s ease;
}
[dir="rtl"] .notion-slash-item {
text-align: right;
@@ -1439,11 +1658,11 @@ html.font-system * {
width: 30px;
height: 30px;
border: 1px solid var(--border);
border-radius: 7px;
border-radius: 6px;
background: var(--background);
color: var(--foreground);
flex-shrink: 0;
transition: background 0.1s ease;
transition: background 0.15s ease;
}
.notion-slash-item-selected .notion-slash-icon,
.notion-slash-item:hover .notion-slash-icon {

View File

@@ -9,8 +9,10 @@ import { DirectionInitializer } from "@/components/direction-initializer";
import { ErrorReporter } from "@/components/error-reporter";
import { auth } from "@/auth";
import Script from "next/script";
import { getThemeScript } from "@/lib/theme-script";
import { normalizeThemeId } from "@/lib/apply-document-theme";
import { Inter, Manrope } from "next/font/google";
import { Inter, Manrope, Playfair_Display } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
@@ -22,6 +24,12 @@ const manrope = Manrope({
variable: "--font-manrope",
});
const playfair = Playfair_Display({
subsets: ["latin"],
variable: "--font-memento-serif",
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "Memento - Your Digital Notepad",
description: "A beautiful note-taking app built with Next.js 16",
@@ -38,13 +46,18 @@ export const metadata: Metadata = {
};
export const viewport: Viewport = {
themeColor: "#3A7CA5",
themeColor: "#1C1C1C",
};
function getHtmlClass(theme?: string): string {
if (theme === 'dark') return 'dark';
if (theme === 'midnight') return 'dark';
return '';
function serverHtmlThemeState(theme?: string | null): { className?: string; dataTheme?: string } {
const t = normalizeThemeId(theme || 'light')
if (t === 'auto') return {}
if (t === 'dark') return { className: 'dark' }
if (t === 'light') return {}
if (t === 'midnight') return { className: 'dark', dataTheme: 'midnight' }
const named = ['sepia', 'rose', 'green', 'lavender', 'sand', 'ocean', 'sunset', 'blue'] as const
if ((named as readonly string[]).includes(t)) return { dataTheme: t }
return {}
}
/**
@@ -77,10 +90,21 @@ export default async function RootLayout({
getUserSettings(userId),
])
const htmlTheme = serverHtmlThemeState(userSettings.theme)
return (
<html suppressHydrationWarning className={getHtmlClass(userSettings.theme)}>
<html
suppressHydrationWarning
className={htmlTheme.className}
data-theme={htmlTheme.dataTheme}
>
<head />
<body className={`${inter.className} ${inter.variable} ${manrope.variable}`}>
<body className={`${inter.className} ${inter.variable} ${manrope.variable} ${playfair.variable}`}>
<Script
id="theme-early"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: getThemeScript(userSettings.theme) }}
/>
<Script
id="sw-cleanup"
strategy="afterInteractive"