feat(ui): panneau repliable, carnets plus lisibles et accueil plus clair

Harmonise la liste des notes et l’éditeur (titres, dates, retour),
rend la saisie rapide et les raccourcis de l’accueil utilisables
sur téléphone, et conserve le panneau latéral repliable.
This commit is contained in:
Antigravity
2026-09-05 20:53:48 +00:00
parent a7b16870a4
commit 3bdbec575a
37 changed files with 1138 additions and 482 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'
import { useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from 'react'
import { useRouter } from 'next/navigation'
import { useReducedMotion } from 'motion/react'
import { Send, Bell, Loader2, PenLine } from 'lucide-react'
@@ -13,7 +13,6 @@ import { toast } from 'sonner'
import { IntelligenceHub } from '@/components/intelligence-hub'
import { DashboardWidgetGrid } from '@/components/dashboard-widget-grid'
import type { DashboardWidgetId } from '@/lib/dashboard/layout'
import { DashboardWidgetHelp } from '@/components/dashboard-widget-help'
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
import { DashboardActionStrip } from '@/components/dashboard-action-strip'
import { DashboardResumeHero } from '@/components/dashboard-resume-hero'
@@ -253,6 +252,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const [sentimentLoading, setSentimentLoading] = useState(true)
const [captureText, setCaptureText] = useState('')
const [capturing, setCapturing] = useState(false)
const [captureError, setCaptureError] = useState(false)
const capturingLockRef = useRef(false)
const [inboxPulse, setInboxPulse] = useState(0)
const [actingSuggestionId, setActingSuggestionId] = useState<string | null>(null)
const [createdAgent, setCreatedAgent] = useState<{ id: string | null; topic: string } | null>(null)
@@ -471,8 +472,10 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const handleCapture = useCallback(async () => {
const text = captureText.trim()
if (!text) return
if (!text || capturingLockRef.current) return
capturingLockRef.current = true
setCapturing(true)
setCaptureError(false)
try {
const words = text.split(/\s+/)
const title = words.slice(0, 5).join(' ') + (words.length > 5 ? '…' : '')
@@ -483,16 +486,25 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
setInboxPulse(p => p + 1)
setData(prev => prev ? { ...prev, inboxCount: prev.inboxCount + 1 } : prev)
toast.success(t('homeDashboard.captured'), { duration: 2000 })
} else {
setCaptureError(true)
toast.error(t('homeDashboard.captureError'))
}
} catch {
setCaptureError(true)
toast.error(t('homeDashboard.captureError'))
} finally {
capturingLockRef.current = false
setCapturing(false)
}
}, [captureText, t])
const handleCaptureKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleCapture() }
if (e.nativeEvent.isComposing || e.key === 'Process') return
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
void handleCapture()
}
}
const markInsightViewed = useCallback(async (insightId: string) => {
@@ -767,41 +779,74 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
switch (id) {
case 'capture':
return wrap(
<div className="relative rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm shadow-sm h-full">
<div className="flex items-center justify-between gap-2 px-3 pt-2">
<div className="flex items-center gap-2 min-w-0">
<PenLine size={11} className="text-brand-accent shrink-0" />
<div className="min-w-0">
<span className="text-[8px] font-mono font-bold uppercase tracking-widest text-concrete block">
{t('homeDashboard.quickCapture')}
</span>
<span className="text-[9px] text-concrete leading-tight">
{t('homeDashboard.captureGoesToFile')}
</span>
</div>
</div>
<DashboardWidgetHelp widgetId="capture" />
</div>
<textarea
value={captureText}
onChange={e => setCaptureText(e.target.value)}
onKeyDown={handleCaptureKeyDown}
placeholder={t('homeDashboard.quickCapturePlaceholder')}
rows={2}
className="w-full text-sm px-3 pb-2 pt-1 pe-12 bg-transparent outline-none text-ink dark:text-dark-ink resize-none leading-snug h-[2.75rem] placeholder:text-concrete/45"
<div
className={`rounded-xl border bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm shadow-sm h-full flex flex-col ${
captureError
? 'border-red-400/70 dark:border-red-500/60'
: 'border-border/30 focus-within:border-brand-accent/50'
} focus-within:ring-2 focus-within:ring-brand-accent focus-within:ring-offset-2 focus-within:ring-offset-background`}
>
<DashboardWidgetTitleRow
widgetId="capture"
icon={<PenLine size={16} className="text-brand-accent" />}
title={t('homeDashboard.quickCapture')}
className="mb-0 px-3 pt-3"
wrapTitle
/>
<button
type="button"
onClick={handleCapture}
disabled={!captureText.trim() || capturing}
className="absolute bottom-1.5 end-2 p-1.5 bg-brand-accent text-white rounded-lg disabled:opacity-25 hover:bg-brand-accent/90 hover:scale-105 active:scale-95 transition-all shadow-sm"
aria-busy={capturing}
aria-label={t('homeDashboard.captureSend')}
<p id="dashboard-capture-hint" className="px-3 pt-1 text-sm text-muted-foreground leading-snug">
{t('homeDashboard.captureGoesToFile')}
</p>
<div className="flex flex-col min-[360px]:flex-row items-stretch min-[360px]:items-end gap-2 px-3 pt-2 pb-3">
<textarea
value={captureText}
onChange={e => {
setCaptureText(e.target.value)
if (captureError) setCaptureError(false)
}}
onKeyDown={handleCaptureKeyDown}
placeholder={t('homeDashboard.quickCapturePlaceholder')}
rows={3}
dir="auto"
readOnly={capturing}
aria-busy={capturing}
aria-invalid={captureError}
aria-describedby={captureError ? 'dashboard-capture-status dashboard-capture-hint' : 'dashboard-capture-hint'}
className="min-h-16 min-w-0 flex-1 resize-none rounded-md bg-transparent px-1 py-2 text-base leading-snug text-foreground outline-none ring-0 shadow-none placeholder:text-foreground/55 read-only:opacity-70"
/>
<button
type="button"
onClick={() => void handleCapture()}
disabled={!captureText.trim() || capturing}
className={`flex h-11 w-11 shrink-0 self-end items-center justify-center rounded-lg bg-brand-accent text-white shadow-sm hover:bg-brand-accent/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-accent focus-visible:ring-offset-2 disabled:opacity-40 disabled:hover:bg-brand-accent ${
prefersReducedMotion ? '' : 'active:scale-95'
}`}
aria-busy={capturing}
aria-label={t('homeDashboard.captureSend')}
title={t('homeDashboard.captureSend')}
>
{capturing
? <Loader2 size={18} className="animate-spin" />
: <Send size={18} />}
</button>
</div>
<p
id="dashboard-capture-status"
role="status"
aria-live="polite"
className={`px-3 pb-3 text-sm leading-snug ${
captureError
? 'text-red-700 dark:text-red-400'
: capturing
? 'text-muted-foreground'
: 'sr-only'
}`}
>
{capturing
? <Loader2 size={12} className="animate-spin" />
: <Send size={12} />}
</button>
{captureError
? t('homeDashboard.captureError')
: capturing
? t('homeDashboard.captureSending')
: ''}
</p>
</div>,
)
case 'next-paths':
@@ -1076,7 +1121,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
return null
}
}, [
t, captureText, capturing, handleCaptureKeyDown, handleCapture, resumeNotes, briefingLoading, pathsLoading, pathsEnriching, sentimentLoading,
t, captureText, capturing, captureError, handleCaptureKeyDown, handleCapture, resumeNotes, briefingLoading, pathsLoading, pathsEnriching, sentimentLoading,
onNoteSelect, relTime, prefersReducedMotion, mindMapLoading, aiActive, hasAiConsent,
aiStatus, insights, topBridgeNotes, bridgeSuggestions, agentActions, handleRefreshEcho,
handleEnableAi, echoRefreshing, handleDismissInsight, handleDismissBridgeSuggestion,