feat: architectural grid editor fullPage + slash commands + doc info panel + AI title
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user