diff --git a/memento-note/app/(main)/insights/page.tsx b/memento-note/app/(main)/insights/page.tsx index c4a6f178..1296579e 100644 --- a/memento-note/app/(main)/insights/page.tsx +++ b/memento-note/app/(main)/insights/page.tsx @@ -19,7 +19,6 @@ import { ChevronRight, ChevronDown, Database, - Menu, Network, List, Search, @@ -442,13 +441,6 @@ export default function InsightsPage() { {/* ── Header ── */}
-
diff --git a/memento-note/app/(main)/layout.tsx b/memento-note/app/(main)/layout.tsx index e009e5d5..9fd57f5b 100644 --- a/memento-note/app/(main)/layout.tsx +++ b/memento-note/app/(main)/layout.tsx @@ -7,6 +7,7 @@ import { detectUserLanguage, parseAcceptLanguage } from "@/lib/i18n/detect-user- import { loadTranslations } from "@/lib/i18n/load-translations"; import { getAISettings } from "@/app/actions/ai-settings"; import { AIChatLayoutBridge } from "@/components/ai-chat-layout-bridge"; +import { MobileNavigationBar } from "@/components/mobile-navigation-bar"; export default async function MainLayout({ @@ -42,6 +43,7 @@ export default async function MainLayout({
+ Skip to content @@ -53,4 +55,3 @@ export default async function MainLayout({ ); } - diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css index 5a63567f..761a7d87 100644 --- a/memento-note/app/globals.css +++ b/memento-note/app/globals.css @@ -164,6 +164,23 @@ html:not(.dark) .memento-active-nav { background: rgba(28, 28, 28, 0.2); } +.dashboard-action-strip { + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--color-brand-accent) 45%, transparent) transparent; + -webkit-overflow-scrolling: touch; + touch-action: pan-x; +} +.dashboard-action-strip::-webkit-scrollbar { + height: 4px; +} +.dashboard-action-strip::-webkit-scrollbar-track { + background: transparent; +} +.dashboard-action-strip::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--color-brand-accent) 40%, transparent); + border-radius: 999px; +} + .sidebar-shadow { box-shadow: 1px 0 10px rgba(0, 0, 0, 0.05); } @@ -2816,7 +2833,7 @@ html.font-system * { /* Rendre l'éditeur plus confortable sur mobile */ .notion-editor.tiptap { - padding-bottom: 80px !important; /* laisser de la place pour la toolbar */ + padding-bottom: calc(120px + env(safe-area-inset-bottom, 0px)) !important; /* laisser de la place pour la toolbar */ } } @@ -2827,7 +2844,8 @@ html.font-system * { left: 0; right: 0; z-index: 150; - height: 52px; + height: calc(52px + env(safe-area-inset-bottom, 0px)); + padding-bottom: env(safe-area-inset-bottom, 0px); background: color-mix(in srgb, var(--popover) 90%, transparent); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); @@ -3078,4 +3096,35 @@ html.font-system * { .tiptap-column > *:first-child { margin-top: 0; } .tiptap-column > *:last-child { margin-bottom: 0; } -.tiptap-column p { margin: 4px 0; } \ No newline at end of file +.tiptap-column p { margin: 4px 0; } + +@media (min-width: 768px) { + .dashboard-sidebar { + --sidebar-expanded-width: 26rem; + width: var(--sidebar-expanded-width); + transition: width 300ms cubic-bezier(0.16, 1, 0.3, 1), transform 300ms ease; + } + + .dashboard-sidebar #sidebar-context-panel { + flex: none; + width: calc(var(--sidebar-expanded-width) - 72px); + transition: opacity 180ms ease, visibility 300ms; + } + + .dashboard-sidebar #sidebar-context-panel[data-collapsed="true"] { + opacity: 0; + visibility: hidden; + pointer-events: none; + } +} + +@media (min-width: 1536px) { + .dashboard-sidebar { --sidebar-expanded-width: 30rem; } +} + +@media (prefers-reduced-motion: reduce) { + .dashboard-sidebar, + .dashboard-sidebar #sidebar-context-panel { + transition: none; + } +} diff --git a/memento-note/components/dashboard-action-strip.tsx b/memento-note/components/dashboard-action-strip.tsx index 1895969a..2775122e 100644 --- a/memento-note/components/dashboard-action-strip.tsx +++ b/memento-note/components/dashboard-action-strip.tsx @@ -1,8 +1,10 @@ 'use client' +import { useCallback, useEffect, useRef, useState } from 'react' import { motion } from 'motion/react' -import { Inbox, GraduationCap, Bell, Sparkles, Layers } from 'lucide-react' +import { Inbox, GraduationCap, Bell, Sparkles, Layers, ChevronLeft, ChevronRight } from 'lucide-react' import { useLanguage } from '@/lib/i18n' +import { cn } from '@/lib/utils' export interface DashboardActionStripProps { inboxCount: number @@ -19,6 +21,16 @@ export interface DashboardActionStripProps { prefersReducedMotion?: boolean } +function updateOverflow(el: HTMLDivElement) { + const max = el.scrollWidth - el.clientWidth + if (max <= 4) return { canPrev: false, canNext: false } + const pos = Math.abs(el.scrollLeft) + return { + canPrev: pos > 6, + canNext: pos < max - 6, + } +} + export function DashboardActionStrip({ inboxCount, dueFlashcards, @@ -33,7 +45,43 @@ export function DashboardActionStrip({ onThemes, prefersReducedMotion, }: DashboardActionStripProps) { - const { t } = useLanguage() + const { t, language } = useLanguage() + const scrollerRef = useRef(null) + const [canPrev, setCanPrev] = useState(false) + const [canNext, setCanNext] = useState(false) + const isRtl = language === 'fa' || language === 'ar' + const numberLocale = language === 'fa' ? 'fa-IR' : language === 'zh' ? 'zh-CN' : language + + const refreshOverflow = useCallback(() => { + const el = scrollerRef.current + if (!el) return + const next = updateOverflow(el) + setCanPrev(next.canPrev) + setCanNext(next.canNext) + }, []) + + useEffect(() => { + const el = scrollerRef.current + if (!el) return + refreshOverflow() + const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(refreshOverflow) : null + ro?.observe(el) + el.addEventListener('scroll', refreshOverflow, { passive: true }) + window.addEventListener('resize', refreshOverflow) + return () => { + ro?.disconnect() + el.removeEventListener('scroll', refreshOverflow) + window.removeEventListener('resize', refreshOverflow) + } + }, [refreshOverflow, inboxCount, dueFlashcards, reminderCount, discoveryCount, themeCount]) + + const scrollByPage = (direction: 1 | -1) => { + const el = scrollerRef.current + if (!el) return + const delta = Math.max(el.clientWidth * 0.7, 140) * direction + const left = isRtl ? -delta : delta + el.scrollBy({ left, behavior: prefersReducedMotion ? 'auto' : 'smooth' }) + } const items = [ { @@ -79,47 +127,104 @@ export function DashboardActionStrip({ }, ] + const chevronBtn = + 'absolute top-1/2 z-10 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full border border-border/40 bg-[#F9F8F6]/95 dark:bg-[#0D0D0D]/95 text-foreground shadow-sm hover:bg-brand-accent/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-accent' + return ( -
- {items.map(item => { - const Icon = item.icon - const Wrapper = item.pulse && !prefersReducedMotion ? motion.button : 'button' - const motionProps = item.pulse && !prefersReducedMotion - ? { - animate: { scale: [1, 1.03, 1] }, - transition: { duration: 0.45 }, - } - : {} - return ( - -
- -
-
-

+ {canPrev && ( + + )} + {canNext && ( + + )} + +

+
+ +
+ {items.map(item => { + const Icon = item.icon + const Wrapper = item.pulse && !prefersReducedMotion ? motion.button : 'button' + const motionProps = item.pulse && !prefersReducedMotion + ? { + animate: { scale: [1, 1.03, 1] }, + transition: { duration: 0.45 }, + } + : {} + return ( + { + e.currentTarget.scrollIntoView({ + inline: 'nearest', + block: 'nearest', + behavior: prefersReducedMotion ? 'auto' : 'smooth', + }) + }} + {...motionProps} + className={`snap-start shrink-0 flex min-h-11 min-w-[128px] items-center gap-2.5 px-3.5 py-2.5 rounded-xl border transition-colors text-start ${ + item.accent + ? 'border-brand-accent/30 bg-brand-accent/[0.06] hover:border-brand-accent/50 hover:bg-brand-accent/10 shadow-sm' + : 'border-border/25 bg-white/60 dark:bg-zinc-900/40 hover:border-border/50' + }`} + > +
- {item.value} -

-

- {item.label} -

-
-
- ) - })} + +
+
+

+ {item.value.toLocaleString(numberLocale)} +

+

+ {item.label} +

+
+ + ) + })} +
) } diff --git a/memento-note/components/dashboard-view.tsx b/memento-note/components/dashboard-view.tsx index c4e0e2a9..426ce905 100644 --- a/memento-note/components/dashboard-view.tsx +++ b/memento-note/components/dashboard-view.tsx @@ -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(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) => { - 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( -
-
-
- -
- - {t('homeDashboard.quickCapture')} - - - {t('homeDashboard.captureGoesToFile')} - -
-
- -
-