From 3bdbec575a352b27608fbd0bc5ce2139167bba94 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 5 Sep 2026 20:53:48 +0000 Subject: [PATCH] feat(ui): panneau repliable, carnets plus lisibles et accueil plus clair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- memento-note/app/(main)/insights/page.tsx | 8 - memento-note/app/(main)/layout.tsx | 3 +- memento-note/app/globals.css | 55 ++++- .../components/dashboard-action-strip.tsx | 187 ++++++++++---- memento-note/components/dashboard-view.tsx | 121 +++++++--- memento-note/components/home-client.tsx | 122 ++++++---- memento-note/components/inbox-bulk-bar.tsx | 19 +- .../components/mobile-navigation-bar.tsx | 22 ++ .../note-editor/note-editor-context.tsx | 4 +- .../note-editor/note-editor-full-page.tsx | 36 ++- .../note-editor/note-editor-split-peek.tsx | 14 +- .../note-editor/note-editor-toolbar.tsx | 228 ++++++++++++------ .../note-editor/note-title-block.tsx | 61 +++-- memento-note/components/note-editor/types.ts | 4 +- .../components/notes-editorial-view.tsx | 95 ++------ memento-note/components/notes-list-views.tsx | 12 +- memento-note/components/search-modal.tsx | 33 ++- memento-note/components/sidebar.tsx | 92 ++++--- memento-note/context/search-modal-context.tsx | 7 + .../lib/editor/editor-back-context.ts | 51 ++++ memento-note/lib/search/restore-search.ts | 25 ++ .../lib/utils/format-localized-date.ts | 39 +++ memento-note/locales/ar.json | 28 ++- memento-note/locales/de.json | 24 +- memento-note/locales/en.json | 20 +- memento-note/locales/es.json | 28 ++- memento-note/locales/fa.json | 24 +- memento-note/locales/fr.json | 28 ++- memento-note/locales/hi.json | 24 +- memento-note/locales/it.json | 26 +- memento-note/locales/ja.json | 28 ++- memento-note/locales/ko.json | 24 +- memento-note/locales/nl.json | 26 +- memento-note/locales/pl.json | 28 ++- memento-note/locales/pt.json | 26 +- memento-note/locales/ru.json | 24 +- memento-note/locales/zh.json | 24 +- 37 files changed, 1138 insertions(+), 482 deletions(-) create mode 100644 memento-note/components/mobile-navigation-bar.tsx create mode 100644 memento-note/lib/editor/editor-back-context.ts create mode 100644 memento-note/lib/search/restore-search.ts 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')} - -
-
- -
-