@@ -657,7 +723,7 @@ export function AdminSettingsForm({ config }: { config: Record }
}}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
- {providerOptions.map(opt => (
+ {embeddingsProviderOptions.map(opt => (
{opt.label}
))}
diff --git a/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx b/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx
index 2af1746..6462e7a 100644
--- a/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx
+++ b/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx
@@ -63,12 +63,18 @@ export function AppearanceSettingsClient({
}
const handleFontFamilyChange = async (value: string) => {
- const font = value === 'system' ? 'system' : 'inter'
+ const font = value === 'system' ? 'system'
+ : value === 'playfair' ? 'playfair'
+ : value === 'jetbrains' ? 'jetbrains'
+ : 'inter'
setFontFamily(font)
localStorage.setItem('font-family', font)
const root = document.documentElement
- font === 'system' ? root.classList.add('font-system') : root.classList.remove('font-system')
- await updateAISettings({ fontFamily: font })
+ root.classList.remove('font-system', 'font-playfair', 'font-jetbrains')
+ if (font === 'system') root.classList.add('font-system')
+ if (font === 'playfair') root.classList.add('font-playfair')
+ if (font === 'jetbrains') root.classList.add('font-jetbrains')
+ await updateAISettings({ fontFamily: font as 'inter' | 'playfair' | 'jetbrains' | 'system' })
toast.success(t('settings.settingsSaved') || 'Saved')
}
@@ -192,7 +198,9 @@ export function AppearanceSettingsClient({
description={t('appearance.fontFamilyDescription') || "Choisissez la police de l'application"}
value={fontFamily}
options={[
- { value: 'inter', label: 'Inter' },
+ { value: 'inter', label: 'Inter (défaut)' },
+ { value: 'playfair', label: 'Playfair Display' },
+ { value: 'jetbrains', label: 'JetBrains Mono' },
{ value: 'system', label: t('appearance.fontSystem') || 'Système' },
]}
onChange={handleFontFamilyChange}
diff --git a/memento-note/app/actions/ai-settings.ts b/memento-note/app/actions/ai-settings.ts
index 9f9de6a..61c5391 100644
--- a/memento-note/app/actions/ai-settings.ts
+++ b/memento-note/app/actions/ai-settings.ts
@@ -23,7 +23,7 @@ export type UserAISettingsData = {
autoLabeling?: boolean
noteHistory?: boolean
noteHistoryMode?: 'manual' | 'auto'
- fontFamily?: 'inter' | 'system'
+ fontFamily?: 'inter' | 'playfair' | 'jetbrains' | 'system'
}
/** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */
@@ -191,7 +191,7 @@ const getCachedAISettings = unstable_cache(
autoLabeling: settings.autoLabeling ?? true,
noteHistory: settings.noteHistory ?? false,
noteHistoryMode: (settings.noteHistoryMode ?? 'manual') as 'manual' | 'auto',
- fontFamily: (settings.fontFamily || 'inter') as 'inter' | 'system',
+ fontFamily: (settings.fontFamily || 'inter') as 'inter' | 'playfair' | 'jetbrains' | 'system',
}
} catch (error) {
console.error('Error getting AI settings:', error)
diff --git a/memento-note/app/actions/note-illustration.ts b/memento-note/app/actions/note-illustration.ts
new file mode 100644
index 0000000..cef999f
--- /dev/null
+++ b/memento-note/app/actions/note-illustration.ts
@@ -0,0 +1,100 @@
+'use server'
+
+import DOMPurify from 'isomorphic-dompurify'
+import { auth } from '@/auth'
+import { prisma } from '@/lib/prisma'
+import { getAIProvider } from '@/lib/ai/factory'
+import { getSystemConfig } from '@/lib/config'
+import { getAISettings } from '@/app/actions/ai-settings'
+import { revalidatePath } from 'next/cache'
+
+function extractSvgSnippet(raw: string): string | null {
+ const trimmed = raw.trim()
+ const fenced = trimmed.match(/```(?:svg)?\s*([\s\S]*?)```/i)
+ const candidate = (fenced ? fenced[1] : trimmed).trim()
+ const start = candidate.indexOf('')
+ if (start === -1 || end === -1 || end <= start) return null
+ return candidate.slice(start, end + 6)
+}
+
+function sanitizeSvgMarkup(svg: string): string {
+ return DOMPurify.sanitize(svg, {
+ USE_PROFILES: { svg: true, svgFilters: true },
+ ADD_TAGS: ['use'],
+ ADD_ATTR: ['viewBox', 'xmlns', 'preserveAspectRatio'],
+ })
+}
+
+/**
+ * Génère une miniature SVG abstraite pour le flux éditorial (via modèle chat configuré).
+ * Respecte les préférences utilisateur (assistant IA activé) et nettoie le SVG.
+ */
+export async function generateNoteIllustrationSvg(noteId: string): Promise<{ ok: true } | { ok: false; error: string }> {
+ const session = await auth()
+ if (!session?.user?.id) return { ok: false, error: 'Non autorisé' }
+
+ try {
+ const settings = await getAISettings(session.user.id)
+ if (settings.paragraphRefactor === false) {
+ return { ok: false, error: 'Assistant IA désactivé dans vos paramètres.' }
+ }
+
+ const note = await prisma.note.findFirst({
+ where: { id: noteId, userId: session.user.id },
+ select: { id: true, title: true, content: true },
+ })
+ if (!note) return { ok: false, error: 'Note introuvable' }
+
+ const plainTitle = (note.title || '').slice(0, 200)
+ const plainBody = note.content
+ .replace(/<[^>]+>/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim()
+ .slice(0, 1200)
+
+ if (!plainBody && !plainTitle) {
+ return { ok: false, error: 'Ajoutez du contenu avant de générer une illustration.' }
+ }
+
+ const config = await getSystemConfig()
+ const provider = getAIProvider(config)
+
+ const prompt = `Tu es un designer minimaliste. Produis UN SEUL document SVG valide pour une vignette de carte note.
+Contraintes strictes:
+- viewBox="0 0 224 168" (rapport 4:3), pas de width/height fixes en px sur la racine ou width="100%" height="100%"
+- Style architectural / papier, 2–4 formes géométriques ou lignes, palette sobre (noir/gris/une couleur douce), pas de texte lisible
+- AUCUN script, AUCUNE balise foreignObject, AUCUN lien externe, AUCUN attribut on*
+- Réponds UNIQUEMENT avec le fragment SVG (commence par et finit par ), sans markdown ni commentaire.
+
+Thème à suggérer visuellement (abstrait, pas littéral):
+Titre: ${plainTitle || '(sans titre)'}
+Extrait: ${plainBody.slice(0, 400)}`
+
+ const raw = await provider.generateText(prompt)
+ const extracted = extractSvgSnippet(raw)
+ if (!extracted) {
+ return { ok: false, error: 'Le modèle n’a pas renvoyé un SVG valide. Réessayez.' }
+ }
+
+ const safe = sanitizeSvgMarkup(extracted)
+ if (!safe.includes(' field in data)
- if (isStructuralChange && !options?.skipRevalidation) {
- revalidatePath('/')
+ console.log('[updateNote] Structural check — data fields:', Object.keys(data), '| isStructural:', isStructuralChange)
+
+ if (!options?.skipRevalidation) {
+ // Always revalidate note individual page on content changes so UI reflects saved data
revalidatePath(`/note/${id}`)
+ revalidatePath('/')
+ }
+
+ if (isStructuralChange) {
if (data.isArchived !== undefined) {
revalidatePath('/archive')
}
diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css
index 9b522d4..a370058 100644
--- a/memento-note/app/globals.css
+++ b/memento-note/app/globals.css
@@ -34,6 +34,19 @@
--transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
}
+/* Font family overrides — toggled on by ThemeInitializer */
+.font-system {
+ --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+}
+
+.font-playfair {
+ --font-sans: var(--font-memento-serif), ui-serif, Georgia, 'Times New Roman', serif;
+}
+
+.font-jetbrains {
+ --font-sans: var(--font-jetbrains-mono), 'JetBrains Mono', ui-monospace, monospace;
+}
+
/* Custom scrollbar for better aesthetics - Architectural Minimalist */
::-webkit-scrollbar {
width: 3px;
@@ -93,6 +106,9 @@
@utility win11-shadow-hover {
box-shadow: var(--shadow-card-hover);
}
+@utility editor-body {
+ font-size: var(--editor-body-size, 16px);
+}
/* Architectural Grid — texture & navigation (réf. architectural-grid1) */
.memento-paper-texture {
@@ -269,7 +285,7 @@ html.dark .memento-active-nav {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-inter);
- --font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, Consolas, monospace;
+ --font-mono: var(--font-jetbrains-mono), 'JetBrains Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, Consolas, monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -423,7 +439,7 @@ html.dark {
--secondary: #2d2d2d;
--secondary-foreground: #ffffff;
--muted: #2d2d2d;
- --muted-foreground: #9e9e9e;
+ --muted-foreground: #a8a8a8;
--accent: #383838;
--accent-foreground: #ffffff;
--destructive: #ff6b6b;
@@ -435,7 +451,7 @@ html.dark {
--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: rgba(32, 32, 32, 0.75);
+ --sidebar: #252525;
--sidebar-foreground: #ffffff;
--sidebar-primary: #d6d3d1;
--sidebar-primary-foreground: #1c1917;
@@ -991,6 +1007,11 @@ html.font-system * {
font-size: var(--user-font-size, 16px);
}
+ /* Editor body size — used for textarea and ProseMirror content */
+ :root {
+ --editor-body-size: var(--user-font-size, 16px);
+ }
+
body {
@apply text-foreground;
background-color: var(--memento-desk);
@@ -1778,3 +1799,25 @@ html.font-system * {
font-size: 1.125rem;
opacity: 0.4;
}
+
+/* ─── OVERRIDE FINAL: Force editorial body text in fullPage using CSS var ──────── */
+/* Sets font-size on the container so TipTap inherits --editor-body-size */
+.fullpage-editor {
+ font-size: var(--editor-body-size, 16px) !important;
+ line-height: 1.85 !important;
+}
+/* Also target TipTap's actual editable div directly */
+.fullpage-editor .tiptap,
+.fullpage-editor .tiptap p,
+.fullpage-editor .ProseMirror,
+.fullpage-editor .ProseMirror > p {
+ font-size: var(--editor-body-size, 16px) !important;
+ line-height: 1.85 !important;
+}
+/* Keep headings at their correct relative sizes */
+.fullpage-editor .tiptap h1,
+.fullpage-editor .ProseMirror h1 { font-size: 2.25rem !important; line-height: 1.25 !important; }
+.fullpage-editor .tiptap h2,
+.fullpage-editor .ProseMirror h2 { font-size: 1.75rem !important; line-height: 1.3 !important; }
+.fullpage-editor .tiptap h3,
+.fullpage-editor .ProseMirror h3 { font-size: 1.375rem !important; line-height: 1.4 !important; }
diff --git a/memento-note/app/layout.tsx b/memento-note/app/layout.tsx
index 2f9b0ab..db48e02 100644
--- a/memento-note/app/layout.tsx
+++ b/memento-note/app/layout.tsx
@@ -12,7 +12,7 @@ import Script from "next/script";
import { getThemeScript } from "@/lib/theme-script";
import { normalizeThemeId } from "@/lib/apply-document-theme";
-import { Inter, Manrope, Playfair_Display } from "next/font/google";
+import { Inter, Manrope, Playfair_Display, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
@@ -30,6 +30,12 @@ const playfair = Playfair_Display({
weight: ["400", "500", "600", "700"],
});
+const jetbrainsMono = JetBrains_Mono({
+ subsets: ["latin"],
+ variable: "--font-jetbrains-mono",
+ weight: ["400", "500"],
+});
+
export const metadata: Metadata = {
title: "Memento - Your Digital Notepad",
description: "A beautiful note-taking app built with Next.js 16",
@@ -99,7 +105,7 @@ export default async function RootLayout({
data-theme={htmlTheme.dataTheme}
>
-
+