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
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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user