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

@@ -0,0 +1,83 @@
/**
* Valeurs persistées pour User.theme / localStorage `theme-preference`.
* Une seule source de vérité pour le DOM : évite les écarts entre header, settings et hydratation.
*/
export const THEME_IDS = [
'light',
'dark',
'auto',
'sepia',
'midnight',
'rose',
'green',
'lavender',
'sand',
'ocean',
'sunset',
'blue',
] as const
export type ThemeId = (typeof THEME_IDS)[number]
const NAMED_PALETTE_IDS: ThemeId[] = [
'sepia',
'midnight',
'rose',
'green',
'lavender',
'sand',
'ocean',
'sunset',
'blue',
]
export function isThemeId(value: string | null | undefined): value is ThemeId {
return value !== undefined && value !== null && (THEME_IDS as readonly string[]).includes(value)
}
/** Corrige les anciennes valeurs (ex. formulaire « slate ») vers un thème supporté. */
export function normalizeThemeId(raw: string | null | undefined): ThemeId {
if (!raw) return 'light'
if (raw === 'slate') return 'light'
if (isThemeId(raw)) return raw
return 'light'
}
/**
* Applique le thème sur `<html>` : `class="dark"` et/ou `data-theme`.
* - `light` → papier par défaut (`:root`), pas de `data-theme`
* - `dark` → sombre global (`.dark`)
* - `auto` → `.dark` si prefers-color-scheme: dark
* - palettes nommées → `data-theme="<id>"` ; `midnight` force aussi `.dark` (variante sombre du thème)
*/
export function applyDocumentTheme(theme: string): void {
if (typeof document === 'undefined') return
const t = normalizeThemeId(theme)
const root = document.documentElement
root.classList.remove('dark')
root.removeAttribute('data-theme')
if (t === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.classList.add('dark')
}
return
}
if (t === 'dark') {
root.classList.add('dark')
return
}
if (t === 'light') {
return
}
if ((NAMED_PALETTE_IDS as readonly string[]).includes(t)) {
root.setAttribute('data-theme', t)
if (t === 'midnight') {
root.classList.add('dark')
}
}
}

View File

@@ -0,0 +1,59 @@
'use client'
import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'
import type { BinaryFiles } from '@excalidraw/excalidraw/types'
/**
* Parses canvas JSON from DB — either a bare elements array or { elements, files }.
* Returns null for PPTX placeholders or invalid JSON.
*/
export function parseExcalidrawSceneFromCanvasData(
dataStr: string
): { elements: readonly ExcalidrawElement[]; files: BinaryFiles | null } | null {
if (!dataStr?.trim()) return null
try {
const parsed = JSON.parse(dataStr)
if (parsed && parsed.type === 'pptx') return null
if (Array.isArray(parsed)) {
return { elements: parsed as ExcalidrawElement[], files: null }
}
if (parsed?.elements && Array.isArray(parsed.elements)) {
const files =
parsed.files && typeof parsed.files === 'object' ? (parsed.files as BinaryFiles) : null
return { elements: parsed.elements as readonly ExcalidrawElement[], files }
}
} catch {
/* ignore */
}
return null
}
/**
* Renders Excali scene to PNG in the browser (uses Excalidraw export helpers).
*/
export async function exportExcalidrawSceneToPngBlob(dataStr: string): Promise<Blob | null> {
const scene = parseExcalidrawSceneFromCanvasData(dataStr)
if (!scene || scene.elements.length === 0) return null
const { exportToBlob, getNonDeletedElements, MIME_TYPES } = await import('@excalidraw/excalidraw')
const elements = getNonDeletedElements(scene.elements as ExcalidrawElement[])
if (!elements.length) return null
try {
return await exportToBlob({
elements,
files: scene.files,
mimeType: MIME_TYPES.png,
exportPadding: 24,
maxWidthOrHeight: 2400,
appState: {
exportBackground: true,
exportWithDarkMode: false,
viewBackgroundColor: '#ffffff',
},
})
} catch (e) {
console.error('[excalidraw-export-image] exportToBlob failed:', e)
return null
}
}

View File

