feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s

- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
Antigravity
2026-05-16 12:59:30 +00:00
parent 1fcea6ed7d
commit bd495be965
2284 changed files with 395285 additions and 2327 deletions

View File

@@ -14,7 +14,7 @@ interface ActivityFeedProps {
function getActionIcon(action: string) {
switch (action) {
case 'manual_idea': return <Lightbulb size={12} className="text-memento-blue" />
case 'manual_idea': return <Lightbulb size={12} className="text-brand-accent" />
case 'wave_generated': return <Zap size={12} className="text-orange-500" />
case 'joined': return <UserPlus size={12} className="text-emerald-500" />
case 'idea_dismissed': return <X size={12} className="text-rose-500" />

View File

@@ -3,11 +3,18 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import { BrainstormSession, BrainstormIdea } from '@/types/brainstorm'
import { useExpandIdea, useDismissIdea, useConvertIdea, useExportBrainstorm } from '@/hooks/use-brainstorm'
import {
useExpandIdea,
useDismissIdea,
useConvertIdea,
useExportBrainstorm,
brainstormQuotaMessageKey,
} from '@/hooks/use-brainstorm'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Sparkles, X, FileText, Download, ChevronDown } from 'lucide-react'
import { Sparkles, X, FileText, Download } from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), {
ssr: false,
@@ -58,6 +65,7 @@ interface BrainstormCanvasProps {
}
export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
const { t } = useLanguage()
const fgRef = useRef<any>(null)
const [selectedIdea, setSelectedIdea] = useState<BrainstormIdea | null>(null)
const [isSheetOpen, setIsSheetOpen] = useState(false)
@@ -83,7 +91,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
sessionId: session.id,
waveNumber: 0,
title: session.seedIdea,
description: 'Original seed idea',
description: t('brainstorm.originalSeedDescription'),
connectionToSeed: null,
noveltyScore: null,
parentIdeaId: null,
@@ -121,7 +129,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
}
return { graphData: { nodes, links } }
}, [session])
}, [session, t])
const handleNodeClick = useCallback((node: any) => {
setSelectedIdea(node.idea)
@@ -131,12 +139,13 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
const handleExpand = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await expandIdea.mutateAsync(selectedIdea.id)
toast.success('Ideas expanded!')
} catch (err: any) {
toast.error(err.message || 'Failed to expand')
await expandIdea.mutateAsync({ ideaId: selectedIdea.id })
toast.success(t('brainstorm.toastExpandSuccess'))
} catch (err: unknown) {
const quotaKey = brainstormQuotaMessageKey(err)
toast.error(quotaKey ? t(quotaKey) : (err instanceof Error ? err.message : t('brainstorm.toastExpandFailed')))
}
}, [selectedIdea, expandIdea])
}, [selectedIdea, expandIdea, t])
const handleDismiss = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
@@ -144,30 +153,30 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
await dismissIdea.mutateAsync(selectedIdea.id)
setIsSheetOpen(false)
setSelectedIdea(null)
toast.success('Idea dismissed')
toast.success(t('brainstorm.toastDismissSuccess'))
} catch (err: any) {
toast.error(err.message || 'Failed to dismiss')
toast.error(err.message || t('brainstorm.toastDismissFailed'))
}
}, [selectedIdea, dismissIdea])
}, [selectedIdea, dismissIdea, t])
const handleConvert = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await convertIdea.mutateAsync(selectedIdea.id)
toast.success('Idea converted to note!')
toast.success(t('brainstorm.toastConvertSuccess'))
} catch (err: any) {
toast.error(err.message || 'Failed to convert')
toast.error(err.message || t('brainstorm.toastConvertFailed'))
}
}, [selectedIdea, convertIdea])
}, [selectedIdea, convertIdea, t])
const handleExport = useCallback(async () => {
try {
const note = await exportBrainstorm.mutateAsync()
toast.success('Exported as note!')
toast.success(t('brainstorm.toastExportNoteSuccess'))
} catch (err: any) {
toast.error(err.message || 'Failed to export')
toast.error(err.message || t('brainstorm.toastExportFailed'))
}
}, [exportBrainstorm])
}, [exportBrainstorm, t])
const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const label = node.name
@@ -211,12 +220,15 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
ctx.globalAlpha = 1
}, [])
const waveLegend = [
{ label: 'Seed', color: WAVE_COLORS[0] },
{ label: 'Variations', color: WAVE_COLORS[1] },
{ label: 'Analogies', color: WAVE_COLORS[2] },
{ label: 'Disruptions', color: WAVE_COLORS[3] },
]
const waveLegend = useMemo(
() => [
{ label: t('brainstorm.legendSeed'), color: WAVE_COLORS[0] },
{ label: t('brainstorm.legendVariations'), color: WAVE_COLORS[1] },
{ label: t('brainstorm.legendAnalogies'), color: WAVE_COLORS[2] },
{ label: t('brainstorm.legendDisruptions'), color: WAVE_COLORS[3] },
],
[t]
)
return (
<div className="relative w-full h-full bg-zinc-950 rounded-xl overflow-hidden">
@@ -264,7 +276,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
disabled={exportBrainstorm.isPending}
>
<Download size={14} className="mr-1" />
{exportBrainstorm.isPending ? 'Exporting...' : 'Export'}
{exportBrainstorm.isPending ? t('brainstorm.exporting') : t('brainstorm.export')}
</Button>
</div>
@@ -280,26 +292,26 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
<div className="mt-6 space-y-4">
{selectedIdea.description && selectedIdea.id !== 'seed' && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Description</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailDescription')}</p>
<p className="text-sm text-zinc-300">{selectedIdea.description}</p>
</div>
)}
{selectedIdea.connectionToSeed && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Connection</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailConnection')}</p>
<p className="text-sm text-zinc-300">{selectedIdea.connectionToSeed}</p>
</div>
)}
{selectedIdea.noveltyScore && (
{(selectedIdea.noveltyScore != null && selectedIdea.noveltyScore > 0) && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Novelty</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailNovelty')}</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-zinc-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-orange-500 via-memento-blue to-purple-500 rounded-full"
style={{ width: `${selectedIdea.noveltyScore * 10}%` }}
className="h-full bg-gradient-to-r from-orange-500 via-brand-accent to-purple-500 rounded-full"
style={{ width: `${Math.min(100, Math.max(0, selectedIdea.noveltyScore * 10))}%` }}
/>
</div>
<span className="text-sm text-zinc-400">{selectedIdea.noveltyScore}/10</span>
@@ -309,7 +321,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
{selectedIdea.waveNumber > 0 && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Wave</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailWave')}</p>
<span
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
style={{
@@ -317,7 +329,11 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
color: WAVE_COLORS[selectedIdea.waveNumber],
}}
>
{selectedIdea.waveNumber === 1 ? 'Variation' : selectedIdea.waveNumber === 2 ? 'Analogy' : 'Disruption'}
{selectedIdea.waveNumber === 1
? t('brainstorm.waveFlavorVariation')
: selectedIdea.waveNumber === 2
? t('brainstorm.waveFlavorAnalogy')
: t('brainstorm.waveFlavorDisruption')}
</span>
</div>
)}
@@ -326,7 +342,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
<p className="text-xs text-green-400 flex items-center gap-1">
<FileText size={12} />
Converted to note
{t('brainstorm.convertedToNoteStatus')}
</p>
</div>
)}
@@ -339,15 +355,15 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
className="w-full bg-orange-600 hover:bg-orange-700 text-white"
>
<Sparkles size={14} className="mr-1" />
{expandIdea.isPending ? 'Generating...' : 'Dig Deeper'}
{expandIdea.isPending ? t('brainstorm.deepening') : t('brainstorm.deepen')}
</Button>
<Button
onClick={handleConvert}
disabled={convertIdea.isPending}
className="w-full bg-memento-blue hover:bg-blue-700 text-white"
className="w-full bg-brand-accent hover:bg-blue-700 text-white"
>
<FileText size={14} className="mr-1" />
{convertIdea.isPending ? 'Converting...' : 'Create Note'}
{convertIdea.isPending ? t('brainstorm.converting') : t('brainstorm.extract')}
</Button>
<Button
onClick={handleDismiss}
@@ -356,7 +372,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
className="w-full border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800"
>
<X size={14} className="mr-1" />
Dismiss
{t('brainstorm.dismiss')}
</Button>
</div>
)}

