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,5 +1,6 @@
'use client'
import { useLayoutEffect, useRef } from 'react'
import { useNoteEditorContext } from './note-editor-context'
import { TitleSuggestions } from '@/components/title-suggestions'
import { Loader2, Sparkles } from 'lucide-react'
@@ -10,27 +11,54 @@ import { toast } from 'sonner'
import { resolveTitleDirection, resolveTitleLang } from '@/lib/clip/rtl-content'
import { InlinePaywall } from '@/components/settings/inline-paywall'
function autosizeTitle(el: HTMLTextAreaElement | null) {
if (!el) return
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}
export function NoteTitleBlock() {
const { note, state, actions, readOnly, fullPage } = useNoteEditorContext()
const { t } = useLanguage()
const { requestAiConsent } = useAiConsent()
const titleRef = useRef<HTMLTextAreaElement>(null)
const titleDir = resolveTitleDirection(state.title || '', note.sourceUrl)
const titleLang = resolveTitleLang(state.title || '', note.sourceUrl)
const titleIsRtl = titleDir === 'rtl'
useLayoutEffect(() => {
autosizeTitle(titleRef.current)
}, [state.title, fullPage])
useLayoutEffect(() => {
const el = titleRef.current
if (!el) return
const onResize = () => autosizeTitle(el)
window.addEventListener('resize', onResize)
const parent = el.parentElement
const ro = typeof ResizeObserver !== 'undefined' && parent
? new ResizeObserver(onResize)
: null
if (ro && parent) ro.observe(parent)
return () => {
window.removeEventListener('resize', onResize)
ro?.disconnect()
}
}, [fullPage])
if (fullPage) {
// Adaptive font size: short = big editorial, long = smaller but still premium
const titleLen = (state.title || '').length
const titleSizeClass =
titleLen === 0 ? 'text-5xl md:text-6xl' :
titleLen < 40 ? 'text-5xl md:text-6xl' :
titleLen < 70 ? 'text-4xl md:text-5xl' :
titleLen < 100 ? 'text-3xl md:text-4xl' :
'text-2xl md:text-3xl'
titleLen === 0 ? 'text-3xl sm:text-5xl md:text-6xl' :
titleLen < 40 ? 'text-3xl sm:text-5xl md:text-6xl' :
titleLen < 70 ? 'text-2xl sm:text-4xl md:text-5xl' :
titleLen < 100 ? 'text-xl sm:text-3xl md:text-4xl' :
'text-xl sm:text-2xl md:text-3xl'
return (
<div className="space-y-4">
<div className="space-y-4 min-w-0">
{state.quotaExceededFeature === 'auto_title' && (
<InlinePaywall
feature="auto_title"
@@ -38,38 +66,27 @@ export function NoteTitleBlock() {
/>
)}
{/* Title — auto-resizing textarea, adaptive size */}
<div className="group relative">
<div className="group relative min-w-0">
<textarea
ref={titleRef}
dir={titleDir}
lang={titleLang}
rows={1}
placeholder={t('notes.titlePlaceholder') || 'Untitled…'}
value={state.title}
onChange={(e) => { actions.setTitle(e.target.value) }}
onInput={(e) => {
const el = e.currentTarget
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}}
onInput={(e) => autosizeTitle(e.currentTarget)}
disabled={readOnly}
className={cn(
'w-full font-bold border-0 outline-none px-0 bg-transparent text-foreground',
'w-full min-w-0 font-bold border-0 outline-none px-0 bg-transparent text-foreground',
titleIsRtl
? 'font-[family-name:var(--font-sans)] text-right'
: 'font-memento-serif text-left',
'leading-[1.15] tracking-tight',
'leading-snug tracking-tight break-words [overflow-wrap:anywhere] whitespace-pre-wrap',
'placeholder:text-foreground/20 resize-none overflow-hidden',
titleSizeClass,
!readOnly && (titleIsRtl ? 'ps-12' : 'pe-12')
)}
style={{ height: 'auto' }}
ref={(el) => {
// Force correct initial height on mount
if (el) {
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
}}
/>
{/* AI title generation — visible on hover */}
{!readOnly && (