fix: restore Expand/Minimize button in AI panel + dynamic width, note thumbnail SVG placeholder with emoji/letter
This commit is contained in:
@@ -516,7 +516,8 @@ export function ContextualAIChat({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={cn(
|
<aside className={cn(
|
||||||
'border-l border-border/40 bg-background flex flex-col h-full w-full flex-shrink-0 z-10 transition-all duration-300',
|
'border-l border-border/40 bg-background flex flex-col h-full flex-shrink-0 z-10 transition-all duration-300',
|
||||||
|
expanded ? 'w-[560px]' : 'w-[360px]',
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
|
|
||||||
@@ -553,6 +554,16 @@ export function ContextualAIChat({
|
|||||||
<Wand2 className="h-3.5 w-3.5" />
|
<Wand2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost" size="icon"
|
||||||
|
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setExpanded(e => !e)}
|
||||||
|
title={expanded ? t('ai.shrinkPanel') : t('ai.expandPanel')}
|
||||||
|
>
|
||||||
|
{expanded
|
||||||
|
? <Minimize2 className="h-3.5 w-3.5" />
|
||||||
|
: <Maximize2 className="h-3.5 w-3.5" />}
|
||||||
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-7 w-7 text-muted-foreground hover:text-foreground">
|
<Button variant="ghost" size="icon" onClick={onClose} className="h-7 w-7 text-muted-foreground hover:text-foreground">
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -899,7 +899,7 @@ export function NoteEditor({ note, readOnly = false, onClose, fullPage = false }
|
|||||||
|
|
||||||
{/* ── Side panel: AI Chat ── */}
|
{/* ── Side panel: AI Chat ── */}
|
||||||
{aiOpen && (
|
{aiOpen && (
|
||||||
<div className="w-[400px] h-full self-stretch border-l border-black/10 dark:border-white/10 bg-background flex flex-col z-50 shrink-0">
|
<div className="h-full self-stretch bg-background flex flex-col z-50 shrink-0">
|
||||||
<ContextualAIChat
|
<ContextualAIChat
|
||||||
onClose={() => setAiOpen(false)}
|
onClose={() => setAiOpen(false)}
|
||||||
noteTitle={title}
|
noteTitle={title}
|
||||||
|
|||||||
@@ -116,6 +116,63 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deterministic hue from a string — consistent per note */
|
||||||
|
function stringToHue(s: string): number {
|
||||||
|
let h = 0
|
||||||
|
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xffff
|
||||||
|
return h % 360
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SVG thumbnail for notes without an image */
|
||||||
|
function NoteThumbnailPlaceholder({ title, noteId }: { title: string; noteId: string }) {
|
||||||
|
// Try to extract the first emoji from the title
|
||||||
|
const emojiMatch = title.match(/\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
|
||||||
|
const emoji = emojiMatch?.[0]
|
||||||
|
const letter = title.replace(/\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu, '').trim()[0]?.toUpperCase() || '?'
|
||||||
|
const hue = stringToHue(noteId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-full w-full flex items-center justify-center relative overflow-hidden"
|
||||||
|
style={{ background: `linear-gradient(145deg, hsl(${hue} 25% 94%) 0%, hsl(${hue} 18% 87%) 100%)` }}
|
||||||
|
>
|
||||||
|
{/* Decorative concentric circles */}
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 w-full h-full"
|
||||||
|
viewBox="0 0 224 168"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden
|
||||||
|
style={{ color: `hsl(${hue} 30% 60%)` }}
|
||||||
|
>
|
||||||
|
<circle cx="112" cy="84" r="90" stroke="currentColor" strokeWidth="0.6" opacity="0.25" />
|
||||||
|
<circle cx="112" cy="84" r="64" stroke="currentColor" strokeWidth="0.6" opacity="0.2" />
|
||||||
|
<circle cx="112" cy="84" r="38" stroke="currentColor" strokeWidth="0.6" opacity="0.15" />
|
||||||
|
<line x1="22" y1="84" x2="202" y2="84" stroke="currentColor" strokeWidth="0.4" opacity="0.15" />
|
||||||
|
<line x1="112" y1="4" x2="112" y2="164" stroke="currentColor" strokeWidth="0.4" opacity="0.15" />
|
||||||
|
</svg>
|
||||||
|
{emoji ? (
|
||||||
|
<span
|
||||||
|
className="relative text-5xl leading-none select-none"
|
||||||
|
style={{ filter: `drop-shadow(0 2px 8px hsl(${hue} 40% 40% / 0.2))` }}
|
||||||
|
>
|
||||||
|
{emoji}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="relative font-memento-serif font-bold select-none leading-none"
|
||||||
|
style={{
|
||||||
|
fontSize: '4.5rem',
|
||||||
|
color: `hsl(${hue} 35% 35%)`,
|
||||||
|
opacity: 0.35,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{letter}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function NotesEditorialView({
|
export function NotesEditorialView({
|
||||||
notes,
|
notes,
|
||||||
onOpen,
|
onOpen,
|
||||||
@@ -166,7 +223,7 @@ export function NotesEditorialView({
|
|||||||
className="w-full h-full object-cover mix-blend-multiply opacity-80 grayscale contrast-125 hover:grayscale-0 hover:opacity-100 transition-all duration-500"
|
className="w-full h-full object-cover mix-blend-multiply opacity-80 grayscale contrast-125 hover:grayscale-0 hover:opacity-100 transition-all duration-500"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full bg-gradient-to-br from-muted/40 to-muted/10" aria-hidden />
|
<NoteThumbnailPlaceholder title={title} noteId={note.id} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 flex-1">
|
<div className="space-y-3 flex-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user