View File

@@ -60,7 +60,7 @@ function CursorTrackerEffect({ containerRef, moveCursor }: { containerRef: React
const WAVE_COLORS: Record<number, { border: string; bg: string; text: string }> = {
1: { border: 'border-orange-200', bg: 'bg-orange-50', text: 'text-orange-600' },
2: { border: 'border-memento-blue', bg: 'bg-memento-blue', text: 'text-memento-blue' },
2: { border: 'border-brand-accent', bg: 'bg-brand-accent', text: 'text-brand-accent' },
3: { border: 'border-violet-200', bg: 'bg-violet-50', text: 'text-violet-600' },
}
@@ -224,8 +224,8 @@ export function BrainstormPage() {
try {
const result = await exportBrainstorm.mutateAsync()
if (result?.id) {
const notebookName = result._notebookName || 'Brainstorm'
setExportToast({ noteTitle: result.title || 'Synthèse', notebookName })
const notebookName = result._notebookName || t('brainstorm.exportDefaultNotebookName')
setExportToast({ noteTitle: result.title || t('brainstorm.exportDefaultNoteTitle'), notebookName })
setTimeout(() => {
setExportToast(null)
router.push(`/?openNote=${result.id}`)
@@ -238,7 +238,7 @@ export function BrainstormPage() {
setTimeout(() => setImpactToast(null), 5000)
}
} catch (err: any) {
setExportError(err?.message || 'Export failed')
setExportError(err?.message || t('brainstorm.exportFailedMessage'))
setTimeout(() => setExportError(null), 4000)
}
}
@@ -283,7 +283,7 @@ export function BrainstormPage() {
duration: 20,
ease: 'linear',
}}
className="w-14 h-14 rounded-2xl bg-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)] flex items-center justify-center text-white"
className="w-14 h-14 rounded-2xl bg-brand-accent shadow-[0_0_20px_rgba(164,113,72,0.2)] flex items-center justify-center text-white"
>
<Wind size={28} />
</motion.div>
@@ -292,7 +292,7 @@ export function BrainstormPage() {
{t('brainstorm.title') || 'Waves of Thought'}
</h1>
<div className="flex items-center gap-2 mt-1">
<span className="w-8 h-px bg-orange-400/40" />
<span className="w-8 h-px bg-brand-accent/40" />
<p className="text-[10px] text-muted-foreground tracking-[0.3em] uppercase font-bold">
{t('brainstorm.subtitle') || 'Unfold dimensions of potentiality'}
</p>
@@ -304,7 +304,7 @@ export function BrainstormPage() {
<button
onClick={handleExport}
disabled={exportBrainstorm.isPending}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 border border-border rounded-xl text-xs font-bold uppercase tracking-widest text-muted-foreground hover:text-orange-500 transition-all shadow-sm disabled:opacity-50"
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 border border-border rounded-xl text-xs font-bold uppercase tracking-widest text-muted-foreground hover:text-brand-accent transition-all shadow-sm disabled:opacity-50"
title={t('brainstorm.export') || 'Export'}
>
<Download size={14} />
@@ -325,18 +325,18 @@ export function BrainstormPage() {
<span className="hidden sm:inline">{t('brainstorm.invite') || 'Invite'}</span>
</button>
<div className="flex items-center px-3 py-1.5 bg-white dark:bg-white/5 border border-border rounded-xl shadow-sm transition-all hover:border-emerald-500/30">
<div className="flex items-center gap-2 mr-3" title="Live Collaboration">
<div className="flex items-center gap-2 mr-3" title={t('brainstorm.liveCollaborationTitle')}>
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.6)]"></span>
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-emerald-600 dark:text-emerald-400 hidden sm:inline-block">Live</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-emerald-600 dark:text-emerald-400 hidden sm:inline-block">{t('brainstorm.liveStatus')}</span>
</div>
<div className="flex items-center group/avatars">
{authSession?.user?.name && (
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm bg-memento-blue z-10 transition-transform hover:scale-110 hover:z-20"
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm bg-brand-accent z-10 transition-transform hover:scale-110 hover:z-20"
title={`${authSession.user.name} (You)`}
>
{authSession.user.name.charAt(0).toUpperCase()}
@@ -368,19 +368,19 @@ export function BrainstormPage() {
</div>
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-orange-500/20 to-memento-blue/20 rounded-[28px] blur-xl opacity-0 group-focus-within:opacity-100 transition-opacity duration-700" />
<div className="absolute -inset-1 bg-gradient-to-r from-brand-accent/20 to-brand-accent/10 rounded-[28px] blur-xl opacity-0 group-focus-within:opacity-100 transition-opacity duration-700" />
<input
type="text"
value={seedInput}
onChange={(e) => setSeedInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleStartBrainstorm()}
placeholder={t('brainstorm.placeholder') || 'Enter a concept to unfold...'}
className="w-full relative bg-white dark:bg-[#1A1A1A] border-2 rounded-2xl px-8 py-7 pr-20 outline-none transition-all text-2xl font-serif italic text-foreground shadow-sm group-hover:shadow-md border-border/40 focus:border-orange-400/40 focus:ring-4 focus:ring-orange-500/5"
className="w-full relative bg-white dark:bg-[#1A1A1A] border-2 rounded-2xl px-8 py-7 pr-20 outline-none transition-all text-2xl font-serif italic text-foreground shadow-sm group-hover:shadow-md border-border/40 focus:border-brand-accent/40 focus:ring-4 focus:ring-brand-accent/5"
/>
<button
onClick={() => handleStartBrainstorm()}
disabled={isGenerating || !seedInput.trim()}
className="absolute right-4 top-4 bottom-4 px-6 bg-foreground dark:bg-orange-500 text-background rounded-xl disabled:opacity-50 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2 min-w-[70px] shadow-lg"
className="absolute right-4 top-4 bottom-4 px-6 bg-foreground dark:bg-brand-accent text-background rounded-xl disabled:opacity-50 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2 min-w-[70px] shadow-lg"
>
{isGenerating ? (
<div className="w-6 h-6 border-3 border-white/30 border-t-white rounded-full animate-spin" />
@@ -395,7 +395,7 @@ export function BrainstormPage() {
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-6 flex items-center gap-4 text-orange-500/80 italic font-serif"
className="mt-6 flex items-center gap-4 text-brand-accent/80 italic font-serif"
>
<div className="flex gap-1.5">
{[0.2, 0.4, 0.6].map((d, i) => (
@@ -403,7 +403,7 @@ export function BrainstormPage() {
key={i}
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: d }}
className="w-1.5 h-1.5 rounded-full bg-orange-500"
className="w-1.5 h-1.5 rounded-full bg-brand-accent"
/>
))}
</div>
@@ -444,7 +444,7 @@ export function BrainstormPage() {
<div className="absolute inset-0 flex items-center justify-center pointer-events-none opacity-20 flex-col gap-6">
<Wind size={120} strokeWidth={1} className="text-muted-foreground animate-pulse" />
<p className="text-xl font-serif italic text-muted-foreground">
The canvas is waiting for your spark...
{t('brainstorm.canvasWaitingHint')}
</p>
</div>
)}
@@ -467,7 +467,7 @@ export function BrainstormPage() {
}}
/>
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
Wave {w}
{t('brainstorm.waveBadge', { wave: w })}
</span>
</div>
))}
@@ -479,7 +479,7 @@ export function BrainstormPage() {
className="px-6 py-3 bg-[#F4F1EA] dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-muted-foreground hover:bg-foreground hover:text-background transition-all"
>
<Plus size={14} />
{t('brainstorm.addManualIdea') || 'Add Manual Idea'}
{t('brainstorm.addIdea')}
</button>
)}
</motion.div>
@@ -519,7 +519,7 @@ export function BrainstormPage() {
selectedIdea.waveNumber === 1
? 'border-orange-300 dark:border-orange-700 bg-orange-50 dark:bg-orange-500/15 text-orange-600 dark:text-orange-400'
: selectedIdea.waveNumber === 2
? 'border-memento-blue dark:border-blue-700 bg-memento-blue dark:bg-memento-blue/15 text-memento-blue dark:text-memento-blue'
? 'border-brand-accent dark:border-blue-700 bg-brand-accent dark:bg-brand-accent/15 text-brand-accent dark:text-brand-accent'
: 'border-violet-300 dark:border-violet-700 bg-violet-50 dark:bg-violet-500/15 text-violet-600 dark:text-violet-400'
}`}
>
@@ -546,18 +546,18 @@ export function BrainstormPage() {
<div className="flex items-center gap-4 mb-8">
<div className="flex items-center gap-1">
<Zap size={14} className="text-orange-500" />
<Zap size={14} className="text-brand-accent" />
<span className="text-xs font-bold text-muted-foreground">
{t('brainstorm.novelty') || 'Novelty'}: {selectedIdea.noveltyScore || 'N/A'}/10
{t('brainstorm.novelty')}: {selectedIdea.noveltyScore ?? t('common.notAvailable')}/10
</span>
</div>
<div className="flex items-center gap-1.5">
{selectedIdea.createdByType === 'human' ? (
<>
<div className="w-4 h-4 rounded-full bg-memento-blue flex items-center justify-center text-[8px] font-bold text-white">
<div className="w-4 h-4 rounded-full bg-brand-accent flex items-center justify-center text-[8px] font-bold text-white">
{(selectedIdea as any).creator?.name?.charAt(0)?.toUpperCase() || 'U'}
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">
<span className="text-[10px] font-bold uppercase tracking-widest text-brand-accent">
{t('brainstorm.humanIdea') || 'Human'}
</span>
</>
@@ -615,13 +615,13 @@ export function BrainstormPage() {
</span>
{isRestricted && !isGuest && (
<span className="shrink-0 px-1.5 py-0.5 rounded-full text-[8px] font-bold uppercase tracking-wider border border-amber-200 dark:border-amber-700 bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center gap-0.5">
<Lock size={8} /> Owner
<Lock size={8} /> {t('brainstorm.ownerBadge')}
</span>
)}
<div className="flex-1 min-w-0">
{ref.note ? (
<p className="text-sm font-medium text-foreground truncate">
{ref.note.title || 'Untitled'}
{ref.note.title || t('notes.untitled')}
</p>
) : (
<p className="text-sm italic text-muted-foreground">
@@ -653,9 +653,9 @@ export function BrainstormPage() {
<button
onClick={() => handleDeepen(selectedIdea)}
disabled={expandIdea.isPending}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-orange-400/40 hover:bg-orange-500/5 transition-all group disabled:opacity-50"
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-brand-accent/40 hover:bg-brand-accent/5 transition-all group disabled:opacity-50"
>
<Wind size={24} className="text-muted-foreground group-hover:text-orange-500 mb-2" />
<Wind size={24} className="text-muted-foreground group-hover:text-brand-accent mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-foreground group-hover:text-foreground">
{expandIdea.isPending ? (t('brainstorm.deepening') || 'Generating...') : (t('brainstorm.deepen') || 'Deepen')}
</span>
@@ -682,10 +682,10 @@ export function BrainstormPage() {
)}
{isGuest && (
<div className="mt-6 px-4 py-3 bg-orange-500/5 border border-orange-500/10 rounded-xl flex items-center gap-2">
<Globe size={14} className="text-orange-500 shrink-0" />
<span className="text-[11px] text-orange-600 dark:text-orange-400">
Vous consultez ce brainstorm en tant qu'invité. Connectez-vous pour modifier.
<div className="mt-6 px-4 py-3 bg-brand-accent/5 border border-brand-accent/10 rounded-xl flex items-center gap-2">
<Globe size={14} className="text-brand-accent shrink-0" />
<span className="text-[11px] text-brand-accent">
{t('brainstorm.guestReadOnlyNotice')}
</span>
</div>
)}
@@ -706,7 +706,7 @@ export function BrainstormPage() {
activeSessionId === s.id
? 'bg-foreground text-background scale-110 shadow-lg'
: (s as any)._owned === false
? 'bg-memento-blue dark:bg-memento-blue/10 text-memento-blue hover:bg-blue-100 hover:text-memento-blue'
? 'bg-brand-accent dark:bg-brand-accent/10 text-brand-accent hover:bg-blue-100 hover:text-brand-accent'
: 'bg-white dark:bg-white/10 text-muted-foreground hover:bg-foreground/5 hover:text-foreground'
}`}
title={s.seedIdea}
@@ -740,11 +740,11 @@ export function BrainstormPage() {
>
<span>
{impactToast.notesEnriched === 0 && impactToast.notesMarkedDry === 0
? (t('brainstorm.linkCopied') || 'Invite link copied!')
? t('brainstorm.linkCopied')
: <>
{impactToast.notesEnriched > 0 && `${impactToast.notesEnriched} note(s) enriched`}
{impactToast.notesEnriched > 0 && impactToast.notesMarkedDry > 0 && ' · '}
{impactToast.notesMarkedDry > 0 && `${impactToast.notesMarkedDry} note(s) marked dry`}
{impactToast.notesEnriched > 0 && t('brainstorm.impactNotesEnriched', { count: impactToast.notesEnriched })}
{impactToast.notesEnriched > 0 && impactToast.notesMarkedDry > 0 && t('brainstorm.impactSeparator')}
{impactToast.notesMarkedDry > 0 && t('brainstorm.impactNotesMarkedDry', { count: impactToast.notesMarkedDry })}
</>
}
</span>
@@ -782,11 +782,13 @@ export function BrainstormPage() {
<Wind size={18} />
<div className="flex flex-col">
<span className="font-bold">{exportToast.noteTitle}</span>
<span className="text-[11px] text-emerald-100">Carnet : {exportToast.notebookName}</span>
<span className="text-[11px] text-emerald-100">
{t('brainstorm.exportNotebookPrefix')} {exportToast.notebookName}
</span>
</div>
<div className="ml-3 flex items-center gap-1.5 text-emerald-200 text-[10px]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-300 animate-pulse" />
Ouverture…
{t('brainstorm.exportOpening')}
</div>
</motion.div>
)}
@@ -807,7 +809,7 @@ export function BrainstormPage() {
</div>
<div className="ml-3 flex items-center gap-1.5 text-emerald-200 text-[10px]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-300 animate-pulse" />
Ouverture
{t('brainstorm.exportOpening')}
</div>
</motion.div>
)}

View File

@@ -11,6 +11,7 @@ import {
import { UserPlus, Check, AlertCircle, Globe, Lock, Copy } from 'lucide-react'
import { createBrainstormShare } from '@/app/actions/brainstorm'
import { useUpdateBrainstormSettings } from '@/hooks/use-brainstorm'
import { useLanguage } from '@/lib/i18n'
interface BrainstormShareDialogProps {
open: boolean
@@ -28,15 +29,15 @@ interface UserResult {
image: string | null
}
const MESSAGES: Record<string, { text: string; type: 'success' | 'info' }> = {
invited: { text: 'Invitation envoyée !', type: 'success' },
re_invited: { text: 'Invitation renvoyée !', type: 'success' },
const FEEDBACK_BY_MESSAGE: Record<string, { key: string; type: 'success' | 'info' }> = {
invited: { key: 'brainstorm.feedbackInviteSent', type: 'success' },
re_invited: { key: 'brainstorm.feedbackInviteResent', type: 'success' },
already_shared: {
text: 'Cette personne a déjà accès à ce brainstorm.',
key: 'brainstorm.feedbackAlreadyShared',
type: 'info',
},
already_pending: {
text: 'Une invitation est déjà en attente pour cette personne.',
key: 'brainstorm.feedbackAlreadyPending',
type: 'info',
},
}
@@ -49,6 +50,7 @@ export function BrainstormShareDialog({
isPublic = false,
guestCanEdit = false,
}: BrainstormShareDialogProps) {
const { t } = useLanguage()
const [query, setQuery] = useState('')
const [results, setResults] = useState<UserResult[]>([])
const [showDropdown, setShowDropdown] = useState(false)
@@ -115,17 +117,17 @@ export function BrainstormShareDialog({
startTransition(async () => {
try {
const result = await createBrainstormShare(sessionId, query.trim())
if (result.message && MESSAGES[result.message]) {
const m = MESSAGES[result.message]
setFeedback({ text: m.text, type: m.type })
if (result.message && FEEDBACK_BY_MESSAGE[result.message]) {
const m = FEEDBACK_BY_MESSAGE[result.message]
setFeedback({ text: t(m.key), type: m.type })
} else {
setFeedback({ text: 'Invitation envoyée !', type: 'success' })
setFeedback({ text: t('brainstorm.feedbackInviteSent'), type: 'success' })
}
if (result.message === 'invited' || result.message === 're_invited') {
setQuery('')
}
} catch (err: any) {
setFeedback({ text: err.message || 'Erreur', type: 'error' })
setFeedback({ text: err.message || t('brainstorm.feedbackGenericError'), type: 'error' })
}
})
}
@@ -138,7 +140,7 @@ export function BrainstormShareDialog({
<div className="w-7 h-7 rounded-lg bg-orange-500/10 flex items-center justify-center">
<UserPlus size={14} className="text-orange-500" />
</div>
<span className="font-serif">Partager le brainstorm</span>
<span className="font-serif">{t('brainstorm.shareDialogTitle')}</span>
</DialogTitle>
<DialogDescription className="text-muted-foreground text-xs italic font-serif truncate">
{seedIdea.length > 60 ? seedIdea.substring(0, 60) + '…' : seedIdea}
@@ -148,7 +150,7 @@ export function BrainstormShareDialog({
<form onSubmit={handleShare} className="space-y-3 mt-2">
<div className="relative" ref={dropdownRef}>
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
Rechercher une personne
{t('brainstorm.shareSearchLabel')}
</label>
<input
type="text"
@@ -156,7 +158,7 @@ export function BrainstormShareDialog({
onChange={(e) => handleInputChange(e.target.value)}
onFocus={() => results.length > 0 && setShowDropdown(true)}
onBlur={() => setTimeout(() => setShowDropdown(false), 150)}
placeholder="Nom ou email…"
placeholder={t('brainstorm.shareNameOrEmailPlaceholder')}
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500/40 transition-all"
autoFocus
/>
@@ -178,7 +180,7 @@ export function BrainstormShareDialog({
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground truncate">
{user.name || 'Sans nom'}
{user.name || t('brainstorm.unnamedPerson')}
</p>
<p className="text-[11px] text-muted-foreground truncate">
{user.email}
@@ -215,12 +217,12 @@ export function BrainstormShareDialog({
className="w-full py-3 bg-orange-500 hover:bg-orange-600 text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center justify-center gap-1.5"
>
<UserPlus size={12} />
{isPending ? 'Envoi…' : 'Partager'}
{isPending ? t('brainstorm.shareSubmitting') : t('brainstorm.shareSubmit')}
</button>
</form>
<p className="text-[10px] text-muted-foreground/60 text-center mt-1">
La personne recevra une notification pour accepter ou refuser.
{t('brainstorm.shareFooterHint')}
</p>
<div className="border-t border-border mt-4 pt-4">
@@ -232,7 +234,7 @@ export function BrainstormShareDialog({
<Lock size={14} className="text-muted-foreground" />
)}
<span className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground">
Lien public
{t('brainstorm.sharePublicLink')}
</span>
</div>
<button
@@ -272,7 +274,7 @@ export function BrainstormShareDialog({
<div className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">
Autoriser les invités à modifier
{t('brainstorm.shareGuestsCanEdit')}
</span>
<button
onClick={() => {

View File

@@ -153,8 +153,8 @@ export function InviteDialog({
onClick={() => setRole('viewer')}
className={`flex-1 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] border transition-all ${
role === 'viewer'
? 'border-memento-blue/40 bg-memento-blue/5 text-memento-blue'
: 'border-border text-muted-foreground hover:border-memento-blue/20'
? 'border-brand-accent/40 bg-brand-accent/5 text-brand-accent'
: 'border-border text-muted-foreground hover:border-brand-accent/20'
}`}
>
{t('brainstorm.roleViewer') || 'Viewer'}

View File

@@ -38,8 +38,8 @@ export function ManualIdeaDialog({
<DialogContent className="sm:max-w-md bg-white dark:bg-[#1A1A1A] border-border rounded-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-foreground">
<div className="w-7 h-7 rounded-lg bg-memento-blue/10 flex items-center justify-center">
<Lightbulb size={14} className="text-memento-blue" />
<div className="w-7 h-7 rounded-lg bg-brand-accent/10 flex items-center justify-center">
<Lightbulb size={14} className="text-brand-accent" />
</div>
<span className="font-serif">{t('brainstorm.addIdea') || 'Add an idea'}</span>
</DialogTitle>
@@ -59,7 +59,7 @@ export function ManualIdeaDialog({
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('brainstorm.manualIdeaTitlePlaceholder') || 'Your idea in a few words...'}
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-memento-blue/20 focus:border-memento-blue/40 transition-all font-serif"
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/40 transition-all font-serif"
autoFocus
/>
</div>
@@ -71,7 +71,7 @@ export function ManualIdeaDialog({
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t('brainstorm.manualIdeaDescPlaceholder') || 'Elaborate on your idea...'}
className="w-full h-24 px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-memento-blue/20 focus:border-memento-blue/40 resize-none transition-all"
className="w-full h-24 px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/40 resize-none transition-all"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
@@ -85,7 +85,7 @@ export function ManualIdeaDialog({
<button
type="submit"
disabled={!title.trim() || isLoading}
className="px-5 py-2.5 bg-memento-blue hover:bg-memento-blue text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center gap-1.5"
className="px-5 py-2.5 bg-brand-accent hover:bg-brand-accent text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center gap-1.5"
>
<Lightbulb size={12} />
{isLoading ? (t('brainstorm.adding') || 'Adding...') : (t('brainstorm.addIdea') || 'Add idea')}

View File

@@ -4,7 +4,7 @@ import React, { useState, useCallback, useRef, useEffect } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { Play, Pause, SkipBack, SkipForward, ChevronDown, ChevronUp, RotateCcw } from 'lucide-react'
import { useBrainstormSnapshots } from '@/hooks/use-brainstorm'
import { useLanguage } from '@/lib/i18n'
interface SnapshotIdea {
id: string
title: string
@@ -32,6 +32,7 @@ interface PlaybackBarProps {
}
export function PlaybackBar({ sessionId, onSnapshotSelect, onExitPlayback }: PlaybackBarProps) {
const { t } = useLanguage()
const { data: snapshots } = useBrainstormSnapshots(sessionId)
const [isOpen, setIsOpen] = useState(false)
const [currentStep, setCurrentStep] = useState(-1)
@@ -105,7 +106,9 @@ export function PlaybackBar({ sessionId, onSnapshotSelect, onExitPlayback }: Pla
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${isLive ? 'bg-emerald-500 animate-pulse' : isPlaying ? 'bg-orange-500 animate-pulse' : 'bg-muted-foreground'}`} />
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{isLive ? 'Live' : `Step ${currentStep + 1}/${parsedSnapshots.length}`}
{isLive
? t('brainstorm.liveStatus')
: t('brainstorm.playbackStep', { current: currentStep + 1, total: parsedSnapshots.length })}
</span>
{parsedSnapshots[currentStep]?.label && (
<span className="text-[10px] text-muted-foreground/60 truncate max-w-[200px]">
@@ -115,12 +118,12 @@ export function PlaybackBar({ sessionId, onSnapshotSelect, onExitPlayback }: Pla
</div>
<div className="flex items-center gap-2">
{isLive ? (
<span className="text-[9px] font-bold text-emerald-500 uppercase tracking-wider">Live</span>
<span className="text-[9px] font-bold text-emerald-500 uppercase tracking-wider">{t('brainstorm.liveStatus')}</span>
) : (
<button
onClick={(e) => { e.stopPropagation(); handleStepChange(-1) }}
className="p-1.5 hover:bg-foreground/10 rounded-lg transition-colors"
title="Return to live"
title={t('brainstorm.playbackReturnToLive')}
>
<RotateCcw size={12} className="text-muted-foreground" />
</button>
@@ -167,8 +170,10 @@ export function PlaybackBar({ sessionId, onSnapshotSelect, onExitPlayback }: Pla
className="w-full h-1.5 bg-border rounded-full appearance-none cursor-pointer accent-orange-500"
/>
<div className="flex justify-between mt-1">
<span className="text-[8px] text-muted-foreground/40">Live</span>
<span className="text-[8px] text-muted-foreground/40">{parsedSnapshots.length} steps</span>
<span className="text-[8px] text-muted-foreground/40">{t('brainstorm.liveStatus')}</span>
<span className="text-[8px] text-muted-foreground/40">
{t('brainstorm.playbackStepsCount', { count: parsedSnapshots.length })}
</span>
</div>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import React, { useEffect, useRef, useState, useCallback } from 'react'
import * as d3 from 'd3'
import { BrainstormSession } from '@/types/brainstorm'
import { useLanguage } from '@/lib/i18n'
interface WaveCanvasProps {
session: BrainstormSession
@@ -31,6 +32,7 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
manualEditTrigger,
playbackIdeas,
}) => {
const { t, language } = useLanguage()
const svgRef = useRef<SVGSVGElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const nodeRef = useRef<d3.Selection<SVGGElement, any, SVGGElement, unknown> | null>(null)
@@ -376,7 +378,7 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
)
.text((d) =>
d.type === 'root'
? 'SEED'
? t('brainstorm.seedNodeBadge')
: d.title.length > 18
? d.title.substring(0, 18) + '...'
: d.title
@@ -476,7 +478,7 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
return () => {
simulation.stop()
}
}, [sessionId, ideasKey, isDark, toSvgCoords, toScreenCoords, playbackIdeas])
}, [sessionId, ideasKey, isDark, toSvgCoords, toScreenCoords, playbackIdeas, language, t])
useEffect(() => {
if (!nodeRef.current) return
@@ -533,13 +535,13 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
>
<div className="w-[260px] bg-white dark:bg-[#1A1A1A] rounded-2xl shadow-2xl border border-black/10 dark:border-white/10 overflow-hidden">
<div className="px-3.5 pt-3 pb-1 flex items-center gap-2">
<div className="w-5 h-5 rounded-md bg-memento-blue/10 flex items-center justify-center">
<div className="w-5 h-5 rounded-md bg-brand-accent/10 flex items-center justify-center">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.5" strokeLinecap="round">
<path d="M12 5v14M5 12h14" />
</svg>
</div>
<span className="text-[10px] font-bold uppercase tracking-[0.15em] text-foreground/50">
{editingNode.parentId ? 'Réponse' : 'Nouvelle idée'}
{editingNode.parentId ? t('brainstorm.canvasEditTitleReply') : t('brainstorm.canvasEditTitleNewIdea')}
</span>
</div>
<div className="px-3.5 pb-3">
@@ -551,19 +553,19 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
if (e.key === 'Enter' && editText.trim()) handleSubmitIdea()
if (e.key === 'Escape') { setEditingNode(null); setEditText('') }
}}
placeholder={editingNode.parentId ? 'Votre réponse…' : 'Votre idée…'}
className="w-full px-3 py-2.5 text-sm font-serif bg-black/[0.03] dark:bg-white/[0.06] border border-black/[0.06] dark:border-white/[0.12] rounded-xl outline-none focus:border-memento-blue/50 focus:bg-white dark:focus:bg-white/[0.08] transition-all placeholder:text-foreground/25 text-foreground"
placeholder={editingNode.parentId ? t('brainstorm.canvasPlaceholderReply') : t('brainstorm.canvasPlaceholderIdea')}
className="w-full px-3 py-2.5 text-sm font-serif bg-black/[0.03] dark:bg-white/[0.06] border border-black/[0.06] dark:border-white/[0.12] rounded-xl outline-none focus:border-brand-accent/50 focus:bg-white dark:focus:bg-white/[0.08] transition-all placeholder:text-foreground/25 text-foreground"
/>
<div className="flex items-center justify-between mt-1.5 px-0.5">
<div className="flex items-center gap-2.5">
<kbd className="text-[9px] text-foreground/30 font-mono bg-black/[0.04] px-1.5 py-0.5 rounded"></kbd>
<span className="text-[9px] text-foreground/25">enregistrer</span>
<span className="text-[9px] text-foreground/25">{t('brainstorm.canvasShortcutSave')}</span>
<kbd className="text-[9px] text-foreground/30 font-mono bg-black/[0.04] px-1.5 py-0.5 rounded">esc</kbd>
<span className="text-[9px] text-foreground/25">annuler</span>
<span className="text-[9px] text-foreground/25">{t('brainstorm.canvasShortcutCancel')}</span>
</div>
{editingNode.parentId && (
<span className="text-[9px] font-medium text-memento-blue/60 flex items-center gap-0.5">
enfant
<span className="text-[9px] font-medium text-brand-accent/60 flex items-center gap-0.5">
{t('brainstorm.canvasChildBranch')}
</span>
)}
</div>
@@ -577,7 +579,7 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
<div className="absolute bottom-6 left-6 pointer-events-none">
<p className="text-[10px] font-bold tracking-[0.3em] uppercase text-gray-400 dark:text-gray-600 opacity-60">
Double-clic pour ajouter une idée
{t('brainstorm.canvasDoubleClickHint')}
</p>
</div>
</div>