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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user