@@ -1,3 +1,8 @@
import type { Note } from '@/lib/types'
const MD_IMG = /!\[[^\]]*\]\(([^)\s]+)\)/g
const HTML_IMG = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi
/**
* Plain-text preview for list view (light markdown stripping).
*/
@@ -31,3 +36,27 @@ export function getNoteDisplayTitle(note: { title: string | null; content: strin
const preview = stripMarkdownPreview(note.content || '', 100)
return preview || untitled
}
export function getNoteFeedImage(note: Note): string | null {
const first = note.images?.[0]?.trim()
if (first) return first
const content = note.content || ''
MD_IMG.lastIndex = 0
const md = MD_IMG.exec(content)
if (md?.[1]) return md[1]
HTML_IMG.lastIndex = 0
const html = HTML_IMG.exec(content)
if (html?.[1]) return html[1]
return null
}
/** Plain excerpt for collection cards (HTML + markdown noise stripped). */
export function getNotePlainExcerpt(note: Note, maxLen = 240): string {
let raw = note.content || ''
raw = raw.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
raw = raw.replace(/<[^>]+>/g, ' ')
return stripMarkdownPreview(raw, maxLen)
}

View File

@@ -1,31 +1,35 @@
export function getThemeScript(theme: string = 'light') {
return `
(function() {
try {
var localTheme = localStorage.getItem('theme-preference');
var theme = localTheme || '${theme}';
var root = document.documentElement;
root.classList.remove('dark');
root.removeAttribute('data-theme');
/**
* Script inline exécuté avant lhydratation : même logique que `applyDocumentTheme`.
*/
import { normalizeThemeId } from './apply-document-theme'
if (theme === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.classList.add('dark');
}
} else if (theme === 'dark') {
root.classList.add('dark');
} else if (theme === 'light') {
// do nothing
} else {
root.setAttribute('data-theme', theme);
if (theme === 'midnight') {
root.classList.add('dark');
}
}
} catch (e) {
console.error('Theme script error', e);
}
})();
`
export function getThemeScript(serverTheme: string = 'light') {
const fallback = normalizeThemeId(serverTheme)
return `
(function() {
try {
var fallback = ${JSON.stringify(fallback)};
var stored = localStorage.getItem('theme-preference');
var raw = stored || fallback;
if (raw === 'slate') raw = 'light';
var allowed = { light:1, dark:1, auto:1, sepia:1, midnight:1, rose:1, green:1, lavender:1, sand:1, ocean:1, sunset:1, blue:1 };
var theme = allowed[raw] ? raw : 'light';
var root = document.documentElement;
root.classList.remove('dark');
root.removeAttribute('data-theme');
if (theme === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.add('dark');
} else if (theme === 'dark') {
root.classList.add('dark');
} else if (theme === 'light') {
/* :root papier */
} else {
root.setAttribute('data-theme', theme);
if (theme === 'midnight') root.classList.add('dark');
}
} catch (e) {
console.error('Theme script error', e);
}
})();
`.trim()
}

View File

@@ -154,10 +154,10 @@ export const LABEL_COLORS = {
icon: 'text-teal-500 dark:text-teal-400'
},
blue: {
bg: 'bg-sky-100 dark:bg-sky-900/40',
text: 'text-sky-700 dark:text-sky-300',
border: 'border-sky-200 dark:border-sky-800',
icon: 'text-sky-500 dark:text-sky-400'
bg: 'bg-slate-100 dark:bg-slate-900/40',
text: 'text-slate-700 dark:text-slate-300',
border: 'border-slate-200 dark:border-slate-700',
icon: 'text-slate-600 dark:text-slate-400'
},
purple: {
bg: 'bg-violet-100 dark:bg-violet-900/40',
@@ -207,9 +207,9 @@ export const NOTE_COLORS = {
card: 'bg-teal-50 dark:bg-teal-950/30 border-teal-100 dark:border-teal-900/50'
},
blue: {
bg: 'bg-blue-50 dark:bg-blue-950/30',
hover: 'hover:bg-blue-100 dark:hover:bg-blue-950/50',
card: 'bg-blue-50 dark:bg-blue-950/30 border-blue-100 dark:border-blue-900/50'
bg: 'bg-slate-50 dark:bg-slate-950/30',
hover: 'hover:bg-slate-100 dark:hover:bg-slate-950/50',
card: 'bg-slate-50 dark:bg-slate-950/30 border-slate-200 dark:border-slate-800/50'
},
purple: {
bg: 'bg-purple-50 dark:bg-purple-950/30',